mnemo_core/index/
usearch.rs1use 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 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 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 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 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 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 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 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 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 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 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 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 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 let index2 = UsearchIndex::new(128).unwrap();
343 index2.load(&index_path).unwrap();
344 assert_eq!(index2.len(), 2);
345
346 let results = index2.search(&random_vector(128, 1), 1).await.unwrap();
348 assert_eq!(results[0].0, id1);
349
350 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}