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 a50a36ff85 [core] Search primary-key vector indexes (#8579)
a50a36ff85 is described below

commit a50a36ff8515eb60cb48b5eb5aa896d122f11317
Author: Jingsong Lee <[email protected]>
AuthorDate: Sun Jul 12 22:00:32 2026 +0800

    [core] Search primary-key vector indexes (#8579)
    
    Adds the complete Core path for single-query vector search on
    primary-key tables. A query captures one snapshot, searches bucket-local
    ANN payloads with exact fallback for uncovered compact files, merges a
    deterministic global Top-K, and materializes the selected physical rows
    through the existing `IndexedSplit` read path.
---
 .../table/source/BatchVectorSearchBuilderImpl.java |   2 +-
 .../table/source/BucketVectorSearchSplit.java      | 110 ++++++++
 ...rReadImpl.java => DataEvolutionVectorRead.java} |   6 +-
 ...rScanImpl.java => DataEvolutionVectorScan.java} |   6 +-
 .../paimon/table/source/DataTableBatchScan.java    |  21 ++
 .../table/source/GlobalIndexSplitResult.java       |  31 +++
 .../paimon/table/source/PrimaryKeyVectorRead.java  | 278 +++++++++++++++++++++
 .../table/source/PrimaryKeyVectorResult.java       | 228 +++++++++++++++++
 .../paimon/table/source/PrimaryKeyVectorScan.java  | 232 +++++++++++++++++
 .../table/source/VectorSearchBuilderImpl.java      |  26 +-
 .../table/source/PrimaryKeyVectorReadTest.java     |  58 +++++
 .../table/source/PrimaryKeyVectorResultTest.java   |  96 +++++++
 .../table/source/PrimaryKeyVectorScanTest.java     | 228 +++++++++++++++++
 .../table/source/PrimaryKeyVectorSearchTest.java   |  97 +++++++
 .../table/source/VectorSearchBuilderTest.java      |  14 +-
 .../paimon/spark/read/SparkVectorReadImpl.java     |   8 +-
 16 files changed, 1421 insertions(+), 20 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 41716fc3d3..ab92a1e668 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
@@ -125,7 +125,7 @@ public class BatchVectorSearchBuilderImpl implements 
BatchVectorSearchBuilder {
 
     @Override
     public VectorScan newVectorScan() {
-        return new VectorScanImpl(table, partitionFilter, filter, 
vectorColumn, options);
+        return new DataEvolutionVectorScan(table, partitionFilter, filter, 
vectorColumn, options);
     }
 
     @Override
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/table/source/BucketVectorSearchSplit.java
 
b/paimon-core/src/main/java/org/apache/paimon/table/source/BucketVectorSearchSplit.java
new file mode 100644
index 0000000000..b21d1605b5
--- /dev/null
+++ 
b/paimon-core/src/main/java/org/apache/paimon/table/source/BucketVectorSearchSplit.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.table.source;
+
+import org.apache.paimon.index.IndexFileMeta;
+import org.apache.paimon.index.IndexFileMetaSerializer;
+import org.apache.paimon.io.DataInputViewStreamWrapper;
+import org.apache.paimon.io.DataOutputViewStreamWrapper;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import static org.apache.paimon.utils.Preconditions.checkArgument;
+
+/** All active data files and vector payloads for one snapshot bucket. */
+public class BucketVectorSearchSplit extends VectorSearchSplit {
+
+    private static final long serialVersionUID = 1L;
+    private static final int VERSION = 1;
+
+    private DataSplit dataSplit;
+    private transient List<IndexFileMeta> payloadFiles;
+
+    public BucketVectorSearchSplit(DataSplit dataSplit, List<IndexFileMeta> 
payloadFiles) {
+        this.dataSplit = dataSplit;
+        for (IndexFileMeta payload : payloadFiles) {
+            checkArgument(
+                    payload.globalIndexMeta() != null
+                            && payload.globalIndexMeta().sourceMeta() != null,
+                    "Primary-key vector payload %s has no source metadata.",
+                    payload.fileName());
+        }
+        this.payloadFiles = Collections.unmodifiableList(new 
ArrayList<>(payloadFiles));
+    }
+
+    public DataSplit dataSplit() {
+        return dataSplit;
+    }
+
+    public List<IndexFileMeta> payloadFiles() {
+        return payloadFiles;
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException {
+        out.defaultWriteObject();
+        out.writeInt(VERSION);
+        out.writeInt(payloadFiles.size());
+        IndexFileMetaSerializer serializer = new IndexFileMetaSerializer();
+        for (IndexFileMeta payloadFile : payloadFiles) {
+            serializer.serialize(payloadFile, new 
DataOutputViewStreamWrapper(out));
+        }
+    }
+
+    private void readObject(ObjectInputStream in) throws IOException, 
ClassNotFoundException {
+        in.defaultReadObject();
+        int version = in.readInt();
+        if (version != VERSION) {
+            throw new IOException("Unsupported BucketVectorSearchSplit 
version: " + version);
+        }
+        int payloadFileCount = in.readInt();
+        if (payloadFileCount < 0) {
+            throw new IOException("Negative primary-key vector payload file 
count.");
+        }
+        List<IndexFileMeta> payloadFiles = new ArrayList<>(payloadFileCount);
+        IndexFileMetaSerializer serializer = new IndexFileMetaSerializer();
+        for (int i = 0; i < payloadFileCount; i++) {
+            payloadFiles.add(serializer.deserialize(new 
DataInputViewStreamWrapper(in)));
+        }
+        this.payloadFiles = Collections.unmodifiableList(payloadFiles);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        BucketVectorSearchSplit that = (BucketVectorSearchSplit) o;
+        return Objects.equals(dataSplit, that.dataSplit)
+                && Objects.equals(payloadFiles, that.payloadFiles);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(dataSplit, payloadFiles);
+    }
+}
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/table/source/VectorReadImpl.java 
b/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionVectorRead.java
similarity index 96%
rename from 
paimon-core/src/main/java/org/apache/paimon/table/source/VectorReadImpl.java
rename to 
paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionVectorRead.java
index 9d8bd1541e..354bdbe15d 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/table/source/VectorReadImpl.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionVectorRead.java
@@ -40,14 +40,14 @@ import java.util.concurrent.ExecutorService;
 
 import static org.apache.paimon.CoreOptions.GLOBAL_INDEX_THREAD_NUM;
 
-/** Implementation for {@link VectorRead}. */
-public class VectorReadImpl extends AbstractVectorRead implements VectorRead {
+/** Data-evolution implementation for {@link VectorRead}. */
+public class DataEvolutionVectorRead extends AbstractVectorRead implements 
VectorRead {
 
     private static final long serialVersionUID = 1L;
 
     protected final float[] vector;
 
-    public VectorReadImpl(
+    public DataEvolutionVectorRead(
             FileStoreTable table,
             @Nullable PartitionPredicate partitionFilter,
             @Nullable Predicate filter,
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/table/source/VectorScanImpl.java 
b/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionVectorScan.java
similarity index 98%
rename from 
paimon-core/src/main/java/org/apache/paimon/table/source/VectorScanImpl.java
rename to 
paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionVectorScan.java
index 63b2dadea3..79a9fe81d0 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/table/source/VectorScanImpl.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/table/source/DataEvolutionVectorScan.java
@@ -46,8 +46,8 @@ import java.util.stream.Collectors;
 import static org.apache.paimon.predicate.PredicateVisitor.collectFieldIds;
 import static org.apache.paimon.utils.Preconditions.checkNotNull;
 
-/** Implementation for {@link VectorScan}. */
-public class VectorScanImpl implements VectorScan {
+/** Data-evolution implementation for {@link VectorScan}. */
+public class DataEvolutionVectorScan implements VectorScan {
 
     private final FileStoreTable table;
     @Nullable private final PartitionPredicate partitionFilter;
@@ -55,7 +55,7 @@ public class VectorScanImpl implements VectorScan {
     private final DataField vectorColumn;
     private final Map<String, String> options;
 
-    public VectorScanImpl(
+    public DataEvolutionVectorScan(
             FileStoreTable table,
             @Nullable PartitionPredicate partitionFilter,
             @Nullable Predicate filter,
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/table/source/DataTableBatchScan.java
 
b/paimon-core/src/main/java/org/apache/paimon/table/source/DataTableBatchScan.java
index 634c95e6ea..4ba3b19b6b 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/table/source/DataTableBatchScan.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/table/source/DataTableBatchScan.java
@@ -19,6 +19,7 @@
 package org.apache.paimon.table.source;
 
 import org.apache.paimon.CoreOptions;
+import org.apache.paimon.globalindex.GlobalIndexResult;
 import org.apache.paimon.manifest.PartitionEntry;
 import org.apache.paimon.predicate.Predicate;
 import org.apache.paimon.predicate.SortValue;
@@ -66,6 +67,7 @@ public class DataTableBatchScan extends AbstractDataTableScan 
{
 
     private final SchemaManager schemaManager;
     @Nullable private String readProtectionTagName;
+    @Nullable private GlobalIndexSplitResult globalIndexSplitResult;
 
     public DataTableBatchScan(
             TableSchema schema,
@@ -110,6 +112,17 @@ public class DataTableBatchScan extends 
AbstractDataTableScan {
 
     @Override
     protected TableScan.Plan planWithoutAuth() {
+        if (globalIndexSplitResult != null) {
+            if (!hasNext) {
+                throw new EndOfScanException();
+            }
+            hasNext = false;
+            if (globalIndexSplitResult.snapshotId() > 0) {
+                
maybeCreateReadProtectionTag(globalIndexSplitResult.snapshotId());
+            }
+            List<Split> splits = new 
ArrayList<>(globalIndexSplitResult.splits());
+            return new PlanImpl(null, globalIndexSplitResult.snapshotId(), 
splits);
+        }
         if (startingScanner == null) {
             startingScanner = createStartingScanner(false);
         }
@@ -135,6 +148,14 @@ public class DataTableBatchScan extends 
AbstractDataTableScan {
         }
     }
 
+    @Override
+    public DataTableBatchScan withGlobalIndexResult(GlobalIndexResult 
globalIndexResult) {
+        if (globalIndexResult instanceof GlobalIndexSplitResult) {
+            this.globalIndexSplitResult = (GlobalIndexSplitResult) 
globalIndexResult;
+        }
+        return this;
+    }
+
     @Override
     public List<PartitionEntry> listPartitionEntries() {
         if (startingScanner == null) {
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/table/source/GlobalIndexSplitResult.java
 
b/paimon-core/src/main/java/org/apache/paimon/table/source/GlobalIndexSplitResult.java
new file mode 100644
index 0000000000..913b3f8833
--- /dev/null
+++ 
b/paimon-core/src/main/java/org/apache/paimon/table/source/GlobalIndexSplitResult.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.table.source;
+
+import org.apache.paimon.globalindex.GlobalIndexResult;
+
+import java.util.List;
+
+/** Global index result materialized as ready-to-read splits from one table 
snapshot. */
+public interface GlobalIndexSplitResult extends GlobalIndexResult {
+
+    long snapshotId();
+
+    List<? extends Split> splits();
+}
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/table/source/PrimaryKeyVectorRead.java
 
b/paimon-core/src/main/java/org/apache/paimon/table/source/PrimaryKeyVectorRead.java
new file mode 100644
index 0000000000..68350ffb83
--- /dev/null
+++ 
b/paimon-core/src/main/java/org/apache/paimon/table/source/PrimaryKeyVectorRead.java
@@ -0,0 +1,278 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.table.source;
+
+import org.apache.paimon.KeyValueFileStore;
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.deletionvectors.DeletionVector;
+import org.apache.paimon.fs.FileIO;
+import org.apache.paimon.globalindex.GlobalIndexReadThreadPool;
+import org.apache.paimon.globalindex.GlobalIndexResult;
+import org.apache.paimon.index.IndexFileHandler;
+import org.apache.paimon.index.pkvector.PkVectorAnnSegmentSearcher;
+import org.apache.paimon.index.pkvector.PkVectorBucketIndexState;
+import org.apache.paimon.index.pkvector.PkVectorDataFileReader;
+import org.apache.paimon.index.pkvector.PkVectorSearchResult;
+import org.apache.paimon.index.pkvector.PkVectorSourcePolicy;
+import org.apache.paimon.index.pkvector.PrimaryKeyVectorBucketSearch;
+import org.apache.paimon.io.DataFileMeta;
+import org.apache.paimon.io.KeyValueFileReaderFactory;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.table.DelegatedFileStoreTable;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.VectorType;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.PriorityQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.stream.Collectors;
+
+import static org.apache.paimon.CoreOptions.GLOBAL_INDEX_THREAD_NUM;
+import static org.apache.paimon.globalindex.VectorSearchMetric.normalize;
+import static org.apache.paimon.utils.Preconditions.checkArgument;
+
+/** Executes bucket-local primary-key vector search and merges physical 
candidates globally. */
+public class PrimaryKeyVectorRead implements VectorRead {
+
+    private static final Comparator<Candidate> BEST_FIRST =
+            (left, right) -> {
+                int distance = Float.compare(left.distance, right.distance);
+                if (distance != 0) {
+                    return distance;
+                }
+                int partition = compareBytes(left.partition.toBytes(), 
right.partition.toBytes());
+                if (partition != 0) {
+                    return partition;
+                }
+                int bucket = Integer.compare(left.bucket, right.bucket);
+                if (bucket != 0) {
+                    return bucket;
+                }
+                int fileName = left.dataFileName.compareTo(right.dataFileName);
+                return fileName != 0 ? fileName : 
Long.compare(left.rowPosition, right.rowPosition);
+            };
+
+    private final FileIO fileIO;
+    private final IndexFileHandler indexFileHandler;
+    private final KeyValueFileReaderFactory.Builder readerFactoryBuilder;
+    private final DataField vectorField;
+    private final String indexType;
+    private final Options indexOptions;
+    private final ExecutorService executor;
+    private final Map<String, String> searchOptions;
+    private final float[] query;
+    private final int limit;
+    private final String metric;
+
+    public PrimaryKeyVectorRead(
+            FileStoreTable table,
+            DataField vectorField,
+            float[] query,
+            int limit,
+            Map<String, String> searchOptions) {
+        checkArgument(
+                vectorField.type() instanceof VectorType, "Vector field must 
use VECTOR type.");
+        checkArgument(
+                query.length == ((VectorType) vectorField.type()).getLength(),
+                "Query dimension %s does not match vector field dimension %s.",
+                query.length,
+                ((VectorType) vectorField.type()).getLength());
+        checkArgument(limit > 0, "Vector search limit must be positive: %s.", 
limit);
+        this.fileIO = table.fileIO();
+        this.indexFileHandler = table.store().newIndexFileHandler();
+        this.readerFactoryBuilder = 
keyValueStore(table).newReaderFactoryBuilder();
+        this.vectorField = vectorField;
+        this.indexType = 
table.coreOptions().primaryKeyVectorIndexType(vectorField.name());
+        this.indexOptions = 
table.coreOptions().primaryKeyVectorIndexOptions(vectorField.name());
+        this.executor =
+                GlobalIndexReadThreadPool.getExecutorService(
+                        
table.coreOptions().toConfiguration().get(GLOBAL_INDEX_THREAD_NUM));
+        this.searchOptions = Collections.unmodifiableMap(new 
HashMap<>(searchOptions));
+        this.query = query.clone();
+        this.limit = limit;
+        this.metric =
+                
normalize(table.coreOptions().primaryKeyVectorDistanceMetric(vectorField.name()));
+    }
+
+    private static KeyValueFileStore keyValueStore(FileStoreTable table) {
+        FileStoreTable unwrapped = table;
+        while (unwrapped instanceof DelegatedFileStoreTable) {
+            unwrapped = ((DelegatedFileStoreTable) unwrapped).wrapped();
+        }
+        checkArgument(
+                unwrapped.store() instanceof KeyValueFileStore,
+                "Primary-key vector search requires a key-value file store.");
+        return (KeyValueFileStore) unwrapped.store();
+    }
+
+    @Override
+    public GlobalIndexResult read(VectorScan.Plan plan) {
+        checkArgument(
+                plan instanceof PrimaryKeyVectorScan.Plan,
+                "Primary-key vector read requires a PrimaryKeyVectorScan 
plan.");
+        PrimaryKeyVectorScan.Plan primaryKeyPlan = (PrimaryKeyVectorScan.Plan) 
plan;
+        try {
+            List<Candidate> candidates = new ArrayList<>();
+            for (VectorSearchSplit searchSplit : primaryKeyPlan.splits()) {
+                BucketVectorSearchSplit split = (BucketVectorSearchSplit) 
searchSplit;
+                checkArgument(
+                        split.dataSplit().snapshotId() == 
primaryKeyPlan.snapshotId(),
+                        "Vector bucket split snapshot does not match its 
plan.");
+                candidates.addAll(search(split));
+            }
+            return new PrimaryKeyVectorResult(primaryKeyPlan, topK(candidates, 
limit), metric);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to search primary-key vector 
index.", e);
+        }
+    }
+
+    private List<Candidate> search(BucketVectorSearchSplit split) throws 
IOException {
+        DataSplit dataSplit = split.dataSplit();
+        List<DataFileMeta> activeFiles =
+                dataSplit.dataFiles().stream()
+                        .filter(PkVectorSourcePolicy::shouldRead)
+                        .collect(Collectors.toList());
+        PkVectorBucketIndexState state =
+                PkVectorBucketIndexState.fromActivePayloads(
+                        vectorField.id(), indexType, split.payloadFiles());
+        Map<String, DeletionVector> deletionVectors = 
deletionVectors(dataSplit);
+        PkVectorDataFileReader.Factory readerFactory =
+                new PkVectorDataFileReader.Factory(
+                        readerFactoryBuilder,
+                        dataSplit.partition(),
+                        dataSplit.bucket(),
+                        vectorField,
+                        ((VectorType) vectorField.type()).getLength());
+        PkVectorAnnSegmentSearcher annSearcher =
+                new PkVectorAnnSegmentSearcher(
+                        fileIO,
+                        indexFileHandler.pkVectorAnnSegment(
+                                dataSplit.partition(), dataSplit.bucket()),
+                        vectorField,
+                        indexOptions,
+                        metric,
+                        executor);
+        PrimaryKeyVectorBucketSearch bucketSearch =
+                new PrimaryKeyVectorBucketSearch(readerFactory, annSearcher, 
searchOptions, metric);
+        List<Candidate> candidates = new ArrayList<>();
+        for (PkVectorSearchResult result :
+                bucketSearch.search(state, activeFiles, deletionVectors, 
query, limit)) {
+            candidates.add(
+                    new Candidate(
+                            dataSplit.partition(),
+                            dataSplit.bucket(),
+                            result.dataFileName(),
+                            result.rowPosition(),
+                            result.distance()));
+        }
+        return candidates;
+    }
+
+    private Map<String, DeletionVector> deletionVectors(DataSplit split) 
throws IOException {
+        DeletionVector.Factory factory =
+                DeletionVector.factory(
+                        fileIO, split.dataFiles(), 
split.deletionFiles().orElse(null));
+        Map<String, DeletionVector> result = new HashMap<>();
+        for (DataFileMeta file : split.dataFiles()) {
+            Optional<DeletionVector> deletionVector = 
factory.create(file.fileName());
+            if (deletionVector.isPresent()) {
+                result.put(file.fileName(), deletionVector.get());
+            }
+        }
+        return result;
+    }
+
+    static List<Candidate> topK(List<Candidate> candidates, int limit) {
+        checkArgument(limit > 0, "Vector search limit must be positive: %s.", 
limit);
+        PriorityQueue<Candidate> nearest = new PriorityQueue<>(limit, 
BEST_FIRST.reversed());
+        for (Candidate candidate : candidates) {
+            if (nearest.size() < limit) {
+                nearest.add(candidate);
+            } else if (BEST_FIRST.compare(candidate, nearest.peek()) < 0) {
+                nearest.poll();
+                nearest.add(candidate);
+            }
+        }
+        List<Candidate> result = new ArrayList<>(nearest);
+        Collections.sort(result, BEST_FIRST);
+        return Collections.unmodifiableList(result);
+    }
+
+    private static int compareBytes(byte[] left, byte[] right) {
+        int count = Math.min(left.length, right.length);
+        for (int i = 0; i < count; i++) {
+            int comparison = Integer.compare(left[i] & 0xFF, right[i] & 0xFF);
+            if (comparison != 0) {
+                return comparison;
+            }
+        }
+        return Integer.compare(left.length, right.length);
+    }
+
+    /** Snapshot-scoped physical row candidate. */
+    public static class Candidate {
+
+        private final BinaryRow partition;
+        private final int bucket;
+        private final String dataFileName;
+        private final long rowPosition;
+        private final float distance;
+
+        Candidate(
+                BinaryRow partition,
+                int bucket,
+                String dataFileName,
+                long rowPosition,
+                float distance) {
+            this.partition = partition.copy();
+            this.bucket = bucket;
+            this.dataFileName = dataFileName;
+            this.rowPosition = rowPosition;
+            this.distance = distance;
+        }
+
+        public BinaryRow partition() {
+            return partition;
+        }
+
+        public int bucket() {
+            return bucket;
+        }
+
+        public String dataFileName() {
+            return dataFileName;
+        }
+
+        public long rowPosition() {
+            return rowPosition;
+        }
+
+        public float distance() {
+            return distance;
+        }
+    }
+}
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/table/source/PrimaryKeyVectorResult.java
 
b/paimon-core/src/main/java/org/apache/paimon/table/source/PrimaryKeyVectorResult.java
new file mode 100644
index 0000000000..3eba24b30c
--- /dev/null
+++ 
b/paimon-core/src/main/java/org/apache/paimon/table/source/PrimaryKeyVectorResult.java
@@ -0,0 +1,228 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.table.source;
+
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.globalindex.IndexedSplit;
+import org.apache.paimon.io.DataFileMeta;
+import org.apache.paimon.utils.Range;
+import org.apache.paimon.utils.RoaringNavigableMap64;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.TreeMap;
+
+import static org.apache.paimon.globalindex.VectorSearchMetric.normalize;
+import static org.apache.paimon.utils.Preconditions.checkArgument;
+
+/** Snapshot-scoped vector result addressed by physical primary-key table row 
positions. */
+public class PrimaryKeyVectorResult implements GlobalIndexSplitResult {
+
+    private final PrimaryKeyVectorScan.Plan plan;
+    private final List<PrimaryKeyVectorRead.Candidate> candidates;
+    private final String metric;
+
+    PrimaryKeyVectorResult(
+            PrimaryKeyVectorScan.Plan plan,
+            List<PrimaryKeyVectorRead.Candidate> candidates,
+            String metric) {
+        this.plan = plan;
+        this.candidates = Collections.unmodifiableList(new 
ArrayList<>(candidates));
+        this.metric = normalize(metric);
+    }
+
+    @Override
+    public long snapshotId() {
+        return plan.snapshotId();
+    }
+
+    @Override
+    public List<IndexedSplit> splits() {
+        Map<FileKey, BucketVectorSearchSplit> sourceSplits = sourceSplits();
+        Map<FileKey, TreeMap<Integer, Float>> selectedByFile = new 
LinkedHashMap<>();
+        for (PrimaryKeyVectorRead.Candidate candidate : candidates) {
+            FileKey key =
+                    new FileKey(
+                            candidate.partition(), candidate.bucket(), 
candidate.dataFileName());
+            BucketVectorSearchSplit bucketSplit = sourceSplits.get(key);
+            checkArgument(
+                    bucketSplit != null,
+                    "Primary-key vector candidate references unknown data file 
%s in bucket %s.",
+                    candidate.dataFileName(),
+                    candidate.bucket());
+            DataFileMeta dataFile = dataFile(bucketSplit.dataSplit(), 
candidate.dataFileName());
+            checkArgument(
+                    candidate.rowPosition() >= 0
+                            && candidate.rowPosition() < dataFile.rowCount()
+                            && candidate.rowPosition() <= Integer.MAX_VALUE,
+                    "Primary-key vector row position %s is outside data file 
%s.",
+                    candidate.rowPosition(),
+                    candidate.dataFileName());
+            TreeMap<Integer, Float> selected =
+                    selectedByFile.computeIfAbsent(key, ignored -> new 
TreeMap<>());
+            checkArgument(
+                    selected.put((int) candidate.rowPosition(), 
score(candidate.distance()))
+                            == null,
+                    "Primary-key vector candidate contains duplicate row 
position %s for data file %s.",
+                    candidate.rowPosition(),
+                    candidate.dataFileName());
+        }
+
+        List<IndexedSplit> result = new ArrayList<>(selectedByFile.size());
+        for (Map.Entry<FileKey, TreeMap<Integer, Float>> entry : 
selectedByFile.entrySet()) {
+            BucketVectorSearchSplit bucketSplit = 
sourceSplits.get(entry.getKey());
+            DataSplit source = bucketSplit.dataSplit();
+            int fileIndex = fileIndex(source, entry.getKey().dataFileName);
+            DataSplit.Builder builder =
+                    DataSplit.builder()
+                            .withSnapshot(source.snapshotId())
+                            .withPartition(source.partition())
+                            .withBucket(source.bucket())
+                            .withBucketPath(source.bucketPath())
+                            .withTotalBuckets(source.totalBuckets())
+                            .withDataFiles(
+                                    
Collections.singletonList(source.dataFiles().get(fileIndex)))
+                            .isStreaming(false)
+                            .rawConvertible(false);
+            if (source.deletionFiles().isPresent()) {
+                builder.withDataDeletionFiles(
+                        
Collections.singletonList(source.deletionFiles().get().get(fileIndex)));
+            }
+            result.add(
+                    new IndexedSplit(
+                            builder.build(), ranges(entry.getValue()), 
scores(entry.getValue())));
+        }
+        return Collections.unmodifiableList(result);
+    }
+
+    @Override
+    public RoaringNavigableMap64 results() {
+        throw new UnsupportedOperationException(
+                "Primary-key vector results use physical file positions, not 
global row ids.");
+    }
+
+    private Map<FileKey, BucketVectorSearchSplit> sourceSplits() {
+        Map<FileKey, BucketVectorSearchSplit> result = new HashMap<>();
+        for (VectorSearchSplit searchSplit : plan.splits()) {
+            BucketVectorSearchSplit bucketSplit = (BucketVectorSearchSplit) 
searchSplit;
+            for (DataFileMeta dataFile : bucketSplit.dataSplit().dataFiles()) {
+                FileKey key =
+                        new FileKey(
+                                bucketSplit.dataSplit().partition(),
+                                bucketSplit.dataSplit().bucket(),
+                                dataFile.fileName());
+                checkArgument(
+                        result.put(key, bucketSplit) == null,
+                        "Data file %s appears more than once in primary-key 
vector plan.",
+                        dataFile.fileName());
+            }
+        }
+        return result;
+    }
+
+    private static List<Range> ranges(TreeMap<Integer, Float> selected) {
+        List<Range> result = new ArrayList<>();
+        long from = -1;
+        long to = -1;
+        for (int position : selected.keySet()) {
+            if (from < 0) {
+                from = position;
+            } else if (position != to + 1) {
+                result.add(new Range(from, to));
+                from = position;
+            }
+            to = position;
+        }
+        if (from >= 0) {
+            result.add(new Range(from, to));
+        }
+        return result;
+    }
+
+    private static float[] scores(TreeMap<Integer, Float> selected) {
+        float[] result = new float[selected.size()];
+        int index = 0;
+        for (float score : selected.values()) {
+            result[index++] = score;
+        }
+        return result;
+    }
+
+    private static DataFileMeta dataFile(DataSplit split, String dataFileName) 
{
+        return split.dataFiles().get(fileIndex(split, dataFileName));
+    }
+
+    private static int fileIndex(DataSplit split, String dataFileName) {
+        for (int i = 0; i < split.dataFiles().size(); i++) {
+            if (dataFileName.equals(split.dataFiles().get(i).fileName())) {
+                return i;
+            }
+        }
+        throw new IllegalArgumentException(
+                "Data file " + dataFileName + " does not exist in vector 
bucket split.");
+    }
+
+    private float score(float distance) {
+        if ("l2".equals(metric)) {
+            return 1F / (1F + distance);
+        } else if ("cosine".equals(metric)) {
+            return 1F - distance;
+        } else if ("inner_product".equals(metric)) {
+            return -distance;
+        }
+        throw new IllegalArgumentException("Unsupported primary-key vector 
metric: " + metric);
+    }
+
+    private static class FileKey {
+
+        private final BinaryRow partition;
+        private final int bucket;
+        private final String dataFileName;
+
+        private FileKey(BinaryRow partition, int bucket, String dataFileName) {
+            this.partition = partition.copy();
+            this.bucket = bucket;
+            this.dataFileName = dataFileName;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            FileKey fileKey = (FileKey) o;
+            return bucket == fileKey.bucket
+                    && partition.equals(fileKey.partition)
+                    && dataFileName.equals(fileKey.dataFileName);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(partition, bucket, dataFileName);
+        }
+    }
+}
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/table/source/PrimaryKeyVectorScan.java
 
b/paimon-core/src/main/java/org/apache/paimon/table/source/PrimaryKeyVectorScan.java
new file mode 100644
index 0000000000..bb88799617
--- /dev/null
+++ 
b/paimon-core/src/main/java/org/apache/paimon/table/source/PrimaryKeyVectorScan.java
@@ -0,0 +1,232 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.table.source;
+
+import org.apache.paimon.Snapshot;
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.index.IndexFileHandler;
+import org.apache.paimon.index.IndexFileMeta;
+import org.apache.paimon.io.DataFileMeta;
+import org.apache.paimon.manifest.FileKind;
+import org.apache.paimon.manifest.IndexManifestEntry;
+import org.apache.paimon.partition.PartitionPredicate;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.source.snapshot.SnapshotReader;
+import org.apache.paimon.table.source.snapshot.TimeTravelUtil;
+import org.apache.paimon.utils.Pair;
+
+import javax.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.apache.paimon.utils.Preconditions.checkArgument;
+
+/** Plans complete bucket-local vector inputs from one captured primary-key 
table snapshot. */
+public class PrimaryKeyVectorScan implements VectorScan {
+
+    private final FileStoreTable table;
+    private final int vectorFieldId;
+    private final String indexType;
+    @Nullable private final PartitionPredicate partitionFilter;
+
+    public PrimaryKeyVectorScan(
+            FileStoreTable table,
+            int vectorFieldId,
+            String indexType,
+            @Nullable PartitionPredicate partitionFilter) {
+        this.table = table;
+        this.vectorFieldId = vectorFieldId;
+        this.indexType = indexType;
+        this.partitionFilter = partitionFilter;
+    }
+
+    @Override
+    public Plan scan() {
+        checkArgument(
+                table.coreOptions().primaryKeyVectorIndexEnabled(),
+                "Primary-key vector search requires a configured primary-key 
vector index.");
+        @Nullable Snapshot snapshot = TimeTravelUtil.tryTravelOrLatest(table);
+        if (snapshot == null) {
+            return new Plan(0, Collections.emptyList());
+        }
+
+        SnapshotReader snapshotReader =
+                
table.newSnapshotReader().withSnapshot(snapshot).withMode(ScanMode.ALL).keepStats();
+        if (partitionFilter != null) {
+            snapshotReader.withPartitionFilter(partitionFilter);
+        }
+        List<DataSplit> dataSplits = snapshotReader.read().dataSplits();
+
+        IndexFileHandler indexFileHandler = 
table.store().newIndexFileHandler();
+        List<IndexManifestEntry> vectorIndexEntries =
+                indexFileHandler.scan(
+                        snapshot,
+                        entry ->
+                                entry.indexFile().indexType().equals(indexType)
+                                        && entry.indexFile().globalIndexMeta() 
!= null
+                                        && 
entry.indexFile().globalIndexMeta().sourceMeta() != null
+                                        && 
entry.indexFile().globalIndexMeta().indexFieldId()
+                                                == vectorFieldId
+                                        && (partitionFilter == null
+                                                || 
partitionFilter.test(entry.partition())));
+        return plan(snapshot.id(), dataSplits, vectorIndexEntries);
+    }
+
+    static Plan plan(
+            long snapshotId,
+            List<DataSplit> dataSplits,
+            List<IndexManifestEntry> vectorIndexEntries) {
+        Map<Pair<BinaryRow, Integer>, List<IndexFileMeta>> payloads = new 
LinkedHashMap<>();
+        for (IndexManifestEntry entry : vectorIndexEntries) {
+            checkArgument(
+                    entry.kind() == FileKind.ADD,
+                    "Primary-key vector index file %s is not active.",
+                    entry.indexFile().fileName());
+            checkArgument(
+                    entry.indexFile().globalIndexMeta() != null
+                            && 
entry.indexFile().globalIndexMeta().sourceMeta() != null,
+                    "Primary-key vector index file %s has no source metadata.",
+                    entry.indexFile().fileName());
+            Pair<BinaryRow, Integer> key = Pair.of(entry.partition(), 
entry.bucket());
+            payloads.computeIfAbsent(key, ignored -> new 
ArrayList<>()).add(entry.indexFile());
+        }
+
+        Map<Pair<BinaryRow, Integer>, BucketAccumulator> buckets = new 
LinkedHashMap<>();
+        for (DataSplit split : dataSplits) {
+            checkArgument(
+                    split.snapshotId() == snapshotId,
+                    "Data split snapshot %s does not match vector scan 
snapshot %s.",
+                    split.snapshotId(),
+                    snapshotId);
+            checkArgument(
+                    !split.isStreaming(), "Primary-key vector search requires 
a batch split.");
+            Pair<BinaryRow, Integer> key = Pair.of(split.partition(), 
split.bucket());
+            BucketAccumulator accumulator = buckets.get(key);
+            if (accumulator == null) {
+                accumulator = new BucketAccumulator(split);
+                buckets.put(key, accumulator);
+            }
+            accumulator.add(split);
+        }
+
+        List<BucketVectorSearchSplit> result = new ArrayList<>(buckets.size());
+        for (Map.Entry<Pair<BinaryRow, Integer>, BucketAccumulator> entry : 
buckets.entrySet()) {
+            result.add(
+                    new BucketVectorSearchSplit(
+                            entry.getValue().build(),
+                            payloads.getOrDefault(entry.getKey(), 
Collections.emptyList())));
+        }
+        return new Plan(snapshotId, result);
+    }
+
+    /** Immutable snapshot vector-search plan. */
+    public static class Plan implements VectorScan.Plan {
+
+        private final long snapshotId;
+        private final List<VectorSearchSplit> splits;
+
+        private Plan(long snapshotId, List<BucketVectorSearchSplit> splits) {
+            this.snapshotId = snapshotId;
+            this.splits = Collections.unmodifiableList(new 
ArrayList<VectorSearchSplit>(splits));
+        }
+
+        public long snapshotId() {
+            return snapshotId;
+        }
+
+        @Override
+        public List<VectorSearchSplit> splits() {
+            return splits;
+        }
+    }
+
+    private static class BucketAccumulator {
+
+        private final long snapshotId;
+        private final BinaryRow partition;
+        private final int bucket;
+        private final String bucketPath;
+        @Nullable private final Integer totalBuckets;
+        private final List<DataFileMeta> dataFiles = new ArrayList<>();
+        private final List<DeletionFile> deletionFiles = new ArrayList<>();
+        private final Set<String> dataFileNames = new HashSet<>();
+        private boolean hasDeletionFile;
+
+        private BucketAccumulator(DataSplit split) {
+            this.snapshotId = split.snapshotId();
+            this.partition = split.partition();
+            this.bucket = split.bucket();
+            this.bucketPath = split.bucketPath();
+            this.totalBuckets = split.totalBuckets();
+        }
+
+        private void add(DataSplit split) {
+            checkArgument(
+                    snapshotId == split.snapshotId()
+                            && partition.equals(split.partition())
+                            && bucket == split.bucket()
+                            && bucketPath.equals(split.bucketPath()),
+                    "Cannot combine data splits from different snapshot 
buckets.");
+            checkArgument(
+                    totalBuckets == null
+                            ? split.totalBuckets() == null
+                            : totalBuckets.equals(split.totalBuckets()),
+                    "Bucket split total-bucket metadata is inconsistent.");
+
+            List<DeletionFile> splitDeletions = 
split.deletionFiles().orElse(null);
+            checkArgument(
+                    splitDeletions == null || splitDeletions.size() == 
split.dataFiles().size(),
+                    "Deletion files must align with data files in a bucket 
split.");
+            for (int i = 0; i < split.dataFiles().size(); i++) {
+                DataFileMeta file = split.dataFiles().get(i);
+                checkArgument(
+                        dataFileNames.add(file.fileName()),
+                        "Data file %s appears more than once in vector bucket 
planning.",
+                        file.fileName());
+                dataFiles.add(file);
+                DeletionFile deletion = splitDeletions == null ? null : 
splitDeletions.get(i);
+                deletionFiles.add(deletion);
+                hasDeletionFile |= deletion != null;
+            }
+        }
+
+        private DataSplit build() {
+            DataSplit.Builder builder =
+                    DataSplit.builder()
+                            .withSnapshot(snapshotId)
+                            .withPartition(partition)
+                            .withBucket(bucket)
+                            .withBucketPath(bucketPath)
+                            .withTotalBuckets(totalBuckets)
+                            .withDataFiles(dataFiles)
+                            .isStreaming(false)
+                            .rawConvertible(false);
+            if (hasDeletionFile) {
+                builder.withDataDeletionFiles(deletionFiles);
+            }
+            return builder.build();
+        }
+    }
+}
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 9b77c9c6a2..f2bcb7af65 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
@@ -33,6 +33,7 @@ import java.util.Map;
 import java.util.Optional;
 
 import static 
org.apache.paimon.partition.PartitionPredicate.splitPartitionPredicatesAndDataPredicates;
+import static org.apache.paimon.utils.Preconditions.checkArgument;
 import static org.apache.paimon.utils.Preconditions.checkNotNull;
 
 /** Implementation for {@link VectorSearchBuilder}. */
@@ -124,13 +125,34 @@ public class VectorSearchBuilderImpl implements 
VectorSearchBuilder {
 
     @Override
     public VectorScan newVectorScan() {
-        return new VectorScanImpl(table, partitionFilter, filter, 
vectorColumn, options);
+        if (isPrimaryKeyVectorSearch()) {
+            checkArgument(
+                    filter == null,
+                    "Primary-key vector search does not support non-partition 
filters.");
+            return new PrimaryKeyVectorScan(
+                    table,
+                    vectorColumn.id(),
+                    
table.coreOptions().primaryKeyVectorIndexType(vectorColumn.name()),
+                    partitionFilter);
+        }
+        return new DataEvolutionVectorScan(table, partitionFilter, filter, 
vectorColumn, options);
     }
 
     @Override
     public VectorRead newVectorRead() {
         checkNotNull(vector, "vector must be set via withVector()");
-        return new VectorReadImpl(
+        if (isPrimaryKeyVectorSearch()) {
+            checkArgument(
+                    filter == null,
+                    "Primary-key vector search does not support non-partition 
filters.");
+            return new PrimaryKeyVectorRead(table, vectorColumn, vector, 
limit, options);
+        }
+        return new DataEvolutionVectorRead(
                 table, partitionFilter, filter, limit, vectorColumn, vector, 
options);
     }
+
+    private boolean isPrimaryKeyVectorSearch() {
+        return vectorColumn != null
+                && 
table.coreOptions().primaryKeyVectorIndexColumns().contains(vectorColumn.name());
+    }
 }
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/table/source/PrimaryKeyVectorReadTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/table/source/PrimaryKeyVectorReadTest.java
new file mode 100644
index 0000000000..cf7b447732
--- /dev/null
+++ 
b/paimon-core/src/test/java/org/apache/paimon/table/source/PrimaryKeyVectorReadTest.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.table.source;
+
+import org.apache.paimon.data.BinaryRow;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests global candidate merging for primary-key vector search. */
+class PrimaryKeyVectorReadTest {
+
+    @Test
+    void testMergesGlobalTopKWithDeterministicTies() {
+        List<PrimaryKeyVectorRead.Candidate> candidates =
+                Arrays.asList(
+                        candidate(1, "file-c", 0, 2F),
+                        candidate(1, "file-b", 1, 1F),
+                        candidate(0, "file-a", 2, 1F));
+
+        List<PrimaryKeyVectorRead.Candidate> result = 
PrimaryKeyVectorRead.topK(candidates, 2);
+
+        assertThat(result)
+                .extracting(
+                        PrimaryKeyVectorRead.Candidate::bucket,
+                        PrimaryKeyVectorRead.Candidate::dataFileName,
+                        PrimaryKeyVectorRead.Candidate::rowPosition)
+                .containsExactly(
+                        org.assertj.core.groups.Tuple.tuple(0, "file-a", 2L),
+                        org.assertj.core.groups.Tuple.tuple(1, "file-b", 1L));
+    }
+
+    private static PrimaryKeyVectorRead.Candidate candidate(
+            int bucket, String fileName, long position, float distance) {
+        return new PrimaryKeyVectorRead.Candidate(
+                BinaryRow.EMPTY_ROW, bucket, fileName, position, distance);
+    }
+}
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/table/source/PrimaryKeyVectorResultTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/table/source/PrimaryKeyVectorResultTest.java
new file mode 100644
index 0000000000..849fd76ba7
--- /dev/null
+++ 
b/paimon-core/src/test/java/org/apache/paimon/table/source/PrimaryKeyVectorResultTest.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.table.source;
+
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.globalindex.IndexedSplit;
+import org.apache.paimon.io.DataFileMeta;
+import org.apache.paimon.manifest.FileSource;
+import org.apache.paimon.stats.SimpleStats;
+import org.apache.paimon.utils.Range;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests conversion from primary-key vector candidates to {@link 
IndexedSplit}s. */
+class PrimaryKeyVectorResultTest {
+
+    @Test
+    void testGroupsCandidatesByFileAndAlignsScoresWithRanges() {
+        DataFileMeta dataFile = dataFile("data-1");
+        DeletionFile deletionFile = new DeletionFile("dv", 10, 20, 1L);
+        DataSplit dataSplit = dataSplit(dataFile, deletionFile);
+        PrimaryKeyVectorScan.Plan plan =
+                PrimaryKeyVectorScan.plan(
+                        11, Collections.singletonList(dataSplit), 
Collections.emptyList());
+        List<PrimaryKeyVectorRead.Candidate> candidates =
+                Arrays.asList(
+                        new PrimaryKeyVectorRead.Candidate(
+                                BinaryRow.EMPTY_ROW, 0, "data-1", 4, 0.1F),
+                        new 
PrimaryKeyVectorRead.Candidate(BinaryRow.EMPTY_ROW, 0, "data-1", 1, 1F),
+                        new PrimaryKeyVectorRead.Candidate(
+                                BinaryRow.EMPTY_ROW, 0, "data-1", 2, 4F));
+
+        List<IndexedSplit> splits = new PrimaryKeyVectorResult(plan, 
candidates, "l2").splits();
+
+        assertThat(splits).hasSize(1);
+        IndexedSplit split = splits.get(0);
+        assertThat(split.dataSplit().snapshotId()).isEqualTo(11);
+        assertThat(split.dataSplit().dataFiles()).containsExactly(dataFile);
+        
assertThat(split.dataSplit().deletionFiles().get()).containsExactly(deletionFile);
+        assertThat(split.rowRanges()).containsExactly(new Range(1, 2), new 
Range(4, 4));
+        assertThat(split.scores())
+                .containsExactly(1F / (1F + 1F), 1F / (1F + 4F), 1F / (1F + 
0.1F));
+    }
+
+    private static DataSplit dataSplit(DataFileMeta dataFile, DeletionFile 
deletionFile) {
+        return DataSplit.builder()
+                .withSnapshot(11)
+                .withPartition(BinaryRow.EMPTY_ROW)
+                .withBucket(0)
+                .withBucketPath("bucket-0")
+                .withTotalBuckets(1)
+                .withDataFiles(Collections.singletonList(dataFile))
+                .withDataDeletionFiles(Collections.singletonList(deletionFile))
+                .build();
+    }
+
+    private static DataFileMeta dataFile(String fileName) {
+        return DataFileMeta.forAppend(
+                fileName,
+                100,
+                5,
+                SimpleStats.EMPTY_STATS,
+                0,
+                1,
+                1,
+                Collections.emptyList(),
+                null,
+                FileSource.COMPACT,
+                null,
+                null,
+                null,
+                null);
+    }
+}
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/table/source/PrimaryKeyVectorScanTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/table/source/PrimaryKeyVectorScanTest.java
new file mode 100644
index 0000000000..d3db372da3
--- /dev/null
+++ 
b/paimon-core/src/test/java/org/apache/paimon/table/source/PrimaryKeyVectorScanTest.java
@@ -0,0 +1,228 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.table.source;
+
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.FileStore;
+import org.apache.paimon.Snapshot;
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.index.GlobalIndexMeta;
+import org.apache.paimon.index.IndexFileHandler;
+import org.apache.paimon.index.IndexFileMeta;
+import org.apache.paimon.index.pkvector.PkVectorSourceFile;
+import org.apache.paimon.index.pkvector.PkVectorSourceMeta;
+import org.apache.paimon.io.DataFileMeta;
+import org.apache.paimon.manifest.FileKind;
+import org.apache.paimon.manifest.FileSource;
+import org.apache.paimon.manifest.IndexManifestEntry;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.stats.SimpleStats;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.source.snapshot.SnapshotReader;
+import org.apache.paimon.utils.Filter;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Answers.CALLS_REAL_METHODS;
+import static org.mockito.Answers.RETURNS_SELF;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/** Tests snapshot-consistent planning for bucket-local primary-key vector 
search. */
+class PrimaryKeyVectorScanTest {
+
+    @Test
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    void testScansOneSnapshotAndFiltersVectorIdentity() {
+        CoreOptions coreOptions = coreOptions();
+        FileStoreTable table = mock(FileStoreTable.class);
+        Snapshot snapshot = mock(Snapshot.class);
+        when(snapshot.id()).thenReturn(11L);
+        when(table.coreOptions()).thenReturn(coreOptions);
+        when(table.latestSnapshot()).thenReturn(Optional.of(snapshot));
+
+        SnapshotReader snapshotReader = mock(SnapshotReader.class, 
RETURNS_SELF);
+        SnapshotReader.Plan snapshotPlan = mock(SnapshotReader.Plan.class, 
CALLS_REAL_METHODS);
+        when(snapshotPlan.splits())
+                
.thenReturn(Collections.singletonList(dataSplit(dataFile("data-1"))));
+        when(snapshotReader.read()).thenReturn(snapshotPlan);
+        when(table.newSnapshotReader()).thenReturn(snapshotReader);
+
+        List<IndexManifestEntry> entries =
+                Arrays.asList(
+                        payloadEntry("ivf-pq", 7, "ann-match"),
+                        payloadEntry("ivf-pq", 8, "ann-other-field"),
+                        payloadEntry("hnsw", 7, "ann-other-type"));
+        IndexFileHandler indexFileHandler = mock(IndexFileHandler.class);
+        when(indexFileHandler.scan(eq(snapshot), any(Filter.class)))
+                .thenAnswer(
+                        invocation -> {
+                            Filter<IndexManifestEntry> filter = 
invocation.getArgument(1);
+                            List<IndexManifestEntry> filtered = new 
ArrayList<>();
+                            for (IndexManifestEntry entry : entries) {
+                                if (filter.test(entry)) {
+                                    filtered.add(entry);
+                                }
+                            }
+                            return filtered;
+                        });
+        FileStore store = mock(FileStore.class);
+        when(store.newIndexFileHandler()).thenReturn(indexFileHandler);
+        when(table.store()).thenReturn(store);
+
+        PrimaryKeyVectorScan.Plan plan = new PrimaryKeyVectorScan(table, 7, 
"ivf-pq", null).scan();
+
+        assertThat(plan.snapshotId()).isEqualTo(11);
+        assertThat(plan.splits()).hasSize(1);
+        BucketVectorSearchSplit bucketSplit = (BucketVectorSearchSplit) 
plan.splits().get(0);
+        assertThat(bucketSplit.payloadFiles())
+                .extracting(IndexFileMeta::fileName)
+                .containsExactly("ann-match");
+    }
+
+    @Test
+    void testMergesDataSplitsForCompleteBucketCoverage() {
+        DataFileMeta data1 = dataFile("data-1");
+        DataFileMeta data2 = dataFile("data-2");
+        DeletionFile deletion = new DeletionFile("dv", 10, 20, 1L);
+        DataSplit split1 = dataSplit(data1, 
Collections.singletonList(deletion));
+        DataSplit split2 = dataSplit(data2, null);
+
+        PrimaryKeyVectorScan.Plan plan =
+                PrimaryKeyVectorScan.plan(
+                        11,
+                        Arrays.asList(split1, split2),
+                        Collections.singletonList(payloadEntry()));
+
+        assertThat(plan.snapshotId()).isEqualTo(11);
+        assertThat(plan.splits()).hasSize(1);
+        BucketVectorSearchSplit bucketSplit = (BucketVectorSearchSplit) 
plan.splits().get(0);
+        assertThat(bucketSplit.dataSplit().dataFiles()).containsExactly(data1, 
data2);
+        assertThat(bucketSplit.dataSplit().deletionFiles()).isPresent();
+        
assertThat(bucketSplit.dataSplit().deletionFiles().get()).containsExactly(deletion,
 null);
+        assertThat(bucketSplit.payloadFiles()).containsExactly(payloadFile());
+    }
+
+    @Test
+    void testBucketSplitSerialization() throws Exception {
+        IndexFileMeta payload = payloadFile();
+        BucketVectorSearchSplit split =
+                new BucketVectorSearchSplit(
+                        dataSplit(dataFile("data-1")), 
Collections.singletonList(payload));
+
+        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+        try (ObjectOutputStream output = new ObjectOutputStream(bytes)) {
+            output.writeObject(split);
+        }
+        BucketVectorSearchSplit restored;
+        try (ObjectInputStream input =
+                new ObjectInputStream(new 
ByteArrayInputStream(bytes.toByteArray()))) {
+            restored = (BucketVectorSearchSplit) input.readObject();
+        }
+
+        assertThat(restored).isEqualTo(split);
+        
assertThat(restored.payloadFiles().get(0).globalIndexMeta().sourceMeta())
+                .isEqualTo(payload.globalIndexMeta().sourceMeta());
+    }
+
+    private static DataSplit dataSplit(DataFileMeta dataFile) {
+        return dataSplit(dataFile, null);
+    }
+
+    private static DataSplit dataSplit(
+            DataFileMeta dataFile, java.util.List<DeletionFile> deletionFiles) 
{
+        DataSplit.Builder builder =
+                DataSplit.builder()
+                        .withSnapshot(11)
+                        .withPartition(BinaryRow.EMPTY_ROW)
+                        .withBucket(0)
+                        .withBucketPath("bucket-0")
+                        .withTotalBuckets(1)
+                        .withDataFiles(Collections.singletonList(dataFile));
+        if (deletionFiles != null) {
+            builder.withDataDeletionFiles(deletionFiles);
+        }
+        return builder.build();
+    }
+
+    private static IndexManifestEntry payloadEntry() {
+        return new IndexManifestEntry(FileKind.ADD, BinaryRow.EMPTY_ROW, 0, 
payloadFile());
+    }
+
+    private static IndexManifestEntry payloadEntry(String indexType, int 
fieldId, String fileName) {
+        return new IndexManifestEntry(
+                FileKind.ADD, BinaryRow.EMPTY_ROW, 0, payloadFile(indexType, 
fieldId, fileName));
+    }
+
+    private static IndexFileMeta payloadFile() {
+        return payloadFile("ivf-pq", 7, "ann");
+    }
+
+    private static IndexFileMeta payloadFile(String indexType, int fieldId, 
String fileName) {
+        byte[] sourceMeta =
+                new PkVectorSourceMeta(
+                                Collections.singletonList(new 
PkVectorSourceFile("data-1", 2)))
+                        .serialize();
+        return new IndexFileMeta(
+                indexType,
+                fileName,
+                100,
+                2,
+                new GlobalIndexMeta(0, 1, fieldId, null, null, sourceMeta),
+                null);
+    }
+
+    private static CoreOptions coreOptions() {
+        Options options = new Options();
+        options.set(CoreOptions.PK_VECTOR_INDEX_COLUMNS, "embedding");
+        options.setString("fields.embedding.pk-vector.index.type", "ivf-pq");
+        return new CoreOptions(options);
+    }
+
+    private static DataFileMeta dataFile(String fileName) {
+        return DataFileMeta.forAppend(
+                fileName,
+                100,
+                2,
+                SimpleStats.EMPTY_STATS,
+                0,
+                0,
+                1,
+                Collections.emptyList(),
+                null,
+                FileSource.COMPACT,
+                null,
+                null,
+                null,
+                null);
+    }
+}
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/table/source/PrimaryKeyVectorSearchTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/table/source/PrimaryKeyVectorSearchTest.java
new file mode 100644
index 0000000000..090488de9e
--- /dev/null
+++ 
b/paimon-core/src/test/java/org/apache/paimon/table/source/PrimaryKeyVectorSearchTest.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.table.source;
+
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.data.BinaryVector;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.globalindex.GlobalIndexResult;
+import org.apache.paimon.globalindex.IndexedSplit;
+import org.apache.paimon.globalindex.testvector.TestVectorGlobalIndexerFactory;
+import org.apache.paimon.reader.RecordReader;
+import org.apache.paimon.schema.Schema;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.TableTestBase;
+import org.apache.paimon.table.sink.BatchTableCommit;
+import org.apache.paimon.table.sink.BatchTableWrite;
+import org.apache.paimon.table.sink.BatchWriteBuilder;
+import org.apache.paimon.types.DataTypes;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** End-to-end tests for bucket-local primary-key vector search. */
+class PrimaryKeyVectorSearchTest extends TableTestBase {
+
+    @Override
+    protected Schema schemaDefault() {
+        return Schema.newBuilder()
+                .column("id", DataTypes.INT())
+                .column("embedding", DataTypes.VECTOR(2, DataTypes.FLOAT()))
+                .primaryKey("id")
+                .option(CoreOptions.BUCKET.key(), "1")
+                .option(CoreOptions.DELETION_VECTORS_ENABLED.key(), "true")
+                .option(CoreOptions.PK_VECTOR_INDEX_COLUMNS.key(), "embedding")
+                .option(
+                        "fields.embedding.pk-vector.index.type",
+                        TestVectorGlobalIndexerFactory.IDENTIFIER)
+                .option("fields.embedding.pk-vector.distance.metric", "l2")
+                .option("test.vector.dimension", "2")
+                .option("test.vector.metric", "l2")
+                .build();
+    }
+
+    @Test
+    void testVectorSearchMaterializesPhysicalRows() throws Exception {
+        createTableDefault();
+        FileStoreTable table = getTableDefault();
+        BatchWriteBuilder writeBuilder = table.newBatchWriteBuilder();
+        try (BatchTableWrite write = writeBuilder.newWrite();
+                BatchTableCommit commit = writeBuilder.newCommit()) {
+            write.withIOManager(ioManager);
+            write.write(GenericRow.of(1, BinaryVector.fromPrimitiveArray(new 
float[] {3, 0})));
+            write.write(GenericRow.of(2, BinaryVector.fromPrimitiveArray(new 
float[] {1, 0})));
+            write.write(GenericRow.of(3, BinaryVector.fromPrimitiveArray(new 
float[] {2, 0})));
+            commit.commit(write.prepareCommit());
+        }
+
+        GlobalIndexResult result =
+                table.newVectorSearchBuilder()
+                        .withVectorColumn("embedding")
+                        .withVector(new float[] {0, 0})
+                        .withLimit(2)
+                        .executeLocal();
+        assertThat(result).isInstanceOf(GlobalIndexSplitResult.class);
+
+        ReadBuilder readBuilder = table.newReadBuilder();
+        TableScan.Plan plan = 
readBuilder.newScan().withGlobalIndexResult(result).plan();
+        assertThat(plan.splits()).allMatch(IndexedSplit.class::isInstance);
+        List<Integer> ids = new ArrayList<>();
+        try (RecordReader<InternalRow> reader = 
readBuilder.newRead().createReader(plan)) {
+            reader.forEachRemaining(row -> ids.add(row.getInt(0)));
+        }
+
+        assertThat(ids).containsExactly(2, 3);
+    }
+}
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 fefe3bce60..432e5333bf 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
@@ -594,7 +594,7 @@ public class VectorSearchBuilderTest extends TableTestBase {
         FileStoreTable table = getTableDefault();
         Predicate idFilter = new 
PredicateBuilder(table.rowType()).greaterOrEqual(0, 1);
 
-        ExposingVectorRead read = new ExposingVectorRead(table, idFilter);
+        ExposingDataEvolutionVectorRead read = new 
ExposingDataEvolutionVectorRead(table, idFilter);
 
         assertThat(read.rawReadType(false).getFieldNames())
                 .containsExactly(VECTOR_FIELD_NAME, 
SpecialFields.ROW_ID.name());
@@ -611,7 +611,7 @@ public class VectorSearchBuilderTest extends TableTestBase {
         RoaringNavigableMap64 candidates = new RoaringNavigableMap64();
         candidates.add(1L);
 
-        ExposingVectorRead read = new ExposingVectorRead(table, null);
+        ExposingDataEvolutionVectorRead read = new 
ExposingDataEvolutionVectorRead(table, null);
         ScoredGlobalIndexResult result =
                 read.rawCandidateSearch(
                         Collections.singletonList(new Range(0, 2)),
@@ -937,7 +937,7 @@ public class VectorSearchBuilderTest extends TableTestBase {
         // Build ONE btree index covering partial range [3,7]
         buildAndCommitBTreeIndex(table, new int[] {3, 4, 5, 6, 7}, new 
Range(3, 7));
 
-        // VectorScanImpl should attach scalar index because [3,7] intersects 
[0,9]
+        // DataEvolutionVectorScan should attach scalar index because [3,7] 
intersects [0,9]
         Predicate idFilter = new 
PredicateBuilder(table.rowType()).greaterOrEqual(0, 5);
         VectorScan.Plan plan =
                 table.newVectorSearchBuilder()
@@ -1409,9 +1409,9 @@ public class VectorSearchBuilderTest extends 
TableTestBase {
         buildAndCommitIndex(table, VECTOR_FIELD_NAME, vectors);
     }
 
-    private static class ExposingVectorRead extends VectorReadImpl {
+    private static class ExposingDataEvolutionVectorRead extends 
DataEvolutionVectorRead {
 
-        private ExposingVectorRead(FileStoreTable table, Predicate filter) {
+        private ExposingDataEvolutionVectorRead(FileStoreTable table, 
Predicate filter) {
             super(
                     table,
                     null,
@@ -1654,7 +1654,7 @@ public class VectorSearchBuilderTest extends 
TableTestBase {
         buildAndCommitBTreeIndex(table, new int[] {0, 1, 2, 3, 4}, range1);
         buildAndCommitBTreeIndex(table, new int[] {5, 6, 7, 8, 9}, range2);
 
-        // --- Test VectorScanImpl: verify splits contain scalar index files 
---
+        // --- Test DataEvolutionVectorScan: verify splits contain scalar 
index files ---
         Predicate idFilter = new 
PredicateBuilder(table.rowType()).greaterOrEqual(0, 5);
         VectorSearchBuilder searchBuilder =
                 table.newVectorSearchBuilder()
@@ -1676,7 +1676,7 @@ public class VectorSearchBuilderTest extends 
TableTestBase {
                         .count();
         assertThat(scalarCount).isGreaterThan(0);
 
-        // --- Test VectorReadImpl: pre-filter should narrow results ---
+        // --- Test DataEvolutionVectorRead: pre-filter should narrow results 
---
         // Query vector near (0,1) with filter id >= 5
         // Without filter: rows 5,6,7,8,9 are closest
         // With filter id >= 5: btree pre-filter restricts to rows 5-9
diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/read/SparkVectorReadImpl.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/read/SparkVectorReadImpl.java
index 1b51e9cce1..3c13fb4c0c 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/read/SparkVectorReadImpl.java
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/read/SparkVectorReadImpl.java
@@ -28,9 +28,9 @@ import org.apache.paimon.index.IndexPathFactory;
 import org.apache.paimon.partition.PartitionPredicate;
 import org.apache.paimon.predicate.Predicate;
 import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.source.DataEvolutionVectorRead;
 import org.apache.paimon.table.source.IndexVectorSearchSplit;
 import org.apache.paimon.table.source.RawVectorSearchSplit;
-import org.apache.paimon.table.source.VectorReadImpl;
 import org.apache.paimon.table.source.VectorScan;
 import org.apache.paimon.types.DataField;
 import org.apache.paimon.utils.InstantiationUtil;
@@ -51,10 +51,10 @@ import java.util.concurrent.ExecutorService;
 import static org.apache.paimon.CoreOptions.GLOBAL_INDEX_THREAD_NUM;
 
 /**
- * Spark-aware {@link VectorReadImpl} that distributes grouped vector index 
evaluation across the
- * Spark cluster instead of evaluating them with the local thread pool.
+ * Spark-aware {@link DataEvolutionVectorRead} that distributes grouped vector 
index evaluation
+ * across the Spark cluster instead of evaluating them with the local thread 
pool.
  */
-public class SparkVectorReadImpl extends VectorReadImpl {
+public class SparkVectorReadImpl extends DataEvolutionVectorRead {
 
     private static final long serialVersionUID = 1L;
 

Reply via email to