xiangfu0 commented on code in PR #19303:
URL: https://github.com/apache/pinot/pull/19303#discussion_r3890385352


##########
pinot-segment-local/src/test/java/org/apache/pinot/segment/local/realtime/impl/vector/MutableVectorIndexTest.java:
##########
@@ -80,12 +84,180 @@ public void testRuntimeControlDebugInfoReflectsOverrides() 
{
       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"), Boolean.FALSE);
+      Assert.assertEquals(debugInfo.get("supportsPreFilter"), Boolean.TRUE);
+      Assert.assertTrue(index.supportsPreFilter(),
+          "The reader must advertise filtered search: that is what makes the 
planner choose it over an exact scan");
     } finally {
       index.close();
     }
   }
 
+  // -----------------------------------------------------------------------
+  // Filtered search (upsert doc-ids snapshot enforcement)
+  // -----------------------------------------------------------------------
+
+  /// 2-D corpus with distinct distances from the query vector {1, 0}:
+  /// docs 0 and 1 are nearest (the "upsert-obsoleted" rows), docs 2 and 3 are 
the valid rows.
+  private static MutableVectorIndex create2DIndex(long commitDocs, int 
docIdOffset) {
+    Map<String, String> properties = new HashMap<>();
+    properties.put("commitDocs", String.valueOf(commitDocs));
+    properties.put("vectorIndexType", "HNSW");
+    properties.put("vectorDimension", "2");
+    VectorIndexConfig config = new VectorIndexConfig(false, "HNSW", 2, 1,
+        VectorIndexConfig.VectorDistanceFunction.EUCLIDEAN, properties);
+    MutableVectorIndex index =
+        new MutableVectorIndex("mutableVectorIndexFilterTest_" + 
System.nanoTime(), COLUMN_NAME, config);
+    addVector(index, new float[]{1.0F, 0.0F}, docIdOffset);
+    addVector(index, new float[]{0.99F, 0.01F}, docIdOffset + 1);
+    addVector(index, new float[]{0.0F, 1.0F}, docIdOffset + 2);
+    addVector(index, new float[]{0.0F, -1.0F}, docIdOffset + 3);
+    return index;
+  }
+
+  @Test
+  public void testFilteredSearchExcludesNearestDisallowedDocs() {
+    // commitDocs=4 commits on the 4th add, so the unfiltered committed-view 
sanity check below sees all rows
+    MutableVectorIndex index = create2DIndex(4, 0);
+    try {
+      // Sanity: unfiltered top-2 returns the physically nearest ("obsolete") 
docs 0 and 1
+      ImmutableRoaringBitmap unfiltered = index.getDocIds(new float[]{1.0F, 
0.0F}, 2);
+      Assert.assertEquals(unfiltered, ImmutableRoaringBitmap.bitmapOf(0, 1));
+
+      // Filtered top-2 restricted to docs 2 and 3 must return exactly those 
docs. A post-intersection
+      // implementation would return empty here (the unfiltered top-2 has no 
overlap with the allowed set),
+      // so this assertion genuinely discriminates filtered candidate 
generation.
+      ImmutableRoaringBitmap filtered =
+          index.getDocIds(new float[]{1.0F, 0.0F}, 2, 
ImmutableRoaringBitmap.bitmapOf(2, 3));
+      Assert.assertEquals(filtered, ImmutableRoaringBitmap.bitmapOf(2, 3),
+          "Filtered search must return the allowed docs, not the nearest 
disallowed ones");
+    } finally {
+      index.close();
+    }
+  }
+
+  @Test
+  public void testConcurrentFilteredSearchWithLiveWriter()
+      throws Exception {
+    // Single writer, concurrent reader: filtered searches must stay correct 
(results always a subset of
+    // the filter bitmap) while rows are being added and commits fire mid-run
+    Map<String, String> properties = new HashMap<>();
+    properties.put("commitDocs", "7");
+    properties.put("vectorIndexType", "HNSW");
+    properties.put("vectorDimension", "2");
+    VectorIndexConfig config = new VectorIndexConfig(false, "HNSW", 2, 1,
+        VectorIndexConfig.VectorDistanceFunction.EUCLIDEAN, properties);
+    MutableVectorIndex index =
+        new MutableVectorIndex("mutableVectorIndexConcurrentTest_" + 
System.nanoTime(), COLUMN_NAME, config);
+    int numDocs = 200;
+    ImmutableRoaringBitmap allowed = ImmutableRoaringBitmap.bitmapOf(2, 3);
+    AtomicReference<Throwable> failure =
+        new AtomicReference<>();
+    try {
+      addVector(index, new float[]{0.0F, 1.0F}, 0);
+      addVector(index, new float[]{0.0F, -1.0F}, 1);
+      addVector(index, new float[]{1.0F, 0.0F}, 2);
+      addVector(index, new float[]{0.99F, 0.01F}, 3);
+
+      Thread writer = new Thread(() -> {
+        try {
+          for (int docId = 4; docId < numDocs; docId++) {
+            addVector(index, new float[]{-1.0F, 0.0F}, docId);
+          }
+        } catch (Throwable t) {
+          failure.compareAndSet(null, t);
+        }
+      });
+      writer.start();
+      while (writer.isAlive() && failure.get() == null) {

Review Comment:
   Fixed. The writer now waits on a latch for the reader to start and holds 
each row until the reader has completed a minimum number of searches, so the 
two genuinely overlap instead of the writer racing to completion. The loop 
asserts that minimum was met, and the test has `timeOut = 60_000` so a 
writer/refresh deadlock -- the bug class it exists to catch -- fails it instead 
of hanging the suite.
   
   _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/readers/vector/IvfPqVectorIndexReader.java:
##########
@@ -217,6 +217,13 @@ public MutableRoaringBitmap getDocIds(float[] searchQuery, 
int topK) {
     return result;
   }
 
+  @Override
+  public boolean supportsPreFilter() {

Review Comment:
   Added 
`IvfPqVectorIndexTest#testSupportsPreFilterAndRestrictsResultsToTheFilter`. It 
asserts `supportsPreFilter()` is true (so removing the override fails rather 
than silently downgrading every IVF_PQ query to an exact scan), that filtered 
results are a subset of the supplied bitmap, and that an empty filter yields 
nothing. IVF_FLAT and IVF_ON_DISK already had filter-aware coverage; IVF_PQ was 
the gap.
   
   _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/realtime/impl/vector/MutableVectorIndex.java:
##########
@@ -152,16 +193,39 @@ public void add(Object[] values, @Nullable int[] dictIds, 
int docId) {
 
   @Override
   public MutableRoaringBitmap getDocIds(float[] vector, int topK) {
+    return submitSearch(vector, topK, null);
+  }
+
+  @Override
+  public boolean supportsPreFilter() {
+    // Every filtered call restricts its candidates to the supplied bitmap; it 
never degrades to an unfiltered
+    // search, so the engine can rely on filtered search instead of an exact 
scan.
+    return true;

Review Comment:
   Worth checking the diff here: this PR changes the default to `false`, so the 
overrides are what keep the in-tree readers doing filtered search.
   
   The reasoning is that `supportsPreFilter()` became a correctness contract in 
this PR -- the planner uses it to choose filtered ANN over an exact scan, and a 
reader that answers yes without honouring the filter silently drops rows. The 
previous Javadoc explicitly allowed conditional filtering, so an out-of-tree 
reader written against it would now be asserting a guarantee it never made. 
Defaulting to `false` makes that degrade to a slower-but-correct exact scan 
instead.
   
   If you would rather keep the default `true` and drop the five overrides, 
that is a smaller diff and I am happy to switch -- it just trades out-of-tree 
safety for brevity.
   
   _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to