Skip to main content

diskann_bftree/
lib.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6//! BfTree-based data provider for DiskANN async indexes.
7//!
8//! This crate provides a [`BfTree`](bf_tree::BfTree)-backed implementation of the DiskANN
9//! [`DataProvider`](diskann::provider::DataProvider) trait, enabling indexes that can
10//! transparently spill to disk for datasets larger than available memory.
11
12pub mod neighbors;
13pub mod provider;
14pub mod quant;
15pub mod vectors;
16
17mod locks;
18
19// Accessors
20pub use provider::{
21    AsVectorDtype, BfTreePaths, BfTreeProvider, BfTreeProviderParameters, CreateQuantProvider,
22    FullAccessor, GraphParams, Hidden, QuantAccessor, StartPoint, VectorDtype,
23};
24
25pub use bf_tree::Config;
26
27use diskann::{
28    error::{RankedError, TransientError},
29    ANNError, ANNResult,
30};
31
32#[derive(Debug, Clone, Copy)]
33pub struct NoStore;
34
35/// Wrapper around [`bf_tree::ConfigError`] that implements [`std::error::Error`].
36#[derive(Debug, Clone)]
37pub struct ConfigError(pub bf_tree::ConfigError);
38
39impl std::fmt::Display for ConfigError {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        write!(f, "BfTree configuration error: {:?}", self.0)
42    }
43}
44
45impl std::error::Error for ConfigError {}
46
47impl From<ConfigError> for ANNError {
48    #[track_caller]
49    #[inline(never)]
50    fn from(error: ConfigError) -> ANNError {
51        ANNError::new(diskann::ANNErrorKind::IndexError, error)
52    }
53}
54
55////////////
56// Errors //
57////////////
58#[derive(Debug)]
59pub enum VectorError {
60    /// the vector has been explicitly deleted
61    Deleted,
62    /// the key was not found
63    NotFound,
64}
65
66#[derive(Debug)]
67pub struct VectorUnavailable {
68    pub id: usize,
69    pub err: VectorError,
70}
71
72impl std::fmt::Display for VectorUnavailable {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        match self.err {
75            VectorError::Deleted => write!(f, "vector {} was deleted", self.id),
76            VectorError::NotFound => write!(f, "vector {} not found", self.id),
77        }
78    }
79}
80
81impl TransientError<ANNError> for VectorUnavailable {
82    fn acknowledge<D>(self, _why: D)
83    where
84        D: std::fmt::Display,
85    {
86        // no-op: we are expecting transient deletion errors during traversal
87    }
88
89    fn escalate<D>(self, why: D) -> ANNError
90    where
91        D: std::fmt::Display,
92    {
93        ANNError::log_index_error(format!("{self}, escalated: {why}"))
94    }
95}
96
97pub type AccessError = RankedError<VectorUnavailable, ANNError>;
98
99/// Validate that a bf-tree config's `cb_max_record_size` is large enough for the given
100/// key + value pair. Used by provider constructors to fail early on misconfiguration.
101pub(crate) fn validate_record_size(
102    provider_name: &str,
103    config: &bf_tree::Config,
104    key_size: usize,
105    value_size: usize,
106) -> ANNResult<()> {
107    let required = key_size + value_size;
108    let configured_max = config.get_cb_max_record_size();
109    if required > configured_max {
110        return Err(ANNError::log_index_error(format!(
111            "{provider_name}: cb_max_record_size ({configured_max}) is too small; \
112             a record requires {required} bytes ({key_size}-byte key + {value_size}-byte value); \
113             increase cb_max_record_size to at least {required}"
114        )));
115    }
116    Ok(())
117}
118
119/// Metrics recorded by [`DefaultContext`](diskann::provider::DefaultContext).
120#[derive(Debug, Clone)]
121#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
122pub struct ContextMetrics {
123    pub spawns: usize,
124    pub clones: usize,
125}
126
127/// An atomic call counter used for test instrumentation.
128///
129/// Under `#[cfg(test)]`, this is a real atomic counter. In production builds,
130/// all methods are no-ops that the compiler can eliminate entirely.
131#[cfg(test)]
132pub(crate) struct TestCallCount {
133    count: std::sync::atomic::AtomicUsize,
134}
135
136#[cfg(test)]
137impl TestCallCount {
138    pub fn new() -> Self {
139        Self {
140            count: std::sync::atomic::AtomicUsize::new(0),
141        }
142    }
143
144    pub fn enabled() -> bool {
145        true
146    }
147
148    pub fn increment(&self) {
149        self.count
150            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
151    }
152
153    pub fn get(&self) -> usize {
154        self.count.load(std::sync::atomic::Ordering::Relaxed)
155    }
156}
157
158#[cfg(not(test))]
159#[allow(dead_code)]
160pub(crate) struct TestCallCount {}
161
162#[cfg(not(test))]
163#[allow(dead_code)]
164impl TestCallCount {
165    pub fn new() -> Self {
166        Self {}
167    }
168
169    pub fn enabled() -> bool {
170        false
171    }
172
173    pub fn increment(&self) {}
174
175    pub fn get(&self) -> usize {
176        0
177    }
178}
179
180impl Default for TestCallCount {
181    fn default() -> Self {
182        Self::new()
183    }
184}
185
186/// `bf_tree::BfTree::insert` can fail, but uses a `LeafInsertResult` that is not `#[must_use]`.
187///
188/// This makes it too easy to drop errors.
189///
190/// We use a `clippy` lint to explicitly disallow `bf_tree::BfTree::insert` and instead
191/// funnel calls through this method to get a proper error.
192#[expect(
193    clippy::disallowed_methods,
194    reason = "this is the allowed way to call this method"
195)]
196fn bftree_insert(tree: &bf_tree::BfTree, key: &[u8], value: &[u8]) -> Result<(), InsertError> {
197    match tree.insert(key, value) {
198        bf_tree::LeafInsertResult::Success => Ok(()),
199        bf_tree::LeafInsertResult::InvalidKV(s) => Err(InsertError(s)),
200    }
201}
202
203#[derive(Debug)]
204pub struct InsertError(String);
205
206impl std::fmt::Display for InsertError {
207    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208        write!(f, "insert into a `bftree` failed: {}", self.0)
209    }
210}
211
212impl std::error::Error for InsertError {}
213
214impl From<InsertError> for ANNError {
215    #[track_caller]
216    fn from(error: InsertError) -> Self {
217        ANNError::new(diskann::ANNErrorKind::IndexError, error)
218    }
219}