Skip to main content

mnemo_core/index/
usearch.rs

1use std::collections::HashMap;
2use std::path::Path;
3use std::sync::RwLock;
4
5use crate::error::{Error, Result};
6use crate::index::VectorIndex;
7use uuid::Uuid;
8
9pub struct UsearchIndex {
10    index: RwLock<usearch::Index>,
11    uuid_to_key: RwLock<HashMap<Uuid, u64>>,
12    key_to_uuid: RwLock<HashMap<u64, Uuid>>,
13    next_key: RwLock<u64>,
14    dimensions: usize,
15}
16
17impl UsearchIndex {
18    pub fn new(dimensions: usize) -> Result<Self> {
19        let opts = usearch::IndexOptions {
20            dimensions,
21            metric: usearch::MetricKind::Cos,
22            quantization: usearch::ScalarKind::F32,
23            ..Default::default()
24        };
25        let index = usearch::Index::new(&opts).map_err(|e| Error::Index(e.to_string()))?;
26        index
27            .reserve(10_000)
28            .map_err(|e| Error::Index(e.to_string()))?;
29
30        Ok(Self {
31            index: RwLock::new(index),
32            uuid_to_key: RwLock::new(HashMap::new()),
33            key_to_uuid: RwLock::new(HashMap::new()),
34            next_key: RwLock::new(0),
35            dimensions,
36        })
37    }
38
39    fn allocate_key(&self, id: Uuid) -> u64 {
40        let mut next = self.next_key.write().unwrap_or_else(|e| e.into_inner());
41        let key = *next;
42        *next += 1;
43        self.uuid_to_key
44            .write()
45            .unwrap_or_else(|e| e.into_inner())
46            .insert(id, key);
47        self.key_to_uuid
48            .write()
49            .unwrap_or_else(|e| e.into_inner())
50            .insert(key, id);
51        key
52    }
53
54    fn rollback_key(&self, id: Uuid, key: u64) {
55        self.uuid_to_key
56            .write()
57            .unwrap_or_else(|e| e.into_inner())
58            .remove(&id);
59        self.key_to_uuid
60            .write()
61            .unwrap_or_else(|e| e.into_inner())
62            .remove(&key);
63    }
64}
65
66#[async_trait::async_trait]
67impl VectorIndex for UsearchIndex {
68    fn add(&self, id: Uuid, vector: &[f32]) -> Result<()> {
69        if vector.len() != self.dimensions {
70            return Err(Error::Validation(format!(
71                "expected {} dimensions, got {}",
72                self.dimensions,
73                vector.len()
74            )));
75        }
76
77        // If this UUID already exists, remove it first
78        if self
79            .uuid_to_key
80            .read()
81            .unwrap_or_else(|e| e.into_inner())
82            .contains_key(&id)
83        {
84            self.remove(id)?;
85        }
86
87        let key = self.allocate_key(id);
88        let index = self.index.read().unwrap_or_else(|e| e.into_inner());
89
90        // Grow capacity if needed
91        if index.size() >= index.capacity() {
92            index
93                .reserve(index.capacity() + 10_000)
94                .map_err(|e| Error::Index(e.to_string()))?;
95        }
96
97        if let Err(e) = index.add(key, vector) {
98            // Rollback orphaned mappings on add failure
99            drop(index);
100            self.rollback_key(id, key);
101            return Err(Error::Index(e.to_string()));
102        }
103        Ok(())
104    }
105
106    fn remove(&self, id: Uuid) -> Result<()> {
107        let key = {
108            let map = self.uuid_to_key.read().unwrap_or_else(|e| e.into_inner());
109            match map.get(&id) {
110                Some(&k) => k,
111                None => return Ok(()),
112            }
113        };
114
115        let index = self.index.read().unwrap_or_else(|e| e.into_inner());
116        index.remove(key).map_err(|e| Error::Index(e.to_string()))?;
117
118        self.uuid_to_key
119            .write()
120            .unwrap_or_else(|e| e.into_inner())
121            .remove(&id);
122        self.key_to_uuid
123            .write()
124            .unwrap_or_else(|e| e.into_inner())
125            .remove(&key);
126        Ok(())
127    }
128
129    // In-memory USearch search is synchronous CPU work; the RwLock guard never
130    // crosses an `.await` (there is none), so the future stays `Send`.
131    async fn search(&self, query: &[f32], limit: usize) -> Result<Vec<(Uuid, f32)>> {
132        let index = self.index.read().unwrap_or_else(|e| e.into_inner());
133        let results = index
134            .search(query, limit)
135            .map_err(|e| Error::Index(e.to_string()))?;
136
137        let key_map = self.key_to_uuid.read().unwrap_or_else(|e| e.into_inner());
138        let mut output = Vec::new();
139        for (key, distance) in results.keys.iter().zip(results.distances.iter()) {
140            if let Some(&uuid) = key_map.get(key) {
141                output.push((uuid, *distance));
142            }
143        }
144        Ok(output)
145    }
146
147    async fn filtered_search(
148        &self,
149        query: &[f32],
150        limit: usize,
151        filter: &(dyn Fn(Uuid) -> bool + Send + Sync),
152    ) -> Result<Vec<(Uuid, f32)>> {
153        let index_size = self.len();
154        if index_size == 0 {
155            return Ok(Vec::new());
156        }
157        // Iterative oversample: start at 3x, double until we have enough or hit index size
158        let mut oversample = (limit * 3).max(1);
159        loop {
160            let results = self.search(query, oversample.min(index_size)).await?;
161            let filtered: Vec<(Uuid, f32)> = results
162                .into_iter()
163                .filter(|(uuid, _)| filter(*uuid))
164                .take(limit)
165                .collect();
166            if filtered.len() >= limit || oversample >= index_size {
167                return Ok(filtered);
168            }
169            oversample = (oversample * 2).min(index_size);
170        }
171    }
172
173    fn save(&self, path: &Path) -> Result<()> {
174        let path_str = path
175            .to_str()
176            .ok_or_else(|| Error::Index("non-UTF-8 index path".to_string()))?;
177        let index = self.index.read().unwrap_or_else(|e| e.into_inner());
178        index
179            .save(path_str)
180            .map_err(|e| Error::Index(e.to_string()))?;
181
182        // Save mappings alongside
183        let mappings_path = path.with_extension("mappings.json");
184        let uuid_to_key = self.uuid_to_key.read().unwrap_or_else(|e| e.into_inner());
185        let next_key = *self.next_key.read().unwrap_or_else(|e| e.into_inner());
186        let data = serde_json::json!({
187            "uuid_to_key": uuid_to_key.iter().map(|(k, v)| (k.to_string(), v)).collect::<HashMap<String, &u64>>(),
188            "next_key": next_key,
189        });
190        let json_str = serde_json::to_string(&data).map_err(|e| Error::Index(e.to_string()))?;
191        std::fs::write(&mappings_path, json_str).map_err(|e| Error::Index(e.to_string()))?;
192        Ok(())
193    }
194
195    fn load(&self, path: &Path) -> Result<()> {
196        let path_str = path
197            .to_str()
198            .ok_or_else(|| Error::Index("non-UTF-8 index path".to_string()))?;
199        let index = self.index.read().unwrap_or_else(|e| e.into_inner());
200        index
201            .load(path_str)
202            .map_err(|e| Error::Index(e.to_string()))?;
203
204        // Load mappings
205        let mappings_path = path.with_extension("mappings.json");
206        if mappings_path.exists() {
207            let data =
208                std::fs::read_to_string(&mappings_path).map_err(|e| Error::Index(e.to_string()))?;
209            let parsed: serde_json::Value =
210                serde_json::from_str(&data).map_err(|e| Error::Index(e.to_string()))?;
211
212            let mut uuid_to_key = self.uuid_to_key.write().unwrap_or_else(|e| e.into_inner());
213            let mut key_to_uuid = self.key_to_uuid.write().unwrap_or_else(|e| e.into_inner());
214            let mut next_key = self.next_key.write().unwrap_or_else(|e| e.into_inner());
215
216            uuid_to_key.clear();
217            key_to_uuid.clear();
218
219            if let Some(map) = parsed["uuid_to_key"].as_object() {
220                for (uuid_str, key_val) in map {
221                    let uuid =
222                        Uuid::parse_str(uuid_str).map_err(|e| Error::Index(e.to_string()))?;
223                    let key = key_val.as_u64().ok_or_else(|| {
224                        Error::Index(format!("invalid key value for UUID {uuid_str}"))
225                    })?;
226                    uuid_to_key.insert(uuid, key);
227                    key_to_uuid.insert(key, uuid);
228                }
229            }
230
231            if let Some(nk) = parsed["next_key"].as_u64() {
232                *next_key = nk;
233            }
234        }
235        Ok(())
236    }
237
238    fn len(&self) -> usize {
239        let index = self.index.read().unwrap_or_else(|e| e.into_inner());
240        index.size()
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    fn random_vector(dims: usize, seed: u64) -> Vec<f32> {
249        // Simple deterministic pseudo-random
250        let mut v = Vec::with_capacity(dims);
251        let mut x = seed;
252        for _ in 0..dims {
253            x = x.wrapping_mul(6364136223846793005).wrapping_add(1);
254            v.push((x as f32) / (u64::MAX as f32));
255        }
256        // Normalize
257        let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
258        if norm > 0.0 {
259            for x in &mut v {
260                *x /= norm;
261            }
262        }
263        v
264    }
265
266    #[tokio::test]
267    async fn test_add_and_search() {
268        let index = UsearchIndex::new(128).unwrap();
269
270        let mut ids = Vec::new();
271        let mut vectors = Vec::new();
272        for i in 0..100 {
273            let id = Uuid::now_v7();
274            let vec = random_vector(128, i);
275            index.add(id, &vec).unwrap();
276            ids.push(id);
277            vectors.push(vec);
278        }
279
280        assert_eq!(index.len(), 100);
281
282        // Search with the first vector should return itself as nearest
283        let results = index.search(&vectors[0], 5).await.unwrap();
284        assert!(!results.is_empty());
285        assert_eq!(results[0].0, ids[0]);
286    }
287
288    #[test]
289    fn test_remove() {
290        let index = UsearchIndex::new(128).unwrap();
291        let id = Uuid::now_v7();
292        let vec = random_vector(128, 42);
293
294        index.add(id, &vec).unwrap();
295        assert_eq!(index.len(), 1);
296
297        index.remove(id).unwrap();
298        assert_eq!(index.len(), 0);
299    }
300
301    #[tokio::test]
302    async fn test_filtered_search() {
303        let index = UsearchIndex::new(128).unwrap();
304
305        let mut ids = Vec::new();
306        for i in 0..50 {
307            let id = Uuid::now_v7();
308            let vec = random_vector(128, i);
309            index.add(id, &vec).unwrap();
310            ids.push(id);
311        }
312
313        // Filter out all even-indexed IDs
314        let excluded: std::collections::HashSet<Uuid> = ids.iter().step_by(2).copied().collect();
315        let query = random_vector(128, 0);
316        let results = index
317            .filtered_search(&query, 10, &|id| !excluded.contains(&id))
318            .await
319            .unwrap();
320
321        // All results should be odd-indexed
322        for (id, _) in &results {
323            assert!(!excluded.contains(id));
324        }
325    }
326
327    #[tokio::test]
328    async fn test_save_and_load() {
329        let dir = std::env::temp_dir().join(format!("usearch_test_{}", Uuid::now_v7()));
330        std::fs::create_dir_all(&dir).unwrap();
331        let index_path = dir.join("test.usearch");
332
333        let index = UsearchIndex::new(128).unwrap();
334        let id1 = Uuid::now_v7();
335        let id2 = Uuid::now_v7();
336        index.add(id1, &random_vector(128, 1)).unwrap();
337        index.add(id2, &random_vector(128, 2)).unwrap();
338
339        index.save(&index_path).unwrap();
340
341        // Load into a new index
342        let index2 = UsearchIndex::new(128).unwrap();
343        index2.load(&index_path).unwrap();
344        assert_eq!(index2.len(), 2);
345
346        // Search should still work
347        let results = index2.search(&random_vector(128, 1), 1).await.unwrap();
348        assert_eq!(results[0].0, id1);
349
350        // Cleanup
351        std::fs::remove_dir_all(&dir).ok();
352    }
353
354    #[test]
355    fn test_dimension_mismatch() {
356        let index = UsearchIndex::new(128).unwrap();
357        let result = index.add(Uuid::now_v7(), &vec![0.1; 64]);
358        assert!(result.is_err());
359    }
360}