Reform Range Search - #1337
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors DiskANN’s range search and filtered range search implementation to reduce scratch-space bloat, clarify parameter semantics, and improve correctness around max_returned and inner-radius behavior, while updating the corresponding golden baselines.
Changes:
- Moves range-search frontier/result bookkeeping out of
SearchScratchand into the range-search algorithms (introducing anInRangehelper). - Adds/updates tests and regenerated baselines to cover
initial_slack/range_slackbehavior and revised second-round triggering. - Adjusts handling of
max_returnedto better account for start-point filtering and apply inner-radius constraints earlier.
Reviewed changes
Copilot reviewed 6 out of 17 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| diskann/src/graph/search/range_search.rs | Introduces InRange and refactors unfiltered range search to manage frontier/results locally and apply radius constraints during search. |
| diskann/src/graph/search/filtered_range_search.rs | Adapts filtered range search to use InRange, adds effective max helpers, and updates second-round search flow. |
| diskann/src/graph/search/scratch.rs | Removes range-search-specific scratch fields (range_frontier, in_range) from shared scratch space. |
| diskann/src/graph/search/diverse_search.rs | Updates SearchScratch initialization to match removed fields. |
| diskann/src/graph/test/cases/range_search.rs | Reworks range-search tests to cover initial_slack and range_slack, and updates expectations for second-round behavior. |
| diskann/src/graph/test/cases/filtered_range_search.rs | Removes a prior max-results test and adds new slack-focused tests for filtered range search. |
| diskann/test/generated/graph/test/cases/range_search/two_round_search.json | Updates recorded baseline stats/results for revised two-round behavior. |
| diskann/test/generated/graph/test/cases/range_search/max_results_respected_and_second_round_triggered.json | Updates recorded baseline to match updated max-results/second-round behavior. |
| diskann/test/generated/graph/test/cases/range_search/max_results_respected_means_no_second_round.json | Removes obsolete baseline corresponding to deleted/renamed test. |
| diskann/test/generated/graph/test/cases/filtered_range_search/max_results_respected_means_no_second_round.json | Removes obsolete baseline corresponding to deleted test. |
Suppressed comments (1)
diskann/src/graph/search/filtered_range_search.rs:284
filtered_range_search_internalstops expanding oncematched_in_range.len() == search_params.max_returned(). This bypasses the earlier “effective max” (max_returned + num_start_ids) slack that the caller sets up to compensate for start points being filtered duringpost_process, so the second round may still return fewer than the requested results even when more exist.
let max_returned = search_params.max_returned().unwrap_or(usize::MAX);
'outer: while !range_frontier.is_empty() && matched_in_range.len() < max_returned {
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| self.range_params.max_returned() | ||
| } | ||
|
|
||
| /// Returns the maximum number of results to return. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1337 +/- ##
==========================================
+ Coverage 91.55% 91.57% +0.01%
==========================================
Files 521 521
Lines 100371 100664 +293
==========================================
+ Hits 91895 92182 +287
- Misses 8476 8482 +6
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
Sorry, accidentally hit the "approve" button. |
| /// Builder for [`Range`] search parameters. | ||
| /// | ||
| /// `max_returned`: If specified, the search will stop and return results once this number of points has been found within both the inner and outer radii. | ||
| /// Note that due to adding some extra slack for start points, occasionally slightly more than this number of points may be returned. Since the initial |
There was a problem hiding this comment.
in this corner case, why cant we truncate the list of starting points and just honor the max_returned list?
There was a problem hiding this comment.
I pushed an update that does this but it is a little awkward. The problem is that filtering out start points happens during post_process, so the truncation needs to happen after that point, and there isn't a built-in way to truncate an output buffer. The workaround is to introduce a new output buffer that handles the truncation after filtering, which I do here, with a corresponding integration test that checks that the desired behavior happens (e.g. if more than the maximum number of points are returned, but none of them are start points, truncation happens).
I do think this adds a lot of code for a relatively small gain, so would like opinions on whether to 1) stick with behavior in the PR as-is, 2) revert to sometimes over-returning, or 3) allow under-returning, thus going back to behavior on current main. CC Mark Hildebrand (@hildebrandmw)
There was a problem hiding this comment.
We already have some precedent for over returning in the normal KNN search because we don't know precisely whether or not start points will be filtered in post processing. For KNN, this is usually solved by the user supplying a fixed size buffer to terminate post-processing. I don't think it's unreasonable to document max_returned as more of an early termination hint rather than hard guarantee.
That said, I've been operating under the assumption that start points could be either valid or invalid depending on the caller. This gives us the flexibility in the future to seed searches with valid points if we need to.
There was a problem hiding this comment.
Restored to the previous functionality after the buffer proof-of-concept turned out to be messy.
…en/range_search_reform
…sers/magdalen/range_search_reform
| } | ||
|
|
||
| /// Builder for [`Range`] search parameters. | ||
| /// |
There was a problem hiding this comment.
Request: can you please wrap the comments at column 100 or so? Otherwise, they get quite difficult to read and edit in editors.
There was a problem hiding this comment.
Yep, done
| /// Note that due to adding some extra slack for start points, occasionally slightly more than this number of points may be returned. Since the initial | ||
| /// search phase does not respect `max_returned`, this parameter may not be set lower than `starting_l`. | ||
| /// | ||
| /// `starting_l`: the L_search parameter for the initial search phase. Must be greater than zero. |
There was a problem hiding this comment.
Maybe cross reference with KNN::search_l?
There was a problem hiding this comment.
Done
| /// | ||
| /// `radius`: the outer radius for the range search. Points within this distance from the query are candidates for inclusion in the results. | ||
| /// | ||
| /// `inner_radius`: the inner radius for the range search. Points closer than this distance from the query are excluded from the results. Must be less than or equal to `radius` if specified. |
There was a problem hiding this comment.
I believe radii must be positive and finite as well?
There was a problem hiding this comment.
Radii do NOT have to be positive because of inner product search. I guess radii should be finite, but we don't explicitly check for it at the moment, is that something you want to change?
There was a problem hiding this comment.
After our side conversation, I added is_finite checks for all float values and also a note on negative radius for inner product.
| /// Builder for [`Range`] search parameters. | ||
| /// | ||
| /// `max_returned`: If specified, the search will stop and return results once this number of points has been found within both the inner and outer radii. | ||
| /// Note that due to adding some extra slack for start points, occasionally slightly more than this number of points may be returned. Since the initial |
There was a problem hiding this comment.
We already have some precedent for over returning in the normal KNN search because we don't know precisely whether or not start points will be filtered in post processing. For KNN, this is usually solved by the user supplying a fixed size buffer to terminate post-processing. I don't think it's unreasonable to document max_returned as more of an early termination hint rather than hard guarantee.
That said, I've been operating under the assumption that start points could be either valid or invalid depending on the caller. This gives us the flexibility in the future to seed searches with valid points if we need to.
Range search had some problems, and the new filtered range search inherited many of them.
Problem 1: Range search added some extra elements to
scratchfor convenience. However, given that the result size of range search can vary wildly, it doesn't make too much sense to reuse them, and it clogged up the scratch space. This PR pulls them out of scratch and manages them directly inside the search algorithm.Problem 2: Range search has a lot of parameters, and their meaning and use was kind of opaque to people who aren't me. This PR adds a nice long docstring that actually explains them.
Problem 3: Two parameters--the initial search slack and the range search slack--weren't really tested. This PR adds some tests that ensure that they behave as intended.
Problem 4: There was a lot of weirdness in handling of
max_results. With start point filtering, if start points fell within the radius, they could be excluded during post-process and the search could return fewer thanmax_resultspoints even if satisfying points existed. This PR sets an "effective" max results that includes the number of start points. Furthermore, since inner radius filtering wasn't done until the end, if the inner radius ended up excluding a lot of points at the end, we could again return way fewer thanmax_resultspoints even when they exist. This PR fixes that by including inner radius filtering during the second round of search.To do this, it includes a new
InRangestruct which automatically handles enforcing the radius constraints, max results, and filtering if applicable. This removes the need for theDistanceFilteredbuffer that was previously handed to the post-processor.NB: this PR also removes one test from both the filtered and unfiltered range search. The test didn't make a lot of sense and it is now subsumed by the testing of
initial_slack.