diskann_benchmark_core/search/graph/
range.rs1use std::{num::NonZeroUsize, sync::Arc};
7
8use diskann::{
9 ANNResult,
10 graph::{self, glue},
11 provider,
12};
13use diskann_benchmark_runner::utils::{MicroSeconds, percentiles};
14use diskann_utils::{future::AsyncFriendly, views::Matrix};
15
16use crate::{
17 recall,
18 search::{self, Search, graph::Strategy},
19};
20
21#[derive(Debug)]
32pub struct Range<DP, T, S>
33where
34 DP: provider::DataProvider,
35{
36 index: Arc<graph::DiskANNIndex<DP>>,
37 queries: Arc<Matrix<T>>,
38 strategy: Strategy<S>,
39}
40
41impl<DP, T, S> Range<DP, T, S>
42where
43 DP: provider::DataProvider,
44{
45 pub fn new(
57 index: Arc<graph::DiskANNIndex<DP>>,
58 queries: Arc<Matrix<T>>,
59 strategy: Strategy<S>,
60 ) -> anyhow::Result<Arc<Self>> {
61 strategy.length_compatible(queries.nrows())?;
62
63 Ok(Arc::new(Self {
64 index,
65 queries,
66 strategy,
67 }))
68 }
69}
70
71#[derive(Debug, Clone, Copy)]
76#[non_exhaustive]
77pub struct Metrics {}
78
79impl<DP, T, S> Search for Range<DP, T, S>
80where
81 DP: provider::DataProvider<Context: Default, ExternalId: search::Id>,
82 S: for<'a> glue::DefaultSearchStrategy<'a, DP, &'a [T], DP::ExternalId> + Clone + AsyncFriendly,
83 graph::search::Range: for<'a> graph::Search<'a, DP, S, &'a [T]>,
84 T: AsyncFriendly + Clone,
85{
86 type Id = DP::ExternalId;
87 type Parameters = graph::search::Range;
88 type Output = Metrics;
89
90 fn num_queries(&self) -> usize {
91 self.queries.nrows()
92 }
93
94 fn id_count(&self, parameters: &Self::Parameters) -> search::IdCount {
95 search::IdCount::Dynamic(NonZeroUsize::new(parameters.starting_l()))
96 }
97
98 async fn search<O>(
99 &self,
100 parameters: &Self::Parameters,
101 buffer: &mut O,
102 index: usize,
103 ) -> ANNResult<Self::Output>
104 where
105 O: graph::SearchOutputBuffer<DP::ExternalId> + Send,
106 {
107 let context = DP::Context::default();
108 let range_search = *parameters;
109 let _ = self
110 .index
111 .search(
112 range_search,
113 self.strategy.get(index)?,
114 &context,
115 self.queries.row(index),
116 buffer,
117 )
118 .await?;
119
120 Ok(Metrics {})
121 }
122}
123
124#[derive(Debug, Clone)]
129#[non_exhaustive]
130pub struct Summary {
131 pub setup: search::Setup,
133
134 pub parameters: graph::search::Range,
136
137 pub end_to_end_latencies: Vec<MicroSeconds>,
139
140 pub mean_latencies: Vec<f64>,
144
145 pub p90_latencies: Vec<MicroSeconds>,
149
150 pub p99_latencies: Vec<MicroSeconds>,
154
155 pub average_precision: recall::AveragePrecisionMetrics,
160}
161
162pub struct Aggregator<'a, I> {
169 groundtruth: &'a dyn crate::recall::Rows<I>,
170}
171
172impl<'a, I> Aggregator<'a, I> {
173 pub fn new(groundtruth: &'a dyn crate::recall::Rows<I>) -> Self {
175 Self { groundtruth }
176 }
177}
178
179impl<I> search::Aggregate<graph::search::Range, I, Metrics> for Aggregator<'_, I>
180where
181 I: crate::recall::RecallCompatible,
182{
183 type Output = Summary;
184
185 #[inline(never)]
186 fn aggregate(
187 &mut self,
188 run: search::Run<graph::search::Range>,
189 mut results: Vec<search::SearchResults<I, Metrics>>,
190 ) -> anyhow::Result<Summary> {
191 let average_precision = match results.first() {
193 Some(first) => {
194 crate::recall::average_precision(first.ids().as_rows(), self.groundtruth)?
195 }
196 None => anyhow::bail!("Results must be non-empty"),
197 };
198
199 let mut mean_latencies = Vec::with_capacity(results.len());
200 let mut p90_latencies = Vec::with_capacity(results.len());
201 let mut p99_latencies = Vec::with_capacity(results.len());
202
203 results.iter_mut().for_each(|r| {
204 match percentiles::compute_percentiles(r.latencies_mut()) {
205 Ok(values) => {
206 let percentiles::Percentiles { mean, p90, p99, .. } = values;
207 mean_latencies.push(mean);
208 p90_latencies.push(p90);
209 p99_latencies.push(p99);
210 }
211 Err(_) => {
212 let zero = MicroSeconds::new(0);
213 mean_latencies.push(0.0);
214 p90_latencies.push(zero);
215 p99_latencies.push(zero);
216 }
217 }
218 });
219
220 Ok(Summary {
221 setup: run.setup().clone(),
222 parameters: *run.parameters(),
223 end_to_end_latencies: results.iter().map(|r| r.end_to_end_latency()).collect(),
224 mean_latencies,
225 p90_latencies,
226 p99_latencies,
227 average_precision,
228 })
229 }
230}
231
232#[cfg(test)]
237mod tests {
238 use super::*;
239
240 use diskann::graph::test::provider;
241
242 #[test]
243 fn test_range() {
244 let index = search::graph::test_grid_provider();
245
246 let mut queries = Matrix::new(0.0f32, 5, index.provider().dim());
247 queries.row_mut(0).copy_from_slice(&[0.0, 0.0, 0.0, 0.0]);
248 queries.row_mut(1).copy_from_slice(&[4.0, 0.0, 0.0, 0.0]);
249 queries.row_mut(2).copy_from_slice(&[0.0, 4.0, 0.0, 0.0]);
250 queries.row_mut(3).copy_from_slice(&[0.0, 0.0, 4.0, 0.0]);
251 queries.row_mut(4).copy_from_slice(&[0.0, 0.0, 0.0, 4.0]);
252
253 let queries = Arc::new(queries);
254
255 let range = Range::new(
256 index,
257 queries.clone(),
258 Strategy::broadcast(provider::Strategy::new()),
259 )
260 .unwrap();
261
262 let rt = crate::tokio::runtime(2).unwrap();
264 let results = search::search(
265 range.clone(),
266 graph::search::Range::with_options(None, 10, None, 2.0, None, 0.8, 1.2).unwrap(),
267 NonZeroUsize::new(2).unwrap(),
268 &rt,
269 )
270 .unwrap();
271
272 assert_eq!(results.len(), queries.nrows());
273 let rows = results.ids().as_rows();
274 assert_eq!(*rows.row(0).first().unwrap(), 0);
275 const TWO: NonZeroUsize = NonZeroUsize::new(2).unwrap();
276 let setup = search::Setup {
277 threads: TWO,
278 tasks: TWO,
279 reps: TWO,
280 };
281
282 let parameters = [
284 search::Run::new(
285 graph::search::Range::with_options(None, 10, None, 2.0, None, 0.8, 1.2).unwrap(),
286 setup.clone(),
287 ),
288 search::Run::new(
289 graph::search::Range::with_options(None, 15, None, 2.0, None, 0.8, 1.2).unwrap(),
290 setup.clone(),
291 ),
292 ];
293
294 let all = search::search_all(range, parameters, Aggregator::new(rows)).unwrap();
295
296 assert_eq!(all.len(), 2);
297 for summary in all {
298 assert_eq!(summary.setup, setup);
299 assert_eq!(summary.end_to_end_latencies.len(), TWO.get());
300 assert_eq!(summary.mean_latencies.len(), TWO.get());
301 assert_eq!(summary.p90_latencies.len(), TWO.get());
302 assert_eq!(summary.p99_latencies.len(), TWO.get());
303
304 let ap = summary.average_precision;
305 assert_eq!(ap.num_queries, queries.nrows());
306 assert_eq!(
307 ap.average_precision, 1.0,
308 "we used a search as the groundtruth"
309 );
310 }
311 }
312
313 #[test]
314 fn test_range_error() {
315 let index = search::graph::test_grid_provider();
316
317 let queries = Arc::new(Matrix::new(0.0f32, 2, index.provider().dim()));
318 let strategy = provider::Strategy::new();
319
320 let err = Range::new(index, queries.clone(), Strategy::collection([strategy])).unwrap_err();
321 let msg = err.to_string();
322 assert!(
323 msg.contains("1 strategy was provided when 2 were expected"),
324 "failed with {msg}"
325 );
326 }
327}