Skip to main content

nodedb_vector/hnsw/graph/index/
backing.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Attaching an external vector segment to a graph-only index.
4//!
5//! Lite's restore path loads the graph topology from a B+ tree blob while the
6//! vectors stay in a pagedb segment, so the index arrives with empty per-node
7//! storage and has to be pointed at that segment before it can answer anything.
8//!
9//! The attachment is where a bad segment has to be caught. Once attached, an
10//! unserviceable segment is indistinguishable from a healthy one until a query
11//! scores a node that has no vector — by which time the collection is already
12//! published and serving.
13
14#[cfg(not(target_arch = "wasm32"))]
15use std::sync::Arc;
16
17use crate::error::VectorError;
18
19use super::state::HnswIndex;
20
21impl HnswIndex {
22    /// Attach a [`crate::segment_backing::VectorSegmentBacking`] to this index.
23    ///
24    /// After this succeeds, `dist_to_node` falls back to the backing whenever an
25    /// F32 node's local vector storage is empty.  This is used by Lite's
26    /// graph-checkpoint-only restore path: the graph topology is loaded from the
27    /// B+ tree blob, but vector data lives in a pagedb segment.
28    ///
29    /// Origin never calls this — its node arenas are always populated.
30    ///
31    /// # Errors
32    ///
33    /// The backing is validated against this index BEFORE being attached, and is
34    /// not attached at all when it cannot serve every node whose local storage
35    /// is empty. An attached-but-unserviceable backing is the worst outcome
36    /// available: the graph looks healthy, so search proceeds and then finds no
37    /// vector for a node it must score. Callers should treat an error as "this
38    /// segment is unusable — rebuild the index from the authoritative vectors,
39    /// or leave the collection unloaded", never as something to ignore.
40    ///
41    /// - [`VectorError::StoredDimensionMismatch`] if the backing's `dim()` is not this
42    ///   index's `dim`.
43    /// - [`VectorError::VectorUnavailable`] if the backing holds fewer vectors
44    ///   than the index has nodes, or if a node that needs the backing has no
45    ///   correctly-sized vector in it.
46    #[cfg(not(target_arch = "wasm32"))]
47    pub fn with_backing(
48        &mut self,
49        b: Arc<dyn crate::segment_backing::VectorSegmentBacking>,
50    ) -> Result<&mut Self, VectorError> {
51        if b.dim() != self.dim {
52            return Err(VectorError::StoredDimensionMismatch {
53                expected: self.dim,
54                got: b.dim(),
55            });
56        }
57        if b.len() < self.nodes.len() {
58            // A segment written from an index that had no vectors to give
59            // declares its real count in the header but carries no payload;
60            // this is where that lie becomes visible. `b.len()` is also the id
61            // of the first node the backing cannot serve.
62            return Err(VectorError::VectorUnavailable { id: b.len() as u32 });
63        }
64        // Every node that will actually read through the backing must resolve to
65        // a correctly-sized vector. Nodes with local storage are unaffected.
66        for (id, node) in self.nodes.iter().enumerate() {
67            if !node.storage.as_bytes().is_empty() {
68                continue;
69            }
70            let id = id as u32;
71            match b.get_vector(id) {
72                Some(v) if v.len() == self.dim => {}
73                _ => return Err(VectorError::VectorUnavailable { id }),
74            }
75        }
76        self.backing = Some(b);
77        Ok(self)
78    }
79
80    /// Fetch node `id`'s vector from the attached segment backing.
81    #[cfg(not(target_arch = "wasm32"))]
82    pub(super) fn backing_vector(&self, id: u32) -> Result<Vec<f32>, VectorError> {
83        self.backing
84            .as_ref()
85            .and_then(|b| b.get_vector(id))
86            .map(<[f32]>::to_vec)
87            .ok_or(VectorError::VectorUnavailable { id })
88    }
89
90    /// WASM targets have no segment backing (it requires mmap).
91    #[cfg(target_arch = "wasm32")]
92    pub(super) fn backing_vector(&self, id: u32) -> Result<Vec<f32>, VectorError> {
93        Err(VectorError::VectorUnavailable { id })
94    }
95}