diskann 0.55.0

DiskANN3 is a composable library for bringing scalable, accurate and cost-effective vector indexing to multiple databases.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
/*
 * Copyright (c) Microsoft Corporation.
 * Licensed under the MIT license.
 */

//! Self-contained test provider for the flat-search module.

use std::{
    borrow::Cow,
    collections::HashSet,
    fmt::{self, Debug},
    future::Future,
    sync::Arc,
};

use diskann_utils::{future::SendFuture, views::Matrix};
use diskann_vector::{PreprocessedDistanceFunction, distance::Metric};
use thiserror::Error;

use crate::{
    ANNError, always_escalate,
    error::{RankedError, ToRanked, TransientError},
    flat::{DistancesUnordered, SearchStrategy},
    graph::test::synthetic::Grid,
    internal::counter::{Counter, LocalCounter},
    provider::{self, ExecutionContext, HasId, NoopGuard},
    utils::VectorRepr,
};

/// Error conditions for [`Provider::new`].
#[derive(Debug, Error)]
pub enum ProviderError {
    #[error("flat::test::Provider needs at least one item")]
    Empty,
    #[error("flat::test::Provider items must have non-zero dimension")]
    ZeroDimension,
}

impl From<ProviderError> for ANNError {
    #[track_caller]
    fn from(err: ProviderError) -> ANNError {
        ANNError::opaque(err)
    }
}

//////////////
// Provider //
//////////////

/// In-memory test provider for flat search.
#[derive(Debug)]
pub struct Provider {
    items: Matrix<f32>,
    get_element: Counter,
}

impl Provider {
    /// Construct a provider from a matrix of vectors.
    ///
    /// # Errors
    ///
    /// Returns an error if the matrix is empty or has zero-width columns.
    pub fn new(items: Matrix<f32>) -> Result<Self, ProviderError> {
        if items.nrows() == 0 {
            return Err(ProviderError::Empty);
        }
        if items.ncols() == 0 {
            return Err(ProviderError::ZeroDimension);
        }
        Ok(Self {
            items,
            get_element: Counter::new(),
        })
    }

    /// Build a provider over the row vectors of [`Grid::data`]. IDs are `0..n` in
    /// row-major order (last coordinate varies fastest).
    ///
    /// Unlike the graph-side `Provider::grid`, this does *not* add a separate
    /// start-point row — flat search has no notion of one.
    ///
    /// # Errors
    ///
    /// Propagates errors from [`Self::new`].
    pub fn grid(grid: Grid, size: usize) -> Result<Self, ProviderError> {
        Self::new(grid.data(size))
    }

    /// Number of vectors in the provider.
    pub fn len(&self) -> usize {
        self.items.nrows()
    }

    /// Dimension of each vector in the provider.
    pub fn dim(&self) -> usize {
        self.items.ncols()
    }

    /// Snapshot of the per-provider counters.
    pub fn metrics(&self) -> ElementCounter {
        ElementCounter {
            count: self.get_element.value(),
        }
    }

    /// Expose the items for brute force.
    pub fn items(&self) -> &Matrix<f32> {
        &self.items
    }
}

/// Counters tracked by [`Provider`].
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct ElementCounter {
    /// The number of times any [`Visitor`] yielded an element.
    pub count: usize,
}

#[cfg(test)]
crate::test::cmp::verbose_eq!(ElementCounter { count });

/////////////
// Context //
/////////////

/// Per-search execution context. No spawn/clone tracking — flat search runs on
/// the calling task and never spawns.
#[derive(Debug, Clone, Default)]
pub struct Context;

impl Context {
    pub fn new() -> Self {
        Self
    }
}

impl ExecutionContext for Context {
    fn wrap_spawn<F, T>(&self, f: F) -> impl Future<Output = T> + Send + 'static
    where
        F: Future<Output = T> + Send + 'static,
    {
        f
    }
}

/////////////////////
// Errors / Guards //
/////////////////////

/// Critical id-validation error: the requested id is out of range.
#[derive(Debug, Clone, Copy, Error, PartialEq, Eq)]
#[error("flat::test::Provider has no id {0}")]
pub struct InvalidId(pub u32);

always_escalate!(InvalidId);

impl From<InvalidId> for ANNError {
    #[track_caller]
    fn from(err: InvalidId) -> ANNError {
        ANNError::opaque(err)
    }
}

/// Transient access error injected by [`Visitor::flaky`].
///
/// Matches the shape of `graph::test::TransientAccessError`: panics in `Drop` if it
/// is dropped without being acknowledged or escalated. This guards against accidental
/// silent suppression of the error in the test code itself.
#[must_use]
#[derive(Debug)]
pub struct TransientGetError {
    id: u32,
    handled: bool,
}

impl TransientGetError {
    fn new(id: u32) -> Self {
        Self { id, handled: false }
    }
}

impl fmt::Display for TransientGetError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "transient failure retrieving id {}", self.id)
    }
}

impl std::error::Error for TransientGetError {}

impl Drop for TransientGetError {
    fn drop(&mut self) {
        assert!(
            self.handled,
            "dropped an unhandled TransientGetError for id {}",
            self.id,
        );
    }
}

impl TransientError<InvalidId> for TransientGetError {
    fn acknowledge<D>(mut self, _why: D)
    where
        D: fmt::Display,
    {
        self.handled = true;
    }

    fn escalate<D>(mut self, _why: D) -> InvalidId
    where
        D: fmt::Display,
    {
        self.handled = true;
        InvalidId(self.id)
    }
}

/// Two-tier error for [`Visitor::distances_unordered`]: a critical [`InvalidId`]
/// or a recoverable [`TransientGetError`].
#[derive(Debug)]
pub enum AccessError {
    InvalidId(InvalidId),
    Transient(TransientGetError),
}

impl fmt::Display for AccessError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidId(e) => fmt::Display::fmt(e, f),
            Self::Transient(e) => fmt::Display::fmt(e, f),
        }
    }
}

impl std::error::Error for AccessError {}

impl ToRanked for AccessError {
    type Transient = TransientGetError;
    type Error = InvalidId;

    fn to_ranked(self) -> RankedError<TransientGetError, InvalidId> {
        match self {
            Self::InvalidId(e) => RankedError::Error(e),
            Self::Transient(e) => RankedError::Transient(e),
        }
    }

    fn from_transient(transient: TransientGetError) -> Self {
        Self::Transient(transient)
    }

    fn from_error(error: InvalidId) -> Self {
        Self::InvalidId(error)
    }
}

//////////////////
// DataProvider //
//////////////////

impl provider::DataProvider for Provider {
    type Context = Context;
    type InternalId = u32;
    type ExternalId = u32;
    type Error = InvalidId;
    type Guard = NoopGuard<u32>;

    fn to_internal_id(&self, _ctx: &Context, gid: &u32) -> Result<u32, InvalidId> {
        if (*gid as usize) < self.items.nrows() {
            Ok(*gid)
        } else {
            Err(InvalidId(*gid))
        }
    }

    fn to_external_id(&self, _ctx: &Context, id: u32) -> Result<u32, InvalidId> {
        if (id as usize) < self.items.nrows() {
            Ok(id)
        } else {
            Err(InvalidId(id))
        }
    }
}

/////////////
// Visitor //
/////////////

/// Per-search visitor over a [`Provider`]. Analog of `graph::test::Accessor`: holds
/// the `'a` borrow of the provider, accumulates a local `get_element` counter that
/// flushes back on drop, and optionally injects transient errors for a configurable
/// set of ids.
pub struct Visitor<'a> {
    provider: &'a Provider,
    transient_ids: Option<Cow<'a, HashSet<u32>>>,
    get_element: LocalCounter<'a>,
}

impl<'a> Visitor<'a> {
    /// Construct a visitor with no fault injection.
    pub fn new(provider: &'a Provider) -> Self {
        Self {
            provider,
            transient_ids: None,
            get_element: provider.get_element.local(),
        }
    }

    /// Construct a visitor that returns a [`TransientGetError`] for any id in
    /// `transient_ids`. Other ids behave normally.
    pub fn flaky(provider: &'a Provider, transient_ids: Cow<'a, HashSet<u32>>) -> Self {
        Self {
            provider,
            transient_ids: Some(transient_ids),
            get_element: provider.get_element.local(),
        }
    }
}

impl Debug for Visitor<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Visitor")
            .field("provider", &self.provider)
            .field("transient_ids", &self.transient_ids)
            .finish_non_exhaustive()
    }
}

impl HasId for Visitor<'_> {
    type Id = u32;
}

impl DistancesUnordered<<f32 as VectorRepr>::QueryDistance> for Visitor<'_> {
    type ElementRef<'a> = &'a [f32];
    type Error = AccessError;

    fn distances_unordered<F>(
        &mut self,
        computer: &<f32 as VectorRepr>::QueryDistance,
        mut f: F,
    ) -> impl SendFuture<Result<(), Self::Error>>
    where
        F: Send + FnMut(Self::Id, f32),
    {
        async move {
            for (i, vector) in self.provider.items.row_iter().enumerate() {
                let id = i as u32;
                if let Some(ids) = &self.transient_ids
                    && ids.contains(&id)
                {
                    return Err(AccessError::Transient(TransientGetError::new(id)));
                }
                self.get_element.increment();
                let dist = computer.evaluate_similarity(vector);
                f(id, dist);
            }
            Ok(())
        }
    }
}

//////////////
// Strategy //
//////////////

/// Error from [`Strategy::create_visitor`] or [`Strategy::build_query_computer`]
/// when dimensions don't match.
#[derive(Debug, Clone, Error)]
#[error("dimension mismatch: strategy expects {expected}, got {actual}")]
pub struct StrategyError {
    pub expected: usize,
    pub actual: usize,
}

impl From<StrategyError> for ANNError {
    #[track_caller]
    fn from(err: StrategyError) -> ANNError {
        ANNError::opaque(err)
    }
}

/// Factory of [`Visitor`]s that validates dimensions and optionally injects
/// transient errors into the scan.
#[derive(Clone, Debug)]
pub struct Strategy {
    dim: usize,
    transient_ids: Option<Arc<HashSet<u32>>>,
}

impl Strategy {
    /// Construct a strategy expecting vectors of dimension `dim`.
    pub fn new(dim: usize) -> Self {
        Self {
            dim,
            transient_ids: None,
        }
    }

    /// Construct a strategy whose visitors return a transient error on `get_element`
    /// for every id in `transient_ids`.
    pub fn with_transient(dim: usize, transient_ids: impl IntoIterator<Item = u32>) -> Self {
        Self {
            dim,
            transient_ids: Some(Arc::new(transient_ids.into_iter().collect())),
        }
    }
}

impl SearchStrategy<Provider, &[f32]> for Strategy {
    type ElementRef<'a> = &'a [f32];
    type QueryComputer = <f32 as VectorRepr>::QueryDistance;
    type QueryComputerError = StrategyError;
    type Visitor<'a> = Visitor<'a>;
    type Error = StrategyError;

    fn create_visitor<'a>(
        &'a self,
        provider: &'a Provider,
        _context: &'a Context,
    ) -> Result<Self::Visitor<'a>, Self::Error> {
        let actual = provider.dim();
        if actual != self.dim {
            return Err(StrategyError {
                expected: self.dim,
                actual,
            });
        }
        let visitor = match &self.transient_ids {
            Some(ids) => Visitor::flaky(provider, Cow::Borrowed(ids)),
            None => Visitor::new(provider),
        };
        Ok(visitor)
    }

    fn build_query_computer(
        &self,
        from: &[f32],
    ) -> Result<Self::QueryComputer, Self::QueryComputerError> {
        if from.len() != self.dim {
            return Err(StrategyError {
                expected: self.dim,
                actual: from.len(),
            });
        }
        Ok(f32::query_distance(from, Metric::L2))
    }
}