Skip to content

Add algorithm and benchmark for filtered range search - #1228

Open
Magdalen Dobson Manohar (magdalendobson) wants to merge 64 commits into
mainfrom
users/magdalen/range_filter
Open

Add algorithm and benchmark for filtered range search#1228
Magdalen Dobson Manohar (magdalendobson) wants to merge 64 commits into
mainfrom
users/magdalen/range_filter

Conversation

@magdalendobson

@magdalendobson Magdalen Dobson Manohar (magdalendobson) commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

This PR adds an algorithm for filtered range search to the DiskANN repository. Details of the algorithm, along with the experiments supporting it, are at this Wiki page.

This PR:

  1. Adds the new algorithm to diskann, along with integration tests.
  2. Adds the new algorithm as an option in diskann-benchmark, along with an integration test.
  3. Adds filtered range groundtruth files in test_data.

Some enhancements/fixes to range search along the way:

  1. Added range groundtruth in test_data for yfcc.
  2. Fixed a bug where range search was not always honoring the maximum number of returned results.
  3. Added integration tests for range search making sure a set maximum number of results is actually respected.

Magdalen Manohar and others added 30 commits May 14, 2026 17:56
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
@codecov-commenter

Codecov Comments Bot (codecov-commenter) commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.82383% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.71%. Comparing base (2ee97ff) to head (557d74a).

Files with missing lines Patch % Lines
...-benchmark-core/src/search/graph/filtered_range.rs 96.29% 7 Missing ⚠️
diskann-benchmark/src/index/benchmarks.rs 86.36% 6 Missing ⚠️
diskann-benchmark/src/index/search/plugins.rs 50.00% 3 Missing ⚠️
...kann/src/graph/test/cases/filtered_range_search.rs 98.90% 3 Missing ⚠️
diskann-benchmark/src/index/search/range.rs 94.11% 1 Missing ⚠️
diskann-benchmark/src/inputs/graph_index.rs 96.66% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1228      +/-   ##
==========================================
+ Coverage   91.46%   91.71%   +0.24%     
==========================================
  Files         516      519       +3     
  Lines       98276    99209     +933     
==========================================
+ Hits        89891    90989    +1098     
+ Misses       8385     8220     -165     
Flag Coverage Δ
miri 91.71% <97.82%> (+0.24%) ⬆️
unittests 91.39% <97.82%> (+0.24%) ⬆️

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

Files with missing lines Coverage Δ
diskann-benchmark-core/src/search/graph/mod.rs 100.00% <ø> (ø)
diskann-benchmark-core/src/search/graph/range.rs 95.33% <100.00%> (+0.40%) ⬆️
diskann-benchmark/src/index/inmem/spherical.rs 83.33% <ø> (ø)
diskann-benchmark/src/index/result.rs 88.05% <100.00%> (+39.04%) ⬆️
diskann-benchmark/src/main.rs 92.21% <100.00%> (+0.15%) ⬆️
diskann-providers/src/index/diskann_async.rs 96.74% <ø> (ø)
diskann/src/graph/search/filtered_range_search.rs 100.00% <100.00%> (ø)
diskann/src/graph/search/inline_filter_search.rs 99.47% <100.00%> (ø)
diskann/src/graph/search/range_search.rs 99.72% <100.00%> (+0.82%) ⬆️
diskann/src/graph/test/cases/range_search.rs 98.05% <100.00%> (+0.37%) ⬆️
... and 6 more

... and 6 files with indirect coverage changes

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

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.

Thanks Magdalen. I've left a number of comments throughout the PR, but they all fall into a few general themes.

  • This PR duplicates a lot of logic when that duplication should be abstracted instead. The worst offender is probably DistanceFiltered.
  • We need to move away from messing around with the internal of scratch space (in_range, range_frontier, visited etc) - that can quickly become very brittle.
  • The logic in the filtered search implementation can be tightened.
  • We should start tagging baselines with more verbose descriptions of what the test is covering and what the corner cases being exercised are.
  • Please keep to high level trait objects in diskann-benchmark instead of generics. The former is significantly better for compile times.

In addition, is the intention of this algorithm for research, or is there a production ask for it? I ask because maintaining such algorithms/code is not free. If this is meant for research, please put it behind an experimental feature flag to avoid committing us to it long term.

Comment thread diskann/src/graph/test/cases/range_search.rs
Comment thread diskann/src/graph/search/range_search.rs Outdated
merged
.entry(neighbor.id)
.and_modify(|best: &mut Neighbor<_>| {
if neighbor.distance < best.distance {

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.

What's happening here? Why are we expecting repeats for IDs and why is picking the best the right choice?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Overwriting the distance was an error and I've gotten rid of it now. We are expecting repeats because we are merging the best elements found so far with all the filter-satisfying elements found so far, which might have overlap.

in_range.sort_unstable_by(|left, right| {
left.distance
.total_cmp(&right.distance)
.then_with(|| left.id.cmp(&right.id))

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.

Don't Neighbors already sort themselves?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, because scratch.best is merged with the predicate-satisfying points, which are not in sorted order.

Comment thread diskann-benchmark-core/src/search/graph/filtered_range.rs
}

/// Create range search with full options.
#[allow(clippy::too_many_arguments)]

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.

We should probably listen to clippy here. This is a lot of arguments, and while it mirrors Range - I don't think that's sufficient justification. There are several problems:

  1. There's no real documentation about how these are expected to affect the algorithm.
  2. Callers of FilteredRange::with_options just have a sea of values, which can be difficult to remember.
  3. Any changes to the parameters requires a breaking change for all callers.

It would be way better to use a builder interface for these. As a side-benefit, the same builder could be used for both Range and FilteredRange.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, I set up a builder

Comment thread diskann/src/graph/search/filtered_range_search.rs
Comment thread diskann/src/graph/glue.rs Outdated
///
/// Both variants carry the item `T` since rejected items are useful for graph navigation.
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

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.

Why are PartialEq and Eq needed? The code compiles fine without them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think this was leftover from an intermediate state. Removed now.

let (filtered_stats, filtered_results) =
run_filtered_range_search(&index, query.as_slice(), filtered_range, &filter);

let baseline = RangeSearchBaseline {

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.

There's probably a better way to construct these baselines. A constructor taking the stats object and the Range could automatically fill in a lot of the fields (and we could capture all the fields of `Range~ rather then just like 3).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestion, I added a constructor

pub(crate) fn run<I>(
runner: &dyn Range<I>,
pub(crate) fn run<I, R, F>(
runner: &R,

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.

Why did this change from a &dyn Range to a true generic? The former is quite important to keep compile times down.

Presumably it's because there's a different in the parameters being passed. Please find an alternate solution that isn't as regressive on compile times.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I found an alternative way to do this, so the generic isn't used anymore

Magdalen Dobson Manohar (magdalendobson) added a commit that referenced this pull request Jul 14, 2026
While working on #1228, I noticed that there were some issues with our
current implementation of range search and its testing.

The main issue with testing is that there were no tests ensuring the
`max_results` parameter was respected. I have added two tests that
ensure this now.

The main code had several issues with how `max_results` was handled:
1. The `max_results` parameter was allowed to be less than the initial
L_search. This is a conceptual issue because the user expects
`max_results` to stop the search from continuing for too long, and the
compute used in the initial search will always be controlled by
`initial_search_l`.
2. A `max_results` check was not enforced before deciding to continue to
the second round search. This meant that if the max results was reached
via the initial search, it might not be respected.
3. The second round search was not terminated when `max_results` was
reached, meaning it would continue to perform unnecessary work.

This PR fixes these issues by adding additional checks of `max_results`
at the correct points in the code.

---------

Co-authored-by: Magdalen Manohar <[email protected]>
Magdalen Manohar added 2 commits July 16, 2026 16:06
weiyaoluo (SeliMeli) pushed a commit to SeliMeli/DiskANN that referenced this pull request Jul 22, 2026
While working on microsoft#1228, I noticed that there were some issues with our
current implementation of range search and its testing.

The main issue with testing is that there were no tests ensuring the
`max_results` parameter was respected. I have added two tests that
ensure this now.

The main code had several issues with how `max_results` was handled:
1. The `max_results` parameter was allowed to be less than the initial
L_search. This is a conceptual issue because the user expects
`max_results` to stop the search from continuing for too long, and the
compute used in the initial search will always be controlled by
`initial_search_l`.
2. A `max_results` check was not enforced before deciding to continue to
the second round search. This meant that if the max results was reached
via the initial search, it might not be respected.
3. The second round search was not terminated when `max_results` was
reached, meaning it would continue to perform unnecessary work.

This PR fixes these issues by adding additional checks of `max_results`
at the correct points in the code.

---------

Co-authored-by: Magdalen Manohar <[email protected]>

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.

Thanks Magdalen, appreciate the work on this iteration!

Comment thread diskann/src/graph/test/cases/range_search.rs
Comment thread diskann/src/graph/test/cases/range_search.rs
Comment thread diskann/src/graph/search/range_search.rs Outdated
Comment thread diskann/src/graph/search/range_search.rs
Comment thread diskann/src/graph/search/inline_filter_search.rs
Comment thread diskann/src/graph/search/filtered_range_search.rs Outdated
Comment thread diskann/src/graph/search/filtered_range_search.rs Outdated
Comment thread diskann/src/graph/search/filtered_range_search.rs
{
let context = DP::Context::default();
let filtered_range_search =
graph::search::FilteredRange::builder(parameters.starting_l(), parameters.radius())

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.

I see what's happening here: we want to keep the parameters the same to manage monomorphization in diskann-benchmark. But, it might be better to define a conversion from Range to FilteredRange rather than going back through the builder (which runs the risk of going stale). I'm not sure what the implications are, though, if we ever decide that FilteredRange needs to have additional parameters, but I guess we'll cross that bridge when we come to it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Defined a conversion

Comment thread diskann-benchmark/src/index/inmem/spherical.rs
Comment thread diskann/src/graph/search/filtered_range_search.rs Outdated
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.

Support filtered range search

5 participants