Coverage Report

Created: 2026-07-13 08:11

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/diskann-0.54.0/src/flat/index.rs
Line
Count
Source
1
/*
2
 * Copyright (c) Microsoft Corporation.
3
 * Licensed under the MIT license.
4
 */
5
6
//! [`FlatIndex`] — the index wrapper for a [`DataProvider`](crate::provider::DataProvider)
7
//! over which we do flat search.
8
use std::num::NonZeroUsize;
9
10
use diskann_utils::future::SendFuture;
11
12
use crate::{
13
    ANNResult,
14
    error::{ErrorExt, IntoANNResult},
15
    flat::{DistancesUnordered, SearchStrategy},
16
    graph::SearchOutputBuffer,
17
    neighbor::{Neighbor, NeighborPriorityQueue, NeighborPriorityQueueIdType},
18
    provider::DataProvider,
19
};
20
21
/// Statistics collected during a flat search.
22
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
23
pub struct SearchStats {
24
    /// The total number of distance computations performed during the scan.
25
    pub cmps: u32,
26
27
    /// The total number of results written to the output buffer.
28
    pub result_count: u32,
29
}
30
31
/// A thin wrapper around a [`DataProvider`] used for flat search.
32
#[derive(Debug)]
33
pub struct FlatIndex<P: DataProvider> {
34
    /// The backing provider.
35
    provider: P,
36
}
37
38
impl<P: DataProvider> FlatIndex<P> {
39
    /// Construct a new [`FlatIndex`] around `provider`.
40
0
    pub fn new(provider: P) -> Self {
41
0
        Self { provider }
42
0
    }
43
44
    /// Borrow the underlying provider.
45
0
    pub fn provider(&self) -> &P {
46
0
        &self.provider
47
0
    }
48
49
    /// Brute-force k-nearest-neighbor flat search.
50
    ///
51
    /// Streams every element produced by the strategy's visitor through the query
52
    /// computer, keeps the best `k` candidates in a [`NeighborPriorityQueue`], and
53
    /// writes the `(id, distance)` survivors into `output` in best-first order.
54
0
    pub fn knn_search<S, T, OB>(
55
0
        &self,
56
0
        k: NonZeroUsize,
57
0
        strategy: &S,
58
0
        context: &P::Context,
59
0
        query: T,
60
0
        output: &mut OB,
61
0
    ) -> impl SendFuture<ANNResult<SearchStats>>
62
0
    where
63
0
        S: SearchStrategy<P, T>,
64
0
        S::Id: NeighborPriorityQueueIdType,
65
0
        T: Send + Sync,
66
0
        OB: SearchOutputBuffer<S::Id> + Send + ?Sized,
67
    {
68
0
        async move {
69
0
            let mut visitor = strategy
70
0
                .create_visitor(&self.provider, context)
71
0
                .into_ann_result()?;
72
73
0
            let computer = strategy.build_query_computer(query).into_ann_result()?;
74
75
0
            let k = k.get();
76
0
            let mut queue = NeighborPriorityQueue::new(k);
77
0
            let mut cmps: u32 = 0;
78
79
0
            visitor
80
0
                .distances_unordered(&computer, |id, dist| {
81
0
                    cmps += 1;
82
0
                    queue.insert(Neighbor::new(id, dist));
83
0
                })
84
0
                .await
85
0
                .escalate("flat scan must complete to produce correct k-NN results")?;
86
87
0
            let result_count =
88
0
                output.extend(queue.iter().take(k).map(|n| (n.id, n.distance))) as u32;
89
90
0
            Ok(SearchStats { cmps, result_count })
91
0
        }
92
0
    }
93
}
94
95
///////////
96
// Tests //
97
///////////
98
99
#[cfg(test)]
100
mod tests {
101
    use crate::flat::{
102
        FlatIndex,
103
        test::{
104
            harness::KnnOracleRun,
105
            provider::{self as flat_provider},
106
        },
107
    };
108
    use crate::graph::test::synthetic::Grid;
109
110
    fn fixture(grid: Grid, size: usize) -> (FlatIndex<flat_provider::Provider>, usize) {
111
        let provider = flat_provider::Provider::grid(grid, size).unwrap();
112
        let len = provider.len();
113
        (FlatIndex::new(provider), len)
114
    }
115
116
    /// `knn_search` returns a `Send` future, and a shared `&FlatIndex` can serve
117
    /// many concurrent searches on a multi-threaded runtime, each producing the
118
    /// correct top-k independently.
119
    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
120
    async fn multithreaded_knn_search() {
121
        use std::sync::Arc;
122
123
        let (index, len) = fixture(Grid::Two, 4);
124
        let index = Arc::new(index);
125
126
        // Mix of corner, axis-aligned, and off-grid queries; k spans 1..=len.
127
        let cases: &[(&[f32], usize)] = &[
128
            (&[-1.0, -1.0], 1),
129
            (&[1.0, 1.0], len),
130
            (&[-1.0, 1.0], len / 2),
131
            (&[1.0, -1.0], len - 1),
132
            (&[0.0, 0.0], 3),
133
            (&[3.0, 3.0], len),
134
            (&[-2.0, 0.5], 2),
135
            (&[0.5, -0.5], len),
136
        ];
137
138
        let mut set = tokio::task::JoinSet::new();
139
        for (query, k) in cases {
140
            let index = Arc::clone(&index);
141
            let query: Vec<f32> = query.to_vec();
142
            let k = *k;
143
            set.spawn(async move {
144
                let outcome = KnnOracleRun::run(
145
                    &index,
146
                    &flat_provider::Strategy::new(index.provider().dim()),
147
                    &query,
148
                    k,
149
                )
150
                .await
151
                .expect("knn_search failed");
152
                (query, k, outcome)
153
            });
154
        }
155
156
        while let Some(joined) = set.join_next().await {
157
            let (query, k, outcome) = joined.expect("task panicked");
158
            assert_eq!(
159
                outcome.top_k, outcome.ground_truth,
160
                "query = {query:?}, k = {k}: top-k must match brute force",
161
            );
162
            assert_eq!(outcome.stats.cmps as usize, len);
163
            assert_eq!(outcome.stats.result_count as usize, k.min(len));
164
        }
165
    }
166
167
    ////////////
168
    // Errors //
169
    ////////////
170
171
    /// A transient error from the visitor's scan must escalate up through `knn_search`.
172
    #[test]
173
    fn transient_scan_error() {
174
        // The flat scan touches every id, so any transient id is guaranteed to be hit.
175
        for transient_ids in [&[0u32][..], &[3][..], &[1, 2, 5][..]] {
176
            let strategy =
177
                flat_provider::Strategy::with_transient(2, transient_ids.iter().copied());
178
            let (index, _) = fixture(Grid::Two, 3);
179
            let err = KnnOracleRun::run_sync(&index, &strategy, &[1.0, 0.0], 4)
180
                .expect_err("transient error during full scan must escalate");
181
182
            let msg = format!("{err}");
183
            assert!(
184
                transient_ids
185
                    .iter()
186
                    .any(|id| msg.contains(&format!("id {id}"))),
187
                "transients = {transient_ids:?}: expected error to name one of the \
188
                 transient ids, got: {msg}",
189
            );
190
        }
191
    }
192
193
    /// Run `knn_search` via the harness, assert it fails, and check the error
194
    /// message contains `expected_msg`.
195
    fn assert_search_error(strategy: &flat_provider::Strategy, query: &[f32], expected_msg: &str) {
196
        let (index, _) = fixture(Grid::Two, 3);
197
        let err = KnnOracleRun::run_sync(&index, strategy, query, 4)
198
            .expect_err("expected knn_search to fail");
199
200
        let msg = format!("{err}");
201
        assert!(
202
            msg.contains(expected_msg),
203
            "expected error containing {expected_msg:?}, got: {msg}",
204
        );
205
    }
206
207
    #[test]
208
    fn strategy_constructor_errors() {
209
        // Strategy/provider expect dim=2, query has dim=3.
210
        assert_search_error(
211
            &flat_provider::Strategy::new(2),
212
            &[0.0, 0.0, 0.0],
213
            "dimension mismatch",
214
        );
215
216
        // Strategy expects dim=5, provider has dim=2.
217
        assert_search_error(
218
            &flat_provider::Strategy::new(5),
219
            &[0.0, 0.0],
220
            "dimension mismatch",
221
        );
222
    }
223
}