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

xiangfu0 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new e6787e17864 Enforce one null pre-filter contract across all 
filter-aware vector readers (#19464)
e6787e17864 is described below

commit e6787e178645fd3c5a887e2aba7e0de8343cd524
Author: Xiang Fu <[email protected]>
AuthorDate: Fri Sep 4 16:37:58 2026 -0700

    Enforce one null pre-filter contract across all filter-aware vector readers 
(#19464)
    
    FilterAwareVectorIndexReader requires a non-null pre-filter bitmap, but the 
five implementations enforced that four
    different ways: two threw NullPointerException with a contract message, one 
threw IllegalArgumentException with a
    different message, and two had no guard at all and dereferenced the null. A 
caller that lost its filter therefore
    got a different failure purely based on which backend indexed the column, 
and in two cases an unmessaged
    NullPointerException raised from inside Lucene's traversal.
    
    - State the failure mode once on the SPI method, including that an empty 
bitmap admits nothing.
    - Guard IvfFlat and IvfPq, which had none, and align IvfOnDisk on the same 
exception type and message.
    - Short-circuit an empty bitmap in HNSW before building the query, whose 
filter clause would otherwise walk every
      doc in every leaf to assemble an accept set that admits nothing.
    - Report supportsPreFilter() in HNSW and IvfOnDisk debug info instead of a 
hardcoded literal the type system
      already owns.
    - Cover the null contract for every reader, and restore two tests dropped 
from #19303 before it merged: the
      mutable null-bitmap rejection and the IVF_PQ assertion that candidate 
generation honors the pre-filter rather
      than intersecting an unfiltered top-K afterwards.
---
 .../readers/vector/HnswVectorIndexReader.java      | 11 +++-
 .../readers/vector/IvfFlatVectorIndexReader.java   |  1 +
 .../readers/vector/IvfOnDiskVectorIndexReader.java |  4 +-
 .../readers/vector/IvfPqVectorIndexReader.java     |  1 +
 .../impl/vector/MutableVectorIndexTest.java        | 14 +++++
 .../index/creator/HnswVectorIndexCreatorTest.java  | 33 ++++++++++
 .../readers/vector/IvfFlatFilterAwareTest.java     | 23 +++++++
 .../readers/vector/IvfOnDiskFilterAwareTest.java   | 23 +++++++
 .../segment/index/vector/IvfPqVectorIndexTest.java | 70 ++++++++++++++++++++++
 .../index/reader/FilterAwareVectorIndexReader.java |  5 +-
 10 files changed, 181 insertions(+), 4 deletions(-)

diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/vector/HnswVectorIndexReader.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/vector/HnswVectorIndexReader.java
index 53f5744f9bb..5c731fe917d 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/vector/HnswVectorIndexReader.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/vector/HnswVectorIndexReader.java
@@ -18,6 +18,7 @@
  */
 package org.apache.pinot.segment.local.segment.index.readers.vector;
 
+import com.google.common.base.Preconditions;
 import java.io.Closeable;
 import java.io.File;
 import java.io.IOException;
@@ -195,6 +196,14 @@ public class HnswVectorIndexReader implements 
FilterAwareVectorIndexReader, EfSe
 
   @Override
   public ImmutableRoaringBitmap getDocIds(float[] searchQuery, int topK, 
ImmutableRoaringBitmap preFilterBitmap) {
+    // Without this the null reaches BaseFilterQuery's iterator and surfaces 
as an unmessaged NullPointerException
+    // from inside Lucene's traversal, where the cause is far harder to read 
than the contract it violated.
+    Preconditions.checkNotNull(preFilterBitmap, "Pre-filter bitmap must not be 
null for filtered vector search");
+    if (preFilterBitmap.isEmpty()) {
+      // Nothing is admitted, so skip the Lucene search entirely -- its filter 
clause would otherwise walk every doc
+      // in every leaf, translating ids and testing membership, to build an 
accept set guaranteed to be empty.
+      return new MutableRoaringBitmap();
+    }
     try {
       Query filterQuery = new RoaringBitmapFilterQuery(preFilterBitmap, 
_docIdTranslator);
       return translateTopDocs(search(searchQuery, topK, filterQuery));
@@ -222,7 +231,7 @@ public class HnswVectorIndexReader implements 
FilterAwareVectorIndexReader, EfSe
     info.put("effectiveEfSearch", getEffectiveEfSearch());
     info.put("effectiveHnswUseRelativeDistance", 
getEffectiveUseRelativeDistance());
     info.put("effectiveHnswUseBoundedQueue", getEffectiveUseBoundedQueue());
-    info.put("supportsPreFilter", true);
+    info.put("supportsPreFilter", supportsPreFilter());
     return info;
   }
 
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfFlatVectorIndexReader.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfFlatVectorIndexReader.java
index f2930d0fe0e..e61e419f121 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfFlatVectorIndexReader.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfFlatVectorIndexReader.java
@@ -250,6 +250,7 @@ public class IvfFlatVectorIndexReader
 
   @Override
   public ImmutableRoaringBitmap getDocIds(float[] searchQuery, int topK, 
ImmutableRoaringBitmap preFilterBitmap) {
+    Preconditions.checkNotNull(preFilterBitmap, "Pre-filter bitmap must not be 
null for filtered vector search");
     Preconditions.checkArgument(searchQuery.length == _dimension,
         "Query dimension mismatch: expected %s, got %s", _dimension, 
searchQuery.length);
     Preconditions.checkArgument(topK > 0, "topK must be positive, got: %s", 
topK);
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfOnDiskVectorIndexReader.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfOnDiskVectorIndexReader.java
index dda0ed153e0..faccfc80a96 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfOnDiskVectorIndexReader.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfOnDiskVectorIndexReader.java
@@ -220,7 +220,7 @@ public class IvfOnDiskVectorIndexReader
 
   @Override
   public ImmutableRoaringBitmap getDocIds(float[] searchQuery, int topK, 
ImmutableRoaringBitmap preFilterBitmap) {
-    Preconditions.checkArgument(preFilterBitmap != null, "preFilterBitmap must 
not be null");
+    Preconditions.checkNotNull(preFilterBitmap, "Pre-filter bitmap must not be 
null for filtered vector search");
     if (preFilterBitmap.isEmpty()) {
       return new MutableRoaringBitmap();
     }
@@ -498,7 +498,7 @@ public class IvfOnDiskVectorIndexReader
     info.put("totalSearches", _totalSearches.get());
     info.put("filteredSearches", _filteredSearches.get());
     info.put("unfilteredSearches", _unfilteredSearches.get());
-    info.put("supportsPreFilter", true);
+    info.put("supportsPreFilter", supportsPreFilter());
     info.put("storageMode", "pinotDataBuffer");
 
     // Compute cache warmth estimate: fraction of centroids accessed at least 
once
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfPqVectorIndexReader.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfPqVectorIndexReader.java
index 376bc7f60bf..d43231e3390 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfPqVectorIndexReader.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfPqVectorIndexReader.java
@@ -219,6 +219,7 @@ public class IvfPqVectorIndexReader
 
   @Override
   public ImmutableRoaringBitmap getDocIds(float[] searchQuery, int topK, 
ImmutableRoaringBitmap preFilterBitmap) {
+    Preconditions.checkNotNull(preFilterBitmap, "Pre-filter bitmap must not be 
null for filtered vector search");
     Preconditions.checkArgument(searchQuery.length == _dimension,
         "Query dimension mismatch: expected %s, got %s", _dimension, 
searchQuery.length);
     Preconditions.checkArgument(topK > 0, "topK must be positive, got: %s", 
topK);
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/realtime/impl/vector/MutableVectorIndexTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/realtime/impl/vector/MutableVectorIndexTest.java
index f077be3aff0..b2814b4398a 100644
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/realtime/impl/vector/MutableVectorIndexTest.java
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/realtime/impl/vector/MutableVectorIndexTest.java
@@ -597,6 +597,20 @@ public class MutableVectorIndexTest {
     }
   }
 
+  /// A null bitmap must not silently degrade into an unfiltered search, which 
would return doc ids the query is
+  /// not allowed to see. The reader rejects it with its contract message 
rather than throwing a bare
+  /// NullPointerException from deep inside Lucene.
+  @Test
+  public void testFilteredSearchRejectsNullBitmap() {
+    try (MutableVectorIndex index = createIndexWithoutCommits()) {
+      NullPointerException thrown = 
Assert.expectThrows(NullPointerException.class,
+          () -> index.getDocIds(new float[]{1.0F, 0.0F, 0.0F, 0.0F, 0.0F}, 1, 
(ImmutableRoaringBitmap) null));
+      Assert.assertNotNull(thrown.getMessage(), "The rejection must carry the 
pre-filter contract message");
+      Assert.assertTrue(thrown.getMessage().contains("must not be null"),
+          "Expected the pre-filter contract message, got: " + 
thrown.getMessage());
+    }
+  }
+
   private static MutableVectorIndex createIndex() {
     return createIndex("mutableVectorIndexTest_" + System.nanoTime(), 
COLUMN_NAME);
   }
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/creator/HnswVectorIndexCreatorTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/creator/HnswVectorIndexCreatorTest.java
index 0c42ec0f9e6..fc86c747495 100644
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/creator/HnswVectorIndexCreatorTest.java
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/creator/HnswVectorIndexCreatorTest.java
@@ -26,6 +26,7 @@ import org.apache.commons.io.FileUtils;
 import 
org.apache.pinot.segment.local.segment.creator.impl.vector.HnswVectorIndexCreator;
 import 
org.apache.pinot.segment.local.segment.index.readers.vector.HnswVectorIndexReader;
 import org.apache.pinot.segment.spi.index.creator.VectorIndexConfig;
+import org.roaringbitmap.buffer.ImmutableRoaringBitmap;
 import org.roaringbitmap.buffer.MutableRoaringBitmap;
 import org.testng.Assert;
 import org.testng.annotations.AfterMethod;
@@ -109,6 +110,36 @@ public class HnswVectorIndexCreatorTest {
     }
   }
 
+  /// A null bitmap must not fall through to an unfiltered search, which would 
return doc ids outside the filter.
+  /// The reader rejects it with its contract message rather than a bare 
NullPointerException from inside Lucene.
+  @Test
+  public void testFilteredReaderRejectsNullBitmap()
+      throws IOException {
+    try (HnswVectorIndexReader reader = new HnswVectorIndexReader("foo", 
INDEX_DIR, 4, _config)) {
+      float[] queryVector = {5.0F, 42.0F, 54.33333F, 42.24F, 1001.045F};
+      NullPointerException thrown = 
Assert.expectThrows(NullPointerException.class,
+          () -> reader.getDocIds(queryVector, 2, (ImmutableRoaringBitmap) 
null));
+      Assert.assertNotNull(thrown.getMessage(), "The rejection must carry the 
pre-filter contract message");
+      Assert.assertTrue(thrown.getMessage().contains("must not be null"),
+          "Expected the pre-filter contract message, got: " + 
thrown.getMessage());
+    }
+  }
+
+  /// An empty filter admits nothing, so the search is skipped entirely. 
Asserting only that the result is empty
+  /// would not discriminate -- Lucene returns no hits for a zero-match filter 
anyway. Disabling the bounded queue
+  /// without an efSearch makes runtime-control validation throw when a query 
is actually built, so reaching an
+  /// empty result here proves the short-circuit ran before query construction.
+  @Test
+  public void testFilteredReaderShortCircuitsEmptyBitmapBeforeBuildingQuery()
+      throws IOException {
+    try (HnswVectorIndexReader reader = new HnswVectorIndexReader("foo", 
INDEX_DIR, 4, _config)) {
+      float[] queryVector = {5.0F, 42.0F, 54.33333F, 42.24F, 1001.045F};
+      reader.setUseBoundedQueue(false);
+      Assert.assertTrue(reader.getDocIds(queryVector, 2, new 
MutableRoaringBitmap()).isEmpty(),
+          "An empty filter must short-circuit before query construction");
+    }
+  }
+
   @Test
   public void testEfSearchChangesRuntimeSearchBehavior()
       throws IOException {
@@ -144,6 +175,8 @@ public class HnswVectorIndexCreatorTest {
       Assert.assertEquals(debugInfo.get("effectiveEfSearch"), 6);
       Assert.assertEquals(debugInfo.get("effectiveHnswUseRelativeDistance"), 
Boolean.FALSE);
       Assert.assertEquals(debugInfo.get("effectiveHnswUseBoundedQueue"), 
Boolean.FALSE);
+      Assert.assertEquals(debugInfo.get("supportsPreFilter"), 
reader.supportsPreFilter(),
+          "Debug info must report the reader's actual pre-filter capability, 
not a hardcoded literal");
     }
   }
 }
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfFlatFilterAwareTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfFlatFilterAwareTest.java
index f4da031f14b..a6243cb01e1 100644
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfFlatFilterAwareTest.java
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfFlatFilterAwareTest.java
@@ -95,6 +95,29 @@ public class IvfFlatFilterAwareTest {
     }
   }
 
+  /// Every filter-aware reader rejects a null bitmap with the same contract 
failure, so a caller that loses its
+  /// filter never silently receives documents outside it. See 
FilterAwareVectorIndexReader#getDocIds.
+  @Test
+  public void testPreFilterRejectsNullBitmap()
+      throws Exception {
+    int numVectors = 50;
+    int dimension = 4;
+    int nlist = 4;
+    float[][] vectors = generateVectors(numVectors, dimension, new 
Random(TEST_SEED));
+    createIndex(vectors, dimension, nlist, 
VectorIndexConfig.VectorDistanceFunction.EUCLIDEAN);
+
+    VectorIndexConfig config = createConfig(dimension, nlist, 
VectorIndexConfig.VectorDistanceFunction.EUCLIDEAN);
+    try (IvfFlatVectorIndexReader reader = new 
IvfFlatVectorIndexReader(COLUMN_NAME,
+        IvfCombinedBuffers.mapCombined(_tempDir, COLUMN_NAME, config, 
"test-vector"), config)) {
+      NullPointerException thrown = 
Assert.expectThrows(NullPointerException.class,
+          () -> reader.getDocIds(vectors[0], 5, (ImmutableRoaringBitmap) 
null));
+      Assert.assertNotNull(thrown.getMessage(), "The rejection must carry the 
pre-filter contract message");
+      Assert.assertTrue(thrown.getMessage().contains("must not be null"),
+          "Expected the pre-filter contract message, got: " + 
thrown.getMessage());
+    }
+  }
+
+
   @Test
   public void testPreFilterWithEmptyBitmapReturnsEmpty()
       throws Exception {
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfOnDiskFilterAwareTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfOnDiskFilterAwareTest.java
index 42b782414f4..0b1b2676bc7 100644
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfOnDiskFilterAwareTest.java
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfOnDiskFilterAwareTest.java
@@ -119,6 +119,29 @@ public class IvfOnDiskFilterAwareTest {
     }
   }
 
+  /// Every filter-aware reader rejects a null bitmap with the same contract 
failure, so a caller that loses its
+  /// filter never silently receives documents outside it. See 
FilterAwareVectorIndexReader#getDocIds.
+  @Test
+  public void testPreFilterRejectsNullBitmap()
+      throws Exception {
+    int numVectors = 24;
+    int dimension = 4;
+    int nlist = 4;
+    float[][] vectors = generateVectors(numVectors, dimension, new 
Random(TEST_SEED));
+    createIvfFlatIndex(vectors, dimension, nlist, 
VectorIndexConfig.VectorDistanceFunction.COSINE);
+
+    VectorIndexConfig readerConfig =
+        createReaderConfig(dimension, nlist, 
VectorIndexConfig.VectorDistanceFunction.COSINE);
+    try (IvfOnDiskVectorIndexReader reader = new 
IvfOnDiskVectorIndexReader(COLUMN_NAME,
+        IvfCombinedBuffers.mapCombined(_tempDir, COLUMN_NAME, readerConfig, 
"test-vector"), readerConfig)) {
+      NullPointerException thrown = 
Assert.expectThrows(NullPointerException.class,
+          () -> reader.getDocIds(vectors[0], 5, (ImmutableRoaringBitmap) 
null));
+      Assert.assertNotNull(thrown.getMessage(), "The rejection must carry the 
pre-filter contract message");
+      Assert.assertTrue(thrown.getMessage().contains("must not be null"),
+          "Expected the pre-filter contract message, got: " + 
thrown.getMessage());
+    }
+  }
+
   @Test
   public void testPreFilterEmptyBitmapReturnsEmpty()
       throws Exception {
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/vector/IvfPqVectorIndexTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/vector/IvfPqVectorIndexTest.java
index e9538a2ec42..acce0261af4 100644
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/vector/IvfPqVectorIndexTest.java
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/vector/IvfPqVectorIndexTest.java
@@ -100,6 +100,76 @@ public class IvfPqVectorIndexTest {
     }
   }
 
+  /// Every filter-aware reader rejects a null bitmap with the same contract 
failure, so a caller that loses its
+  /// filter never silently receives documents outside it. See 
FilterAwareVectorIndexReader#getDocIds.
+  @Test
+  public void testPreFilterRejectsNullBitmap()
+      throws Exception {
+    int dimension = 8;
+    int nlist = 4;
+    VectorIndexConfig config = 
createConfig(VectorIndexConfig.VectorDistanceFunction.EUCLIDEAN, dimension, 
nlist, 2,
+        4, 64, TEST_SEED);
+    float[][] vectors = generateClusteredVectors(8, 20, dimension, 0.04f, 
TEST_SEED);
+
+    try (IvfPqVectorIndexCreator creator = new 
IvfPqVectorIndexCreator(COLUMN_NAME, _tempDir, config)) {
+      for (float[] vector : vectors) {
+        creator.add(vector);
+      }
+      creator.seal();
+    }
+
+    try (IvfPqVectorIndexReader reader = new 
IvfPqVectorIndexReader(COLUMN_NAME,
+        IvfCombinedBuffers.mapCombined(_tempDir, COLUMN_NAME, config, 
"test-vector"), config)) {
+      NullPointerException thrown = 
Assert.expectThrows(NullPointerException.class,
+          () -> reader.getDocIds(vectors[3], 5, (ImmutableRoaringBitmap) 
null));
+      Assert.assertNotNull(thrown.getMessage(), "The rejection must carry the 
pre-filter contract message");
+      Assert.assertTrue(thrown.getMessage().contains("must not be null"),
+          "Expected the pre-filter contract message, got: " + 
thrown.getMessage());
+    }
+  }
+
+  /// The engine relies on IVF_PQ's filter-aware search to drop disallowed 
documents during candidate generation
+  /// rather than intersecting an unfiltered top-K afterwards. The allowed 
documents here are deliberately outside
+  /// the unfiltered top-K, so an implementation that post-intersected would 
return nothing and fail.
+  @Test
+  public void testPreFilterExcludesNearestDocsFromCandidateGeneration()
+      throws IOException {
+    int dimension = 8;
+    int nlist = 4;
+    VectorIndexConfig config = 
createConfig(VectorIndexConfig.VectorDistanceFunction.EUCLIDEAN, dimension, 
nlist, 2,
+        4, 64, TEST_SEED);
+    float[][] vectors = generateClusteredVectors(8, 20, dimension, 0.04f, 
TEST_SEED);
+
+    try (IvfPqVectorIndexCreator creator = new 
IvfPqVectorIndexCreator(COLUMN_NAME, _tempDir, config)) {
+      for (float[] vector : vectors) {
+        creator.add(vector);
+      }
+      creator.seal();
+    }
+
+    try (IvfPqVectorIndexReader reader = new 
IvfPqVectorIndexReader(COLUMN_NAME,
+        IvfCombinedBuffers.mapCombined(_tempDir, COLUMN_NAME, config, 
"test-vector"), config)) {
+      Assert.assertTrue(reader.supportsPreFilter(),
+          "IVF_PQ must advertise filtered search, otherwise the engine falls 
back to an exact scan");
+      reader.setNprobe(nlist);
+
+      int topK = 2;
+      MutableRoaringBitmap allowed = MutableRoaringBitmap.bitmapOf(40, 100);
+      MutableRoaringBitmap unfiltered = (MutableRoaringBitmap) 
reader.getDocIds(vectors[3], topK);
+      MutableRoaringBitmap unfilteredAllowedOverlap = unfiltered.clone();
+      unfilteredAllowedOverlap.and(allowed);
+      Assert.assertTrue(unfilteredAllowedOverlap.isEmpty(),
+          "Sanity check: the allowed documents must sit outside the unfiltered 
top-K for this test to prove"
+              + " anything, got unfiltered=" + unfiltered);
+
+      ImmutableRoaringBitmap matches = reader.getDocIds(vectors[3], topK, 
allowed);
+      Assert.assertEquals(matches, allowed,
+          "Filtered candidate generation must return the farther allowed docs; 
post-intersection would be empty");
+      Assert.assertTrue(reader.getDocIds(vectors[3], topK, new 
MutableRoaringBitmap()).isEmpty(),
+          "An empty filter must yield no results");
+    }
+  }
+
   @Test
   public void testEmptyIndexIsReadable()
       throws IOException {
diff --git 
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/reader/FilterAwareVectorIndexReader.java
 
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/reader/FilterAwareVectorIndexReader.java
index f3283383280..ebd0b6ebcf6 100644
--- 
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/reader/FilterAwareVectorIndexReader.java
+++ 
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/index/reader/FilterAwareVectorIndexReader.java
@@ -38,8 +38,11 @@ public interface FilterAwareVectorIndexReader extends 
VectorIndexReader {
   /// @param vector the query vector
   /// @param topK number of closest vectors to return
   /// @param preFilterBitmap bitmap of document IDs to restrict the search to;
-  ///                        must not be null (use [#getDocIds(float[], int)] 
for unfiltered search)
+  ///                        must not be null (use [#getDocIds(float[], int)] 
for unfiltered search).
+  ///                        An empty bitmap admits no documents and yields an 
empty result.
   /// @return bitmap of top-K closest vectors from the filtered document set
+  /// @throws NullPointerException if preFilterBitmap is null. Implementations 
must reject null rather than
+  ///         silently searching unfiltered, which would return documents 
outside the caller's filter.
   ImmutableRoaringBitmap getDocIds(float[] vector, int topK, 
ImmutableRoaringBitmap preFilterBitmap);
 
   /// Returns true if this reader supports efficient pre-filter search.


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to