Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
97b36ef
finish up recall computation patch
May 14, 2026
07b3671
Merge branch 'main' of github.com:microsoft/DiskANN
May 15, 2026
eac3ffb
Merge branch 'main' of github.com:microsoft/DiskANN
May 19, 2026
17780f8
fix conflict
May 22, 2026
43eb517
fix conflict
May 22, 2026
1d3a52b
Merge branch 'main' of github.com:microsoft/DiskANN
May 25, 2026
17eac62
Merge branch 'main' of github.com:microsoft/DiskANN
May 26, 2026
0ab4baa
Merge branch 'main' of github.com:microsoft/DiskANN
May 27, 2026
4ddca60
Merge branch 'main' of github.com:microsoft/DiskANN
Jun 2, 2026
54ee01b
fix conflict
Jun 2, 2026
9e1743f
Merge branch 'main' of github.com:microsoft/DiskANN
Jun 5, 2026
93504e6
Merge branch 'main' of github.com:microsoft/DiskANN
Jun 8, 2026
b7c27ce
Merge branch 'main' of github.com:microsoft/DiskANN
Jun 11, 2026
1dafc55
Merge branch 'main' of github.com:microsoft/DiskANN
Jun 12, 2026
33285e2
Merge branch 'main' of github.com:microsoft/DiskANN
Jun 15, 2026
824bdb3
Merge branch 'main' of github.com:microsoft/DiskANN
Jun 16, 2026
826600f
Merge branch 'main' of github.com:microsoft/DiskANN
Jun 26, 2026
1bea9c5
Merge branch 'main' of github.com:microsoft/DiskANN
Jun 30, 2026
c864722
add range groundtruth calculator, rename vector_filters_file
Jun 30, 2026
bd05e0f
fmt, clippy, add missing file
Jun 30, 2026
1477ac4
Potential fix for pull request finding
magdalendobson Jun 30, 2026
4f6f80a
Potential fix for pull request finding
magdalendobson Jun 30, 2026
2044ba1
fix: return error for filter bitmap mismatch
Copilot Jun 30, 2026
df844be
standardize naming to kebab case
Jun 30, 2026
6827f9d
standardize variable names, one more kebab-case instance
Jun 30, 2026
eb70a6f
merge with main
Jun 30, 2026
5edc8e9
add range filter algorithm
Jul 1, 2026
db3f379
cargo fmt
Jul 1, 2026
37796db
add range searhc
Jul 6, 2026
39a319f
add benchmark search file
Jul 6, 2026
703bdac
merge with main
Jul 6, 2026
9187776
actually finish merge
Jul 6, 2026
60c421a
more merge issues
Jul 6, 2026
caa63ce
add integration test
Jul 6, 2026
427cd18
add groundtruth for test datasets
Jul 6, 2026
e05a718
add filtered range search to tests, add range search tests
Jul 6, 2026
c4c7ae1
fmt
Jul 6, 2026
8603760
remove dashes that were making clippy mad
Jul 6, 2026
cd6c7e7
clippy
Jul 6, 2026
1f1ae8a
regenerate baselines with new tie breaking protocol
Jul 6, 2026
0bb0279
Potential fix for pull request finding
magdalendobson Jul 7, 2026
8374b99
fix copilot induced errors
Jul 10, 2026
1e51766
regenerate baselines after bug fix
Jul 10, 2026
c2ad6f9
Merge branch 'main' of github.com:microsoft/DiskANN into users/magdal…
Jul 13, 2026
1895710
remove range search parts to split out to separate PR
Jul 13, 2026
cb78955
added a description field for each test
Jul 14, 2026
c79529f
overwrite benchmarks to include descriptions
Jul 14, 2026
e5bf7ba
fmt + clippy
Jul 14, 2026
eefcd3e
address comments about filtered range search
Jul 14, 2026
918a6c0
use vector sort + dedup instead of hashset
Jul 14, 2026
bbf3a35
get rid of unnecessary PartialEq and Eq
Jul 14, 2026
4585f84
add tests
Jul 14, 2026
b0a6677
add a builder for range search params
Jul 14, 2026
9571fd0
get rid of duplicate builder
Jul 14, 2026
251a0ba
change old description
Jul 14, 2026
1440049
remove duplicated code
Jul 14, 2026
85bf3af
tighten up benchmark
Jul 15, 2026
e30697f
remove edits done by clippy
Jul 15, 2026
e4bbced
fix merge conflict
Jul 16, 2026
34fb7a0
fmt
Jul 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
379 changes: 379 additions & 0 deletions diskann-benchmark-core/src/search/graph/filtered_range.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,379 @@
/*
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*/

use std::{num::NonZeroUsize, sync::Arc};

use diskann::{
ANNResult,
graph::{self, ext::labeled, glue},
provider,
};
use diskann_benchmark_runner::utils::{MicroSeconds, percentiles};
use diskann_utils::{future::AsyncFriendly, views::Matrix};

use crate::{
recall,
search::{self, Search, graph::Strategy},
};

/// A built-in helper for benchmarking the filtered range search method
/// [`graph::DiskANNIndex::search`].
///
/// This is intended to be used in conjunction with [`search::search`] or
/// [`search::search_all`] and provides some basic additional metrics for the
/// latter. Result aggregation for [`search::search_all`] is provided by the
/// [`Aggregator`] type.
///
/// The provided implementation of [`Search`] accepts
/// [`graph::search::Range`] and returns [`Metrics`] as additional output.
#[derive(Debug)]
pub struct FilteredRange<DP, T, S>
where
DP: provider::DataProvider,
{
index: Arc<graph::DiskANNIndex<DP>>,
queries: Arc<Matrix<T>>,
strategy: Strategy<S>,
labels: Arc<[Arc<dyn labeled::QueryLabelProvider<DP::InternalId>>]>,
}

impl<DP, T, S> FilteredRange<DP, T, S>
where
DP: provider::DataProvider,
{
/// Construct a new [`FilteredRange`] searcher.
///
/// If `strategy` is one of the container variants of [`Strategy`], its length
/// must match the number of rows in `queries`. If this is the case, then the
/// strategies will have a querywise correspondence (see [`search::SearchResults`])
/// with the query matrix.
///
/// # Errors
///
/// Returns an error if the number of elements in `strategy` is not compatible with
/// the number of rows in `queries`.
pub fn new(
index: Arc<graph::DiskANNIndex<DP>>,
queries: Arc<Matrix<T>>,
strategy: Strategy<S>,
labels: Arc<[Arc<dyn labeled::QueryLabelProvider<DP::InternalId>>]>,
) -> anyhow::Result<Arc<Self>> {
strategy.length_compatible(queries.nrows())?;

if labels.len() != queries.nrows() {
Err(anyhow::anyhow!(
"Number of label providers ({}) must be equal to the number of queries ({})",
labels.len(),
queries.nrows()
))
} else {
Ok(Arc::new(Self {
index,
queries,
strategy,
labels,
}))
}
}
}

/// Placeholder for filtered range search metrics.
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct Metrics {}

impl<DP, T, S> Search for FilteredRange<DP, T, S>
where
DP: provider::DataProvider<Context: Default, ExternalId: search::Id>,
S: for<'a> glue::DefaultSearchStrategy<
'a,
DP,
&'a [T],
DP::ExternalId,
SearchAccessor: glue::SearchAccessor,
> + Clone
+ AsyncFriendly,
T: AsyncFriendly + Clone,
{
type Id = DP::ExternalId;
type Parameters = graph::search::Range;
type Output = Metrics;

fn num_queries(&self) -> usize {
self.queries.nrows()
}

fn id_count(&self, parameters: &Self::Parameters) -> search::IdCount {
search::IdCount::Dynamic(NonZeroUsize::new(parameters.starting_l()))
}

async fn search<O>(
&self,
parameters: &Self::Parameters,
buffer: &mut O,
index: usize,
) -> ANNResult<Self::Output>
where
O: graph::SearchOutputBuffer<DP::ExternalId> + Send,
{
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.

.max_returned(parameters.max_returned())
.beam_width(parameters.beam_width())
.inner_radius(parameters.inner_radius())
.initial_slack(parameters.initial_slack())
.range_slack(parameters.range_slack())
.build_filtered()?;
let strategy =
labeled::Filtered::new(self.strategy.get(index)?.clone(), &*self.labels[index]);
let _ = self
.index
.search(
filtered_range_search,
&strategy,
&context,
self.queries.row(index),
buffer,
)
.await?;

Ok(Metrics {})
}
}

/// An [`search::Aggregate`]d summary of multiple [`FilteredRange`] search runs returned by
/// the provided [`Aggregator`].
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Summary {
pub setup: search::Setup,
pub parameters: graph::search::Range,
pub end_to_end_latencies: Vec<MicroSeconds>,
pub mean_latencies: Vec<f64>,
pub p90_latencies: Vec<MicroSeconds>,
pub p99_latencies: Vec<MicroSeconds>,
pub average_precision: recall::AveragePrecisionMetrics,
}

/// A [`search::Aggregate`] for collecting the results of multiple [`FilteredRange`] search
/// runs.
pub struct Aggregator<'a, I> {
groundtruth: &'a dyn crate::recall::Rows<I>,
}

impl<'a, I> Aggregator<'a, I> {
pub fn new(groundtruth: &'a dyn crate::recall::Rows<I>) -> Self {
Self { groundtruth }
}
}

impl<I> search::Aggregate<graph::search::Range, I, Metrics> for Aggregator<'_, I>
where
I: crate::recall::RecallCompatible,
{
type Output = Summary;

#[inline(never)]
fn aggregate(
&mut self,
run: search::Run<graph::search::Range>,
mut results: Vec<search::SearchResults<I, Metrics>>,
) -> anyhow::Result<Summary> {
let average_precision = match results.first() {
Some(first) => {
crate::recall::average_precision(first.ids().as_rows(), self.groundtruth)?
}
None => anyhow::bail!("Results must be non-empty"),
};

let mut mean_latencies = Vec::with_capacity(results.len());
let mut p90_latencies = Vec::with_capacity(results.len());
let mut p99_latencies = Vec::with_capacity(results.len());

results.iter_mut().for_each(|r| {
match percentiles::compute_percentiles(r.latencies_mut()) {
Ok(values) => {
let percentiles::Percentiles { mean, p90, p99, .. } = values;
mean_latencies.push(mean);
p90_latencies.push(p90);
p99_latencies.push(p99);
}
Err(_) => {
let zero = MicroSeconds::new(0);
mean_latencies.push(0.0);
p90_latencies.push(zero);
p99_latencies.push(zero);
}
}
});

Ok(Summary {
setup: run.setup().clone(),
parameters: *run.parameters(),
end_to_end_latencies: results.iter().map(|r| r.end_to_end_latency()).collect(),
mean_latencies,
p90_latencies,
p99_latencies,
average_precision,
})
}
}

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.

Please add tests like all the other entries in search/graph/

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.

Yes, done


///////////
// Tests //
///////////

#[cfg(test)]
mod tests {
use super::*;

use diskann::graph::{ext::labeled::QueryLabelProvider, test::provider};

#[derive(Debug)]
struct NoOdds;

impl labeled::QueryLabelProvider<u32> for NoOdds {
fn is_match(&self, id: u32) -> bool {
id.is_multiple_of(2)
}
}

#[test]
fn test_filtered_range() {
let index = search::graph::test_grid_provider();

let mut queries = Matrix::new(0.0f32, 5, index.provider().dim());
queries.row_mut(0).copy_from_slice(&[0.0, 0.0, 0.0, 0.0]);
queries.row_mut(1).copy_from_slice(&[4.0, 0.0, 0.0, 0.0]);
queries.row_mut(2).copy_from_slice(&[0.0, 4.0, 0.0, 0.0]);
queries.row_mut(3).copy_from_slice(&[0.0, 0.0, 4.0, 0.0]);
queries.row_mut(4).copy_from_slice(&[0.0, 0.0, 0.0, 4.0]);

let queries = Arc::new(queries);
let labels: Arc<[_]> = (0..queries.nrows())
.map(|_| -> Arc<dyn QueryLabelProvider<_>> { Arc::new(NoOdds {}) })
.collect();

let filtered_range = FilteredRange::new(
index,
queries.clone(),
Strategy::broadcast(provider::Strategy::new()),
labels,
)
.unwrap();

// Test the standard search interface.
let rt = crate::tokio::runtime(2).unwrap();
let results = search::search(
filtered_range.clone(),
graph::search::Range::builder(10, 2.0)
.initial_slack(0.8)
.range_slack(1.2)
.build()
.unwrap(),
NonZeroUsize::new(2).unwrap(),
&rt,
)
.unwrap();

assert_eq!(results.len(), queries.nrows());
let rows = results.ids().as_rows();

// Check that only even IDs are returned.
for r in 0..rows.nrows() {
for &id in rows.row(r) {
assert_eq!(id % 2, 0, "Found odd ID {} in row {}", id, r);
}
}

const TWO: NonZeroUsize = NonZeroUsize::new(2).unwrap();
let setup = search::Setup {
threads: TWO,
tasks: TWO,
reps: TWO,
};

// Try the aggregated strategy.
let parameters = [
search::Run::new(
graph::search::Range::builder(10, 2.0)
.initial_slack(0.8)
.range_slack(1.2)
.build()
.unwrap(),
setup.clone(),
),
search::Run::new(
graph::search::Range::builder(15, 2.0)
.initial_slack(0.8)
.range_slack(1.2)
.build()
.unwrap(),
setup.clone(),
),
];

let all = search::search_all(filtered_range, parameters, Aggregator::new(rows)).unwrap();

assert_eq!(all.len(), 2);
for summary in all {
assert_eq!(summary.setup, setup);
assert_eq!(summary.end_to_end_latencies.len(), TWO.get());
assert_eq!(summary.mean_latencies.len(), TWO.get());
assert_eq!(summary.p90_latencies.len(), TWO.get());
assert_eq!(summary.p99_latencies.len(), TWO.get());

let ap = summary.average_precision;
assert_eq!(ap.num_queries, queries.nrows());
assert_eq!(
ap.average_precision, 1.0,
"we used a search as the groundtruth"
);
}
}

#[test]
fn test_filtered_range_error() {
let index = search::graph::test_grid_provider();
let queries = Arc::new(Matrix::new(0.0f32, 2, index.provider().dim()));

let labels: Arc<[_]> = (0..queries.nrows() + 1)
.map(|_| -> Arc<dyn QueryLabelProvider<_>> { Arc::new(NoOdds {}) })
.collect();

let strategy = provider::Strategy::new();

// Error for a mismatch between strategies and queries.
let err = FilteredRange::new(
index.clone(),
queries.clone(),
Strategy::collection([strategy.clone()]),
labels.clone(),
)
.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("1 strategy was provided when 2 were expected"),
"failed with {msg}"
);

// Error for a mismatch between label providers and queries.
let err = FilteredRange::new(
index,
queries.clone(),
Strategy::broadcast(strategy),
labels,
)
.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains(
"Number of label providers (3) must be equal to the number of queries (2)"
),
"failed with {msg}"
);
}
}
1 change: 1 addition & 0 deletions diskann-benchmark-core/src/search/graph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Licensed under the MIT license.
*/

pub mod filtered_range;
pub mod inline;
pub mod knn;
pub mod multihop;
Expand Down
Loading
Loading