Skip to content

Filtered top-k search on the dynamic Vamana index via the search builder - #442

Draft
yuejiaointel wants to merge 20 commits into
intel:mainfrom
yuejiaointel:feature/filtered-search-valid-builder
Draft

yuejiaointel wants to merge 20 commits into
intel:mainfrom
yuejiaointel:feature/filtered-search-valid-builder

Conversation

@yuejiaointel

@yuejiaointel yuejiaointel commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

  • The dynamic index's search already marks each node it finds as valid or invalid; today "invalid" means deleted. Invalid nodes are still navigated through but do not count toward the search window, and cleanup() drops them before results are returned.
  • The existing ValidBuilder now also marks a node invalid when it fails the filter. Filtered top-k search is then the normal greedy search with one extra check per node.
  • Filtered vs regular search is chosen at compile time: ValidBuilder<bool Filtered = false>. Regular search (Filtered == false) has no filter check.
  • The filter is the optional filter argument of the existing MutableVamanaIndex::search(). The search buffer, greedy_search, the static index and the batch iterator are unchanged.

Files

  • include/svs/index/vamana/dynamic_index.h: ValidBuilder<Filtered>; search() takes the optional filter and runs its per-thread body for the filtered or the regular case.
  • include/svs/index/vamana/extensions.h: the top-up step uses the filter and rows with fewer than k passing entries are padded with id size_t(-1) and the worst score.
  • include/svs/orchestrators/dynamic_vamana.h: DynamicVamana::filtered_search (appended last in the interface). Returns false if the index's search has no filter argument (e.g. the concurrent index).

Notes

  • The filter is called from all search threads and must be thread safe.
  • LVQ and LeanVec have their own search code. They need a separate change to pass the filter to their top-up step and to pad short LeanVec rows, so do not rely on this path for them until that lands.
  • The branch is based on v0.5.0; it conflicts with main after Reduce code duplication between concurrent and non-concurrent indexes #439 and will be updated.

Tests

Existing tests plus the filtered-search test in tests/svs/index/vamana/dynamic_index.cpp.

Results

Not benchmarked yet for this version.

🤖 Generated with Claude Code

Reuse the MutableBuffer valid/invalid mechanism: a FilteredValidBuilder marks an
entry invalid if it is deleted or the predicate rejects its external id. New
MutableVamanaIndex::search overload taking a predicate, padding rows with
id -1 / +inf and returning per-query found counts. Supplement path checks the
predicate before computing the distance. Orchestrator virtual appended last.
Replace the separate filtered search overload with two optional trailing
arguments on MutableVamanaIndex::search (an id predicate and a per-query
result count). Without a predicate the existing code path runs unchanged.
check_and_supplement_search_buffer gets an optional predicate that is
checked before the distance is computed. The filter predicate type is now
a type-erased std::function, so FilteredValidBuilder is no longer a
template. The orchestrator keeps one appended virtual that forwards to the
index search.
The optional filter on MutableVamanaIndex::search now only chooses which
builder the existing greedy search closure uses; both cases call the same
per_thread_batch_search. FilteredValidBuilder derives from ValidBuilder.
The buffer top-up in single_search applies the filter when the search
closure carries one. The per-query result count out-parameter is removed
(callers count non-padding ids).

per_thread_batch_search now pads result rows with fewer than k entries
(id size_t(-1), distance +inf) instead of reading past the end of the
search buffer, and the post-search id translation skips the padding.
Give ValidBuilder an optional predicate and pass it down through
internal_search_builder, greedy_search_closure and search() as optional
arguments, instead of a separate builder and search path. Filtered-only
checks: the supplement fallback skips failing ids, copy-out pads short rows
with id -1 and the worst score, translation skips padded ids.
The orchestrator reports filtered search as unsupported for indexes whose
search has no filter argument (the concurrent index), instead of failing to
compile. In the filtered test, drop the unfiltered-padding section (padding is
filtered-only) and add an inner-product case checking padding uses -inf.
ValidBuilder can be given the search buffer. With a filter, it first
asks the buffer whether the entry would be dropped anyway (can_skip)
and, if so, returns it as invalid without looking up the external id or
calling the filter. Results are unchanged.
@yuejiaointel

Copy link
Copy Markdown
Contributor Author

Closing: experiment concluded; the work continues in #456.

@yuejiaointel yuejiaointel reopened this Nov 2, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant