nodedb_lite/engine/vector/
segment.rs1use std::collections::HashMap;
14
15use nodedb_vector::segment_backing::VectorSegmentBacking;
16
17use crate::engine::vector::{HnswIndex, IndexIdMap};
18use crate::error::LiteError;
19use crate::storage::engine::StorageEngine;
20
21pub(crate) enum NodeSource {
23 Bound(String),
25 Local(Vec<f32>),
28}
29
30pub(crate) fn doc_fingerprint(doc_id: &str) -> u64 {
33 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
35 for byte in doc_id.as_bytes() {
36 hash ^= u64::from(*byte);
37 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
38 }
39 hash.max(1)
40}
41
42pub(crate) fn node_sources(index: &HnswIndex, ids: Option<&IndexIdMap>) -> Option<Vec<NodeSource>> {
46 (0..index.len() as u32)
47 .map(|node| match ids.and_then(|m| m.doc_id(node)) {
48 Some(doc_id) => Some(NodeSource::Bound(doc_id.to_owned())),
49 None => index
50 .get_vector_or_backing(node)
51 .map(|v| NodeSource::Local(v.into_owned())),
52 })
53 .collect()
54}
55
56pub(crate) async fn segment_payload<S: StorageEngine>(
60 storage: &S,
61 index_key: &str,
62 sources: Vec<NodeSource>,
63) -> Result<Option<(usize, Vec<Vec<f32>>, Vec<u64>)>, LiteError> {
64 if sources.is_empty() {
65 return Ok(None);
66 }
67 let mut durable: HashMap<String, Vec<f32>> =
68 crate::engine::vector::durable::load_collection(storage, index_key)
69 .await?
70 .into_iter()
71 .collect();
72 let mut vectors = Vec::with_capacity(sources.len());
73 let mut surrogates = Vec::with_capacity(sources.len());
74 for source in sources {
75 match source {
76 NodeSource::Bound(doc_id) => {
77 let vector = durable.remove(&doc_id).ok_or_else(|| LiteError::Corrupted {
78 detail: format!("vector segment '{index_key}' has no durable row for '{doc_id}': restore the vector row"),
79 })?;
80 surrogates.push(doc_fingerprint(&doc_id));
81 vectors.push(vector);
82 }
83 NodeSource::Local(vector) => {
84 surrogates.push(0);
85 vectors.push(vector);
86 }
87 }
88 }
89 let dim = vectors.first().map_or(0, Vec::len);
90 if dim == 0 {
91 return Err(LiteError::Corrupted {
92 detail: format!(
93 "vector segment '{index_key}' has zero dimensions: restore valid vector rows"
94 ),
95 });
96 }
97 if let Some(vector) = vectors.iter().find(|v| v.len() != dim) {
98 return Err(LiteError::Corrupted {
99 detail: format!(
100 "vector segment '{index_key}' expects {dim} dimensions, found {}: restore matching vector rows",
101 vector.len()
102 ),
103 });
104 }
105 Ok(Some((dim, vectors, surrogates)))
106}
107
108pub(crate) fn segment_serves_index(
113 backing: &dyn VectorSegmentBacking,
114 node_count: usize,
115 ids: Option<&IndexIdMap>,
116) -> bool {
117 if backing.len() < node_count {
118 return false;
119 }
120 let Some(ids) = ids else {
121 return node_count == 0;
122 };
123 ids.iter().all(|(doc_id, node)| {
124 (node as usize) < node_count && backing.get_surrogate(node) == Some(doc_fingerprint(doc_id))
125 })
126}
127
128pub(crate) fn attach_verified<B: VectorSegmentBacking + 'static>(
133 index: &mut HnswIndex,
134 backing: B,
135 ids: Option<&IndexIdMap>,
136) -> Result<(), LiteError> {
137 if !segment_serves_index(&backing, index.len(), ids) {
138 return Err(LiteError::Corrupted {
139 detail: format!(
140 "vector segment of {} slots does not carry the documents bound to the \
141 index's {} nodes in node order",
142 backing.len(),
143 index.len()
144 ),
145 });
146 }
147 index.with_backing(std::sync::Arc::new(backing))?;
148 Ok(())
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn fingerprints_are_stable_and_never_zero() {
157 assert_eq!(doc_fingerprint("a"), doc_fingerprint("a"));
158 assert_ne!(doc_fingerprint("a"), doc_fingerprint("b"));
159 assert_ne!(doc_fingerprint(""), 0);
160 }
161 #[tokio::test]
162 async fn payload_requires_durable_bound_rows_and_consistent_widths() {
163 use crate::storage::pagedb_storage::PagedbStorageMem;
164 let storage = PagedbStorageMem::open_in_memory().await.unwrap();
165 assert!(
166 segment_payload(&storage, "vectors", Vec::new())
167 .await
168 .unwrap()
169 .is_none()
170 );
171 let missing = segment_payload(
172 &storage,
173 "vectors",
174 vec![NodeSource::Bound("missing".into())],
175 )
176 .await
177 .unwrap_err();
178 assert!(matches!(missing, LiteError::Corrupted { .. }));
179 assert!(missing.to_string().contains("missing"));
180 let zero = segment_payload(&storage, "vectors", vec![NodeSource::Local(Vec::new())])
181 .await
182 .unwrap_err();
183 assert!(matches!(zero, LiteError::Corrupted { .. }));
184 let mismatch = segment_payload(
185 &storage,
186 "vectors",
187 vec![
188 NodeSource::Local(vec![1.0, 2.0]),
189 NodeSource::Local(vec![3.0]),
190 ],
191 )
192 .await
193 .unwrap_err();
194 assert!(matches!(mismatch, LiteError::Corrupted { .. }));
195 storage
196 .batch_write(&[crate::engine::vector::durable::put_op(
197 "vectors",
198 "bound",
199 &[4.0, 5.0],
200 )])
201 .await
202 .unwrap();
203 let payload = segment_payload(
204 &storage,
205 "vectors",
206 vec![
207 NodeSource::Bound("bound".into()),
208 NodeSource::Local(vec![1.0, 2.0]),
209 ],
210 )
211 .await
212 .unwrap()
213 .unwrap();
214 assert_eq!(payload.0, 2);
215 assert_eq!(payload.1, vec![vec![4.0, 5.0], vec![1.0, 2.0]]);
216 assert_eq!(payload.2, vec![doc_fingerprint("bound"), 0]);
217 }
218}