From 2d61492fad8f5641142204ca06b5f5d083e5d017 Mon Sep 17 00:00:00 2001 From: yangjie01 Date: Tue, 15 Sep 2026 12:07:17 +0800 Subject: [PATCH 1/2] [core] Fall back to raw vector search when scalar index cannot evaluate filter Vector search with a WHERE filter delegates pre-filtering to the scalar global index, and scalarMatchedRows turned "the index cannot evaluate this predicate" (no scalar index files, or an unsupported function such as IS NOT NULL on a multivalue-indexed array column, or an OR with an unsupported branch) into an empty bitmap. That bitmap was AND-ed into every index split while field-level coverage still marked the rows as indexed, so no raw split covered them: all index-covered rows silently vanished from the result. Suppressing the index splits alone would let the unfiltered top-K be polluted by non-matching rows, so route the ranges the raw splits do not already cover through the raw search instead, where the exact final-read filter decides; already-covered ranges keep the existing dedup. The scalar pre-filter computed while deciding is cached for preFilters, and every reader entry (local, batch, Spark, Flink) demotes right after splitting its splits. Assisted-by: GLM-5.3 --- .../AbstractDataEvolutionVectorRead.java | 68 +++++++++++- .../source/DataEvolutionBatchVectorRead.java | 1 + .../table/source/DataEvolutionVectorRead.java | 1 + .../table/source/VectorSearchBuilderTest.java | 101 ++++++++++++++++++ .../FlinkDataEvolutionVectorRead.java | 1 + .../read/SparkDataEvolutionVectorRead.java | 1 + 6 files changed, 170 insertions(+), 3 deletions(-) diff --git a/paimon-core/src/main/java/org/apache/paimon/table/source/AbstractDataEvolutionVectorRead.java b/paimon-core/src/main/java/org/apache/paimon/table/source/AbstractDataEvolutionVectorRead.java index 83ea140270e6..e17c5bdcc896 100644 --- a/paimon-core/src/main/java/org/apache/paimon/table/source/AbstractDataEvolutionVectorRead.java +++ b/paimon-core/src/main/java/org/apache/paimon/table/source/AbstractDataEvolutionVectorRead.java @@ -88,6 +88,13 @@ public abstract class AbstractDataEvolutionVectorRead implements Serializable { /** Snapshot the plan was built against; pins filters and raw reads to it. */ @Nullable protected Snapshot planSnapshot; + /** + * Scalar pre-filter computed while splitting index/raw splits for the current read; lets {@link + * #preFilters} reuse it instead of scanning the scalar index twice. Only written and read on + * the single thread performing the read. + */ + @Nullable private transient RoaringNavigableMap64 scalarPreFilter; + private static final Comparator WEAKEST_SCORE_FIRST = Comparator.comparingDouble(a -> Float.intBitsToFloat((int) a[1])) .thenComparing((a, b) -> Long.compare(b[0], a[0])); @@ -132,6 +139,55 @@ private GlobalIndexer createGlobalIndexer(String indexType, GlobalIndexMeta meta table.coreOptions().toConfiguration()); } + /** + * Moves index splits aside when the scalar index cannot evaluate {@link #filter}: the index + * path answers with all-or-nothing bitmaps, so with an unevaluable filter it can neither honor + * the filter (an empty bitmap silently drops every covered row) nor ignore it (the top-K would + * be polluted by non-matching rows). Instead, suppress the index splits and route the ranges + * the raw splits do not already cover through the raw search, where the exact final-read filter + * decides; already-covered ranges would duplicate that work. Splits routed this way reuse the + * vector index type so raw scoring keeps its metric. Must run after {@link #splitSearchSplits} + * and before the index splits are read, on the single thread performing the read; the computed + * pre-filter is cached for {@link #preFilters} to reuse. + */ + protected void demoteUncoverableIndexSplits( + List indexSplits, List rawSplits) { + scalarPreFilter = null; + if (filter == null || indexSplits.isEmpty()) { + return; + } + RoaringNavigableMap64 matchedRows = scalarMatchedRows(indexSplits); + if (matchedRows != null) { + scalarPreFilter = matchedRows; + return; + } + String indexType = vectorIndexType(indexSplits); + List uncovered = new ArrayList<>(); + List scalarIndexFiles = new ArrayList<>(); + for (IndexVectorSearchSplit split : indexSplits) { + if (!coveredByRawSplits(split.rowRangeStart(), split.rowRangeEnd(), rawSplits)) { + uncovered.add(new Range(split.rowRangeStart(), split.rowRangeEnd())); + scalarIndexFiles.addAll(split.scalarIndexFiles()); + } + } + if (!uncovered.isEmpty()) { + rawSplits.add(new RawVectorSearchSplit(uncovered, scalarIndexFiles, indexType)); + } + indexSplits.clear(); + } + + private static boolean coveredByRawSplits( + long start, long end, List rawSplits) { + for (RawVectorSearchSplit raw : rawSplits) { + for (Range range : raw.rowRanges()) { + if (range.from <= start && end <= range.to) { + return true; + } + } + } + return false; + } + protected List preFilters(List splits) { List indexedRowRanges = new ArrayList<>(splits.size()); for (IndexVectorSearchSplit split : splits) { @@ -141,7 +197,8 @@ protected List preFilters(List sp RoaringNavigableMap64 liveRows = GlobalIndexLiveRowFilter.liveRows( table, planSnapshot, partitionFilter, indexedRowRanges); - RoaringNavigableMap64 matchedRows = scalarMatchedRows(splits); + RoaringNavigableMap64 matchedRows = + scalarPreFilter != null ? scalarPreFilter : scalarMatchedRows(splits); List includeRowIds = new ArrayList<>(splits.size()); boolean hasFilter = false; @@ -167,6 +224,11 @@ protected List preFilters(List sp return hasFilter ? includeRowIds : Collections.emptyList(); } + /** + * Row ids the scalar index reports as matching {@link #filter}, or {@code null} when the index + * cannot evaluate the predicate (no scalar index files, or a function the reader does not + * support). {@code null} means "cannot decide", never "no rows match". + */ @Nullable private RoaringNavigableMap64 scalarMatchedRows(List splits) { if (filter == null) { @@ -183,13 +245,13 @@ private RoaringNavigableMap64 scalarMatchedRows(List spl DataEvolutionGlobalIndexScanner.create( table, planSnapshot, partitionFilter, scalarIndexFiles); if (!optionalScanner.isPresent()) { - return new RoaringNavigableMap64(); + return null; } try (DataEvolutionGlobalIndexScanner scanner = optionalScanner.get()) { Optional result = scanner.scan(filter); if (!result.isPresent()) { - return new RoaringNavigableMap64(); + return null; } return result.get().results(); } catch (IOException e) { diff --git a/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionBatchVectorRead.java b/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionBatchVectorRead.java index 6c83a62b2368..88a02923db61 100644 --- a/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionBatchVectorRead.java +++ b/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionBatchVectorRead.java @@ -71,6 +71,7 @@ private List readBatch(List splits) { List indexSplits = new ArrayList<>(); List rawSplits = new ArrayList<>(); splitSearchSplits(splits, indexSplits, rawSplits); + demoteUncoverableIndexSplits(indexSplits, rawSplits); if (indexSplits.isEmpty() && rawSplits.isEmpty()) { List empty = new ArrayList<>(n); for (int i = 0; i < n; i++) { diff --git a/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionVectorRead.java b/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionVectorRead.java index 9179dd7512c5..5d2c98b629b1 100644 --- a/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionVectorRead.java +++ b/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionVectorRead.java @@ -69,6 +69,7 @@ protected GlobalIndexResult readSplits(List splits) List indexSplits = new ArrayList<>(); List rawSplits = new ArrayList<>(); splitSearchSplits(splits, indexSplits, rawSplits); + demoteUncoverableIndexSplits(indexSplits, rawSplits); if (indexSplits.isEmpty() && rawSplits.isEmpty()) { return GlobalIndexResult.createEmpty(); } 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 efec98c1f370..e9c972d97852 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 @@ -20,6 +20,7 @@ import org.apache.paimon.CoreOptions; import org.apache.paimon.data.BinaryRow; +import org.apache.paimon.data.BinaryString; import org.apache.paimon.data.GenericArray; import org.apache.paimon.data.GenericRow; import org.apache.paimon.data.InternalRow; @@ -31,6 +32,7 @@ import org.apache.paimon.globalindex.GlobalIndexSingleColumnWriter; import org.apache.paimon.globalindex.ResultEntry; import org.apache.paimon.globalindex.ScoredGlobalIndexResult; +import org.apache.paimon.globalindex.bitmap.MultiValueGlobalIndexerFactory; import org.apache.paimon.globalindex.btree.BTreeGlobalIndexerFactory; import org.apache.paimon.globalindex.testvector.TestVectorGlobalIndexer; import org.apache.paimon.globalindex.testvector.TestVectorGlobalIndexerFactory; @@ -587,6 +589,105 @@ public void testVectorSearchFullModeScansFilteredUnindexedData() throws Exceptio assertThat(result.results()).containsExactlyInAnyOrder(2L, 3L); } + @Test + public void testVectorSearchFilterIndexCannotEvaluate() throws Exception { + catalog.createTable( + identifier("vector_search_unsupported_scalar_predicate"), + withVectorSchemaOptions( + Schema.newBuilder() + .column("id", DataTypes.INT()) + .column("tags", new ArrayType(DataTypes.STRING())) + .column( + VECTOR_FIELD_NAME, + new ArrayType(DataTypes.FLOAT()))) + .build(), + false); + FileStoreTable table = getTable(identifier("vector_search_unsupported_scalar_predicate")); + + // row 0 has NULL tags and is the closest to the query: dropping the index + // pre-filter alone would pollute the top-K with it, and keeping the old empty + // bitmap would drop every row — only routing the search through the exact + // final-read filter returns the closest *matching* rows + float[][] vectors = {{0.0f, 0.0f}, {1.0f, 0.0f}, {2.0f, 0.0f}, {3.0f, 0.0f}}; + writeVectorsWithTags(table, vectors, 0); + buildAndCommitIndex(table, vectors); + buildAndCommitMultiValueIndex(table, vectors.length); + + // IS NOT NULL on the multivalue-indexed array column is a function the reader + // cannot evaluate; the scalar pre-filter must step aside and let the exact + // final-read filter decide instead of answering "no rows match" + Predicate filter = + new PredicateBuilder(table.rowType()) + .isNotNull(table.rowType().getFieldIndex("tags")); + GlobalIndexResult result = + table.newVectorSearchBuilder() + .withVector(new float[] {0.0f, 0.0f}) + .withLimit(2) + .withVectorColumn(VECTOR_FIELD_NAME) + .withFilter(filter) + .executeLocal(); + + assertThat(result.results()).containsExactly(1L, 2L); + assertThat(readIds(table, result)).containsExactly(1, 2); + } + + private void writeVectorsWithTags(FileStoreTable table, float[][] vectors, int nullTagsRow) + throws Exception { + BatchWriteBuilder writeBuilder = table.newBatchWriteBuilder(); + try (BatchTableWrite write = writeBuilder.newWrite(); + BatchTableCommit commit = writeBuilder.newCommit()) { + for (int i = 0; i < vectors.length; i++) { + GenericArray tags = + i == nullTagsRow + ? null + : new GenericArray( + new BinaryString[] {BinaryString.fromString("t" + i)}); + write.write(GenericRow.of(i, tags, new GenericArray(vectors[i]))); + } + commit.commit(write.prepareCommit()); + } + } + + private void buildAndCommitMultiValueIndex(FileStoreTable table, int rowCount) + throws Exception { + Options options = table.coreOptions().toConfiguration(); + DataField tagsField = table.rowType().getField("tags"); + + GlobalIndexSingleColumnWriter writer = + (GlobalIndexSingleColumnWriter) + GlobalIndexBuilderUtils.createIndexWriter( + table, + MultiValueGlobalIndexerFactory.IDENTIFIER, + tagsField, + options); + for (int row = 0; row < rowCount; row++) { + writer.write(BinaryString.fromString("t" + row), row); + } + List entries = writer.finish(rowCount); + + List indexFiles = + GlobalIndexBuilderUtils.toIndexFileMetas( + table.fileIO(), + table.store().pathFactory().globalIndexFileFactory(), + table.coreOptions(), + new Range(0, rowCount - 1), + tagsField.id(), + MultiValueGlobalIndexerFactory.IDENTIFIER, + entries); + + DataIncrement dataIncrement = DataIncrement.indexIncrement(indexFiles); + CommitMessage message = + new CommitMessageImpl( + BinaryRow.EMPTY_ROW, + 0, + null, + dataIncrement, + CompactIncrement.emptyIncrement()); + try (BatchTableCommit commit = table.newBatchWriteBuilder().newCommit()) { + commit.commit(Collections.singletonList(message)); + } + } + @Test public void testVectorSearchRawSearchUsesScalarPreFilter() throws Exception { catalog.createTable( diff --git a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/vectorsearch/FlinkDataEvolutionVectorRead.java b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/vectorsearch/FlinkDataEvolutionVectorRead.java index 0e2b9a9df219..111bcf395c0a 100644 --- a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/vectorsearch/FlinkDataEvolutionVectorRead.java +++ b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/vectorsearch/FlinkDataEvolutionVectorRead.java @@ -92,6 +92,7 @@ public GlobalIndexResult read(VectorScan.Plan plan) { List indexSplits = new ArrayList<>(); List rawSplits = new ArrayList<>(); splitSearchSplits(plan.splits(), indexSplits, rawSplits); + demoteUncoverableIndexSplits(indexSplits, rawSplits); if (indexSplits.isEmpty() && rawSplits.isEmpty()) { return GlobalIndexResult.createEmpty(); } diff --git a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/read/SparkDataEvolutionVectorRead.java b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/read/SparkDataEvolutionVectorRead.java index a65f7dcb8264..e9d14dceb950 100644 --- a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/read/SparkDataEvolutionVectorRead.java +++ b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/read/SparkDataEvolutionVectorRead.java @@ -75,6 +75,7 @@ public GlobalIndexResult read(VectorScan.Plan plan) { List indexSplits = new ArrayList<>(); List rawSplits = new ArrayList<>(); splitSearchSplits(plan.splits(), indexSplits, rawSplits); + demoteUncoverableIndexSplits(indexSplits, rawSplits); if (indexSplits.isEmpty() && rawSplits.isEmpty()) { return GlobalIndexResult.createEmpty(); } From f9da03d7ecb42e3c6ec9097507d6d8c894bd49b1 Mon Sep 17 00:00:00 2001 From: yangjie01 Date: Thu, 17 Sep 2026 13:52:30 +0800 Subject: [PATCH 2/2] [core] Fold demote into prepareSplits and drop the coverage check Addressing review on #9909: - Wrap splitSearchSplits + the demote step in a single prepareSplits instance method so the ordering is structural, not a javadoc contract; splitSearchSplits becomes private. - Drop the coveredByRawSplits / uncovered bookkeeping: the raw read merges ranges (Range.sortAndMergeOverlap) and dedups by row id, so adding all index-split ranges equals adding only the uncovered ones. --- .../AbstractDataEvolutionVectorRead.java | 53 +++++++------------ .../source/DataEvolutionBatchVectorRead.java | 3 +- .../table/source/DataEvolutionVectorRead.java | 3 +- .../FlinkDataEvolutionVectorRead.java | 3 +- .../read/SparkDataEvolutionVectorRead.java | 3 +- 5 files changed, 24 insertions(+), 41 deletions(-) diff --git a/paimon-core/src/main/java/org/apache/paimon/table/source/AbstractDataEvolutionVectorRead.java b/paimon-core/src/main/java/org/apache/paimon/table/source/AbstractDataEvolutionVectorRead.java index e17c5bdcc896..0f0b43810d5b 100644 --- a/paimon-core/src/main/java/org/apache/paimon/table/source/AbstractDataEvolutionVectorRead.java +++ b/paimon-core/src/main/java/org/apache/paimon/table/source/AbstractDataEvolutionVectorRead.java @@ -140,18 +140,21 @@ private GlobalIndexer createGlobalIndexer(String indexType, GlobalIndexMeta meta } /** - * Moves index splits aside when the scalar index cannot evaluate {@link #filter}: the index - * path answers with all-or-nothing bitmaps, so with an unevaluable filter it can neither honor - * the filter (an empty bitmap silently drops every covered row) nor ignore it (the top-K would - * be polluted by non-matching rows). Instead, suppress the index splits and route the ranges - * the raw splits do not already cover through the raw search, where the exact final-read filter - * decides; already-covered ranges would duplicate that work. Splits routed this way reuse the - * vector index type so raw scoring keeps its metric. Must run after {@link #splitSearchSplits} - * and before the index splits are read, on the single thread performing the read; the computed - * pre-filter is cached for {@link #preFilters} to reuse. + * Splits the search splits into index and raw splits, then moves the index splits aside when + * the scalar index cannot evaluate {@link #filter}: the index path answers with all-or-nothing + * bitmaps, so with an unevaluable filter it can neither honor the filter (an empty bitmap + * silently drops every covered row) nor ignore it (the top-K would be polluted by non-matching + * rows). Instead, suppress the index splits and route their ranges through the raw search, + * where the exact final-read filter decides; the raw read merges ranges, so overlap with an + * existing raw split is deduplicated. Splits routed this way reuse the vector index type so raw + * scoring keeps its metric. The computed scalar pre-filter is cached for {@link #preFilters} to + * reuse. */ - protected void demoteUncoverableIndexSplits( - List indexSplits, List rawSplits) { + protected void prepareSplits( + List splits, + List indexSplits, + List rawSplits) { + splitSearchSplits(splits, indexSplits, rawSplits); scalarPreFilter = null; if (filter == null || indexSplits.isEmpty()) { return; @@ -161,33 +164,17 @@ protected void demoteUncoverableIndexSplits( scalarPreFilter = matchedRows; return; } - String indexType = vectorIndexType(indexSplits); - List uncovered = new ArrayList<>(); + List ranges = new ArrayList<>(); List scalarIndexFiles = new ArrayList<>(); for (IndexVectorSearchSplit split : indexSplits) { - if (!coveredByRawSplits(split.rowRangeStart(), split.rowRangeEnd(), rawSplits)) { - uncovered.add(new Range(split.rowRangeStart(), split.rowRangeEnd())); - scalarIndexFiles.addAll(split.scalarIndexFiles()); - } - } - if (!uncovered.isEmpty()) { - rawSplits.add(new RawVectorSearchSplit(uncovered, scalarIndexFiles, indexType)); + ranges.add(new Range(split.rowRangeStart(), split.rowRangeEnd())); + scalarIndexFiles.addAll(split.scalarIndexFiles()); } + rawSplits.add( + new RawVectorSearchSplit(ranges, scalarIndexFiles, vectorIndexType(indexSplits))); indexSplits.clear(); } - private static boolean coveredByRawSplits( - long start, long end, List rawSplits) { - for (RawVectorSearchSplit raw : rawSplits) { - for (Range range : raw.rowRanges()) { - if (range.from <= start && end <= range.to) { - return true; - } - } - } - return false; - } - protected List preFilters(List splits) { List indexedRowRanges = new ArrayList<>(splits.size()); for (IndexVectorSearchSplit split : splits) { @@ -694,7 +681,7 @@ private FileStoreTable rawReadTable() { return table.copyWithoutTimeTravel(pinOptions); } - protected static void splitSearchSplits( + private static void splitSearchSplits( List splits, List indexSplits, List rawSplits) { diff --git a/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionBatchVectorRead.java b/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionBatchVectorRead.java index 88a02923db61..a94f0d6dba9b 100644 --- a/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionBatchVectorRead.java +++ b/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionBatchVectorRead.java @@ -70,8 +70,7 @@ private List readBatch(List splits) { int n = vectors.length; List indexSplits = new ArrayList<>(); List rawSplits = new ArrayList<>(); - splitSearchSplits(splits, indexSplits, rawSplits); - demoteUncoverableIndexSplits(indexSplits, rawSplits); + prepareSplits(splits, indexSplits, rawSplits); if (indexSplits.isEmpty() && rawSplits.isEmpty()) { List empty = new ArrayList<>(n); for (int i = 0; i < n; i++) { diff --git a/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionVectorRead.java b/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionVectorRead.java index 5d2c98b629b1..fbac5ef7b786 100644 --- a/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionVectorRead.java +++ b/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionVectorRead.java @@ -68,8 +68,7 @@ public GlobalIndexResult read(VectorScan.Plan plan) { protected GlobalIndexResult readSplits(List splits) { List indexSplits = new ArrayList<>(); List rawSplits = new ArrayList<>(); - splitSearchSplits(splits, indexSplits, rawSplits); - demoteUncoverableIndexSplits(indexSplits, rawSplits); + prepareSplits(splits, indexSplits, rawSplits); if (indexSplits.isEmpty() && rawSplits.isEmpty()) { return GlobalIndexResult.createEmpty(); } diff --git a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/vectorsearch/FlinkDataEvolutionVectorRead.java b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/vectorsearch/FlinkDataEvolutionVectorRead.java index 111bcf395c0a..192bd54002ba 100644 --- a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/vectorsearch/FlinkDataEvolutionVectorRead.java +++ b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/vectorsearch/FlinkDataEvolutionVectorRead.java @@ -91,8 +91,7 @@ public GlobalIndexResult read(VectorScan.Plan plan) { this.planSnapshot = plan.snapshot(); List indexSplits = new ArrayList<>(); List rawSplits = new ArrayList<>(); - splitSearchSplits(plan.splits(), indexSplits, rawSplits); - demoteUncoverableIndexSplits(indexSplits, rawSplits); + prepareSplits(plan.splits(), indexSplits, rawSplits); if (indexSplits.isEmpty() && rawSplits.isEmpty()) { return GlobalIndexResult.createEmpty(); } diff --git a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/read/SparkDataEvolutionVectorRead.java b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/read/SparkDataEvolutionVectorRead.java index e9d14dceb950..47e2a1114b23 100644 --- a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/read/SparkDataEvolutionVectorRead.java +++ b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/read/SparkDataEvolutionVectorRead.java @@ -74,8 +74,7 @@ public GlobalIndexResult read(VectorScan.Plan plan) { this.planSnapshot = plan.snapshot(); List indexSplits = new ArrayList<>(); List rawSplits = new ArrayList<>(); - splitSearchSplits(plan.splits(), indexSplits, rawSplits); - demoteUncoverableIndexSplits(indexSplits, rawSplits); + prepareSplits(plan.splits(), indexSplits, rawSplits); if (indexSplits.isEmpty() && rawSplits.isEmpty()) { return GlobalIndexResult.createEmpty(); }