nodedb_lite/engine/vector/
id_map.rs1use std::collections::HashMap;
17
18#[derive(Debug, Default, Clone)]
20pub struct IndexIdMap {
21 by_node: HashMap<u32, String>,
22 by_doc: HashMap<String, u32>,
23}
24
25impl IndexIdMap {
26 pub fn bind(&mut self, doc_id: &str, node: u32) -> Option<u32> {
30 if let Some(prev_doc) = self.by_node.insert(node, doc_id.to_owned())
31 && prev_doc != doc_id
32 && self.by_doc.get(&prev_doc) == Some(&node)
33 {
34 self.by_doc.remove(&prev_doc);
35 }
36 match self.by_doc.insert(doc_id.to_owned(), node) {
37 Some(old) if old != node => {
38 self.by_node.remove(&old);
39 Some(old)
40 }
41 _ => None,
42 }
43 }
44
45 pub fn node(&self, doc_id: &str) -> Option<u32> {
47 self.by_doc.get(doc_id).copied()
48 }
49
50 pub fn doc_id(&self, node: u32) -> Option<&str> {
52 self.by_node.get(&node).map(String::as_str)
53 }
54
55 pub fn unbind_doc(&mut self, doc_id: &str) -> Option<u32> {
57 let node = self.by_doc.remove(doc_id)?;
58 self.by_node.remove(&node);
59 Some(node)
60 }
61
62 pub fn unbind_node(&mut self, node: u32) -> Option<String> {
64 let doc_id = self.by_node.remove(&node)?;
65 self.by_doc.remove(&doc_id);
66 Some(doc_id)
67 }
68
69 pub fn len(&self) -> usize {
71 self.by_doc.len()
72 }
73
74 pub fn is_empty(&self) -> bool {
75 self.by_doc.is_empty()
76 }
77
78 pub fn iter(&self) -> impl Iterator<Item = (&str, u32)> {
80 self.by_doc.iter().map(|(doc, node)| (doc.as_str(), *node))
81 }
82}
83
84#[derive(Debug, Default, Clone)]
86pub struct VectorIdMap {
87 indexes: HashMap<String, IndexIdMap>,
88}
89
90#[derive(Debug, Default, Clone, PartialEq, Eq)]
92pub struct AttachedVectors {
93 pub base: bool,
95 pub named: Vec<String>,
98}
99
100impl AttachedVectors {
101 pub fn any(&self) -> bool {
103 self.base || !self.named.is_empty()
104 }
105}
106
107pub type PersistedIdEntry = (String, String, u32);
109
110pub fn persisted_index_key(entry: &PersistedIdEntry) -> Option<&str> {
114 let (composite, _, node) = entry;
115 composite.strip_suffix(&format!(":{node}"))
116}
117
118impl VectorIdMap {
119 pub fn new() -> Self {
120 Self::default()
121 }
122
123 pub fn index(&self, index_key: &str) -> Option<&IndexIdMap> {
125 self.indexes.get(index_key)
126 }
127
128 pub fn bind(&mut self, index_key: &str, doc_id: &str, node: u32) -> Option<u32> {
131 self.indexes
132 .entry(index_key.to_owned())
133 .or_default()
134 .bind(doc_id, node)
135 }
136
137 pub fn node(&self, index_key: &str, doc_id: &str) -> Option<u32> {
139 self.indexes.get(index_key)?.node(doc_id)
140 }
141
142 pub fn doc_id(&self, index_key: &str, node: u32) -> Option<&str> {
144 self.indexes.get(index_key)?.doc_id(node)
145 }
146
147 pub fn unbind_doc(&mut self, index_key: &str, doc_id: &str) -> Option<u32> {
149 let ids = self.indexes.get_mut(index_key)?;
150 let node = ids.unbind_doc(doc_id);
151 if ids.is_empty() {
152 self.indexes.remove(index_key);
153 }
154 node
155 }
156
157 pub fn unbind_node(&mut self, index_key: &str, node: u32) -> Option<String> {
159 let ids = self.indexes.get_mut(index_key)?;
160 let doc_id = ids.unbind_node(node);
161 if ids.is_empty() {
162 self.indexes.remove(index_key);
163 }
164 doc_id
165 }
166
167 pub fn attached_vectors(&self, collection: &str, doc_id: &str) -> AttachedVectors {
170 let mut attached = AttachedVectors::default();
171 for (index_key, ids) in &self.indexes {
172 if ids.node(doc_id).is_none() {
173 continue;
174 }
175 if index_key == collection {
176 attached.base = true;
177 } else if let Some(field) = index_key
178 .strip_prefix(collection)
179 .and_then(|rest| rest.strip_prefix(':'))
180 {
181 attached.named.push(field.to_owned());
182 }
183 }
184 attached.named.sort();
185 attached
186 }
187
188 pub fn remove_index(&mut self, index_key: &str) {
190 self.indexes.remove(index_key);
191 }
192
193 pub fn replace_index(&mut self, index_key: &str, ids: IndexIdMap) {
195 if ids.is_empty() {
196 self.indexes.remove(index_key);
197 } else {
198 self.indexes.insert(index_key.to_owned(), ids);
199 }
200 }
201
202 pub fn len(&self) -> usize {
204 self.indexes.values().map(IndexIdMap::len).sum()
205 }
206
207 pub fn is_empty(&self) -> bool {
208 self.indexes.is_empty()
209 }
210
211 pub fn to_entries(&self) -> Vec<PersistedIdEntry> {
214 let mut out = Vec::with_capacity(self.len());
215 for (index_key, ids) in &self.indexes {
216 for (doc_id, node) in ids.iter() {
217 out.push((format!("{index_key}:{node}"), doc_id.to_owned(), node));
218 }
219 }
220 out
221 }
222
223 pub fn from_entries(entries: Vec<PersistedIdEntry>) -> (Self, Vec<(String, u32)>) {
232 let mut parsed: Vec<(String, String, u32)> = entries
233 .into_iter()
234 .filter_map(|entry| {
235 let index_key = persisted_index_key(&entry)?.to_owned();
236 let (_, doc_id, node) = entry;
237 Some((index_key, doc_id, node))
238 })
239 .collect();
240 parsed.sort_by_key(|(_, _, node)| *node);
241 let mut map = Self::new();
242 let mut displaced = Vec::new();
243 for (index_key, doc_id, node) in parsed {
244 if let Some(old) = map.bind(&index_key, &doc_id, node) {
245 displaced.push((index_key, old));
246 }
247 }
248 (map, displaced)
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn rebinding_a_document_returns_the_displaced_node() {
258 let mut map = VectorIdMap::new();
259 assert_eq!(map.bind("c", "a", 0), None);
260 assert_eq!(map.bind("c", "a", 1), Some(0));
261 assert_eq!(map.node("c", "a"), Some(1));
262 assert_eq!(map.doc_id("c", 0), None);
263 assert_eq!(map.doc_id("c", 1), Some("a"));
264 assert_eq!(map.len(), 1);
265 }
266
267 #[test]
268 fn indexes_with_shared_prefixes_stay_separate() {
269 let mut map = VectorIdMap::new();
270 map.bind("chat", "a", 0);
271 map.bind("chat2", "b", 0);
272 map.bind("chat:emb", "c", 0);
273 assert_eq!(map.doc_id("chat", 0), Some("a"));
274 assert_eq!(map.index("chat").map(IndexIdMap::len), Some(1));
275 map.remove_index("chat");
276 assert_eq!(map.doc_id("chat2", 0), Some("b"));
277 assert_eq!(map.doc_id("chat:emb", 0), Some("c"));
278 }
279
280 #[test]
281 fn unbind_removes_both_directions() {
282 let mut map = VectorIdMap::new();
283 map.bind("c", "a", 3);
284 assert_eq!(map.unbind_doc("c", "a"), Some(3));
285 assert_eq!(map.doc_id("c", 3), None);
286 map.bind("c", "b", 4);
287 assert_eq!(map.unbind_node("c", 4), Some("b".to_owned()));
288 assert_eq!(map.node("c", "b"), None);
289 assert!(map.is_empty());
290 }
291
292 #[test]
293 fn persisted_entries_round_trip_named_keys() {
294 let mut map = VectorIdMap::new();
295 map.bind("chat", "x:1", 0);
296 map.bind("chat:emb", "y", 7);
297 let (restored, displaced) = VectorIdMap::from_entries(map.to_entries());
298 assert!(displaced.is_empty());
299 assert_eq!(restored.doc_id("chat", 0), Some("x:1"));
300 assert_eq!(restored.doc_id("chat:emb", 7), Some("y"));
301 }
302
303 #[test]
304 fn attached_vectors_cover_base_and_named_indexes_only() {
305 let mut map = VectorIdMap::new();
306 map.bind("chat", "a", 0);
307 map.bind("chat:emb", "a", 0);
308 map.bind("chat2", "a", 0);
309 map.bind("chat:title", "b", 0);
310 assert_eq!(
311 map.attached_vectors("chat", "a"),
312 AttachedVectors {
313 base: true,
314 named: vec!["emb".to_owned()],
315 }
316 );
317 assert!(!map.attached_vectors("chat", "zzz").any());
318 }
319
320 #[test]
321 fn persisted_duplicates_keep_the_latest_node() {
322 let entries = vec![
323 ("c:5".to_owned(), "a".to_owned(), 5),
324 ("c:2".to_owned(), "a".to_owned(), 2),
325 ];
326 let (map, displaced) = VectorIdMap::from_entries(entries);
327 assert_eq!(map.node("c", "a"), Some(5));
328 assert_eq!(displaced, vec![("c".to_owned(), 2)]);
329 }
330}