This is an automated email from the ASF dual-hosted git repository.
XiaoHongbo-Hope pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new 6c9c057267 [core][python] Default scalar-index.search-mode to fast
(#8891)
6c9c057267 is described below
commit 6c9c057267aa5ab215edda0ec1ef744e26ad7950
Author: XiaoHongbo <[email protected]>
AuthorDate: Wed Jul 29 11:42:37 2026 +0800
[core][python] Default scalar-index.search-mode to fast (#8891)
Align the scalar index query default with the vector and full-text index
families, which already default to 'fast'. Previously scalar index
queries defaulted to 'full', triggering an extra scan of unindexed row
ranges on every scalar predicate read.
### Purpose
### Tests
---
docs/docs/multimodal-table/global-index.mdx | 2 +-
docs/generated/core_configuration.html | 2 +-
.../src/main/java/org/apache/paimon/CoreOptions.java | 2 +-
.../src/test/java/org/apache/paimon/CoreOptionsTest.java | 2 +-
.../apache/paimon/table/BtreeGlobalIndexTableTest.java | 15 ++++++++++-----
.../paimon/table/source/VectorSearchBuilderTest.java | 12 +++++++-----
paimon-python/pypaimon/common/options/core_options.py | 2 +-
.../pypaimon/tests/e2e/java_py_read_write_test.py | 4 +++-
.../tests/global_index_scalar_search_mode_test.py | 6 +++---
9 files changed, 28 insertions(+), 19 deletions(-)
diff --git a/docs/docs/multimodal-table/global-index.mdx
b/docs/docs/multimodal-table/global-index.mdx
index 731fb8c078..54aa978f1a 100644
--- a/docs/docs/multimodal-table/global-index.mdx
+++ b/docs/docs/multimodal-table/global-index.mdx
@@ -395,7 +395,7 @@ These table options affect global index build and read
behavior:
|---|---|---|
| `global-index.enabled` | `true` | Whether scans can use global indexes. |
| `global-index.search-mode` | Not set | Legacy fallback search mode for
global-index queries. Family-specific options take precedence. |
-| `scalar-index.search-mode` | `full` | Search mode for BTree and Bitmap
queries. |
+| `scalar-index.search-mode` | `fast` | Search mode for BTree and Bitmap
queries. |
| `vector-index.search-mode` | `fast` | Search mode for vector queries. |
| `full-text-index.search-mode` | `fast` | Search mode for full-text queries. |
| `global-index.external-path` | Not set | Root directory for global index
files. If not set, files are stored under the table index directory. |
diff --git a/docs/generated/core_configuration.html
b/docs/generated/core_configuration.html
index a4ea3baecd..6e80db093f 100644
--- a/docs/generated/core_configuration.html
+++ b/docs/generated/core_configuration.html
@@ -1364,7 +1364,7 @@ For an internal format table in a REST catalog, it also
makes the catalog own th
</tr>
<tr>
<td><h5>scalar-index.search-mode</h5></td>
- <td style="word-wrap: break-word;">full</td>
+ <td style="word-wrap: break-word;">fast</td>
<td><p>Enum</p></td>
<td>Search mode for scalar index queries.<br /><br />Possible
values:<ul><li>"fast": Only search indexed data.</li><li>"full": Use snapshot
next row id and global index coverage to detect missing row ids, and scan raw
data only when a gap exists.</li><li>"detail": Scan data files to find exact
unindexed rows. This can handle index invalidation caused by updates or
rewrites.</li></ul></td>
</tr>
diff --git a/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
b/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
index 92a62c344a..f3f22b6e7b 100644
--- a/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
+++ b/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
@@ -2796,7 +2796,7 @@ public class CoreOptions implements Serializable {
public static final ConfigOption<GlobalIndexSearchMode>
SCALAR_INDEX_SEARCH_MODE =
key("scalar-index.search-mode")
.enumType(GlobalIndexSearchMode.class)
- .defaultValue(GlobalIndexSearchMode.FULL)
+ .defaultValue(GlobalIndexSearchMode.FAST)
.withDescription("Search mode for scalar index queries.");
public static final ConfigOption<GlobalIndexSearchMode>
VECTOR_INDEX_SEARCH_MODE =
diff --git a/paimon-core/src/test/java/org/apache/paimon/CoreOptionsTest.java
b/paimon-core/src/test/java/org/apache/paimon/CoreOptionsTest.java
index 6f7fe93eef..1eefc0377c 100644
--- a/paimon-core/src/test/java/org/apache/paimon/CoreOptionsTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/CoreOptionsTest.java
@@ -113,7 +113,7 @@ public class CoreOptionsTest {
CoreOptions options = new CoreOptions(conf);
assertThat(options.globalIndexSearchMode()).isNull();
assertThat(options.scalarIndexSearchMode())
- .isEqualTo(CoreOptions.GlobalIndexSearchMode.FULL);
+ .isEqualTo(CoreOptions.GlobalIndexSearchMode.FAST);
assertThat(options.vectorIndexSearchMode())
.isEqualTo(CoreOptions.GlobalIndexSearchMode.FAST);
assertThat(options.fullTextIndexSearchMode())
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/BtreeGlobalIndexTableTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/BtreeGlobalIndexTableTest.java
index 53f1ecc0e1..676a1c305e 100644
---
a/paimon-core/src/test/java/org/apache/paimon/table/BtreeGlobalIndexTableTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/table/BtreeGlobalIndexTableTest.java
@@ -208,7 +208,7 @@ public class BtreeGlobalIndexTableTest extends
DataEvolutionTestBase {
assertThat(logs)
.containsPattern(
"INFO Scan table '[^']+' with global index\\. "
- + "searchMode='full', total=\\d+ ms,
metadata=\\d+ ms, "
+ + "searchMode='fast', total=\\d+ ms,
metadata=\\d+ ms, "
+ "lookup=\\d+ ms, coverage=\\d+ ms\\.")
.containsPattern(
"INFO Global index lookup table='[^']+',
type='btree', "
@@ -238,7 +238,9 @@ public class BtreeGlobalIndexTableTest extends
DataEvolutionTestBase {
BinaryString.fromString("a100"),
BinaryString.fromString("a700")));
- assertThat(readF1(table, predicate)).containsExactly("a100", "a700");
+ // Default scalar-index.search-mode is 'fast': only indexed rows are
+ // returned, so the unindexed a700 is dropped.
+ assertThat(readF1(table, predicate)).containsExactly("a100");
assertThat(readF1(tableWithSearchMode(table, "fast"),
predicate)).containsExactly("a100");
assertThat(readF1(tableWithSearchMode(table, "full"), predicate))
@@ -252,7 +254,8 @@ public class BtreeGlobalIndexTableTest extends
DataEvolutionTestBase {
builder.equal(1, BinaryString.fromString("a700")),
builder.equal(2, BinaryString.fromString("b700")));
- assertThat(readF1(table,
andWithUnindexedField)).containsExactly("a700");
+ // Default 'fast': a700 lives in the unindexed range, so it is dropped.
+ assertThat(readF1(table, andWithUnindexedField)).isEmpty();
assertThat(readF1(tableWithSearchMode(table, "fast"),
andWithUnindexedField)).isEmpty();
assertThat(readF1(tableWithSearchMode(table, "full"),
andWithUnindexedField))
.containsExactly("a700");
@@ -365,7 +368,8 @@ public class BtreeGlobalIndexTableTest extends
DataEvolutionTestBase {
builder.equal(1, BinaryString.fromString("a700")),
builder.equal(2, BinaryString.fromString("b700")));
- assertThat(readF1(table, andPredicate)).containsExactly("a700");
+ // Default 'fast': a700 is in the unindexed range, so it is dropped.
+ assertThat(readF1(table, andPredicate)).isEmpty();
assertThat(readF1(tableWithSearchMode(table, "fast"),
andPredicate)).isEmpty();
assertThat(readF1(tableWithSearchMode(table, "full"), andPredicate))
.containsExactly("a700");
@@ -377,7 +381,8 @@ public class BtreeGlobalIndexTableTest extends
DataEvolutionTestBase {
builder.equal(1, BinaryString.fromString("a700")),
builder.equal(2, BinaryString.fromString("b701")));
- assertThat(readF1(table, orPredicate)).containsExactly("a700", "a701");
+ // Default 'fast': a700 is unindexed and dropped; a701 (f2 indexed)
stays.
+ assertThat(readF1(table, orPredicate)).containsExactly("a701");
assertThat(readF1(tableWithSearchMode(table, "fast"),
orPredicate)).containsExactly("a701");
assertThat(readF1(tableWithSearchMode(table, "full"), orPredicate))
.containsExactly("a700", "a701");
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/source/VectorSearchBuilderTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/source/VectorSearchBuilderTest.java
index 9f747807c0..85528388c9 100644
---
a/paimon-core/src/test/java/org/apache/paimon/table/source/VectorSearchBuilderTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/table/source/VectorSearchBuilderTest.java
@@ -1065,12 +1065,14 @@ public class VectorSearchBuilderTest extends
TableTestBase {
}
@Test
- public void testPartialScalarPreFilterMustNotDropUnindexedScalarRows()
throws Exception {
+ public void testPartialScalarPreFilterDropsUnindexedRowsInFastMode()
throws Exception {
+ // Default fast scalar mode drops unindexed rows: the btree covers ids
+ // 3-7, so id>=8 (rows 8,9 unindexed) yields an empty pre-filter.
catalog.createTable(
- identifier("default_scalar_full_partial_index_table"),
+ identifier("default_scalar_fast_partial_index_table"),
vectorSchemaBuilder(VECTOR_FIELD_NAME).build(),
false);
- FileStoreTable table =
getTable(identifier("default_scalar_full_partial_index_table"));
+ FileStoreTable table =
getTable(identifier("default_scalar_fast_partial_index_table"));
float[][] vectors = new float[10][];
for (int i = 0; i < vectors.length; i++) {
@@ -1091,7 +1093,7 @@ public class VectorSearchBuilderTest extends
TableTestBase {
VectorScan.Plan vectorPlan = searchBuilder.newVectorScan().scan();
GlobalIndexResult result =
searchBuilder.newVectorRead().read(vectorPlan);
- assertThat(result.results()).contains(8L);
+ assertThat(result.results().isEmpty()).isTrue();
ReadBuilder readBuilder = table.newReadBuilder().withFilter(idFilter);
TableScan.Plan readPlan =
readBuilder.newScan().withGlobalIndexResult(result).plan();
@@ -1099,7 +1101,7 @@ public class VectorSearchBuilderTest extends
TableTestBase {
try (RecordReader<InternalRow> reader =
readBuilder.newRead().createReader(readPlan)) {
reader.forEachRemaining(row -> ids.add(row.getInt(0)));
}
- assertThat(ids).containsExactly(8);
+ assertThat(ids).isEmpty();
}
@Test
diff --git a/paimon-python/pypaimon/common/options/core_options.py
b/paimon-python/pypaimon/common/options/core_options.py
index 2b168ee09f..f53c3722b4 100644
--- a/paimon-python/pypaimon/common/options/core_options.py
+++ b/paimon-python/pypaimon/common/options/core_options.py
@@ -738,7 +738,7 @@ class CoreOptions:
SCALAR_INDEX_SEARCH_MODE: ConfigOption[GlobalIndexSearchMode] = (
ConfigOptions.key("scalar-index.search-mode")
.enum_type(GlobalIndexSearchMode)
- .default_value(GlobalIndexSearchMode.FULL)
+ .default_value(GlobalIndexSearchMode.FAST)
.with_description("Search mode for scalar index queries.")
)
diff --git a/paimon-python/pypaimon/tests/e2e/java_py_read_write_test.py
b/paimon-python/pypaimon/tests/e2e/java_py_read_write_test.py
index b9213bd898..89b9833f1e 100644
--- a/paimon-python/pypaimon/tests/e2e/java_py_read_write_test.py
+++ b/paimon-python/pypaimon/tests/e2e/java_py_read_write_test.py
@@ -544,7 +544,9 @@ class JavaPyReadWriteTest(unittest.TestCase):
fast_builder.new_scan().plan().splits())
self.assertEqual(0, fast_result.num_rows)
- read_builder = table.new_read_builder()
+ # full mode falls back to a raw scan for the unindexed k4 row
+ full_table = table.copy({'scalar-index.search-mode': 'full'})
+ read_builder = full_table.new_read_builder()
read_builder.with_filter(
read_builder.new_predicate_builder().equal('k', 'k4'))
actual = read_builder.new_read().to_arrow(
diff --git
a/paimon-python/pypaimon/tests/global_index_scalar_search_mode_test.py
b/paimon-python/pypaimon/tests/global_index_scalar_search_mode_test.py
index b900c52ca0..1985af3287 100644
--- a/paimon-python/pypaimon/tests/global_index_scalar_search_mode_test.py
+++ b/paimon-python/pypaimon/tests/global_index_scalar_search_mode_test.py
@@ -53,7 +53,7 @@ class ScalarGlobalIndexSearchModeTest(unittest.TestCase):
options = CoreOptions(Options.from_none())
self.assertIsNone(options.global_index_search_mode())
self.assertEqual(
- GlobalIndexSearchMode.FULL, options.scalar_index_search_mode())
+ GlobalIndexSearchMode.FAST, options.scalar_index_search_mode())
self.assertEqual(
GlobalIndexSearchMode.FAST, options.vector_index_search_mode())
self.assertEqual(
@@ -88,7 +88,7 @@ class ScalarGlobalIndexSearchModeTest(unittest.TestCase):
full = coverage.unindexed_ranges(1,
search_mode=GlobalIndexSearchMode.FULL)
self.assertEqual([(100, 199)], [(r.from_, r.to) for r in full])
self.assertEqual(
- [(100, 199)],
+ [],
[(r.from_, r.to) for r in coverage.unindexed_ranges(1)])
def test_scanner_applies_passed_scalar_mode(self):
@@ -102,7 +102,7 @@ class ScalarGlobalIndexSearchModeTest(unittest.TestCase):
coverage = _coverage(CoreOptions(Options.from_none()))
scanner = SimpleNamespace(_coverage=coverage, _fields=[1])
result = DataEvolutionGlobalIndexScanner.unindexed_rows(scanner, None)
- self.assertEqual([(100, 199)], _ranges(result))
+ self.assertEqual([], _ranges(result))
if __name__ == "__main__":