This is an automated email from the ASF dual-hosted git repository.

JingsongLi 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 d6662001ae [vector] Fix partition filter handling in vector search 
(#8366)
d6662001ae is described below

commit d6662001ae11f44fab17dec6a7cb2525cda1a8b9
Author: QuakeWang <[email protected]>
AuthorDate: Sat Jun 27 15:03:42 2026 +0800

    [vector] Fix partition filter handling in vector search (#8366)
    
    Fix partition filter handling in vector search builders.
    
    Previously, `withFilter` extracted partition predicates but still kept
    the full predicate in the scalar filter. For partition-only filters,
    indexed vector reads could incorrectly enter the scalar pre-filter path
    and return empty results when no scalar index files existed. In
    addition, explicit `withPartitionFilter` calls and partition predicates
    extracted from `withFilter` could overwrite each other instead of
    behaving as a stable intersection.
---
 .../table/source/BatchVectorSearchBuilderImpl.java |  39 +++++--
 .../table/source/VectorSearchBuilderImpl.java      |  39 +++++--
 .../table/source/VectorSearchBuilderTest.java      | 130 +++++++++++++++++++++
 3 files changed, 192 insertions(+), 16 deletions(-)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/table/source/BatchVectorSearchBuilderImpl.java
 
b/paimon-core/src/main/java/org/apache/paimon/table/source/BatchVectorSearchBuilderImpl.java
index 8a34691b7a..41716fc3d3 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/table/source/BatchVectorSearchBuilderImpl.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/table/source/BatchVectorSearchBuilderImpl.java
@@ -24,11 +24,15 @@ import org.apache.paimon.predicate.PredicateBuilder;
 import org.apache.paimon.table.FileStoreTable;
 import org.apache.paimon.table.InnerTable;
 import org.apache.paimon.types.DataField;
+import org.apache.paimon.utils.Pair;
 
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
-import static 
org.apache.paimon.partition.PartitionPredicate.splitPartitionPredicate;
+import static 
org.apache.paimon.partition.PartitionPredicate.splitPartitionPredicatesAndDataPredicates;
 import static org.apache.paimon.utils.Preconditions.checkArgument;
 import static org.apache.paimon.utils.Preconditions.checkNotNull;
 
@@ -52,22 +56,41 @@ public class BatchVectorSearchBuilderImpl implements 
BatchVectorSearchBuilder {
 
     @Override
     public BatchVectorSearchBuilder withPartitionFilter(PartitionPredicate 
partitionFilter) {
-        this.partitionFilter = partitionFilter;
+        addPartitionFilter(partitionFilter);
         return this;
     }
 
     @Override
     public BatchVectorSearchBuilder withFilter(Predicate predicate) {
-        if (this.filter == null) {
-            this.filter = predicate;
-        } else {
-            this.filter = PredicateBuilder.and(this.filter, predicate);
+        Pair<Optional<PartitionPredicate>, List<Predicate>> pair =
+                splitPartitionPredicatesAndDataPredicates(
+                        predicate, table.rowType(), table.partitionKeys());
+        if (pair.getLeft().isPresent()) {
+            addPartitionFilter(pair.getLeft().get());
+        }
+        if (!pair.getRight().isEmpty()) {
+            Predicate dataFilter = PredicateBuilder.and(pair.getRight());
+            if (this.filter == null) {
+                this.filter = dataFilter;
+            } else {
+                this.filter = PredicateBuilder.and(this.filter, dataFilter);
+            }
         }
-        splitPartitionPredicate(predicate, table.rowType(), 
table.partitionKeys())
-                .ifPresent(value -> this.partitionFilter = value);
         return this;
     }
 
+    private void addPartitionFilter(PartitionPredicate partitionFilter) {
+        if (partitionFilter == null) {
+            return;
+        }
+        if (this.partitionFilter == null) {
+            this.partitionFilter = partitionFilter;
+        } else {
+            this.partitionFilter =
+                    PartitionPredicate.and(Arrays.asList(this.partitionFilter, 
partitionFilter));
+        }
+    }
+
     @Override
     public BatchVectorSearchBuilder withLimit(int limit) {
         this.limit = limit;
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/table/source/VectorSearchBuilderImpl.java
 
b/paimon-core/src/main/java/org/apache/paimon/table/source/VectorSearchBuilderImpl.java
index 996f8b4f19..9b77c9c6a2 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/table/source/VectorSearchBuilderImpl.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/table/source/VectorSearchBuilderImpl.java
@@ -24,11 +24,15 @@ import org.apache.paimon.predicate.PredicateBuilder;
 import org.apache.paimon.table.FileStoreTable;
 import org.apache.paimon.table.InnerTable;
 import org.apache.paimon.types.DataField;
+import org.apache.paimon.utils.Pair;
 
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
-import static 
org.apache.paimon.partition.PartitionPredicate.splitPartitionPredicate;
+import static 
org.apache.paimon.partition.PartitionPredicate.splitPartitionPredicatesAndDataPredicates;
 import static org.apache.paimon.utils.Preconditions.checkNotNull;
 
 /** Implementation for {@link VectorSearchBuilder}. */
@@ -51,22 +55,41 @@ public class VectorSearchBuilderImpl implements 
VectorSearchBuilder {
 
     @Override
     public VectorSearchBuilder withPartitionFilter(PartitionPredicate 
partitionFilter) {
-        this.partitionFilter = partitionFilter;
+        addPartitionFilter(partitionFilter);
         return this;
     }
 
     @Override
     public VectorSearchBuilder withFilter(Predicate predicate) {
-        if (this.filter == null) {
-            this.filter = predicate;
-        } else {
-            this.filter = PredicateBuilder.and(this.filter, predicate);
+        Pair<Optional<PartitionPredicate>, List<Predicate>> pair =
+                splitPartitionPredicatesAndDataPredicates(
+                        predicate, table.rowType(), table.partitionKeys());
+        if (pair.getLeft().isPresent()) {
+            addPartitionFilter(pair.getLeft().get());
+        }
+        if (!pair.getRight().isEmpty()) {
+            Predicate dataFilter = PredicateBuilder.and(pair.getRight());
+            if (this.filter == null) {
+                this.filter = dataFilter;
+            } else {
+                this.filter = PredicateBuilder.and(this.filter, dataFilter);
+            }
         }
-        splitPartitionPredicate(predicate, table.rowType(), 
table.partitionKeys())
-                .ifPresent(value -> this.partitionFilter = value);
         return this;
     }
 
+    private void addPartitionFilter(PartitionPredicate partitionFilter) {
+        if (partitionFilter == null) {
+            return;
+        }
+        if (this.partitionFilter == null) {
+            this.partitionFilter = partitionFilter;
+        } else {
+            this.partitionFilter =
+                    PartitionPredicate.and(Arrays.asList(this.partitionFilter, 
partitionFilter));
+        }
+    }
+
     @Override
     public VectorSearchBuilder withLimit(int limit) {
         this.limit = limit;
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 b861ea29a4..b937efe223 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
@@ -679,6 +679,129 @@ public class VectorSearchBuilderTest extends 
TableTestBase {
                                 + result2.results().getIntCardinality());
     }
 
+    @Test
+    public void 
testVectorSearchPartitionFilterAndExtractedFilterAreConjunctive() throws 
Exception {
+        catalog.createTable(
+                identifier("partitioned_filter_table"),
+                withVectorSchemaOptions(
+                                Schema.newBuilder()
+                                        .column("pt", DataTypes.INT())
+                                        .column("id", DataTypes.INT())
+                                        .column(VECTOR_FIELD_NAME, new 
ArrayType(DataTypes.FLOAT()))
+                                        .partitionKeys("pt"))
+                        .build(),
+                false);
+        FileStoreTable table = 
getTable(identifier("partitioned_filter_table"));
+
+        float[][] pt1Vectors = {{1.0f, 0.0f}, {0.95f, 0.1f}};
+        float[][] pt2Vectors = {{0.0f, 1.0f}, {0.1f, 0.95f}};
+
+        writePartitionedVectors(table, 1, pt1Vectors);
+        writePartitionedVectors(table, 2, pt2Vectors);
+
+        RowType partitionType = RowType.of(DataTypes.INT());
+        InternalRowSerializer serializer = new 
InternalRowSerializer(partitionType);
+        BinaryRow partition1 = serializer.toBinaryRow(GenericRow.of(1)).copy();
+        BinaryRow partition2 = serializer.toBinaryRow(GenericRow.of(2)).copy();
+
+        buildAndCommitPartitionedIndex(table, pt1Vectors, partition1, new 
Range(0, 1));
+        buildAndCommitPartitionedIndex(table, pt2Vectors, partition2, new 
Range(2, 3));
+
+        PartitionPredicate partitionFilter =
+                PartitionPredicate.fromMultiple(
+                        partitionType, Collections.singletonList(partition1));
+        Predicate matchingPartitionFilter = new 
PredicateBuilder(table.rowType()).equal(0, 1);
+        Predicate extractedPartitionFilter = new 
PredicateBuilder(table.rowType()).equal(0, 2);
+
+        VectorScan.Plan plan =
+                table.newVectorSearchBuilder()
+                        .withPartitionFilter(partitionFilter)
+                        .withFilter(extractedPartitionFilter)
+                        .withVector(new float[] {1.0f, 0.0f})
+                        .withLimit(2)
+                        .withVectorColumn(VECTOR_FIELD_NAME)
+                        .newVectorScan()
+                        .scan();
+
+        assertThat(plan.splits()).isEmpty();
+
+        VectorScan.Plan reverseOrderPlan =
+                table.newVectorSearchBuilder()
+                        .withFilter(extractedPartitionFilter)
+                        .withPartitionFilter(partitionFilter)
+                        .withVector(new float[] {1.0f, 0.0f})
+                        .withLimit(2)
+                        .withVectorColumn(VECTOR_FIELD_NAME)
+                        .newVectorScan()
+                        .scan();
+
+        assertThat(reverseOrderPlan.splits()).isEmpty();
+
+        VectorScan.Plan batchPlan =
+                table.newBatchVectorSearchBuilder()
+                        .withPartitionFilter(partitionFilter)
+                        .withFilter(extractedPartitionFilter)
+                        .withVectors(new float[][] {new float[] {1.0f, 0.0f}})
+                        .withLimit(2)
+                        .withVectorColumn(VECTOR_FIELD_NAME)
+                        .newVectorScan()
+                        .scan();
+
+        assertThat(batchPlan.splits()).isEmpty();
+
+        VectorScan.Plan reverseOrderBatchPlan =
+                table.newBatchVectorSearchBuilder()
+                        .withFilter(extractedPartitionFilter)
+                        .withPartitionFilter(partitionFilter)
+                        .withVectors(new float[][] {new float[] {1.0f, 0.0f}})
+                        .withLimit(2)
+                        .withVectorColumn(VECTOR_FIELD_NAME)
+                        .newVectorScan()
+                        .scan();
+
+        assertThat(reverseOrderBatchPlan.splits()).isEmpty();
+
+        GlobalIndexResult matchingResult =
+                table.newVectorSearchBuilder()
+                        .withFilter(matchingPartitionFilter)
+                        .withVector(new float[] {1.0f, 0.0f})
+                        .withLimit(2)
+                        .withVectorColumn(VECTOR_FIELD_NAME)
+                        .executeLocal();
+        assertResultRowsBetween(matchingResult, 0, 1);
+
+        GlobalIndexResult matchingResultWithPartitionFilter =
+                table.newVectorSearchBuilder()
+                        .withPartitionFilter(partitionFilter)
+                        .withFilter(matchingPartitionFilter)
+                        .withVector(new float[] {1.0f, 0.0f})
+                        .withLimit(2)
+                        .withVectorColumn(VECTOR_FIELD_NAME)
+                        .executeLocal();
+        assertResultRowsBetween(matchingResultWithPartitionFilter, 0, 1);
+
+        List<GlobalIndexResult> matchingBatchResults =
+                table.newBatchVectorSearchBuilder()
+                        .withFilter(matchingPartitionFilter)
+                        .withVectors(new float[][] {new float[] {1.0f, 0.0f}})
+                        .withLimit(2)
+                        .withVectorColumn(VECTOR_FIELD_NAME)
+                        .executeBatchLocal();
+        assertThat(matchingBatchResults).hasSize(1);
+        assertResultRowsBetween(matchingBatchResults.get(0), 0, 1);
+
+        List<GlobalIndexResult> matchingBatchResultsWithPartitionFilter =
+                table.newBatchVectorSearchBuilder()
+                        .withPartitionFilter(partitionFilter)
+                        .withFilter(matchingPartitionFilter)
+                        .withVectors(new float[][] {new float[] {1.0f, 0.0f}})
+                        .withLimit(2)
+                        .withVectorColumn(VECTOR_FIELD_NAME)
+                        .executeBatchLocal();
+        assertThat(matchingBatchResultsWithPartitionFilter).hasSize(1);
+        
assertResultRowsBetween(matchingBatchResultsWithPartitionFilter.get(0), 0, 1);
+    }
+
     @Test
     public void testScanPartialRangeIntersection() throws Exception {
         catalog.createTable(
@@ -1163,6 +1286,13 @@ public class VectorSearchBuilderTest extends 
TableTestBase {
         return ids;
     }
 
+    private void assertResultRowsBetween(GlobalIndexResult result, long start, 
long end) {
+        assertThat(result.results().isEmpty()).isFalse();
+        for (long rowId : result.results()) {
+            assertThat(rowId).isBetween(start, end);
+        }
+    }
+
     private void buildAndCommitIndex(FileStoreTable table, float[][] vectors) 
throws Exception {
         buildAndCommitIndex(table, VECTOR_FIELD_NAME, vectors);
     }

Reply via email to