Skip to content

[diskann-inmem] Add support for filtered searches - #1601

Open
Mark Hildebrand (hildebrandmw) wants to merge 4 commits into
mainfrom
mhildebr/filtered
Open

Mark Hildebrand (hildebrandmw) wants to merge 4 commits into
mainfrom
mhildebr/filtered

Conversation

@hildebrandmw

Copy link
Copy Markdown
Contributor

Add support for InlineFilter and MultihopFilter searches to diskann-inmem.

This PR is huge for a number of reasons:

  • It regenerates a bunch of JSON baselines for the filtered searches.
  • I needed to rework how diskann-inmem's integration test framework was structured to split the static build into its own benchmark. This is because I have no interest in supporting filtered search yet for the static build path and instead of special casing the un-supported methods and somehow trying to keep everything in-sync.
  • It has a small custom label filter evaluator to avoid depending on diskann-label-filter or diskann-tools.

It also adds much more robust integration test coverage for inline-filtered and multihop-filtered searches, which we are currently lacking.

In addition, I had to kind of fight infrastructure at every step and take on some technical debt. The goal here is to be able to deprecate inmem1 through near-feature-parity with inmem2, then fix-up the abstractions in diskann to undo the workarounds needed in this PR.

To help pretend it's a little more reviewable, I've broken it into 4 commits.

Commit 1: Support Loading Range Groundtruth

Filtered and range searches use the range-search file format since the number of items satisfying the predicate are not necessarily uniform.

We currently do not have a central shared utility for loading this format. This PR adds read_staggered_bin to diskann_utils::io for reading these types of files and reroutes diskann-benchmark/src/utils/datafiles.rs and diskann-tools/src/utils/search_index_utils.rs to use this shared helper.

In addition, this PR adds a few helpers to diskann:

  • A bool based constructor for Decision.
  • Member accessors to AdaptiveL and a Clone bounds to InlineFilterSearch and MultihopFilterSearch.

Commit 2: Add filtered search to diskann-inmem

Up to now, we've largely been relying on some form of QueryLabelProvider for predicate evaluation. As noted in the module level documentation, there is some magic needed for a QueryLabelProvider to work, especially in the presence of a dynamic index: it needs to somehow operate on the internal ID space and needs to handle the race window between the predicate evaluation and the distance evaluation.

This commit addresses those issues in the following way:

  • The public filtering interface takes a function applied to external IDs and uses that to build a bit-vector over internal IDs.

  • Internal bitvector construction and subsequent use are all done under an epoch guard, which prevents ID recycling and thus a situation where an internal ID is marked as accepted, gets deleted, and then rebound to a new external ID (by preventing the rebinding).

    This also naturally handles the evaluation time mismatch between the bit-vector and the distance. If an item is deleted after the evaluation of the bit-vector but before the distance calculation, the distance evaluation returns None and the item is not returned.

    If the item is deleted after the distance evaluation, then final post-processing will not find an valid ID translation and it still will not be returned.

  • To help amortize the set-up cost while benchmarking algorithms, a scoped Filtered is introduced that has the FilteredAccessor ready-to-go. This then uses synchronous methods to run multiple inline-filtered and multihop-filtered searches.

    Synchronous methods can be used because we know the internally created futures will always run to completion on their first poll.

This last bit is direction I think diskann and diskann-inmem need to move towards. By not using DiskANNIndex as the public API and instead simply having diskann export algorithms, we increase the flexibility of user code to do the most efficient and ergonomic thing.

The section marked with the HERE BE DRAGONS banner shows how hard I had to fight against the current diskann API to make this work like I wanted. My longterm vision after we deprecate inmem1 is that we can rework diskann so these kinds of crazy hacks are no longer needed.

Commit 3: Reworking Integration Tests

This consists of two parts: the first is adding predicate evaluation and the second is refactoring the integration tests.

For predicate evaluation, I opted to add a small inverted-index based evaluator for base labels and the simple query predicates in the test YFCC dataset. This is done to largely avoid taking dependencies on the bitmap computation in diskann-tools and transitively diskann-label-filter.

For the second part, I kind of shot myself in the foot with #1589 with the static index shadowing the Index trait object. The refactor is generally pretty boring: the Index trait gets split into PartialIndex (with support for just insert, search, and counter retrieval), and Index, which adds multi-insert and filtered search. The static index then gets a separate benchmark for just PartialIndex. All of the changes are basically shuffling things around so this works and then adding inline-filtered and multi-hop filtered searches to the full Index runs.

Note that only the integration tests that use L2 are extended to have filtered search runs since that's all we have groundtruth for. All other runs just have empty arrays for their filtered searches.

Commit 4: Regenerating Baselines

This is just rerunning the integration tests with DISKANN_TEST=overwrite to regenerate the expected results.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Filtered search can deadlock when a user-supplied predicate modifies the same DashMap shard whose read lock is still held by the iterator.

1 open issue
What changed in this PR

Adds inline and multihop filtered searches to diskann-inmem, moving it closer to feature parity with the older in-memory implementation. Filtering uses epoch-protected ID mappings, while static-build integration tests remain separate.

Changes:

  • Adds filtering support infrastructure, reusable read guards, and synchronous search execution.
  • Routes ground-truth loading through shared binary readers.
  • Refactors integration tests and updates configurations and baselines for filtered L2 searches.
File Description
diskann/​src/​graph/​search/​multihop_filter_search.rs Makes multihop search parameters cloneable.
diskann/​src/​graph/​search/​inline_filter_search.rs Adds adaptive-L accessors and cloneable search parameters.
diskann/​src/​graph/​glue.rs Adds a boolean-based filter decision constructor.
diskann/​src/​error/​ann_error.rs Converts staggered-reader errors into algorithm errors.
diskann-tools/​src/​utils/​search_index_utils.rs Uses the shared range ground-truth reader.
diskann-inmem/​src/​repr/​quantized/​mod.rs Supports supplied read guards in quantized accessors.
diskann-inmem/​src/​repr/​mod.rs Defines read guards and updates accessor contracts.
diskann-inmem/​src/​repr/​internal/​macros.rs Implements representation read-guard acquisition.
diskann-inmem/​src/​repr/​full.rs Threads read guards through full-precision accessors.
diskann-inmem/​src/​provider/​buildonly.rs Adapts static-build calls to updated accessor signatures.
diskann-inmem/​src/​poll.rs Adds a tested single-poll synchronous executor.
diskann-inmem/​src/​lib.rs Registers bitvector and polling modules.
diskann-inmem/​src/​ids.rs Exposes ID-map iteration for predicate evaluation.
diskann-inmem/​src/​epoch.rs Makes shared epoch guards available without quantization.
diskann-inmem/​src/​bitvector.rs Adds a tested compact predicate bitvector.
diskann-inmem/​integration/​support/​mod.rs Registers filters and adds collection comparison support.
diskann-inmem/​integration/​support/​io.rs Adds buffered file opening with error context.
diskann-inmem/​integration/​main.rs Separates static baselines and simplifies regression execution.
diskann-inmem/​integration/​jsons/​graph/​static/​f32.json Moves static builds to the partial integration job.
diskann-inmem/​integration/​jsons/​graph/​static/​f32-baseline.json Adds a dedicated static-build baseline.
diskann-inmem/​integration/​jsons/​graph/​static/​checks.json Adds static-build regression tolerances.
diskann-inmem/​integration/​jsons/​graph/​spherical/​two-bit-l2.json Adds two-bit spherical filtered L2 searches.
diskann-inmem/​integration/​jsons/​graph/​spherical/​two-bit-l2-baseline.json Records filtered-search results.
diskann-inmem/​integration/​jsons/​graph/​spherical/​two-bit-ip.json Migrates configuration; filtered runs remain empty.
diskann-inmem/​integration/​jsons/​graph/​spherical/​two-bit-ip-baseline.json Updates the baseline schema.
diskann-inmem/​integration/​jsons/​graph/​spherical/​two-bit-cosine.json Migrates configuration; filtered runs remain empty.
diskann-inmem/​integration/​jsons/​graph/​spherical/​two-bit-cosine-baseline.json Updates the baseline schema.
diskann-inmem/​integration/​jsons/​graph/​spherical/​one-bit-l2.json Adds one-bit spherical filtered L2 searches.
diskann-inmem/​integration/​jsons/​graph/​spherical/​one-bit-ip.json Migrates configuration; filtered runs remain empty.
diskann-inmem/​integration/​jsons/​graph/​spherical/​one-bit-ip-baseline.json Updates the baseline schema.
diskann-inmem/​integration/​jsons/​graph/​spherical/​one-bit-cosine.json Migrates configuration; filtered runs remain empty.
diskann-inmem/​integration/​jsons/​graph/​spherical/​one-bit-cosine-baseline.json Updates the baseline schema.
diskann-inmem/​integration/​jsons/​graph/​spherical/​four-bit-l2.json Adds four-bit spherical filtered L2 searches.
diskann-inmem/​integration/​jsons/​graph/​spherical/​four-bit-l2-baseline.json Records filtered-search results.
diskann-inmem/​integration/​jsons/​graph/​spherical/​four-bit-ip.json Migrates configuration; filtered runs remain empty.
diskann-inmem/​integration/​jsons/​graph/​spherical/​four-bit-ip-baseline.json Updates the baseline schema.
diskann-inmem/​integration/​jsons/​graph/​spherical/​four-bit-cosine.json Migrates configuration; filtered runs remain empty.
diskann-inmem/​integration/​jsons/​graph/​spherical/​four-bit-cosine-baseline.json Updates the baseline schema.
diskann-inmem/​integration/​jsons/​graph/​scalar/​two-bit-cosine.json Adds filtered searches for the configured L2 dataset.
diskann-inmem/​integration/​jsons/​graph/​scalar/​two-bit-cosine-baseline.json Records filtered-search results.
diskann-inmem/​integration/​jsons/​graph/​scalar/​one-bit-l2.json Adds one-bit scalar filtered L2 searches.
diskann-inmem/​integration/​jsons/​graph/​scalar/​four-bit-ip.json Migrates configuration; filtered runs remain empty.
diskann-inmem/​integration/​jsons/​graph/​scalar/​four-bit-ip-baseline.json Updates the baseline schema.
diskann-inmem/​integration/​jsons/​graph/​scalar/​eight-bit-l2.json Adds eight-bit scalar filtered L2 searches.
diskann-inmem/​integration/​jsons/​graph/​scalar/​eight-bit-l2-multi-insert.json Adds filtered searches after scalar multi-insert builds.
diskann-inmem/​integration/​jsons/​graph/​scalar/​eight-bit-l2-multi-insert-baseline.json Records multi-insert filtered-search results.
diskann-inmem/​integration/​jsons/​graph/​scalar/​eight-bit-l2-baseline.json Records filtered-search results.
diskann-inmem/​integration/​jsons/​graph/​product/​l2.json Adds product-quantized filtered L2 searches.
diskann-inmem/​integration/​jsons/​graph/​product/​ip.json Migrates configuration; filtered runs remain empty.
diskann-inmem/​integration/​jsons/​graph/​product/​ip-baseline.json Updates the baseline schema.
diskann-inmem/​integration/​jsons/​graph/​product/​cosine.json Migrates configuration; filtered runs remain empty.
diskann-inmem/​integration/​jsons/​graph/​product/​cosine-baseline.json Updates the baseline schema.
diskann-inmem/​integration/​jsons/​graph/​full/​integration-u8.json Adds filtered searches for unsigned-byte vectors.
diskann-inmem/​integration/​jsons/​graph/​full/​integration-u8-baseline.json Records unsigned-byte filtered-search results.
diskann-inmem/​integration/​jsons/​graph/​full/​integration-i8.json Adds filtered searches for signed-byte vectors.
diskann-inmem/​integration/​jsons/​graph/​full/​integration-i8-baseline.json Records signed-byte filtered-search results.
diskann-inmem/​integration/​jsons/​graph/​full/​integration-f32.json Adds filtered searches for full-precision vectors.
diskann-inmem/​integration/​jsons/​graph/​full/​integration-f32-multi-insert.json Adds filtered searches after full-precision multi-insert builds.
diskann-inmem/​integration/​jsons/​graph/​full/​integration-f32-multi-insert-baseline.json Records multi-insert filtered-search results.
diskann-inmem/​integration/​jsons/​graph/​full/​integration-f32-baseline.json Records full-precision filtered-search results.
diskann-inmem/​integration/​jsons/​graph/​full/​integration-f16.json Adds filtered searches for half-precision vectors.
diskann-inmem/​integration/​jsons/​graph/​full/​integration-f16-baseline.json Records half-precision filtered-search results.
diskann-inmem/​integration/​index/​stats.rs Consolidates statistics and regression checks.
diskann-inmem/​integration/​index/​ops.rs Loads labels and runs filtered-search recall checks.
diskann-inmem/​integration/​index/​mod.rs Registers refactored integration modules.
diskann-inmem/​integration/​index/​factory.rs Separates dynamic and static index construction.
diskann-benchmark/​src/​utils/​datafiles.rs Reuses shared fixed and staggered binary readers.

Review effort: Balanced

Comment thread diskann-inmem/src/ids.rs Resolved
@codecov-commenter

Codecov Comments Bot (codecov-commenter) commented Nov 13, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.15867% with 134 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.22%. Comparing base (a3e8963) to head (b6e9e5a).

Files with missing lines Patch % Lines
diskann-inmem/integration/index/parse.rs 90.99% 48 Missing ⚠️
diskann-inmem/integration/index/factory.rs 79.02% 43 Missing ⚠️
diskann-inmem/integration/index/runner.rs 83.21% 23 Missing ⚠️
diskann-inmem/integration/index/ops.rs 93.39% 7 Missing ⚠️
diskann-utils/src/io.rs 96.46% 7 Missing ⚠️
diskann-inmem/src/provider/mod.rs 98.96% 3 Missing ⚠️
diskann-inmem/integration/index/object.rs 95.65% 1 Missing ⚠️
diskann-inmem/integration/index/stats.rs 99.52% 1 Missing ⚠️
diskann-inmem/integration/main.rs 83.33% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1601      +/-   ##
==========================================
- Coverage   92.14%   91.22%   -0.92%     
==========================================
  Files         621      628       +7     
  Lines      130552   131862    +1310     
==========================================
+ Hits       120293   120297       +4     
- Misses      10259    11565    +1306     
Flag Coverage Δ
miri 91.22% <94.15%> (-0.92%) ⬇️
unittests 90.95% <94.15%> (-1.14%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
diskann-benchmark/src/utils/datafiles.rs 67.59% <100.00%> (-6.49%) ⬇️
diskann-inmem/integration/index/mod.rs 100.00% <ø> (ø)
diskann-inmem/integration/support/filters.rs 100.00% <100.00%> (ø)
diskann-inmem/integration/support/io.rs 54.54% <100.00%> (+13.36%) ⬆️
diskann-inmem/integration/support/mod.rs 100.00% <100.00%> (ø)
diskann-inmem/src/bitvector.rs 100.00% <100.00%> (ø)
diskann-inmem/src/epoch.rs 99.32% <ø> (ø)
diskann-inmem/src/poll.rs 100.00% <100.00%> (ø)
diskann-inmem/src/provider/buildonly.rs 98.14% <100.00%> (+0.02%) ⬆️
diskann-inmem/src/repr/full.rs 98.97% <100.00%> (+0.03%) ⬆️
... and 17 more

... and 52 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@hildebrandmw
Mark Hildebrand (hildebrandmw) force-pushed the mhildebr/filtered branch 2 times, most recently from fbfd1d5 to 071a09b Compare November 14, 2026 01:15

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.

3 participants