Skip to main content

diskann_benchmark_core/
recall.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6use 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    /// The `k` value for `k-recall-at-n`.
18    pub recall_k: usize,
19    /// The `n` value for `k-recall-at-n`.
20    pub recall_n: usize,
21    /// The number of queries.
22    pub num_queries: usize,
23    /// The average recall across queries with non-empty groundtruth.
24    /// Queries with zero groundtruth results are excluded from the average.
25    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
44/// An abstraction over data-structures such as vector-of-vectors.
45///
46/// This is used in recall calculations such as [`knn`] and [`average_precision`] and
47/// is purposely `dyn` compatible to reduce compilation overhead.
48///
49/// Implementations should ensure that if [`Self::nrows`] returns a value `N` that `row(i)`
50/// returns a slice for all `i` in `0..N`. Accesses outside of that range are allowed to
51/// panic.
52///
53/// The implementation [`Self::ncols`] is optional and can be implemented if the length of
54/// all inner vectors is known and identical for all inner vectors, enabling faster error
55/// paths during recall calculation.
56///
57/// If [`Self::ncols`] returns `Some(K)`, then the length of each slice returned from
58/// [`Self::row`] should have a length equal to `K`. Note that unsafe code may **not** rely
59/// on this behavior.
60///
61/// If [`Self::ncols`] returns `None` then no assumption can be made about the length of
62/// the slices yielded from [`Self::row`].
63pub trait Rows<T> {
64    /// Return the number of subslices in `Self`.
65    fn nrows(&self) -> usize;
66
67    /// Return the `i`th subslice contained in self.
68    fn row(&self, i: usize) -> &[T];
69
70    /// Return `Some(K)` if all subslices are known to have length `K`. Otherwise, return
71    /// `None`.
72    ///
73    /// # Provided Implementation
74    ///
75    /// The provided implementation returns `None`.
76    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
114/// Aggregate trait for required behavior when computing recall and average precision.
115pub trait RecallCompatible: Eq + Hash + Clone + std::fmt::Debug {}
116
117impl<T> RecallCompatible for T where T: Eq + Hash + Clone + std::fmt::Debug {}
118
119// Enum representing whether the ground truth has fixed size
120// (every row should have the same number, >= recall_k entries)
121// or flexible size (row may have any number of entries, including none)
122#[derive(Copy, Clone, Debug)]
123pub enum GroundTruthMode {
124    Fixed,
125    Flexible,
126}
127
128/// Compute the K-nearest-neighbors recall value "K-recall-at-N".
129///
130/// For each entry in `groundtruth` and `results`, this computes the `recall_k` number of
131/// elements of `groundtruth` that are present in the first `recall_n` entries of `results`.
132///
133/// If `groundtruth_distances` is provided, then it will be used to allow ties when matching
134/// the last values of each entry of `results`. Values will be counted towards the recall if
135/// they have the same distance as the last ordered candidate.
136///
137/// Note that an error will NOT be given if an entry in `results`
138/// has fewer than `recall_n` candidates.
139///
140/// If `ground_truth_mode` is `GroundTruthMode::Fixed`, then an
141/// error will be given if any entry in `groundtruth` has fewer
142/// than `recall_k` candidates.
143pub 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        // Validate that all rows in `groundtruth` have at least `recall_k` entries.
165        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 `groundtruth_distances` are present, validate that there are
177    // enough rows and that each row has the same
178    // number of entries as the corresponding row in `groundtruth`.
179    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    // The actual recall computation for groundtruth
200    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        // `groundtruth` does not have to be fixed-size,
209        // so we compute `recall_k` for this row based on its gt length
210        let this_recall_k = gt_row.len().min(recall_k);
211
212        if this_recall_k == 0 {
213            continue;
214        }
215
216        // Populate the groundtruth using the top-k
217        this_groundtruth.clear();
218        this_groundtruth.extend(gt_row.iter().take(this_recall_k).cloned());
219
220        // If we have distances, then continue to append distances as long as the distance
221        // value is constant
222        if let Some(distances) = groundtruth_distances {
223            let distances_row = distances.row(i);
224
225            // we've already checked that `results` and `distances` have at lesat
226            // `recall_k >= this_recall_k` entries, so it's safe to access `distances_row[this_recall_k - 1]`
227            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        // Count the overlap
241        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    // Compute the average recall
253    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    /// The number of queries.
272    pub num_queries: usize,
273    /// The average precision
274    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
283/// Compute average precision of a range search result
284pub 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    // The actual recall computation.
301    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    // Perform post-processing.
318    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///////////
327// Tests //
328///////////
329
330#[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        // Recall for each component.
368        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, // row 0
395                5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // row 1
396                0, 1, 2, 3, 4, 5, 6, 7, 8, 9, // row 2
397                0, 1, 2, 3, 4, 5, 6, 7, 8, 9, // row 3
398            ]
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, // row 0
408                2.0, 3.0, 3.0, 3.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, // row 1
409                0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, // row 2
410                0.0, 1.0, 2.0, 3.0, 3.0, 3.0, 3.0, 4.0, 5.0, 6.0, // row 3
411            ]
412            .into(),
413            4,
414            10,
415        )
416        .unwrap();
417
418        // Shift row 0 by one and row 1 by two.
419        let our_results = Matrix::try_from(
420            vec![
421                100, 0, 1, 2, 5, 6, // row 0
422                100, 101, 7, 8, 9, 10, // row 1
423                0, 1, 2, 3, 4, 5, // row 2
424                0, 1, 2, 3, 4, 5, // row 3
425            ]
426            .into(),
427            4,
428            6,
429        )
430        .unwrap();
431
432        //---------//
433        // No Ties //
434        //---------//
435        let expected_no_ties = vec![
436            // Equal `k` and `n`
437            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            // Unequal `k` and `n`.
444            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; // Define a small tolerance
450
451        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        //-----------//
479        // With Ties //
480        //-----------//
481        let expected_with_ties = vec![
482            // Equal `k` and `n`
483            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]), // tie-breaker kicks in
488            ExpectedRecall::new(6, 6, vec![5, 4, 6, 6]), // tie-breaker kicks in
489            // Unequal `k` and `n`.
490            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        // Should succeed: each row uses this_recall_k = min(5, 10) = 5
565        // Should succeed in Flexible mode, but fail in Fixed mode
566        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        // Should fail in Fixed mode
577        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        // groundtruth: [1, 2, 3, 4, 5]; results contain [1, 2, 3, 6, 7, 8, 9, 10, 11, 12]
607        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        // First 5 rows: non-empty groundtruth
632        for _ in 0..5 {
633            groundtruth.push((1..=5).collect());
634        }
635        // Last 5 rows: empty groundtruth
636        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        // groundtruth: 2 rows, first row has 3 elements, second row has 2
720        let groundtruth: Vec<Vec<u32>> = vec![vec![1, 2, 3], vec![4, 5]];
721        // distances: first row has 2 elements (should be 3), second row has 2 (matches)
722        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        // results: 2 rows, each with 3 elements
730        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}