Enable filtered vector search on mutable HNSW and share the doc-id filter query - #19303
Enable filtered vector search on mutable HNSW and share the doc-id filter query#19303xiangfu0 wants to merge 1 commit into
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #19303 +/- ##
============================================
+ Coverage 57.71% 67.58% +9.86%
- Complexity 7 1430 +1423
============================================
Files 2686 3487 +801
Lines 163987 224234 +60247
Branches 26627 35390 +8763
============================================
+ Hits 94640 151539 +56899
+ Misses 61352 60667 -685
- Partials 7995 12028 +4033
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
9d26def to
aebbdae
Compare
Review — enforce the upsert doc-ids snapshot in vector candidate generationReviewed at Context first: half of this PR has already landed#19297–#19300 merged the base fix, so What is not in
The recommendation is to reduce this PR to those rows and let it be the mutable-index/SPI PR, rather than close it — closing it as superseded would silently drop the items above. What is genuinely good
1. (Blocking, process) Drop the already-merged halfRebase onto current 2. (Major)
|
aebbdae to
4ea4eb3
Compare
0b0b498 to
27a628f
Compare
Jackie-Jiang
left a comment
There was a problem hiding this comment.
The added test is failing
| /// Instances are single-use per search and must never be cached by Lucene ([Weight#isCacheable] returns | ||
| /// false), since the accepted docs depend on the bitmap instance. | ||
| public abstract class BasePinotDocIdBitmapFilterQuery extends Query { | ||
| protected final ImmutableRoaringBitmap _bitmap; |
There was a problem hiding this comment.
Renamed to BaseDocIdBitmapFilterQuery. The Pinot prefix was redundant inside the repo, and the neighbouring DocIdTranslator in HnswVectorIndexReader already uses unprefixed DocId for the Pinot side, so this matches what is there. The Lucene-vs-Pinot doc id distinction that motivated the prefix is stated in the class Javadoc instead, which is where it actually needs explaining.
The subclasses keep their existing names, so the family now reads BaseDocIdBitmapFilterQuery -> RoaringBitmapFilterQuery / NumericDocValuesBitmapFilterQuery. Happy to use a different name if you had one in mind.
🤖 Addressed by Claude Code
012f494 to
4af8867
Compare
…lter query The FULL-upsert candidate-generation fix (apache#19297-apache#19300) makes FilterPlanNode select filtered ANN whenever a reader advertises the capability, and fall back to a correctness-first exact scan when it does not. Consuming segments of upsert tables are left on that fallback because the mutable index cannot restrict its search. This makes it one of the readers that can. - MutableVectorIndex stores the supplied Pinot document ID as a numeric doc value, drives both filtered traversal and hit translation from it, and reports supportsPreFilter()=true, so consuming segments use filtered ANN. No planner change is needed: master already routes on that capability. Filtered search refreshes a shared near-real-time searcher so it sees every added row, committed or not; the unfiltered path keeps the cheaper last-committed view. - Give each index instance a private directory under a segment-named parent, so two replicas of one segment hosted in the same JVM cannot collide on the Lucene write lock, while segment and column stay in the path for diagnostics. - Extract BasePinotDocIdBitmapFilterQuery so the mutable index and HnswVectorIndexReader share one Lucene filter iterator instead of two copies (HnswVectorIndexReader.RoaringBitmapFilterQuery now extends it). - Make FilterAwareVectorIndexReader#supportsPreFilter a hard contract: a reader returning true must always return a strict subset of the supplied bitmap and must never heuristically degrade to unfiltered search, because the engine relies on filtered search to keep obsolete row versions out of candidate generation. Readers that can only honor the filter conditionally must return false and take the exact-scan fallback. - VectorUpsertTableTest asserted that consuming segments fall back to the exact scan because the mutable index was not filter-aware. That is what this changes, so those assertions now require filtered ANN there.
4af8867 to
a69086e
Compare
Summary
The FULL-upsert vector candidate-generation fix (#19297–#19300) makes
FilterPlanNodeselect filtered ANN whenever a reader advertises the capability, and fall back to a
correctness-first exact scan when it does not. Consuming segments of upsert tables are
left on that fallback, because the mutable index cannot restrict its search. This PR makes
it one of the readers that can.
Mutable index becomes filter-aware.
MutableVectorIndexstores the supplied Pinotdocument ID as a numeric doc value alongside the existing stored field, translates hits
through it, and reports
supportsPreFilter() == true. Consuming segments therefore usefiltered ANN rather than a brute-force scan. No planner change is needed — master
already routes on that capability.
Directory isolation. Each index instance gets a private directory under a
segment-named parent, so two replicas of one segment hosted in the same JVM cannot collide
on the Lucene write lock. Segment and column stay in the path, so a leaked directory can
still be attributed to its owner.
One Lucene filter iterator instead of two.
BasePinotDocIdBitmapFilterQueryisextracted so the mutable index and
HnswVectorIndexReadershare a single implementation;HnswVectorIndexReader.RoaringBitmapFilterQuerynow extends it (−76 lines there).supportsPreFilter()becomes a hard contract. A reader returningtruecommits toreturning a strict subset of the supplied bitmap on every filtered call, and must never
heuristically degrade to unfiltered search — the engine relies on filtered search to keep
obsolete row versions out of candidate generation. Readers that can only honor the filter
conditionally must return
falseand take the exact-scan fallback. The previous wordingexplicitly invited the conditional behavior this now forbids.
Consuming-segment expectations updated.
VectorUpsertTableTest(added in #19301)asserted that consuming segments fall back to the exact scan because the mutable index
was not filter-aware. That is exactly what this changes, so those assertions now require
filtered ANN on the mutable index instead.
Behavior change (release notes)
Consuming segments of tables with an HNSW vector index switch from a brute-force exact scan
to filtered ANN. This is on by default and has no opt-out: vector search on upsert tables is
new, and the exact-scan fallback was a stopgap for the mutable index not honoring filters,
not behavior worth preserving. Top-K on those segments becomes approximate, as it already is
on sealed segments.
FilterAwareVectorIndexReader#supportsPreFilter()now defaults tofalseand is a hardcontract: returning
truecommits to always returning a subset of the supplied bitmap, withno heuristic degradation to unfiltered search. The previous Javadoc explicitly sanctioned
conditional filtering, so an out-of-tree reader written against it must now either opt in
deliberately or keep the safe default and take the exact-scan path. All five in-tree readers
opt in explicitly.
Validation
All commands ran with JDK 25 against current master.
pinot-segment-local: 26 tests (MutableVectorIndexTest,HnswFilterAwareUpsertTest,VectorIndexTypeTest,HnswVectorIndexCreatorTest).pinot-core: 572 tests, unchanged by this PR but run as a regression check, includingTableIndexingTest(506) and the vector filter/planner suites.VectorUpsertTableTest: 2 query-engine invocations pass, now exercising thefiltered-ANN path on the consuming segment.
Relationship to #19336
#19336 implemented the same mutable-index capability on top of the pre-split #19287. This PR
supersedes it, and #19336 is closed. The two ideas worth keeping from it are folded in here:
a single doc-id mechanism (numeric doc values for both filtering and hit translation, dropping
a redundant stored field per document) and per-instance directory isolation. The rest of this
PR's implementation is smaller than #19336's for the same capability, reuses the existing
HnswVectorIndexReaderfilter query rather than adding a third copy, and refreshes a sharedSearcherManagerinstead of opening a fresh reader on every query.Scope notes
This PR touches no
pinot-coreproduction code — the required-document plumbing itoriginally carried is already in master via #19300, so it was dropped rather than
duplicated. Also dropped while rebasing:
VectorSearchSpec(master takes these asconstructor arguments, per review feedback on #19297), a duplicate
VectorUpsertTableTest(#19301 owns it), duplicate
ExactVectorScanFilterOperatorTestcases, a new validationrejecting vector indexes on dictionary-encoded columns (an independent, arguably
backward-incompatible config change that broke the existing
dict_mv_FLOAT,vector_indexcase — worth its own PR), and a
VECTOR_SIMILARITY_RADIUScandidate restriction (aperformance optimization unrelated to this PR's purpose; radius results were already
correct via the saturation fallback plus the outer snapshot AND).