Skip to main content

nodedb_vector/collection/
rollback.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Roll a collection back to the state it held before a write.
4//!
5//! A [`VectorWriteMark`] records what a write can change: the id counter,
6//! the binding of every surrogate the write names, which of the named nodes
7//! were live, and the multi-vector documents it names. Rolling back drops
8//! every node inserted since the mark from the growing segment, so the id
9//! counter returns to where it was and the next insert takes the same id it
10//! would have taken without the write. Then it puts every binding and every
11//! tombstone back.
12//!
13//! The inserted nodes must still sit in the growing segment or the IVF-PQ
14//! index: a seal or an IVF-PQ training between the mark and the rollback
15//! moves them out, and the rollback then reports that it cannot restore the
16//! mark.
17
18use nodedb_types::Surrogate;
19
20use super::lifecycle::VectorCollection;
21use crate::flat::FlatIndex;
22
23/// A collection's state before a write. See the module docs.
24#[derive(Debug, Clone)]
25pub struct VectorWriteMark {
26    next_id: u32,
27    growing_base_id: u32,
28    /// Each named surrogate with the node bound to it, if any.
29    bindings: Vec<(Surrogate, Option<u32>)>,
30    /// Named nodes that were live.
31    live: Vec<u32>,
32    /// Each named multi-vector document with its node list, if any.
33    multi_docs: Vec<(Surrogate, Option<Vec<u32>>)>,
34}
35
36impl VectorCollection {
37    /// Whether node `id` exists and is not soft-deleted, whichever segment
38    /// holds it.
39    pub fn is_live(&self, id: u32) -> bool {
40        if let Some(ivf) = &self.ivf
41            && ivf.contains(id)
42        {
43            return !ivf.is_deleted(id);
44        }
45        if id >= self.growing_base_id {
46            let local = id - self.growing_base_id;
47            if (local as usize) < self.growing.len() {
48                return !self.growing.is_deleted(local);
49            }
50        }
51        for seg in &self.sealed {
52            if id >= seg.base_id {
53                let local = id - seg.base_id;
54                if (local as usize) < seg.index.len() {
55                    return !seg.index.is_deleted(local);
56                }
57            }
58        }
59        for seg in &self.building {
60            if id >= seg.base_id {
61                let local = id - seg.base_id;
62                if (local as usize) < seg.flat.len() {
63                    return !seg.flat.is_deleted(local);
64                }
65            }
66        }
67        false
68    }
69
70    /// Mark the state a write naming `surrogates` and node `ids` can change.
71    pub fn write_mark(&self, surrogates: &[Surrogate], ids: &[u32]) -> VectorWriteMark {
72        let mut bindings = Vec::with_capacity(surrogates.len() + ids.len());
73        let mut live = Vec::new();
74        let mut multi_docs = Vec::with_capacity(surrogates.len());
75        for &surrogate in surrogates {
76            let bound = self.surrogate_to_local.get(&surrogate).copied();
77            bindings.push((surrogate, bound));
78            if let Some(id) = bound
79                && self.is_live(id)
80            {
81                live.push(id);
82            }
83            let doc = self.multi_doc_map.get(&surrogate).cloned();
84            if let Some(doc_ids) = &doc {
85                live.extend(doc_ids.iter().copied().filter(|id| self.is_live(*id)));
86            }
87            multi_docs.push((surrogate, doc));
88        }
89        for &id in ids {
90            if !self.is_live(id) {
91                continue;
92            }
93            live.push(id);
94            if let Some(&surrogate) = self.surrogate_map.get(&id) {
95                bindings.push((surrogate, Some(id)));
96            }
97        }
98        VectorWriteMark {
99            next_id: self.next_id,
100            growing_base_id: self.growing_base_id,
101            bindings,
102            live,
103            multi_docs,
104        }
105    }
106
107    /// Put the collection back to `mark`. Returns `false`, changing nothing,
108    /// when a seal or an IVF-PQ training moved the nodes inserted since the
109    /// mark out of the growing segment.
110    pub fn roll_back_to(&mut self, mark: VectorWriteMark) -> bool {
111        if self.growing_base_id != mark.growing_base_id || mark.next_id < self.growing_base_id {
112            return false;
113        }
114        for id in mark.next_id..self.next_id {
115            if let Some(surrogate) = self.surrogate_map.remove(&id)
116                && self.surrogate_to_local.get(&surrogate) == Some(&id)
117            {
118                self.surrogate_to_local.remove(&surrogate);
119            }
120        }
121        self.growing
122            .truncate((mark.next_id - self.growing_base_id) as usize);
123        if let Some(ivf) = &mut self.ivf {
124            ivf.roll_back_to(mark.next_id);
125        }
126        self.next_id = mark.next_id;
127
128        for (surrogate, prior) in mark.bindings {
129            if let Some(current) = self.surrogate_to_local.remove(&surrogate)
130                && Some(current) != prior
131            {
132                self.surrogate_map.remove(&current);
133            }
134            if let Some(id) = prior {
135                self.surrogate_to_local.insert(surrogate, id);
136                self.surrogate_map.insert(id, surrogate);
137            }
138        }
139        for (surrogate, prior) in mark.multi_docs {
140            match prior {
141                Some(ids) => {
142                    for id in &ids {
143                        self.surrogate_map.insert(*id, surrogate);
144                    }
145                    self.multi_doc_map.insert(surrogate, ids);
146                }
147                None => {
148                    self.multi_doc_map.remove(&surrogate);
149                }
150            }
151        }
152        for id in mark.live {
153            if !self.is_live(id) {
154                self.undelete(id);
155            }
156        }
157        true
158    }
159
160    /// An empty collection with this one's configuration and id counters:
161    /// same dimension, parameters, index config, quantization, seal
162    /// threshold, storage settings, registered payload fields and
163    /// watermarks. The next insert takes the id this collection's next insert
164    /// would have taken.
165    pub fn detached_empty(&self) -> Self {
166        let mut fresh = Self::with_seal_threshold_and_config(
167            self.dim,
168            self.index_config.clone(),
169            self.seal_threshold,
170        );
171        fresh.params = self.params.clone();
172        fresh.growing = FlatIndex::new(self.dim, self.params.metric);
173        fresh.next_id = self.next_id;
174        fresh.growing_base_id = self.next_id;
175        fresh.next_segment_id = self.next_segment_id;
176        fresh.data_dir = self.data_dir.clone();
177        fresh.ram_budget_bytes = self.ram_budget_bytes;
178        fresh.mmap_fallback_count = self.mmap_fallback_count;
179        fresh.quantization = self.quantization;
180        fresh.payload = self.payload.definitions_only();
181        fresh.arena_index = self.arena_index;
182        fresh.checkpoint_wal_lsn = self.checkpoint_wal_lsn;
183        fresh.applied_wal_lsn = self.applied_wal_lsn;
184        fresh
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use crate::hnsw::HnswParams;
192
193    fn collection() -> VectorCollection {
194        VectorCollection::new(2, HnswParams::default())
195    }
196
197    #[test]
198    fn a_rolled_back_insert_returns_the_id_counter_and_the_binding() {
199        let mut coll = collection();
200        coll.insert_with_surrogate(vec![0.1, 0.2], Surrogate::new(1))
201            .unwrap();
202        let mark = coll.write_mark(&[Surrogate::new(2)], &[]);
203        coll.insert_with_surrogate(vec![0.3, 0.4], Surrogate::new(2))
204            .unwrap();
205        assert!(coll.roll_back_to(mark));
206        assert_eq!(coll.local_for_surrogate(Surrogate::new(2)), None);
207        assert_eq!(
208            coll.insert_with_surrogate(vec![0.5, 0.6], Surrogate::new(3))
209                .unwrap(),
210            1,
211            "the next insert takes the id the rolled-back insert took"
212        );
213    }
214
215    #[test]
216    fn a_rolled_back_rebind_restores_the_replaced_node() {
217        let mut coll = collection();
218        let first = coll
219            .insert_with_surrogate(vec![0.1, 0.2], Surrogate::new(7))
220            .unwrap();
221        let mark = coll.write_mark(&[Surrogate::new(7)], &[]);
222        coll.insert_with_surrogate(vec![0.3, 0.4], Surrogate::new(7))
223            .unwrap();
224        assert!(!coll.is_live(first));
225        assert!(coll.roll_back_to(mark));
226        assert!(coll.is_live(first));
227        assert_eq!(coll.local_for_surrogate(Surrogate::new(7)), Some(first));
228    }
229
230    #[test]
231    fn a_rolled_back_delete_restores_the_node_and_its_binding() {
232        let mut coll = collection();
233        let id = coll
234            .insert_with_surrogate(vec![0.1, 0.2], Surrogate::new(4))
235            .unwrap();
236        let mark = coll.write_mark(&[], &[id]);
237        coll.delete(id);
238        assert!(coll.roll_back_to(mark));
239        assert!(coll.is_live(id));
240        assert_eq!(coll.local_for_surrogate(Surrogate::new(4)), Some(id));
241    }
242
243    #[test]
244    fn a_seal_after_the_mark_refuses_the_rollback() {
245        let mut coll = VectorCollection::with_seal_threshold(2, HnswParams::default(), 1);
246        let mark = coll.write_mark(&[Surrogate::new(1)], &[]);
247        coll.insert_with_surrogate(vec![0.1, 0.2], Surrogate::new(1))
248            .unwrap();
249        assert!(coll.seal("k").is_some());
250        assert!(!coll.roll_back_to(mark));
251    }
252
253    #[test]
254    fn a_detached_empty_collection_continues_the_id_counter() {
255        let mut coll = collection();
256        coll.insert(vec![0.1, 0.2]).unwrap();
257        coll.insert(vec![0.3, 0.4]).unwrap();
258        let mut fresh = coll.detached_empty();
259        assert_eq!(fresh.live_count(), 0);
260        assert_eq!(fresh.insert(vec![0.5, 0.6]).unwrap(), 2);
261    }
262}