1use std::{collections::HashSet, hash::Hash};
7
8use diskann_utils::{
9 strided::StridedView,
10 views::{Matrix, MatrixView},
11};
12use thiserror::Error;
13
14#[derive(Debug, Clone)]
15#[non_exhaustive]
16pub struct RecallMetrics {
17 pub recall_k: usize,
19 pub recall_n: usize,
21 pub num_queries: usize,
23 pub average: f64,
26}
27
28#[derive(Debug, Error)]
29pub enum ComputeRecallError {
30 #[error("results matrix has {0} rows but ground truth has {1}")]
31 RowsMismatch(usize, usize),
32 #[error("distances matrix has {0} rows but ground truth has {1}")]
33 DistanceRowsMismatch(usize, usize),
34 #[error("recall k value {0} must be less than or equal to recall n {1}")]
35 RecallKAndNError(usize, usize),
36 #[error(
37 "number of groundtruth values per query {0} must be at least the specified recall n {1}"
38 )]
39 NotEnoughGroundTruth(usize, usize),
40 #[error("number of groundtruth distances {0} does not match groundtruth entries {1}")]
41 GroundTruthDistanceMismatch(usize, usize),
42}
43
44pub trait Rows<T> {
64 fn nrows(&self) -> usize;
66
67 fn row(&self, i: usize) -> &[T];
69
70 fn ncols(&self) -> Option<usize> {
77 None
78 }
79}
80
81impl<T> Rows<T> for Matrix<T> {
82 fn nrows(&self) -> usize {
83 Matrix::<T>::nrows(self)
84 }
85 fn row(&self, i: usize) -> &[T] {
86 Matrix::<T>::row(self, i)
87 }
88 fn ncols(&self) -> Option<usize> {
89 Some(Matrix::<T>::ncols(self))
90 }
91}
92
93impl<T> Rows<T> for MatrixView<'_, T> {
94 fn nrows(&self) -> usize {
95 MatrixView::<'_, T>::nrows(self)
96 }
97 fn row(&self, i: usize) -> &[T] {
98 MatrixView::<'_, T>::row(self, i)
99 }
100 fn ncols(&self) -> Option<usize> {
101 Some(MatrixView::<'_, T>::ncols(self))
102 }
103}
104
105impl<T> Rows<T> for Vec<Vec<T>> {
106 fn nrows(&self) -> usize {
107 self.len()
108 }
109 fn row(&self, i: usize) -> &[T] {
110 &self[i]
111 }
112}
113
114pub trait RecallCompatible: Eq + Hash + Clone + std::fmt::Debug {}
116
117impl<T> RecallCompatible for T where T: Eq + Hash + Clone + std::fmt::Debug {}
118
119#[derive(Copy, Clone, Debug)]
123pub enum GroundTruthMode {
124 Fixed,
125 Flexible,
126}
127
128pub fn knn<T>(
144 groundtruth: &dyn Rows<T>,
145 groundtruth_distances: Option<StridedView<'_, f32>>,
146 results: &dyn Rows<T>,
147 recall_k: usize,
148 recall_n: usize,
149 ground_truth_mode: GroundTruthMode,
150) -> Result<RecallMetrics, ComputeRecallError>
151where
152 T: RecallCompatible,
153{
154 if recall_k > recall_n {
155 return Err(ComputeRecallError::RecallKAndNError(recall_k, recall_n));
156 }
157
158 let nrows = results.nrows();
159 if nrows != groundtruth.nrows() {
160 return Err(ComputeRecallError::RowsMismatch(nrows, groundtruth.nrows()));
161 }
162
163 if let GroundTruthMode::Fixed = ground_truth_mode {
164 for i in 0..nrows {
166 let gt_row = groundtruth.row(i);
167 if gt_row.len() < recall_k {
168 return Err(ComputeRecallError::NotEnoughGroundTruth(
169 gt_row.len(),
170 recall_k,
171 ));
172 }
173 }
174 }
175
176 if let Some(distances) = groundtruth_distances {
180 if nrows != distances.nrows() {
181 return Err(ComputeRecallError::DistanceRowsMismatch(
182 distances.nrows(),
183 nrows,
184 ));
185 }
186
187 for i in 0..nrows {
188 let gt_row = groundtruth.row(i);
189 let distances_row = distances.row(i);
190 if gt_row.len() != distances_row.len() {
191 return Err(ComputeRecallError::GroundTruthDistanceMismatch(
192 distances_row.len(),
193 gt_row.len(),
194 ));
195 }
196 }
197 }
198
199 let mut recall_values: Vec<f64> = Vec::new();
201 let mut this_groundtruth = HashSet::new();
202 let mut this_results = HashSet::new();
203
204 for i in 0..results.nrows() {
205 let result = results.row(i);
206
207 let gt_row = groundtruth.row(i);
208 let this_recall_k = gt_row.len().min(recall_k);
211
212 if this_recall_k == 0 {
213 continue;
214 }
215
216 this_groundtruth.clear();
218 this_groundtruth.extend(gt_row.iter().take(this_recall_k).cloned());
219
220 if let Some(distances) = groundtruth_distances {
223 let distances_row = distances.row(i);
224
225 let last_distance = distances_row[this_recall_k - 1];
228 for (d, g) in distances_row.iter().zip(gt_row.iter()).skip(this_recall_k) {
229 if *d == last_distance {
230 this_groundtruth.insert(g.clone());
231 } else {
232 break;
233 }
234 }
235 }
236
237 this_results.clear();
238 this_results.extend(result.iter().take(recall_n).cloned());
239
240 let r = this_groundtruth
242 .iter()
243 .filter(|i| this_results.contains(i))
244 .count()
245 .min(this_recall_k);
246
247 let recall = (r as f64) / (this_recall_k as f64);
248
249 recall_values.push(recall);
250 }
251
252 let total: f64 = recall_values.iter().sum();
254 let average = if recall_values.is_empty() {
255 0.0
256 } else {
257 total / (recall_values.len() as f64)
258 };
259
260 Ok(RecallMetrics {
261 recall_k,
262 recall_n,
263 num_queries: nrows,
264 average,
265 })
266}
267
268#[derive(Debug, Clone)]
269#[non_exhaustive]
270pub struct AveragePrecisionMetrics {
271 pub num_queries: usize,
273 pub average_precision: f64,
275}
276
277#[derive(Debug, Error)]
278pub enum AveragePrecisionError {
279 #[error("results has {0} elements but ground truth has {1}")]
280 EntriesMismatch(usize, usize),
281}
282
283pub fn average_precision<T>(
285 results: &dyn Rows<T>,
286 groundtruth: &dyn Rows<T>,
287) -> Result<AveragePrecisionMetrics, AveragePrecisionError>
288where
289 T: RecallCompatible,
290{
291 let nrows = results.nrows();
292 let groundtruth_nrows = groundtruth.nrows();
293 if nrows != groundtruth_nrows {
294 return Err(AveragePrecisionError::EntriesMismatch(
295 nrows,
296 groundtruth_nrows,
297 ));
298 }
299
300 let mut num_gt_results = 0;
302 let mut num_reported_results = 0;
303
304 let mut scratch = HashSet::new();
305 let nrows = results.nrows();
306
307 for i in 0..nrows {
308 let result = results.row(i);
309 let gt = groundtruth.row(i);
310
311 scratch.clear();
312 scratch.extend(result.iter().cloned());
313 num_reported_results += gt.iter().filter(|i| scratch.contains(i)).count();
314 num_gt_results += gt.len();
315 }
316
317 let average_precision = (num_reported_results as f64) / (num_gt_results as f64);
319
320 Ok(AveragePrecisionMetrics {
321 average_precision,
322 num_queries: nrows,
323 })
324}
325
326#[cfg(test)]
331mod tests {
332 use diskann_utils::views::{self, Matrix};
333
334 use super::*;
335
336 fn test_rows_inner(rows: &dyn Rows<usize>, ncols: Option<usize>) {
337 assert_eq!(rows.ncols(), ncols);
338 assert_eq!(rows.nrows(), 3);
339 assert_eq!(rows.row(0), &[0, 1, 2, 3]);
340 assert_eq!(rows.row(1), &[4, 5, 6, 7]);
341 assert_eq!(rows.row(2), &[8, 9, 10, 11]);
342 }
343
344 #[test]
345 fn test_rows() {
346 let mut i = 0usize;
347 let mat = Matrix::new(
348 views::Init(|| {
349 let v = i;
350 i += 1;
351 v
352 }),
353 3,
354 4,
355 );
356
357 test_rows_inner(&mat, Some(4));
358 test_rows_inner(&(mat.as_view()), Some(4));
359
360 let vecs = vec![vec![0, 1, 2, 3], vec![4, 5, 6, 7], vec![8, 9, 10, 11]];
361 test_rows_inner(&vecs, None);
362 }
363
364 struct ExpectedRecall {
365 recall_k: usize,
366 recall_n: usize,
367 components: Vec<usize>,
369 }
370
371 impl ExpectedRecall {
372 fn new(recall_k: usize, recall_n: usize, components: Vec<usize>) -> Self {
373 assert!(recall_k <= recall_n);
374 components.iter().for_each(|x| {
375 assert!(*x <= recall_k);
376 });
377 Self {
378 recall_k,
379 recall_n,
380 components,
381 }
382 }
383
384 fn compute_recall(&self) -> f64 {
385 (self.components.iter().sum::<usize>() as f64)
386 / ((self.components.len() * self.recall_k) as f64)
387 }
388 }
389
390 #[test]
391 fn test_happy_path() {
392 let groundtruth = Matrix::try_from(
393 vec![
394 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ]
399 .into(),
400 4,
401 10,
402 )
403 .unwrap();
404
405 let distances = Matrix::try_from(
406 vec![
407 0.0, 1.0, 2.0, 3.0, 3.0, 3.0, 3.0, 4.0, 5.0, 6.0, 2.0, 3.0, 3.0, 3.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 0.0, 1.0, 2.0, 3.0, 3.0, 3.0, 3.0, 4.0, 5.0, 6.0, ]
412 .into(),
413 4,
414 10,
415 )
416 .unwrap();
417
418 let our_results = Matrix::try_from(
420 vec![
421 100, 0, 1, 2, 5, 6, 100, 101, 7, 8, 9, 10, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, ]
426 .into(),
427 4,
428 6,
429 )
430 .unwrap();
431
432 let expected_no_ties = vec![
436 ExpectedRecall::new(1, 1, vec![0, 0, 1, 1]),
438 ExpectedRecall::new(2, 2, vec![1, 0, 2, 2]),
439 ExpectedRecall::new(3, 3, vec![2, 1, 3, 3]),
440 ExpectedRecall::new(4, 4, vec![3, 2, 4, 4]),
441 ExpectedRecall::new(5, 5, vec![3, 3, 5, 5]),
442 ExpectedRecall::new(6, 6, vec![4, 4, 6, 6]),
443 ExpectedRecall::new(1, 2, vec![1, 0, 1, 1]),
445 ExpectedRecall::new(1, 3, vec![1, 0, 1, 1]),
446 ExpectedRecall::new(2, 3, vec![2, 0, 2, 2]),
447 ExpectedRecall::new(3, 5, vec![3, 1, 3, 3]),
448 ];
449 let epsilon = 1e-6; for (i, expected) in expected_no_ties.iter().enumerate() {
452 assert_eq!(expected.components.len(), our_results.nrows());
453 let recall = knn(
454 &groundtruth,
455 None,
456 &our_results,
457 expected.recall_k,
458 expected.recall_n,
459 GroundTruthMode::Fixed,
460 )
461 .unwrap();
462
463 let left = recall.average;
464 let right = expected.compute_recall();
465 assert!(
466 (left - right).abs() < epsilon,
467 "left = {}, right = {} on input {}",
468 left,
469 right,
470 i
471 );
472
473 assert_eq!(recall.num_queries, our_results.nrows());
474 assert_eq!(recall.recall_k, expected.recall_k);
475 assert_eq!(recall.recall_n, expected.recall_n);
476 }
477
478 let expected_with_ties = vec![
482 ExpectedRecall::new(1, 1, vec![0, 0, 1, 1]),
484 ExpectedRecall::new(2, 2, vec![1, 0, 2, 2]),
485 ExpectedRecall::new(3, 3, vec![2, 1, 3, 3]),
486 ExpectedRecall::new(4, 4, vec![3, 2, 4, 4]),
487 ExpectedRecall::new(5, 5, vec![4, 3, 5, 5]), ExpectedRecall::new(6, 6, vec![5, 4, 6, 6]), ExpectedRecall::new(1, 2, vec![1, 0, 1, 1]),
491 ExpectedRecall::new(1, 3, vec![1, 0, 1, 1]),
492 ExpectedRecall::new(2, 3, vec![2, 1, 2, 2]),
493 ExpectedRecall::new(4, 5, vec![4, 3, 4, 4]),
494 ];
495
496 for (i, expected) in expected_with_ties.iter().enumerate() {
497 assert_eq!(expected.components.len(), our_results.nrows());
498 let recall = knn(
499 &groundtruth,
500 Some(distances.as_view().into()),
501 &our_results,
502 expected.recall_k,
503 expected.recall_n,
504 GroundTruthMode::Fixed,
505 )
506 .unwrap();
507
508 let left = recall.average;
509 let right = expected.compute_recall();
510 assert!(
511 (left - right).abs() < epsilon,
512 "left = {}, right = {} on input {}",
513 left,
514 right,
515 i
516 );
517
518 assert_eq!(recall.num_queries, our_results.nrows());
519 assert_eq!(recall.recall_k, expected.recall_k);
520 assert_eq!(recall.recall_n, expected.recall_n);
521 }
522 }
523
524 #[test]
525 fn test_error_recall_k_and_n() {
526 let groundtruth = Matrix::<u32>::new(0, 10, 10);
527 let results = Matrix::<u32>::new(0, 10, 10);
528 let err = knn(&groundtruth, None, &results, 11, 10, GroundTruthMode::Fixed).unwrap_err();
529 assert!(matches!(err, ComputeRecallError::RecallKAndNError(..)));
530 }
531
532 #[test]
533 fn test_error_rows_mismatch() {
534 let groundtruth = Matrix::<u32>::new(0, 11, 10);
535 let results = Matrix::<u32>::new(0, 10, 10);
536 let err = knn(&groundtruth, None, &results, 10, 10, GroundTruthMode::Fixed).unwrap_err();
537 assert!(matches!(err, ComputeRecallError::RowsMismatch(..)));
538 let err_allow_insufficient_results =
539 knn(&groundtruth, None, &results, 10, 10, GroundTruthMode::Fixed).unwrap_err();
540 assert!(matches!(
541 err_allow_insufficient_results,
542 ComputeRecallError::RowsMismatch(..)
543 ));
544 }
545
546 #[test]
547 fn test_error_not_enough_groundtruth() {
548 let groundtruth = Matrix::<u32>::new(0, 10, 5);
549 let results = Matrix::<u32>::new(0, 10, 10);
550 let err = knn(&groundtruth, None, &results, 10, 10, GroundTruthMode::Fixed).unwrap_err();
551 assert!(matches!(err, ComputeRecallError::NotEnoughGroundTruth(..)));
552 let err_allow_insufficient_results =
553 knn(&groundtruth, None, &results, 10, 10, GroundTruthMode::Fixed).unwrap_err();
554 assert!(matches!(
555 err_allow_insufficient_results,
556 ComputeRecallError::NotEnoughGroundTruth(..)
557 ));
558 }
559
560 #[test]
561 fn test_dynamic_groundtruth_valid() {
562 let groundtruth: Vec<_> = (0..10).map(|_| vec![0u32; 5]).collect();
563 let results = Matrix::<u32>::new(0, 10, 10);
564 let recall_flexible = knn(
567 &groundtruth,
568 None,
569 &results,
570 10,
571 10,
572 GroundTruthMode::Flexible,
573 )
574 .unwrap();
575 assert_eq!(recall_flexible.num_queries, 10);
576 let err = knn(&groundtruth, None, &results, 10, 10, GroundTruthMode::Fixed).unwrap_err();
578 assert!(matches!(err, ComputeRecallError::NotEnoughGroundTruth(..)));
579 assert_eq!(recall_flexible.num_queries, 10);
580 }
581
582 #[test]
583 fn test_dynamic_groundtruth_full_match() {
584 let gt_row: Vec<u32> = (1..=5).collect();
585 let groundtruth: Vec<_> = (0..10).map(|_| gt_row.clone()).collect();
586 let mut results = Matrix::<u32>::new(0, 10, 10);
587 for i in 0..10 {
588 for (j, v) in (1u32..=10).enumerate() {
589 results[(i, j)] = v;
590 }
591 }
592 let recall = knn(
593 &groundtruth,
594 None,
595 &results,
596 10,
597 10,
598 GroundTruthMode::Flexible,
599 )
600 .unwrap();
601 assert!((recall.average - 1.0).abs() < 1e-10);
602 }
603
604 #[test]
605 fn test_dynamic_groundtruth_partial_match() {
606 let gt_row: Vec<u32> = (1..=5).collect();
608 let groundtruth: Vec<_> = (0..10).map(|_| gt_row.clone()).collect();
609 let mut results = Matrix::<u32>::new(0, 10, 10);
610 let res_row: Vec<u32> = vec![1, 2, 3, 6, 7, 8, 9, 10, 11, 12];
611 for i in 0..10 {
612 for (j, &v) in res_row.iter().enumerate() {
613 results[(i, j)] = v;
614 }
615 }
616 let recall = knn(
617 &groundtruth,
618 None,
619 &results,
620 10,
621 10,
622 GroundTruthMode::Flexible,
623 )
624 .unwrap();
625 assert!((recall.average - 0.6).abs() < 1e-10);
626 }
627
628 #[test]
629 fn test_dynamic_groundtruth_mixed_zero_nonzero() {
630 let mut groundtruth: Vec<Vec<u32>> = Vec::new();
631 for _ in 0..5 {
633 groundtruth.push((1..=5).collect());
634 }
635 for _ in 0..5 {
637 groundtruth.push(vec![]);
638 }
639
640 let mut results = Matrix::<u32>::new(0, 10, 10);
641 for i in 0..10 {
642 for (j, v) in (1u32..=10).enumerate() {
643 results[(i, j)] = v;
644 }
645 }
646
647 let recall = knn(
648 &groundtruth,
649 None,
650 &results,
651 10,
652 10,
653 GroundTruthMode::Flexible,
654 )
655 .unwrap();
656 assert_eq!(recall.num_queries, 10);
657 assert!((recall.average - 1.0).abs() < 1e-10);
658 }
659
660 #[test]
661 fn test_dynamic_groundtruth_all_zero() {
662 let groundtruth: Vec<Vec<u32>> = (0..10).map(|_| vec![]).collect();
663 let results = Matrix::<u32>::new(0, 10, 10);
664
665 let recall = knn(
666 &groundtruth,
667 None,
668 &results,
669 10,
670 10,
671 GroundTruthMode::Flexible,
672 )
673 .unwrap();
674 assert_eq!(recall.num_queries, 10);
675 assert_eq!(recall.average, 0.0);
676 assert!(!recall.average.is_nan());
677 assert!(!recall.average.is_infinite());
678 }
679
680 #[test]
681 fn test_error_distance_rows_mismatch() {
682 let groundtruth = Matrix::<u32>::new(0, 10, 10);
683 let distances = Matrix::<f32>::new(0.0, 9, 10);
684 let results = Matrix::<u32>::new(0, 10, 10);
685 let err = knn(
686 &groundtruth,
687 Some(distances.as_view().into()),
688 &results,
689 10,
690 10,
691 GroundTruthMode::Fixed,
692 )
693 .unwrap_err();
694 assert!(matches!(err, ComputeRecallError::DistanceRowsMismatch(..)));
695 }
696
697 #[test]
698 fn test_error_distance_cols_mismatch() {
699 let groundtruth = Matrix::<u32>::new(0, 10, 10);
700 let distances = Matrix::<f32>::new(0.0, 10, 9);
701 let results = Matrix::<u32>::new(0, 10, 10);
702 let err = knn(
703 &groundtruth,
704 Some(distances.as_view().into()),
705 &results,
706 10,
707 10,
708 GroundTruthMode::Fixed,
709 )
710 .unwrap_err();
711 assert!(matches!(
712 err,
713 ComputeRecallError::GroundTruthDistanceMismatch(..)
714 ));
715 }
716
717 #[test]
718 fn test_error_distance_cols_mismatch_variable_size_groundtruth() {
719 let groundtruth: Vec<Vec<u32>> = vec![vec![1, 2, 3], vec![4, 5]];
721 let distances: Vec<Vec<f32>> = vec![vec![0.1, 0.2], vec![0.3, 0.4]];
723 let distances = Matrix::try_from(
724 distances.into_iter().flatten().collect::<Vec<_>>().into(),
725 2,
726 2,
727 )
728 .unwrap();
729 let results: Vec<Vec<u32>> = vec![vec![1, 2, 3], vec![4, 5, 6]];
731 let err = knn(
732 &groundtruth,
733 Some(distances.as_view().into()),
734 &results,
735 3,
736 3,
737 GroundTruthMode::Flexible,
738 )
739 .unwrap_err();
740
741 println!("{err}");
742
743 assert!(matches!(
744 err,
745 ComputeRecallError::GroundTruthDistanceMismatch(2, 3)
746 ));
747 }
748}