1use std::sync::Arc;
9
10use diskann::{
11 ANNResult,
12 graph::{self, glue},
13 provider,
14};
15use diskann_benchmark_runner::utils::{MicroSeconds, percentiles};
16use diskann_utils::{future::AsyncFriendly, views::Matrix};
17
18use crate::{
19 recall,
20 recall::GroundTruthMode,
21 search::{self, Search, graph::Strategy},
22 utils,
23};
24
25#[derive(Debug)]
36pub struct KNN<DP, T, S>
37where
38 DP: provider::DataProvider,
39{
40 index: Arc<graph::DiskANNIndex<DP>>,
41 queries: Arc<Matrix<T>>,
42 strategy: Strategy<S>,
43}
44
45impl<DP, T, S> KNN<DP, T, S>
46where
47 DP: provider::DataProvider,
48{
49 pub fn new(
61 index: Arc<graph::DiskANNIndex<DP>>,
62 queries: Arc<Matrix<T>>,
63 strategy: Strategy<S>,
64 ) -> anyhow::Result<Arc<Self>> {
65 strategy.length_compatible(queries.nrows())?;
66
67 Ok(Arc::new(Self {
68 index,
69 queries,
70 strategy,
71 }))
72 }
73}
74
75#[derive(Debug, Clone, Copy)]
81#[non_exhaustive]
82pub struct Metrics {
83 pub comparisons: u32,
85 pub hops: u32,
87}
88
89impl<DP, T, S> Search for KNN<DP, T, S>
90where
91 DP: provider::DataProvider<Context: Default, ExternalId: search::Id>,
92 S: for<'a> glue::DefaultSearchStrategy<'a, DP, &'a [T], DP::ExternalId> + Clone + AsyncFriendly,
93 T: AsyncFriendly + Clone,
94{
95 type Id = DP::ExternalId;
96 type Parameters = graph::search::Knn;
97 type Output = Metrics;
98
99 fn num_queries(&self) -> usize {
100 self.queries.nrows()
101 }
102
103 fn id_count(&self, parameters: &Self::Parameters) -> search::IdCount {
104 search::IdCount::Fixed(parameters.k_value())
105 }
106
107 async fn search<O>(
108 &self,
109 parameters: &Self::Parameters,
110 buffer: &mut O,
111 index: usize,
112 ) -> ANNResult<Self::Output>
113 where
114 O: graph::SearchOutputBuffer<DP::ExternalId> + Send,
115 {
116 let context = DP::Context::default();
117 let knn_search = *parameters;
118 let stats = self
119 .index
120 .search(
121 knn_search,
122 self.strategy.get(index)?,
123 &context,
124 self.queries.row(index),
125 buffer,
126 )
127 .await?;
128
129 Ok(Metrics {
130 comparisons: stats.cmps,
131 hops: stats.hops,
132 })
133 }
134}
135
136#[derive(Debug, Clone)]
141#[non_exhaustive]
142pub struct Summary {
143 pub setup: search::Setup,
145
146 pub parameters: graph::search::Knn,
148
149 pub end_to_end_latencies: Vec<MicroSeconds>,
151
152 pub mean_latencies: Vec<f64>,
156
157 pub p90_latencies: Vec<MicroSeconds>,
161
162 pub p99_latencies: Vec<MicroSeconds>,
166
167 pub recall: recall::RecallMetrics,
172
173 pub mean_cmps: f64,
175
176 pub mean_hops: f64,
178}
179
180pub struct Aggregator<'a, I> {
187 groundtruth: &'a dyn crate::recall::Rows<I>,
188 recall_k: usize,
189 recall_n: usize,
190 groundtruth_mode: GroundTruthMode,
191}
192
193impl<'a, I> Aggregator<'a, I> {
194 pub fn new(
201 groundtruth: &'a dyn crate::recall::Rows<I>,
202 recall_k: usize,
203 recall_n: usize,
204 groundtruth_mode: GroundTruthMode,
205 ) -> Self {
206 Self {
207 groundtruth,
208 recall_k,
209 recall_n,
210 groundtruth_mode,
211 }
212 }
213}
214
215impl<I> search::Aggregate<graph::search::Knn, I, Metrics> for Aggregator<'_, I>
216where
217 I: crate::recall::RecallCompatible,
218{
219 type Output = Summary;
220
221 fn aggregate(
222 &mut self,
223 run: search::Run<graph::search::Knn>,
224 mut results: Vec<search::SearchResults<I, Metrics>>,
225 ) -> anyhow::Result<Summary> {
226 let recall = match results.first() {
228 Some(first) => crate::recall::knn(
229 self.groundtruth,
230 None,
231 first.ids().as_rows(),
232 self.recall_k,
233 self.recall_n,
234 self.groundtruth_mode,
235 )?,
236 None => anyhow::bail!("Results must be non-empty"),
237 };
238
239 let mut mean_latencies = Vec::with_capacity(results.len());
240 let mut p90_latencies = Vec::with_capacity(results.len());
241 let mut p99_latencies = Vec::with_capacity(results.len());
242
243 results.iter_mut().for_each(|r| {
244 match percentiles::compute_percentiles(r.latencies_mut()) {
245 Ok(values) => {
246 let percentiles::Percentiles { mean, p90, p99, .. } = values;
247 mean_latencies.push(mean);
248 p90_latencies.push(p90);
249 p99_latencies.push(p99);
250 }
251 Err(_) => {
252 let zero = MicroSeconds::new(0);
253 mean_latencies.push(0.0);
254 p90_latencies.push(zero);
255 p99_latencies.push(zero);
256 }
257 }
258 });
259
260 Ok(Summary {
261 setup: run.setup().clone(),
262 parameters: *run.parameters(),
263 end_to_end_latencies: results.iter().map(|r| r.end_to_end_latency()).collect(),
264 recall,
265 mean_latencies,
266 p90_latencies,
267 p99_latencies,
268 mean_cmps: utils::average_all(
269 results
270 .iter()
271 .flat_map(|r| r.output().iter().map(|o| o.comparisons)),
272 ),
273 mean_hops: utils::average_all(
274 results
275 .iter()
276 .flat_map(|r| r.output().iter().map(|o| o.hops)),
277 ),
278 })
279 }
280}
281
282#[cfg(test)]
287mod tests {
288 use std::num::NonZeroUsize;
289
290 use super::*;
291
292 use diskann::graph::test::provider;
293
294 #[test]
295 fn test_knn() {
296 let nearest_neighbors = 5;
297
298 let index = search::graph::test_grid_provider();
299
300 let mut queries = Matrix::new(0.0f32, 5, index.provider().dim());
301 queries.row_mut(0).copy_from_slice(&[0.0, 0.0, 0.0, 0.0]);
302 queries.row_mut(1).copy_from_slice(&[4.0, 0.0, 0.0, 0.0]);
303 queries.row_mut(2).copy_from_slice(&[0.0, 4.0, 0.0, 0.0]);
304 queries.row_mut(3).copy_from_slice(&[0.0, 0.0, 4.0, 0.0]);
305 queries.row_mut(4).copy_from_slice(&[0.0, 0.0, 0.0, 4.0]);
306
307 let queries = Arc::new(queries);
308
309 let knn = KNN::new(
310 index,
311 queries.clone(),
312 Strategy::broadcast(provider::Strategy::new()),
313 )
314 .unwrap();
315
316 let rt = crate::tokio::runtime(2).unwrap();
318 let results = search::search(
319 knn.clone(),
320 graph::search::Knn::new(nearest_neighbors, 10, None).unwrap(),
321 NonZeroUsize::new(2).unwrap(),
322 &rt,
323 )
324 .unwrap();
325
326 assert_eq!(results.len(), queries.nrows());
327 let rows = results.ids().as_rows();
328 assert_eq!(*rows.row(0).first().unwrap(), 0);
329
330 for r in 0..rows.nrows() {
331 assert_eq!(rows.row(r).len(), nearest_neighbors);
332 }
333
334 const TWO: NonZeroUsize = NonZeroUsize::new(2).unwrap();
335 let setup = search::Setup {
336 threads: TWO,
337 tasks: TWO,
338 reps: TWO,
339 };
340
341 let parameters = [
343 search::Run::new(
344 graph::search::Knn::new(nearest_neighbors, 10, None).unwrap(),
345 setup.clone(),
346 ),
347 search::Run::new(
348 graph::search::Knn::new(nearest_neighbors, 15, None).unwrap(),
349 setup.clone(),
350 ),
351 ];
352
353 let recall_k = nearest_neighbors;
354 let recall_n = nearest_neighbors;
355
356 let all = search::search_all(
357 knn,
358 parameters,
359 Aggregator::new(rows, recall_k, recall_n, GroundTruthMode::Fixed),
360 )
361 .unwrap();
362
363 assert_eq!(all.len(), 2);
364 for summary in all {
365 assert_eq!(summary.setup, setup);
366 assert_eq!(summary.end_to_end_latencies.len(), TWO.get());
367 assert_eq!(summary.mean_latencies.len(), TWO.get());
368 assert_eq!(summary.p90_latencies.len(), TWO.get());
369 assert_eq!(summary.p99_latencies.len(), TWO.get());
370
371 assert_ne!(summary.mean_cmps, 0.0);
372 assert_ne!(summary.mean_hops, 0.0);
373
374 let recall = summary.recall;
375 assert_eq!(recall.recall_k, recall_k);
376 assert_eq!(recall.recall_n, recall_n);
377 assert_eq!(recall.num_queries, queries.nrows());
378 assert_eq!(recall.average, 1.0, "we used a search as the groundtruth");
379 }
380 }
381
382 #[test]
383 fn test_knn_error() {
384 let index = search::graph::test_grid_provider();
385
386 let queries = Arc::new(Matrix::new(0.0f32, 1, index.provider().dim()));
387 let strategy = provider::Strategy::new();
388
389 let err = KNN::new(
390 index,
391 queries.clone(),
392 Strategy::collection([strategy.clone(), strategy.clone()]),
393 )
394 .unwrap_err();
395 let msg = err.to_string();
396 assert!(
397 msg.contains("2 strategies were provided when 1 was expected"),
398 "failed with {msg}"
399 );
400 }
401}