Skip to main content

nodedb_vector/hnsw/graph/index/
vectors.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Reading vector data back out of the index.
4//!
5//! Three things can hold a node's vector: its own F32 arena, its own
6//! dtype-encoded bytes, or an attached segment backing. Reading `node.storage`
7//! directly gets the third case wrong and yields an empty placeholder, so every
8//! accessor that copies data out goes through [`HnswIndex::materialize_vector`],
9//! which reports what is missing instead of handing back a plausible-looking
10//! empty vector.
11
12use crate::error::VectorError;
13
14use super::super::types::NodeStorage;
15use super::state::HnswIndex;
16
17impl HnswIndex {
18    /// Returns a `&[f32]` view of the stored vector for node `id`.
19    ///
20    /// Returns `Some` only when the index dtype is `F32`. For `F16` or `BF16`
21    /// indexes this method returns `None` — use [`Self::get_vector_bytes`]
22    /// instead and decode via [`crate::dtype::cast_to_f32`] if an f32 view is
23    /// needed.
24    ///
25    /// In debug builds, calling this on a non-F32 index triggers a
26    /// `debug_assert!` to flag misuse early. In release builds the
27    /// `debug_assert!` is a no-op and `None` is returned silently.
28    pub fn get_vector(&self, id: u32) -> Option<&[f32]> {
29        debug_assert!(
30            self.params.dtype == nodedb_types::vector_dtype::VectorStorageDtype::F32,
31            "get_vector: called on non-F32 index (dtype={}); use get_vector_bytes instead",
32            self.params.dtype,
33        );
34        self.nodes
35            .get(id as usize)
36            .and_then(|n| n.storage.as_f32_slice())
37    }
38
39    /// Dtype-agnostic byte view of the stored vector for node `id`.
40    ///
41    /// Returns `None` if `id` is out of range. Pair the returned slice with
42    /// [`Self::dtype`] to interpret the encoding.
43    pub fn get_vector_bytes(&self, id: u32) -> Option<&[u8]> {
44        self.nodes.get(id as usize).map(|n| n.storage.as_bytes())
45    }
46
47    /// Returns the stored f32 vector for node `id`, consulting the pagedb
48    /// segment backing when the node's local storage is empty.
49    ///
50    /// This is the rerank-safe accessor. It covers both cases that reading
51    /// `node.storage` directly gets wrong:
52    ///
53    /// - Lite's graph-checkpoint-only restore path: after `from_checkpoint` +
54    ///   `with_backing`, per-node vectors are empty placeholders and the data
55    ///   must be fetched through the backing.
56    /// - A narrower storage dtype (F16/BF16): the node holds encoded bytes, not
57    ///   f32, so the vector is decoded into an owned `Cow`.
58    ///
59    /// Returns `None` when `id` is out of range, or the node has no local vector
60    /// and no backing supplies one. Decode failure also yields `None` — callers
61    /// needing the reason should use [`Self::materialize_vector`].
62    ///
63    /// Only available on non-WASM targets (the backing type requires mmap).
64    #[cfg(not(target_arch = "wasm32"))]
65    pub fn get_vector_or_backing(&self, id: u32) -> Option<std::borrow::Cow<'_, [f32]>> {
66        use std::borrow::Cow;
67        let node = self.nodes.get(id as usize)?;
68        match &node.storage {
69            NodeStorage::F32(v) if !v.is_empty() => Some(Cow::Borrowed(v.as_slice())),
70            NodeStorage::Bytes { bytes, dtype } if !bytes.is_empty() => {
71                crate::dtype::cast_to_f32(bytes, *dtype, self.dim)
72                    .ok()
73                    .map(Cow::Owned)
74            }
75            // Empty local storage — the vector lives in the segment backing.
76            NodeStorage::F32(_) | NodeStorage::Bytes { .. } => self
77                .backing
78                .as_ref()
79                .and_then(|b| b.get_vector(id))
80                .map(Cow::Borrowed),
81        }
82    }
83
84    /// Materialize node `id`'s vector as an owned `Vec<f32>`, consulting the
85    /// segment backing when the node's local storage is empty.
86    ///
87    /// This is the authoritative accessor for every caller that copies vector
88    /// data out of the index — segment serialization, checkpointing, snapshot
89    /// export, parameter rebuilds. Reading `node.storage` directly is wrong on
90    /// the graph-checkpoint-only restore path, where per-node storage is an
91    /// empty placeholder and the real data lives in the attached backing.
92    ///
93    /// # Errors
94    ///
95    /// - [`VectorError::VectorUnavailable`] if `id` is out of range, or local
96    ///   storage is empty and no backing provides the vector.
97    /// - [`VectorError::VectorDecodeFailed`] if dtype-encoded bytes cannot be
98    ///   decoded to f32.
99    /// - [`VectorError::StoredDimensionMismatch`] if the materialized vector's length
100    ///   is not `self.dim`.
101    pub fn materialize_vector(&self, id: u32) -> Result<Vec<f32>, VectorError> {
102        let node = self
103            .nodes
104            .get(id as usize)
105            .ok_or(VectorError::VectorUnavailable { id })?;
106        let local = match &node.storage {
107            NodeStorage::F32(v) if !v.is_empty() => Some(v.clone()),
108            NodeStorage::Bytes { bytes, dtype } if !bytes.is_empty() => Some(
109                crate::dtype::cast_to_f32(bytes, *dtype, self.dim).map_err(|e| {
110                    VectorError::VectorDecodeFailed {
111                        id,
112                        detail: e.to_string(),
113                    }
114                })?,
115            ),
116            // Empty local storage: the vector must come from the backing.
117            NodeStorage::F32(_) | NodeStorage::Bytes { .. } => None,
118        };
119        let vector = match local {
120            Some(v) => v,
121            None => self.backing_vector(id)?,
122        };
123        if vector.len() != self.dim {
124            return Err(VectorError::StoredDimensionMismatch {
125                expected: self.dim,
126                got: vector.len(),
127            });
128        }
129        Ok(vector)
130    }
131
132    /// Extract all node vectors as owned F32 vecs for segment serialization.
133    ///
134    /// The second tuple element is always empty — `HnswIndex` has no surrogate
135    /// map.  Surrogates live at the `VectorCollection` layer in Origin.  Lite
136    /// passes an empty slice so `write_vector_segment` writes no surrogate block.
137    ///
138    /// # Errors
139    ///
140    /// Propagates [`Self::materialize_vector`] for the first node whose vector
141    /// cannot be materialized. Serializing an empty or partial payload would
142    /// write a segment whose header declares vectors it does not contain, so
143    /// this fails loudly instead.
144    pub fn extract_vectors_and_surrogates(&self) -> Result<(Vec<Vec<f32>>, Vec<u64>), VectorError> {
145        Ok((self.export_vectors()?, Vec::new()))
146    }
147
148    /// Export all vectors as F32 for snapshot transfer.
149    ///
150    /// For F32 indexes this is a clone. For F16/BF16 indexes each vector is
151    /// decoded to F32 on the fly. Nodes whose local storage is empty are read
152    /// through the attached segment backing.
153    ///
154    /// # Errors
155    ///
156    /// Propagates [`Self::materialize_vector`] for the first node whose vector
157    /// cannot be materialized. A caller copying vectors out of the index cannot
158    /// use an empty placeholder, so this reports the failure rather than
159    /// yielding one.
160    pub fn export_vectors(&self) -> Result<Vec<Vec<f32>>, VectorError> {
161        (0..self.nodes.len() as u32)
162            .map(|id| self.materialize_vector(id))
163            .collect()
164    }
165}