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}