1use std::collections::{HashMap, HashSet};
2use std::path::Path;
3
4use serde::{Deserialize, Serialize};
5
6use crate::errors::{Result, VecDbError};
7use crate::index::distance;
8use crate::types::{CollectionConfig, DistanceMetric, IndexType, Vector, VectorId};
9
10pub trait IndexBackend: Send + Sync {
15 fn build(&mut self, vectors: Vec<(VectorId, Vector)>) -> Result<()>;
16 fn insert(&mut self, id: VectorId, vector: Vector) -> Result<()>;
17 fn search(&self, query: &Vector, k: usize) -> Result<Vec<(VectorId, f32)>>;
18 fn delete(&mut self, id: &VectorId) -> Result<()>;
19 fn save(&self, path: &Path) -> Result<()>;
20 fn load_from(path: &Path, config: &CollectionConfig) -> Result<Box<dyn IndexBackend>>
21 where
22 Self: Sized;
23 fn len(&self) -> usize;
24 fn is_empty(&self) -> bool {
25 self.len() == 0
26 }
27 fn index_type(&self) -> IndexType;
28 fn rebuild(&mut self, vectors: Vec<(VectorId, Vector)>) -> Result<()> {
29 self.build(vectors)
30 }
31}
32
33#[derive(Clone)]
40struct Point {
41 data: Vec<f32>,
42 metric: DistanceMetric,
43}
44
45impl instant_distance::Point for Point {
46 fn distance(&self, other: &Self) -> f32 {
47 distance::compute_distance(&self.data, &other.data, &self.metric)
48 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct HnswConfig {
57 pub m: usize,
60 pub ef_construction: usize,
61 pub ef_search: usize,
62 pub metric: DistanceMetric,
63}
64
65impl Default for HnswConfig {
66 fn default() -> Self {
67 Self {
68 m: 16,
69 ef_construction: 200,
70 ef_search: 50,
71 metric: DistanceMetric::Cosine,
72 }
73 }
74}
75
76pub struct HnswIndex {
81 pub dimension: usize,
82 pub metric: DistanceMetric,
83 pub config: HnswConfig,
84 id_to_pos: HashMap<VectorId, usize>,
86 pos_to_id: Vec<VectorId>,
88 vectors: Vec<Vector>,
90 deleted: HashSet<VectorId>,
92 hnsw: Option<instant_distance::HnswMap<Point, VectorId>>,
94 dirty: bool,
96}
97
98impl HnswIndex {
99 pub fn new(dimension: usize, config: HnswConfig) -> Self {
100 let metric = config.metric.clone();
101 Self {
102 dimension,
103 metric,
104 config,
105 id_to_pos: HashMap::new(),
106 pos_to_id: Vec::new(),
107 vectors: Vec::new(),
108 deleted: HashSet::new(),
109 hnsw: None,
110 dirty: false,
111 }
112 }
113
114 pub fn from_collection_config(config: &CollectionConfig) -> Self {
115 let hnsw_config = HnswConfig {
116 m: config.hnsw_m,
117 ef_construction: config.hnsw_ef_construction,
118 ef_search: config.hnsw_ef_search,
119 metric: config.metric.clone(),
120 };
121 Self::new(config.dimension, hnsw_config)
122 }
123
124 pub fn load_file(path: &Path, config: &CollectionConfig) -> Result<Self> {
127 let json = std::fs::read_to_string(path)
128 .map_err(|e| VecDbError::StorageError(format!("hnsw load failed: {e}")))?;
129 let saved: SerializedHnswIndex = serde_json::from_str(&json)
130 .map_err(|e| VecDbError::SerializationError(e.to_string()))?;
131
132 let hnsw_config = HnswConfig {
133 m: config.hnsw_m,
134 ef_construction: config.hnsw_ef_construction,
135 ef_search: config.hnsw_ef_search,
136 metric: config.metric.clone(),
137 };
138 let metric = hnsw_config.metric.clone();
139
140 let mut index = Self {
141 dimension: saved.dimension,
142 metric,
143 config: hnsw_config,
144 id_to_pos: saved.id_to_pos,
145 pos_to_id: saved.pos_to_id,
146 vectors: saved.vectors,
147 deleted: saved.deleted.into_iter().collect(),
148 hnsw: None,
149 dirty: false,
150 };
151
152 let live: Vec<(VectorId, Vector)> = index
154 .pos_to_id
155 .iter()
156 .zip(index.vectors.iter())
157 .filter(|(id, _)| !index.deleted.contains(*id))
158 .map(|(id, v)| (id.clone(), v.clone()))
159 .collect();
160 if !live.is_empty() {
161 index.build(live)?;
162 }
163
164 tracing::info!("HNSW index loaded from {:?}", path);
165 Ok(index)
166 }
167
168 fn maybe_normalize(&self, v: Vec<f32>) -> Vec<f32> {
171 if matches!(self.metric, DistanceMetric::Cosine) {
172 distance::normalize(&v)
173 } else {
174 v
175 }
176 }
177
178 fn to_score(&self, dist: f32) -> f32 {
179 match self.metric {
180 DistanceMetric::Cosine => 1.0 - dist,
181 DistanceMetric::Euclidean => 1.0 / (1.0 + dist),
182 DistanceMetric::DotProduct => 1.0 - dist,
183 }
184 }
185
186 fn brute_force_search(&self, query: &Vector, k: usize) -> Result<Vec<(VectorId, f32)>> {
187 let q = self.maybe_normalize(query.clone());
188 let mut scored: Vec<(VectorId, f32)> = self
189 .pos_to_id
190 .iter()
191 .enumerate()
192 .filter(|(_, id)| !self.deleted.contains(*id))
193 .map(|(pos, id)| {
194 let dist = distance::compute_distance(&q, &self.vectors[pos], &self.metric);
195 (id.clone(), self.to_score(dist))
196 })
197 .collect();
198
199 scored.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
200 scored.truncate(k);
201 Ok(scored)
202 }
203
204 fn collect_live_vectors(&self) -> Vec<(VectorId, Vector)> {
205 self.pos_to_id
206 .iter()
207 .zip(self.vectors.iter())
208 .filter(|(id, _)| !self.deleted.contains(*id))
209 .map(|(id, v)| (id.clone(), v.clone()))
210 .collect()
211 }
212}
213
214impl IndexBackend for HnswIndex {
219 fn build(&mut self, vectors: Vec<(VectorId, Vector)>) -> Result<()> {
220 self.id_to_pos.clear();
221 self.pos_to_id.clear();
222 self.vectors.clear();
223 self.deleted.clear();
224
225 if vectors.is_empty() {
226 self.hnsw = None;
227 self.dirty = false;
228 return Ok(());
229 }
230
231 for (id, raw_vec) in vectors {
232 if raw_vec.len() != self.dimension {
233 return Err(VecDbError::DimensionMismatch {
234 expected: self.dimension,
235 got: raw_vec.len(),
236 });
237 }
238 let v = self.maybe_normalize(raw_vec);
239 let pos = self.vectors.len();
240 self.id_to_pos.insert(id.clone(), pos);
241 self.pos_to_id.push(id.clone());
242 self.vectors.push(v);
243 }
244
245 let points: Vec<Point> = self
246 .vectors
247 .iter()
248 .map(|v| Point {
249 data: v.clone(),
250 metric: self.metric.clone(),
251 })
252 .collect();
253 let values: Vec<VectorId> = self.pos_to_id.clone();
254
255 let hnsw = instant_distance::Builder::default()
256 .ef_construction(self.config.ef_construction)
257 .ef_search(self.config.ef_search)
258 .build(points, values);
259
260 self.hnsw = Some(hnsw);
261 self.dirty = false;
262
263 tracing::info!(
264 "HNSW index built: {} vectors, metric={:?}",
265 self.vectors.len(),
266 self.metric
267 );
268 Ok(())
269 }
270
271 fn insert(&mut self, id: VectorId, vector: Vector) -> Result<()> {
272 if vector.len() != self.dimension {
273 return Err(VecDbError::DimensionMismatch {
274 expected: self.dimension,
275 got: vector.len(),
276 });
277 }
278 let v = self.maybe_normalize(vector);
279
280 if let Some(&pos) = self.id_to_pos.get(&id) {
281 self.deleted.remove(&id);
283 self.vectors[pos] = v;
284 } else {
285 let pos = self.vectors.len();
287 self.vectors.push(v);
288 self.id_to_pos.insert(id.clone(), pos);
289 self.pos_to_id.push(id.clone());
290 }
291
292 self.dirty = true;
293 tracing::debug!("HNSW insert: id={}, dirty={}", id, self.dirty);
294
295 let n = self.vectors.len();
297 if n <= 100 || n.is_multiple_of(1000) {
298 let live = self.collect_live_vectors();
299 self.build(live)?;
300 }
301
302 Ok(())
303 }
304
305 fn search(&self, query: &Vector, k: usize) -> Result<Vec<(VectorId, f32)>> {
306 if query.len() != self.dimension {
307 return Err(VecDbError::DimensionMismatch {
308 expected: self.dimension,
309 got: query.len(),
310 });
311 }
312
313 if self.hnsw.is_none() || self.dirty {
315 return self.brute_force_search(query, k);
316 }
317
318 let q = self.maybe_normalize(query.clone());
319 let query_point = Point {
320 data: q,
321 metric: self.metric.clone(),
322 };
323
324 let hnsw = self.hnsw.as_ref().unwrap();
325 let mut search = instant_distance::Search::default();
326
327 let mut results: Vec<(VectorId, f32)> = hnsw
328 .search(&query_point, &mut search)
329 .filter(|item| !self.deleted.contains(item.value))
330 .map(|item| (item.value.clone(), self.to_score(item.distance)))
331 .collect();
332
333 results.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
334 results.truncate(k);
335 Ok(results)
336 }
337
338 fn delete(&mut self, id: &VectorId) -> Result<()> {
339 if !self.id_to_pos.contains_key(id) {
340 return Err(VecDbError::NotFound { id: id.clone() });
341 }
342 self.deleted.insert(id.clone());
343 self.dirty = true;
344
345 tracing::debug!(
346 "HNSW delete: id={}, deleted_count={}",
347 id,
348 self.deleted.len()
349 );
350
351 if self.deleted.len() > self.vectors.len() / 4 {
353 let live = self.collect_live_vectors();
354 self.build(live)?;
355 }
356
357 Ok(())
358 }
359
360 fn save(&self, path: &Path) -> Result<()> {
361 let saved = SerializedHnswIndex {
362 dimension: self.dimension,
363 config: self.config.clone(),
364 id_to_pos: self.id_to_pos.clone(),
365 pos_to_id: self.pos_to_id.clone(),
366 vectors: self.vectors.clone(),
367 deleted: self.deleted.iter().cloned().collect(),
368 version: 1,
369 };
370 let json = serde_json::to_string(&saved)
371 .map_err(|e| VecDbError::SerializationError(e.to_string()))?;
372 std::fs::write(path, json)
373 .map_err(|e| VecDbError::StorageError(format!("hnsw save failed: {e}")))?;
374 tracing::info!("HNSW index saved to {:?}", path);
375 Ok(())
376 }
377
378 fn load_from(path: &Path, config: &CollectionConfig) -> Result<Box<dyn IndexBackend>>
379 where
380 Self: Sized,
381 {
382 Ok(Box::new(Self::load_file(path, config)?))
383 }
384
385 fn len(&self) -> usize {
386 self.vectors.len().saturating_sub(self.deleted.len())
387 }
388
389 fn index_type(&self) -> IndexType {
390 IndexType::HNSW
391 }
392}
393
394#[derive(Serialize, Deserialize)]
399struct SerializedHnswIndex {
400 dimension: usize,
401 config: HnswConfig,
402 id_to_pos: HashMap<VectorId, usize>,
403 pos_to_id: Vec<VectorId>,
404 vectors: Vec<Vector>,
405 deleted: Vec<VectorId>,
406 version: u32,
407}
408
409#[cfg(test)]
414mod tests {
415 use super::*;
416 use crate::types::{CollectionConfig, VectorRecord};
417 use chrono::Utc;
418 use serde_json::json;
419 use tempfile::tempdir;
420
421 fn make_index(dim: usize, metric: DistanceMetric) -> HnswIndex {
422 HnswIndex::new(
423 dim,
424 HnswConfig {
425 m: 16,
426 ef_construction: 100,
427 ef_search: 50,
428 metric,
429 },
430 )
431 }
432
433 fn dummy_record(id: &str, vector: Vector) -> VectorRecord {
434 VectorRecord {
435 id: id.to_string(),
436 vector,
437 payload: json!({ "id": id }),
438 text: Some(format!("text for {id}")),
439 created_at: Utc::now(),
440 updated_at: Utc::now(),
441 }
442 }
443
444 #[test]
446 fn test_distance_cosine_basic() {
447 let mut idx = make_index(3, DistanceMetric::Cosine);
448 idx.insert("a".into(), vec![1.0, 0.0, 0.0]).unwrap();
449 idx.insert("b".into(), vec![0.0, 1.0, 0.0]).unwrap();
450 idx.insert("c".into(), vec![0.0, 0.0, 1.0]).unwrap();
451 idx.insert("d".into(), vec![1.0, 0.0, 0.0]).unwrap();
452
453 let results = idx.search(&vec![1.0, 0.0, 0.0], 2).unwrap();
454 let ids: Vec<&str> = results.iter().map(|(id, _)| id.as_str()).collect();
455 assert!(ids.contains(&"a"), "expected 'a' in top-2");
456 assert!(ids.contains(&"d"), "expected 'd' in top-2");
457 assert!(results.iter().all(|(_, s)| *s > 0.99));
458 }
459
460 #[test]
462 fn test_euclidean_search() {
463 let mut idx = make_index(2, DistanceMetric::Euclidean);
464 idx.insert("near".into(), vec![1.0, 1.0]).unwrap();
465 idx.insert("far".into(), vec![100.0, 100.0]).unwrap();
466 idx.insert("medium".into(), vec![5.0, 5.0]).unwrap();
467
468 let results = idx.search(&vec![1.1, 1.1], 1).unwrap();
469 assert_eq!(results[0].0, "near");
470 }
471
472 #[test]
474 fn test_brute_force_fallback() {
475 let mut idx = make_index(2, DistanceMetric::Euclidean);
479 for i in 0..5usize {
482 idx.insert(format!("v{i}"), vec![i as f32, 0.0]).unwrap();
483 }
484 idx.hnsw = None;
486 idx.dirty = false; let results = idx.search(&vec![2.0, 0.0], 1).unwrap();
489 assert!(!results.is_empty());
490 assert_eq!(results[0].0, "v2");
491 }
492
493 #[test]
495 fn test_soft_delete() {
496 let mut idx = make_index(2, DistanceMetric::Euclidean);
497 idx.insert("a".into(), vec![1.0, 0.0]).unwrap();
498 idx.insert("b".into(), vec![2.0, 0.0]).unwrap();
499 idx.insert("c".into(), vec![3.0, 0.0]).unwrap();
500 idx.insert("d".into(), vec![4.0, 0.0]).unwrap();
501 idx.insert("e".into(), vec![5.0, 0.0]).unwrap();
502
503 idx.delete(&"a".to_string()).unwrap();
504 assert_eq!(idx.len(), 4);
505
506 let results = idx.search(&vec![1.0, 0.0], 5).unwrap();
507 let ids: Vec<&str> = results.iter().map(|(id, _)| id.as_str()).collect();
508 assert!(
509 !ids.contains(&"a"),
510 "deleted 'a' must not appear in results"
511 );
512 }
513
514 #[test]
516 fn test_save_and_load() {
517 let dir = tempdir().unwrap();
518 let path = dir.path().join("test.hnsw.json");
519 let config = CollectionConfig::new("test", 3);
520
521 let mut idx = HnswIndex::from_collection_config(&config);
522 for i in 0..10usize {
523 idx.insert(
524 format!("v{i}"),
525 vec![i as f32, (i + 1) as f32, (i + 2) as f32],
526 )
527 .unwrap();
528 }
529
530 let original = idx.search(&vec![5.0, 6.0, 7.0], 3).unwrap();
531 idx.save(&path).unwrap();
532
533 let loaded = HnswIndex::load_file(&path, &config).unwrap();
534 let after = loaded.search(&vec![5.0, 6.0, 7.0], 3).unwrap();
535
536 assert_eq!(original.len(), after.len());
537 assert_eq!(original[0].0, after[0].0);
539 }
540
541 #[test]
543 fn test_dimension_mismatch_on_insert() {
544 let mut idx = make_index(4, DistanceMetric::Cosine);
545 let res = idx.insert("bad".into(), vec![1.0, 2.0, 3.0]);
546 assert!(
547 matches!(
548 res,
549 Err(VecDbError::DimensionMismatch {
550 expected: 4,
551 got: 3
552 })
553 ),
554 "expected DimensionMismatch"
555 );
556 }
557
558 #[test]
560 fn test_storage_search_integration() {
561 use crate::storage::Storage;
562
563 let dir = tempdir().unwrap();
564 let config = CollectionConfig::new("srch", 4);
565 let mut storage = Storage::create(dir.path(), &config).unwrap();
566
567 for i in 0..10usize {
568 let rec = dummy_record(
569 &format!("doc{i}"),
570 vec![
571 (i as f32 + 1.0),
572 (i as f32 + 2.0),
573 (i as f32 + 3.0),
574 (i as f32 + 4.0),
575 ],
576 );
577 storage.upsert(rec).unwrap();
578 }
579
580 let query = vec![5.0f32, 6.0, 7.0, 8.0];
581 let results = storage.search(&query, 10).unwrap();
582 assert!(!results.is_empty());
583 assert!(results.len() <= 10);
584
585 for r in &results {
586 assert!(
587 storage.metadata.get(&r.id).is_ok(),
588 "id {} not in metadata",
589 r.id
590 );
591 assert!(
592 r.score >= 0.0 && r.score <= 1.0,
593 "score {} out of [0,1]",
594 r.score
595 );
596 }
597 }
598
599 #[test]
601 fn test_index_rebuild_after_reopen() {
602 use crate::storage::Storage;
603
604 let dir = tempdir().unwrap();
605 let config = CollectionConfig::new("reopen", 2);
606
607 {
608 let mut storage = Storage::create(dir.path(), &config).unwrap();
609 for i in 0..5usize {
610 storage
611 .upsert(dummy_record(&format!("r{i}"), vec![i as f32, 1.0]))
612 .unwrap();
613 }
614 storage.save_indexes().unwrap();
615 }
616
617 let storage = Storage::open(dir.path(), "reopen").unwrap();
618 let results = storage.search(&vec![2.0, 1.0], 3).unwrap();
619 assert!(!results.is_empty());
620 }
621
622 #[test]
624 fn test_auto_rebuild_on_large_insert() {
625 let mut idx = make_index(2, DistanceMetric::Euclidean);
626 for i in 0..1000usize {
627 idx.insert(format!("v{i}"), vec![i as f32, 0.0]).unwrap();
628 }
629 assert_eq!(idx.len(), 1000);
630
631 let results = idx.search(&vec![500.0, 0.0], 1).unwrap();
632 assert!(!results.is_empty());
633 assert_eq!(results[0].0, "v500");
634 }
635}