Repository navigation
Single-pass filtered top-k search for the dynamic Vamana index (C API) - #422
Draft
yuejiaointel wants to merge 3 commits into
Draft
yuejiaointel wants to merge 3 commits into
yuejiaointel wants to merge 3 commits into
Conversation
Filtered top-k in the C API ran an unfiltered search through BatchIterator, filtered each batch and widened the window until k results passed. At selective filters the search buffer grows to about k / selectivity entries, and with the visited set enabled the resumed search cannot re-admit candidates that were seen and then dropped, which lowers recall. Add index::vamana::two_heap_filtered_search: one graph traversal with a candidate min-heap, a result max-heap of non-deleted nodes that pass the filter (capped at max(k, search window)), and an exact visited set. Failing nodes are still expanded. DynamicVamana::two_heap_filtered_search exposes it through a new virtual appended last in DynamicVamanaInterface; it returns false for data types that need a custom search path (anything other than uncompressed and scalar-quantized data), and the C API then keeps using the batch iterator. The static index is unchanged.
… C API index Use random data in the existing dynamic filtered search test: the periodic generator produces a few distinct vectors, which leaves the graph disconnected.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Filtered top-k on the dynamic Vamana index through the C API runs an unfiltered search via
BatchIterator, filters each batch, and widens the window until k results pass. At selective filters the sorted search buffer grows to about k / selectivity entries, so per-query cost rises sharply.search_buffer_visited_setoff (the old default), duplicate distance computations dominate.Change
A single-pass, hnswlib-style search for filtered top-k on the dynamic index:
Results are padded when fewer than k nodes pass, as before. Unfiltered search, the static index,
BatchIterator, build and rerank are not touched.The new entry point is
DynamicVamana::two_heap_filtered_search, backed by one new virtual appended last inDynamicVamanaInterface. It returnsfalsefor data types that need a custom search path (anything other than uncompressed and scalar-quantized data, so LVQ and LeanVec), and the C API then keeps using the batch iterator for those.Results
1M x 128-d synthetic data, k=10, single search thread, mean latency per query. Each row compares the old path (visited set off), the new path, and hnswlib at matched recall (each recall in brackets). One run each.
At 10 % the new path's recall is above the hnswlib row, so that ratio understates its lead. With the visited set on, the old path's recall at 1 % and below is 0.63 to 0.81 at windows 10 to 100, so it has no valid latency comparison there.
In a Valkey server benchmark (16 clients) at 1 % and below, the new path's mean latency was 0.58 to 0.60 times hnswlib's, with recall about 0.005 lower (for example 0.9812 against 0.9872 at 1 %).
Trade-off: the new path calls the predicate on more candidates than the old path (for example about 360 against 99 per query at 10 %, window 10). In the server benchmark at 10 to 25 % selectivity, where each predicate call is more expensive, it was about 1.1 times hnswlib's latency, and the old path with the visited set on was faster there.
Comparison against current
main(same machine and session, two passes each)Single search thread, 1M x 128-d synthetic data, k=10.
mainhere hassearch_buffer_visited_seton by default.Unfiltered search (unchanged code path), mean latency per query with recall@10 in brackets:
mainThe largest pass-to-pass spread was 1.2 %, so there is no measurable regression.
Filtered search on
maintoday: recall@10 at 1 % selectivity is 0.627 to 0.656 at search windows 10, 20, 50 and 100. It is below 0.95 in 16 of 24 tested (window, selectivity) cells, down to 0.2 % and 0.1 % selectivity. With this PR the lowest filtered recall in the same grid is 0.985. At matched recall (cheapest window reaching 0.99) this PR is 0.66xmain's latency at 50 % selectivity and 0.63x at 10 %.Note on the search window: on the new path the search window now sets the size of the filtered result set, not a batch size. A large window therefore does more work than before. For example, at 10 % selectivity a window of 100 takes 1157 us against 217 us on
main(both recall >= 0.9996), while a window of 10 already reaches recall 1.0000 in 138 us. Small windows are enough for filtered top-k on this path.Limitations and follow-ups
Testing
C API Dynamic Filtered Search recall(10k x 32-d random data, search window 100, k=10, 20 queries): recall against brute force restricted to the filter is at least 0.95 for modulus filters at 1/2, 1/10 and 1/100; every returned id passes the filter; after deleting half of the ids that pass the filter, none of them is returned and recall stays at least 0.95; a filter matching nothing returns no results. Onmainthe 1/100 and deletion cases fail this test.svs_c_api_test(all 24 test cases): 23 pass.C API Logger Outputfails with and without this change.tests "*Dynamic*"(C++ unit tests, 19 test cases): all pass.pre-commit run --all-filesis clean.🤖 Generated with Claude Code