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 fde4de5566 [core] Exclude deletion-vector rows from primary-key ANN
builds (#8669)
fde4de5566 is described below
commit fde4de55664e4441f118ba76295ae74874baacbd
Author: Jingsong Lee <[email protected]>
AuthorDate: Wed Jul 15 23:54:13 2026 +0800
[core] Exclude deletion-vector rows from primary-key ANN builds (#8669)
---
.../designs/2026-07-15-pk-vector-dv-aware-build.md | 59 +++++++++++
.../pk/BucketedPrimaryKeyIndexMaintainer.java | 26 ++++-
.../pkvector/BucketedVectorIndexMaintainer.java | 82 +++++++++++++++-
.../index/pkvector/PkVectorAnnSegmentFile.java | 21 ++++
.../index/pkvector/PkVectorAnnSegmentSearcher.java | 3 +
.../paimon/operation/AbstractFileStoreWrite.java | 4 +-
.../BucketedVectorIndexMaintainerTest.java | 108 ++++++++++++++++++++-
.../index/pkvector/PkVectorAnnSegmentFileTest.java | 40 ++++++++
.../paimon/operation/PrimaryKeyIndexWriteTest.java | 13 ++-
9 files changed, 347 insertions(+), 9 deletions(-)
diff --git a/docs/designs/2026-07-15-pk-vector-dv-aware-build.md
b/docs/designs/2026-07-15-pk-vector-dv-aware-build.md
new file mode 100644
index 0000000000..fa8873a844
--- /dev/null
+++ b/docs/designs/2026-07-15-pk-vector-dv-aware-build.md
@@ -0,0 +1,59 @@
+# DV-aware Primary-key Vector Index Build Design
+
+## Problem Statement
+
+Primary-key vector index maintenance currently reads compact source files
without deletion-vector
+filtering and therefore indexes physical row positions that were already
deleted when an ANN build
+was scheduled. Query-time deletion-vector filtering preserves correctness, but
rebuilding the same
+active source cannot reclaim those holes.
+
+## Chosen Approach
+
+Capture the deletion vectors visible to the bucket writer when a pending ANN
build is created. Pass
+an immutable per-source snapshot into the existing `excludedPosition` hook
while preserving each
+vector's physical ordinal (`fileOffset + rowPosition`).
+
+## Design Details
+
+### Maintenance integration
+
+The bucket writer already restores and updates `BucketedDvMaintainer` before
primary-key index
+maintenance prepares its commit. Its deletion-vector factory will be passed to
the primary-key
+index maintainer and then to the vector maintainer.
+
+### Asynchronous snapshot semantics
+
+`PendingBuild` will clone relevant deletion vectors synchronously when the
build is scheduled. The
+executor must not observe later mutations of the writer's deletion-vector
state. Deletions created
+after scheduling remain covered by query-time filtering.
+
+### Row-id stability
+
+The vector reader continues to read every physical row without applying a
filtering reader.
+`PkVectorAnnSegmentFile` skips excluded vectors but writes included ids as
+`fileOffset + physicalRowPosition`, preserving source metadata and positional
reads.
+
+### Fully deleted sources
+
+If every source row is excluded, publish a zero-row segment backed by an empty
marker file. This
+keeps the source covered without passing deleted row ids to an ANN writer or
retrying the same
+build indefinitely. Search recognizes the zero-row segment and returns an
empty result without
+opening the marker as an ANN payload.
+
+### Tests
+
+1. A deletion present before scheduling is absent from the newly built ANN
payload.
+2. Mutating the writer deletion vector after scheduling does not change the
pending build snapshot.
+3. A fully deleted source publishes a searchable zero-row segment.
+4. Existing query-time deletion-vector filtering remains unchanged.
+
+## Open Questions
+
+None for this scoped change.
+
+## Out of Scope
+
+- Paimon vindex changes.
+- Stale asynchronous build discard/retry policy.
+- Source-file stale-ratio scheduling.
+- Removing query-time deletion-vector filtering.
diff --git
a/paimon-core/src/main/java/org/apache/paimon/index/pk/BucketedPrimaryKeyIndexMaintainer.java
b/paimon-core/src/main/java/org/apache/paimon/index/pk/BucketedPrimaryKeyIndexMaintainer.java
index dd3f244bb7..27faa7774f 100644
---
a/paimon-core/src/main/java/org/apache/paimon/index/pk/BucketedPrimaryKeyIndexMaintainer.java
+++
b/paimon-core/src/main/java/org/apache/paimon/index/pk/BucketedPrimaryKeyIndexMaintainer.java
@@ -20,6 +20,7 @@ package org.apache.paimon.index.pk;
import org.apache.paimon.CoreOptions;
import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.deletionvectors.DeletionVector;
import org.apache.paimon.disk.IOManager;
import org.apache.paimon.index.IndexFileHandler;
import org.apache.paimon.index.IndexFileMeta;
@@ -427,6 +428,24 @@ public final class BucketedPrimaryKeyIndexMaintainer {
@Nullable List<IndexFileMeta> restoredPayloads,
ExecutorService executor,
@Nullable IOManager ioManager) {
+ return create(
+ partition,
+ bucket,
+ restoredDataFiles,
+ restoredPayloads,
+ executor,
+ ioManager,
+ DeletionVector.emptyFactory());
+ }
+
+ public BucketedPrimaryKeyIndexMaintainer create(
+ BinaryRow partition,
+ int bucket,
+ @Nullable List<DataFileMeta> restoredDataFiles,
+ @Nullable List<IndexFileMeta> restoredPayloads,
+ ExecutorService executor,
+ @Nullable IOManager ioManager,
+ DeletionVector.Factory deletionVectorFactory) {
List<DataFileMeta> dataFiles =
restoredDataFiles == null ? Collections.emptyList() :
restoredDataFiles;
List<IndexFileMeta> payloads =
@@ -435,7 +454,12 @@ public final class BucketedPrimaryKeyIndexMaintainer {
vectorFactory == null
? null
: vectorFactory.create(
- partition, bucket, dataFiles, payloads,
executor);
+ partition,
+ bucket,
+ dataFiles,
+ payloads,
+ executor,
+ deletionVectorFactory);
BucketedFullTextIndexMaintainer fullText =
fullTextFactory == null
? null
diff --git
a/paimon-core/src/main/java/org/apache/paimon/index/pkvector/BucketedVectorIndexMaintainer.java
b/paimon-core/src/main/java/org/apache/paimon/index/pkvector/BucketedVectorIndexMaintainer.java
index 016b0d1320..014e7a3e34 100644
---
a/paimon-core/src/main/java/org/apache/paimon/index/pkvector/BucketedVectorIndexMaintainer.java
+++
b/paimon-core/src/main/java/org/apache/paimon/index/pkvector/BucketedVectorIndexMaintainer.java
@@ -20,6 +20,7 @@ package org.apache.paimon.index.pkvector;
import org.apache.paimon.CoreOptions;
import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.deletionvectors.DeletionVector;
import org.apache.paimon.index.IndexFileHandler;
import org.apache.paimon.index.IndexFileMeta;
import org.apache.paimon.index.pk.PrimaryKeyIndexLevels;
@@ -51,6 +52,7 @@ import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
+import java.util.function.LongPredicate;
import static org.apache.paimon.utils.Preconditions.checkArgument;
@@ -64,6 +66,7 @@ public class BucketedVectorIndexMaintainer {
private final String metric;
private final String algorithm;
private final PkVectorDataFileReader.Factory vectorReaderFactory;
+ private final DeletionVector.Factory deletionVectorFactory;
private final PrimaryKeyIndexLevels<IndexFileMeta> annLevels;
private ExecutorService executor;
private final List<IndexFileMeta> annSegments;
@@ -81,6 +84,32 @@ public class BucketedVectorIndexMaintainer {
List<DataFileMeta> restoredDataFiles,
List<IndexFileMeta> restoredPayloads,
ExecutorService executor) {
+ this(
+ vectorFieldId,
+ annSegmentFile,
+ vectorField,
+ indexOptions,
+ metric,
+ algorithm,
+ vectorReaderFactory,
+ DeletionVector.emptyFactory(),
+ restoredDataFiles,
+ restoredPayloads,
+ executor);
+ }
+
+ BucketedVectorIndexMaintainer(
+ int vectorFieldId,
+ PkVectorAnnSegmentFile annSegmentFile,
+ DataField vectorField,
+ Options indexOptions,
+ String metric,
+ String algorithm,
+ PkVectorDataFileReader.Factory vectorReaderFactory,
+ DeletionVector.Factory deletionVectorFactory,
+ List<DataFileMeta> restoredDataFiles,
+ List<IndexFileMeta> restoredPayloads,
+ ExecutorService executor) {
this.vectorFieldId = vectorFieldId;
this.annSegmentFile = annSegmentFile;
this.vectorField = vectorField;
@@ -88,6 +117,7 @@ public class BucketedVectorIndexMaintainer {
this.metric = metric;
this.algorithm = algorithm;
this.vectorReaderFactory = vectorReaderFactory;
+ this.deletionVectorFactory = deletionVectorFactory;
CoreOptions coreOptions = new CoreOptions(indexOptions);
this.annLevels =
new PrimaryKeyIndexLevels<>(
@@ -215,7 +245,7 @@ public class BucketedVectorIndexMaintainer {
}
private void startPendingBuild(
- List<DataFileMeta> sourceFiles, List<IndexFileMeta> inputSegments)
{
+ List<DataFileMeta> sourceFiles, List<IndexFileMeta> inputSegments)
throws IOException {
PendingBuild build = new PendingBuild(sourceFiles, inputSegments);
build.start();
pendingBuild = build;
@@ -343,15 +373,21 @@ public class BucketedVectorIndexMaintainer {
}
}
- private IndexFileMeta buildAnnSegment(List<DataFileMeta> files) {
+ private IndexFileMeta buildAnnSegment(
+ List<DataFileMeta> files, Map<String, DeletionVector>
deletionVectors) {
try {
List<PkVectorAnnSegmentFile.Source> sources = new
ArrayList<>(files.size());
for (DataFileMeta file : files) {
PrimaryKeyIndexSourceFile sourceFile =
new PrimaryKeyIndexSourceFile(file.fileName(),
file.rowCount());
+ DeletionVector deletionVector =
deletionVectors.get(file.fileName());
+ LongPredicate excludedPosition =
+ deletionVector != null ? deletionVector::isDeleted :
position -> false;
sources.add(
PkVectorAnnSegmentFile.Source.lazy(
- sourceFile, () ->
vectorReaderFactory.create(file)));
+ sourceFile,
+ () -> vectorReaderFactory.create(file),
+ excludedPosition));
}
return annSegmentFile.build(sources, vectorField, indexOptions,
metric, algorithm);
} catch (IOException e) {
@@ -359,6 +395,21 @@ public class BucketedVectorIndexMaintainer {
}
}
+ private Map<String, DeletionVector>
snapshotDeletionVectors(List<DataFileMeta> files)
+ throws IOException {
+ Map<String, DeletionVector> snapshots = new LinkedHashMap<>();
+ for (DataFileMeta file : files) {
+ Optional<DeletionVector> deletionVector =
deletionVectorFactory.create(file.fileName());
+ if (deletionVector.isPresent() && !deletionVector.get().isEmpty())
{
+ snapshots.put(
+ file.fileName(),
+ DeletionVector.deserializeFromBytes(
+
DeletionVector.serializeToBytes(deletionVector.get())));
+ }
+ }
+ return snapshots;
+ }
+
private void validateCoverage(
List<IndexFileMeta> candidateAnn, Map<String, DataFileMeta>
sourceFiles) {
PkVectorBucketIndexState state =
@@ -392,20 +443,24 @@ public class BucketedVectorIndexMaintainer {
private final List<DataFileMeta> sourceFiles;
private final List<IndexFileMeta> inputSegments;
+ private final Map<String, DeletionVector> deletionVectors;
@Nullable private IndexFileMeta result;
@Nullable private Future<IndexFileMeta> future;
private boolean cancelled;
- private PendingBuild(List<DataFileMeta> sourceFiles,
List<IndexFileMeta> inputSegments) {
+ private PendingBuild(List<DataFileMeta> sourceFiles,
List<IndexFileMeta> inputSegments)
+ throws IOException {
this.sourceFiles = new ArrayList<>(sourceFiles);
this.inputSegments = new ArrayList<>(inputSegments);
+ this.deletionVectors = snapshotDeletionVectors(sourceFiles);
}
private void start() {
future =
executor.submit(
() -> {
- IndexFileMeta segment =
buildAnnSegment(sourceFiles);
+ IndexFileMeta segment =
+ buildAnnSegment(sourceFiles,
deletionVectors);
synchronized (PendingBuild.this) {
if (!cancelled) {
result = segment;
@@ -587,6 +642,22 @@ public class BucketedVectorIndexMaintainer {
@Nullable List<DataFileMeta> restoredDataFiles,
@Nullable List<IndexFileMeta> restoredPayloads,
ExecutorService executor) {
+ return create(
+ partition,
+ bucket,
+ restoredDataFiles,
+ restoredPayloads,
+ executor,
+ DeletionVector.emptyFactory());
+ }
+
+ public BucketedVectorIndexMaintainer create(
+ BinaryRow partition,
+ int bucket,
+ @Nullable List<DataFileMeta> restoredDataFiles,
+ @Nullable List<IndexFileMeta> restoredPayloads,
+ ExecutorService executor,
+ DeletionVector.Factory deletionVectorFactory) {
checkArgument(indexOptions != null, "ANN index options are not
configured.");
List<DataFileMeta> dataFiles =
restoredDataFiles == null ? Collections.emptyList() :
restoredDataFiles;
@@ -601,6 +672,7 @@ public class BucketedVectorIndexMaintainer {
algorithm,
new PkVectorDataFileReader.Factory(
readerFactoryBuilder, partition, bucket,
vectorField, vectorDimension),
+ deletionVectorFactory,
dataFiles,
payloads,
executor);
diff --git
a/paimon-core/src/main/java/org/apache/paimon/index/pkvector/PkVectorAnnSegmentFile.java
b/paimon-core/src/main/java/org/apache/paimon/index/pkvector/PkVectorAnnSegmentFile.java
index ed59ac9cf3..c466024f69 100644
---
a/paimon-core/src/main/java/org/apache/paimon/index/pkvector/PkVectorAnnSegmentFile.java
+++
b/paimon-core/src/main/java/org/apache/paimon/index/pkvector/PkVectorAnnSegmentFile.java
@@ -43,6 +43,7 @@ import javax.annotation.Nullable;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -142,6 +143,9 @@ public class PkVectorAnnSegmentFile extends IndexFile {
}
List<ResultEntry> results = writer.finish();
+ if (liveRowCount == 0 && results.isEmpty()) {
+ results = Collections.singletonList(fileWriter.emptyResult());
+ }
checkArgument(
results.size() == 1,
"ANN segment build must produce exactly one payload file,
but produced %s.",
@@ -205,6 +209,16 @@ public class PkVectorAnnSegmentFile extends IndexFile {
return path;
}
+ private ResultEntry emptyResult() throws IOException {
+ deleteCreatedFiles();
+ createdFiles.clear();
+ String fileName = newFileName("empty-vector");
+ try (PositionOutputStream ignored = newOutputStream(fileName)) {
+ // The searcher does not open payloads for segments without
live rows.
+ }
+ return new ResultEntry(fileName, 0, null);
+ }
+
private void deleteCreatedFiles() {
for (Path path : createdFiles.values()) {
fileIO.deleteQuietly(path);
@@ -253,6 +267,13 @@ public class PkVectorAnnSegmentFile extends IndexFile {
return new Source(sourceFile, readerFactory, position -> false);
}
+ static Source lazy(
+ PrimaryKeyIndexSourceFile sourceFile,
+ ReaderFactory readerFactory,
+ LongPredicate excludedPosition) {
+ return new Source(sourceFile, readerFactory, excludedPosition);
+ }
+
private PkVectorReader openReader() throws IOException {
return vectors != null ? vectors : readerFactory.open();
}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/index/pkvector/PkVectorAnnSegmentSearcher.java
b/paimon-core/src/main/java/org/apache/paimon/index/pkvector/PkVectorAnnSegmentSearcher.java
index 58696e5a0b..788ff56121 100644
---
a/paimon-core/src/main/java/org/apache/paimon/index/pkvector/PkVectorAnnSegmentSearcher.java
+++
b/paimon-core/src/main/java/org/apache/paimon/index/pkvector/PkVectorAnnSegmentSearcher.java
@@ -176,6 +176,9 @@ public class PkVectorAnnSegmentSearcher {
globalIndexMeta != null && globalIndexMeta.sourceMeta() !=
null,
"Vector segment %s has no source metadata.",
segment.fileName());
+ if (segment.rowCount() == 0) {
+ return Collections.emptyList();
+ }
GlobalIndexer indexer =
GlobalIndexer.create(segment.indexType(), vectorField,
indexOptions);
checkArgument(
diff --git
a/paimon-core/src/main/java/org/apache/paimon/operation/AbstractFileStoreWrite.java
b/paimon-core/src/main/java/org/apache/paimon/operation/AbstractFileStoreWrite.java
index a393613b35..214cfbb603 100644
---
a/paimon-core/src/main/java/org/apache/paimon/operation/AbstractFileStoreWrite.java
+++
b/paimon-core/src/main/java/org/apache/paimon/operation/AbstractFileStoreWrite.java
@@ -26,6 +26,7 @@ import org.apache.paimon.compact.CompactDeletionFile;
import org.apache.paimon.data.BinaryRow;
import org.apache.paimon.data.BlobConsumer;
import org.apache.paimon.deletionvectors.BucketedDvMaintainer;
+import org.apache.paimon.deletionvectors.DeletionVector;
import org.apache.paimon.disk.IOManager;
import org.apache.paimon.index.DynamicBucketIndexMaintainer;
import org.apache.paimon.index.IndexFileHandler;
@@ -517,7 +518,8 @@ public abstract class AbstractFileStoreWrite<T> implements
FileStoreWrite<T> {
restored.dataFiles(),
restored.sourceIndexPayloads(),
primaryKeyIndexExecutor(),
- ioManager);
+ ioManager,
+ DeletionVector.factory(dvMaintainer));
List<DataFileMeta> restoreFiles = restored.dataFiles();
if (restoreFiles == null) {
diff --git
a/paimon-core/src/test/java/org/apache/paimon/index/pkvector/BucketedVectorIndexMaintainerTest.java
b/paimon-core/src/test/java/org/apache/paimon/index/pkvector/BucketedVectorIndexMaintainerTest.java
index 35196debc0..4d06fb5fef 100644
---
a/paimon-core/src/test/java/org/apache/paimon/index/pkvector/BucketedVectorIndexMaintainerTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/index/pkvector/BucketedVectorIndexMaintainerTest.java
@@ -18,6 +18,7 @@
package org.apache.paimon.index.pkvector;
+import org.apache.paimon.deletionvectors.BitmapDeletionVector;
import org.apache.paimon.fs.Path;
import org.apache.paimon.fs.local.LocalFileIO;
import org.apache.paimon.index.GlobalIndexMeta;
@@ -44,6 +45,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
@@ -364,6 +366,106 @@ class BucketedVectorIndexMaintainerTest {
assertThat(commit.compactIncrement().get().newIndexFiles()).hasSize(1);
}
+ @Test
+ void testBuildExcludesDeletionVectorPositions() throws Exception {
+ LocalFileIO fileIO = LocalFileIO.create();
+ PkVectorAnnSegmentFile annFile = new PkVectorAnnSegmentFile(fileIO,
pathFactory());
+ DataField vectorField =
+ new DataField(7, "embedding", DataTypes.VECTOR(2,
DataTypes.FLOAT()));
+ DataFileMeta data = dataFile("data", 2);
+ PkVectorDataFileReader.Factory readerFactory =
mock(PkVectorDataFileReader.Factory.class);
+ PkVectorDataFileReader dataReader = reader(new float[][] {{1, 0}, {2,
0}});
+ when(readerFactory.create(data)).thenReturn(dataReader);
+ BitmapDeletionVector deletionVector = new BitmapDeletionVector();
+ deletionVector.delete(0);
+ BucketedVectorIndexMaintainer maintainer =
+ new BucketedVectorIndexMaintainer(
+ 7,
+ annFile,
+ vectorField,
+ indexOptions(),
+ "l2",
+ "test-vector-ann",
+ readerFactory,
+ fileName -> Optional.of(deletionVector),
+ Collections.emptyList(),
+ Collections.emptyList(),
+ executor);
+
+ BucketedVectorIndexMaintainer.VectorIndexCommit commit =
+ maintainer.prepareCommit(
+ DataIncrement.emptyIncrement(),
+ new CompactIncrement(
+ Collections.emptyList(),
+ Collections.singletonList(data),
+ Collections.emptyList()),
+ true);
+
+ assertThat(commit.compactIncrement()).isPresent();
+ assertThat(commit.compactIncrement().get().newIndexFiles())
+ .singleElement()
+ .extracting(IndexFileMeta::rowCount)
+ .isEqualTo(1L);
+ }
+
+ @Test
+ void testPendingBuildUsesDeletionVectorSnapshot() throws Exception {
+ LocalFileIO fileIO = LocalFileIO.create();
+ PkVectorAnnSegmentFile annFile = new PkVectorAnnSegmentFile(fileIO,
pathFactory());
+ DataField vectorField =
+ new DataField(7, "embedding", DataTypes.VECTOR(2,
DataTypes.FLOAT()));
+ DataFileMeta data = dataFile("data", 3);
+ PkVectorDataFileReader.Factory readerFactory =
mock(PkVectorDataFileReader.Factory.class);
+ PkVectorDataFileReader dataReader = reader(new float[][] {{1, 0}, {2,
0}, {3, 0}});
+ when(readerFactory.create(data)).thenReturn(dataReader);
+ BitmapDeletionVector deletionVector = new BitmapDeletionVector();
+ deletionVector.delete(0);
+ BucketedVectorIndexMaintainer maintainer =
+ new BucketedVectorIndexMaintainer(
+ 7,
+ annFile,
+ vectorField,
+ indexOptions(),
+ "l2",
+ "test-vector-ann",
+ readerFactory,
+ fileName -> Optional.of(deletionVector),
+ Collections.emptyList(),
+ Collections.emptyList(),
+ executor);
+ CountDownLatch executorBlocked = new CountDownLatch(1);
+ CountDownLatch releaseExecutor = new CountDownLatch(1);
+ executor.submit(
+ () -> {
+ executorBlocked.countDown();
+ releaseExecutor.await();
+ return null;
+ });
+ assertThat(executorBlocked.await(30, TimeUnit.SECONDS)).isTrue();
+
+ try {
+ maintainer.prepareCommit(
+ DataIncrement.emptyIncrement(),
+ new CompactIncrement(
+ Collections.emptyList(),
+ Collections.singletonList(data),
+ Collections.emptyList()),
+ false);
+ deletionVector.delete(1);
+ } finally {
+ releaseExecutor.countDown();
+ }
+
+ BucketedVectorIndexMaintainer.VectorIndexCommit commit =
+ maintainer.prepareCommit(
+ DataIncrement.emptyIncrement(),
CompactIncrement.emptyIncrement(), true);
+ assertThat(commit.appendIncrement()).isPresent();
+ assertThat(commit.appendIncrement().get().newIndexFiles())
+ .singleElement()
+ .extracting(IndexFileMeta::rowCount)
+ .isEqualTo(2L);
+ }
+
@Test
void testRebuildsDerivedLevelAndAtomicallyReplacesInputs() throws
Exception {
LocalFileIO fileIO = LocalFileIO.create();
@@ -708,10 +810,14 @@ class BucketedVectorIndexMaintainerTest {
}
private static DataFileMeta dataFile(String fileName) {
+ return dataFile(fileName, 1);
+ }
+
+ private static DataFileMeta dataFile(String fileName, long rowCount) {
return DataFileMeta.forAppend(
fileName,
100,
- 1,
+ rowCount,
SimpleStats.EMPTY_STATS,
0,
0,
diff --git
a/paimon-core/src/test/java/org/apache/paimon/index/pkvector/PkVectorAnnSegmentFileTest.java
b/paimon-core/src/test/java/org/apache/paimon/index/pkvector/PkVectorAnnSegmentFileTest.java
index a316af28a7..8e89ac6fc8 100644
---
a/paimon-core/src/test/java/org/apache/paimon/index/pkvector/PkVectorAnnSegmentFileTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/index/pkvector/PkVectorAnnSegmentFileTest.java
@@ -80,6 +80,46 @@ class PkVectorAnnSegmentFileTest {
.containsExactly("data-1");
}
+ @Test
+ void testBuildsSearchableEmptySegmentWhenAllRowsAreExcluded() throws
Exception {
+ LocalFileIO fileIO = LocalFileIO.create();
+ PkVectorAnnSegmentFile annFile = annFile(fileIO);
+ IndexFileMeta segment =
+ annFile.build(
+ Collections.singletonList(
+ new PkVectorAnnSegmentFile.Source(
+ dataFile("data-1", 2),
+ new ArrayReader(new float[][] {{0, 0},
{1, 0}}),
+ position -> true)),
+ vectorField(),
+ indexOptions(),
+ "l2",
+ "test-vector-ann");
+
+ assertThat(segment.rowCount()).isZero();
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ try {
+ assertThat(
+ new PkVectorAnnSegmentSearcher(
+ fileIO,
+ annFile,
+ vectorField(),
+ indexOptions(),
+ "l2",
+ executor)
+ .search(
+ segment,
+
PrimaryKeyIndexSourceMeta.fromIndexFile(segment),
+ new float[] {0, 0},
+ 1,
+ Collections.emptyMap(),
+ Collections.emptyMap()))
+ .isEmpty();
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+
@Test
void testBuildsAndSearchesMultiSourceSegment() throws Exception {
LocalFileIO fileIO = LocalFileIO.create();
diff --git
a/paimon-core/src/test/java/org/apache/paimon/operation/PrimaryKeyIndexWriteTest.java
b/paimon-core/src/test/java/org/apache/paimon/operation/PrimaryKeyIndexWriteTest.java
index ed16b21e01..0fd641f157 100644
---
a/paimon-core/src/test/java/org/apache/paimon/operation/PrimaryKeyIndexWriteTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/operation/PrimaryKeyIndexWriteTest.java
@@ -22,6 +22,8 @@ import org.apache.paimon.CoreOptions;
import org.apache.paimon.KeyValue;
import org.apache.paimon.TestFileStore;
import org.apache.paimon.TestKeyValueGenerator;
+import org.apache.paimon.deletionvectors.BitmapDeletionVector;
+import org.apache.paimon.deletionvectors.DeletionVector;
import org.apache.paimon.disk.IOManager;
import org.apache.paimon.disk.IOManagerImpl;
import org.apache.paimon.fs.Path;
@@ -122,9 +124,18 @@ class PrimaryKeyIndexWriteTest {
AbstractFileStoreWrite.WriterContainer<KeyValue> container =
write.createWriterContainer(generator.getPartition(record), 1);
- assertThat(readField(container.primaryKeyIndexMaintainer,
"vectorMaintainer")).isNotNull();
+ Object vectorMaintainer =
+ readField(container.primaryKeyIndexMaintainer,
"vectorMaintainer");
+ assertThat(vectorMaintainer).isNotNull();
assertThat((List<?>) readField(container.primaryKeyIndexMaintainer,
"sortedMaintainers"))
.hasSize(4);
+
+ BitmapDeletionVector deletionVector = new BitmapDeletionVector();
+ deletionVector.delete(1);
+ container.deletionVectorsMaintainer.deletionVectors().put("data-file",
deletionVector);
+ DeletionVector.Factory deletionVectorFactory =
+ (DeletionVector.Factory) readField(vectorMaintainer,
"deletionVectorFactory");
+
assertThat(deletionVectorFactory.create("data-file").get()).isSameAs(deletionVector);
write.close();
}