Skip to main content

nodedb_vector/collection/
lifecycle_insert_ops.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Insert, delete, and surrogate-map operations for `VectorCollection`.
4
5use nodedb_types::Surrogate;
6
7use super::lifecycle::VectorCollection;
8use crate::error::{VectorError, check_dim};
9
10impl VectorCollection {
11    /// Insert a vector. Returns the global vector ID. A trained IVF-PQ
12    /// collection inserts into its IVF index, every other collection into
13    /// the growing segment.
14    ///
15    /// A vector without the collection dimension fails with
16    /// [`VectorError::DimensionMismatch`] and changes nothing.
17    pub fn insert(&mut self, vector: Vec<f32>) -> Result<u32, VectorError> {
18        check_dim(self.dim, vector.len())?;
19        let id = self.next_id;
20        match &mut self.ivf {
21            Some(ivf) => ivf.insert_with_id(id, vector)?,
22            None => {
23                self.growing.insert(vector)?;
24            }
25        }
26        self.next_id += 1;
27        Ok(id)
28    }
29
30    /// Insert a vector with an associated surrogate. The surrogate is
31    /// allocated by the Control Plane before the call; the engine only
32    /// stores the binding.
33    ///
34    /// One surrogate names one live node. A node already bound to
35    /// `surrogate` is soft-deleted before the new one is bound, so a
36    /// re-insert never leaves an unreachable node scoring in searches.
37    /// The caller owns the payload bitmap entries of the old node and
38    /// removes them with [`Self::local_for_surrogate`] before this call.
39    ///
40    /// A vector without the collection dimension fails with
41    /// [`VectorError::DimensionMismatch`] before the old binding is touched.
42    ///
43    /// [`Surrogate::ZERO`] binds nothing: a headless vector has no surrogate,
44    /// so it never enters `surrogate_map` or `surrogate_to_local`, and no
45    /// lookup or delete by `ZERO` reaches it. Compaction, checkpoint restore
46    /// and rollback rebuild both maps from `surrogate_map` alone, so they
47    /// never map `ZERO` either.
48    pub fn insert_with_surrogate(
49        &mut self,
50        vector: Vec<f32>,
51        surrogate: Surrogate,
52    ) -> Result<u32, VectorError> {
53        check_dim(self.dim, vector.len())?;
54        if surrogate != Surrogate::ZERO
55            && let Some(old) = self.surrogate_to_local.get(&surrogate).copied()
56        {
57            self.delete_inner(old);
58            self.surrogate_map.remove(&old);
59        }
60        let id = self.insert(vector)?;
61        if surrogate != Surrogate::ZERO {
62            self.surrogate_map.insert(id, surrogate);
63            self.surrogate_to_local.insert(surrogate, id);
64        }
65        Ok(id)
66    }
67
68    /// Insert a batch of vectors, the `i`-th bound to `surrogates[i]`
69    /// ([`Surrogate::ZERO`] when the slice is shorter). Returns the global ids
70    /// in batch order.
71    ///
72    /// Every vector is checked against the collection dimension before any is
73    /// inserted, so a mismatch fails with [`VectorError::DimensionMismatch`]
74    /// and inserts none.
75    pub fn insert_batch_with_surrogates(
76        &mut self,
77        vectors: &[Vec<f32>],
78        surrogates: &[Surrogate],
79    ) -> Result<Vec<u32>, VectorError> {
80        for v in vectors {
81            check_dim(self.dim, v.len())?;
82        }
83        let mut ids = Vec::with_capacity(vectors.len());
84        for (i, v) in vectors.iter().enumerate() {
85            let surrogate = surrogates.get(i).copied().unwrap_or(Surrogate::ZERO);
86            ids.push(self.insert_with_surrogate(v.clone(), surrogate)?);
87        }
88        Ok(ids)
89    }
90
91    /// Insert multiple vectors for a single document (ColBERT-style).
92    /// All N vectors are bound to the same `document_surrogate`.
93    ///
94    /// Every vector is checked against the collection dimension before any
95    /// is inserted, so a mismatch fails with
96    /// [`VectorError::DimensionMismatch`] and inserts none.
97    pub fn insert_multi_vector(
98        &mut self,
99        vectors: &[&[f32]],
100        document_surrogate: Surrogate,
101    ) -> Result<Vec<u32>, VectorError> {
102        for v in vectors {
103            check_dim(self.dim, v.len())?;
104        }
105        let mut ids = Vec::with_capacity(vectors.len());
106        for &v in vectors {
107            let id = self.insert(v.to_vec())?;
108            if document_surrogate != Surrogate::ZERO {
109                self.surrogate_map.insert(id, document_surrogate);
110            }
111            ids.push(id);
112        }
113        if document_surrogate != Surrogate::ZERO {
114            self.multi_doc_map.insert(document_surrogate, ids.clone());
115        }
116        Ok(ids)
117    }
118
119    /// Delete all vectors belonging to a multi-vector document.
120    pub fn delete_multi_vector(&mut self, document_surrogate: Surrogate) -> usize {
121        let Some(ids) = self.multi_doc_map.remove(&document_surrogate) else {
122            return 0;
123        };
124        let mut deleted = 0;
125        for id in &ids {
126            if self.delete(*id) {
127                deleted += 1;
128            }
129            self.surrogate_map.remove(id);
130        }
131        self.surrogate_to_local.remove(&document_surrogate);
132        deleted
133    }
134
135    /// Look up the surrogate for a global vector ID.
136    pub fn get_surrogate(&self, vector_id: u32) -> Option<Surrogate> {
137        self.surrogate_map.get(&vector_id).copied()
138    }
139
140    /// Resolve a surrogate back to its global vector ID, if bound.
141    pub fn local_for_surrogate(&self, surrogate: Surrogate) -> Option<u32> {
142        self.surrogate_to_local.get(&surrogate).copied()
143    }
144
145    /// Soft-delete a vector by global ID.
146    pub fn delete(&mut self, id: u32) -> bool {
147        let ok = self.delete_inner(id);
148        if ok && let Some(s) = self.surrogate_map.remove(&id) {
149            self.surrogate_to_local.remove(&s);
150        }
151        ok
152    }
153
154    pub(super) fn delete_inner(&mut self, id: u32) -> bool {
155        if let Some(ivf) = &mut self.ivf
156            && ivf.contains(id)
157        {
158            return ivf.delete(id);
159        }
160        if id >= self.growing_base_id {
161            let local = id - self.growing_base_id;
162            if (local as usize) < self.growing.len() {
163                return self.growing.delete(local);
164            }
165        }
166        for seg in &mut self.sealed {
167            if id >= seg.base_id {
168                let local = id - seg.base_id;
169                if (local as usize) < seg.index.len() {
170                    return seg.index.delete(local);
171                }
172            }
173        }
174        for seg in &mut self.building {
175            if id >= seg.base_id {
176                let local = id - seg.base_id;
177                if (local as usize) < seg.flat.len() {
178                    return seg.flat.delete(local);
179                }
180            }
181        }
182        false
183    }
184
185    /// The live FP32 vector stored under global `id`, whichever segment
186    /// holds it. `None` for an unknown or soft-deleted id.
187    pub fn vector_for_id(&self, id: u32) -> Option<Vec<f32>> {
188        if let Some(ivf) = &self.ivf
189            && ivf.contains(id)
190        {
191            return ivf.get_vector(id).map(<[f32]>::to_vec);
192        }
193        if id >= self.growing_base_id {
194            let local = id - self.growing_base_id;
195            if (local as usize) < self.growing.len() {
196                return self.growing.get_vector(local).map(<[f32]>::to_vec);
197            }
198        }
199        for seg in &self.sealed {
200            if id >= seg.base_id {
201                let local = id - seg.base_id;
202                if (local as usize) < seg.index.len() {
203                    if seg.index.is_deleted(local) {
204                        return None;
205                    }
206                    return sealed_vector(seg, local);
207                }
208            }
209        }
210        for seg in &self.building {
211            if id >= seg.base_id {
212                let local = id - seg.base_id;
213                if (local as usize) < seg.flat.len() {
214                    return seg.flat.get_vector(local).map(<[f32]>::to_vec);
215                }
216            }
217        }
218        None
219    }
220
221    /// The live FP32 vector bound to `surrogate`, if any.
222    pub fn vector_for_surrogate(&self, surrogate: Surrogate) -> Option<Vec<f32>> {
223        self.local_for_surrogate(surrogate)
224            .and_then(|id| self.vector_for_id(id))
225    }
226
227    /// Soft-delete a vector by surrogate.
228    pub fn delete_by_surrogate(&mut self, surrogate: Surrogate) -> bool {
229        let Some(global_id) = self.surrogate_to_local.get(&surrogate).copied() else {
230            return false;
231        };
232        self.delete(global_id)
233    }
234
235    /// Un-delete a previously soft-deleted vector (for transaction rollback).
236    ///
237    /// Symmetric to [`Self::delete_inner`]: the vector may live in the IVF
238    /// index, the growing segment (the common case for a just-inserted
239    /// vector), a sealed HNSW segment, or an in-flight building segment. The
240    /// tombstone is reversed wherever it landed.
241    pub fn undelete(&mut self, id: u32) -> bool {
242        if let Some(ivf) = &mut self.ivf
243            && ivf.contains(id)
244        {
245            return ivf.undelete(id);
246        }
247        if id >= self.growing_base_id {
248            let local = id - self.growing_base_id;
249            if (local as usize) < self.growing.len() {
250                return self.growing.undelete(local);
251            }
252        }
253        for seg in &mut self.sealed {
254            if id >= seg.base_id {
255                let local = id - seg.base_id;
256                if (local as usize) < seg.index.len() {
257                    return seg.index.undelete(local);
258                }
259            }
260        }
261        for seg in &mut self.building {
262            if id >= seg.base_id {
263                let local = id - seg.base_id;
264                if (local as usize) < seg.flat.len() {
265                    return seg.flat.undelete(local);
266                }
267            }
268        }
269        false
270    }
271
272    /// Un-delete `id` and bind it to `surrogate` again: the reverse of
273    /// [`Self::delete`] on a bound node, which drops the binding.
274    ///
275    /// Returns `false` and changes nothing when `id` carries no tombstone.
276    /// [`Surrogate::ZERO`] binds nothing, as in [`Self::insert_with_surrogate`].
277    pub fn undelete_bound(&mut self, id: u32, surrogate: Surrogate) -> bool {
278        if !self.undelete(id) {
279            return false;
280        }
281        if surrogate != Surrogate::ZERO {
282            self.surrogate_map.insert(id, surrogate);
283            self.surrogate_to_local.insert(surrogate, id);
284        }
285        true
286    }
287}
288
289/// The FP32 vector at `local` in a sealed segment: the mmap tier when the
290/// segment lives there, else the HNSW node (decoded from a narrow dtype or
291/// fetched from the segment backing when the node holds no local copy).
292pub(super) fn sealed_vector(seg: &super::segment::SealedSegment, local: u32) -> Option<Vec<f32>> {
293    if let Some(mmap) = &seg.mmap_vectors {
294        return mmap.get_vector(local).map(<[f32]>::to_vec);
295    }
296    #[cfg(not(target_arch = "wasm32"))]
297    {
298        seg.index
299            .get_vector_or_backing(local)
300            .map(std::borrow::Cow::into_owned)
301    }
302    #[cfg(target_arch = "wasm32")]
303    {
304        seg.index.get_vector(local).map(<[f32]>::to_vec)
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311    use crate::hnsw::HnswParams;
312
313    fn collection() -> VectorCollection {
314        VectorCollection::new(2, HnswParams::default())
315    }
316
317    #[test]
318    fn re_insert_under_the_same_surrogate_leaves_one_live_node() {
319        let mut coll = collection();
320        let s = Surrogate::new(7);
321        let first = coll.insert_with_surrogate(vec![1.0, 0.0], s).unwrap();
322        let second = coll.insert_with_surrogate(vec![0.0, 1.0], s).unwrap();
323        assert_ne!(first, second);
324        assert_eq!(coll.live_count(), 1, "the old node must be tombstoned");
325        assert_eq!(coll.local_for_surrogate(s), Some(second));
326        assert_eq!(coll.get_surrogate(first), None);
327        assert_eq!(coll.get_surrogate(second), Some(s));
328    }
329
330    #[test]
331    fn delete_then_insert_under_the_same_surrogate_leaves_one_live_node() {
332        let mut coll = collection();
333        let s = Surrogate::new(9);
334        let first = coll.insert_with_surrogate(vec![1.0, 0.0], s).unwrap();
335        assert!(coll.delete_by_surrogate(s));
336        assert_eq!(coll.local_for_surrogate(s), None);
337        let second = coll.insert_with_surrogate(vec![0.0, 1.0], s).unwrap();
338        assert_ne!(first, second);
339        assert_eq!(coll.live_count(), 1);
340        assert_eq!(coll.local_for_surrogate(s), Some(second));
341        assert!(!coll.delete(first), "the first node is already gone");
342    }
343
344    #[test]
345    fn undelete_bound_restores_the_node_and_its_binding() {
346        let mut coll = collection();
347        let s = Surrogate::new(5);
348        let id = coll.insert_with_surrogate(vec![1.0, 0.0], s).unwrap();
349        assert!(coll.delete(id));
350        assert_eq!(coll.local_for_surrogate(s), None);
351
352        assert!(coll.undelete_bound(id, s));
353        assert_eq!(coll.live_count(), 1);
354        assert_eq!(coll.local_for_surrogate(s), Some(id));
355        assert_eq!(coll.get_surrogate(id), Some(s));
356        assert!(
357            !coll.undelete_bound(id, s),
358            "a live node has no tombstone to clear"
359        );
360    }
361
362    #[test]
363    fn delete_by_surrogate_is_idempotent() {
364        let mut coll = collection();
365        let s = Surrogate::new(3);
366        coll.insert_with_surrogate(vec![1.0, 0.0], s).unwrap();
367        assert!(coll.delete_by_surrogate(s));
368        assert!(!coll.delete_by_surrogate(s));
369        assert_eq!(coll.live_count(), 0);
370    }
371
372    #[test]
373    fn vector_for_surrogate_reads_the_growing_segment_and_hides_deletes() {
374        let mut coll = collection();
375        let s = Surrogate::new(11);
376        coll.insert_with_surrogate(vec![0.5, 0.25], s).unwrap();
377        assert_eq!(coll.vector_for_surrogate(s), Some(vec![0.5, 0.25]));
378        assert!(coll.delete_by_surrogate(s));
379        assert_eq!(coll.vector_for_surrogate(s), None);
380        assert_eq!(coll.vector_for_id(999), None);
381    }
382
383    #[test]
384    fn headless_vectors_bind_no_surrogate() {
385        let mut coll = collection();
386        let first = coll
387            .insert_with_surrogate(vec![1.0, 0.0], Surrogate::ZERO)
388            .unwrap();
389        let second = coll
390            .insert_with_surrogate(vec![0.0, 1.0], Surrogate::ZERO)
391            .unwrap();
392        coll.insert_multi_vector(&[&[0.5, 0.5]], Surrogate::ZERO)
393            .unwrap();
394        assert_ne!(first, second);
395        assert_eq!(coll.live_count(), 3, "every headless insert stays live");
396        assert_eq!(coll.local_for_surrogate(Surrogate::ZERO), None);
397        assert_eq!(coll.get_surrogate(first), None);
398        assert_eq!(coll.get_surrogate(second), None);
399        assert!(coll.surrogate_to_local.is_empty());
400        assert!(coll.multi_doc_map.is_empty());
401
402        assert!(
403            !coll.delete_by_surrogate(Surrogate::ZERO),
404            "a delete by ZERO reaches no vector"
405        );
406        assert_eq!(coll.live_count(), 3);
407    }
408}