1pub mod metadata;
2pub mod mmap;
3pub mod wal;
4
5pub use metadata::MetadataStore;
6pub use mmap::MmapVectorStore;
7pub use wal::{WalEntry, WriteAheadLog};
8
9use std::path::{Path, PathBuf};
10
11use crate::errors::{Result, VecDbError};
12use crate::hybrid::{FusionStrategy, HybridEngine};
13use crate::index::{AnyIndex, IndexBackend};
14use crate::planner::{PlanExecutor, QueryPlanner};
15use crate::sparse::SparseIndex;
16use crate::types::{
17 CollectionConfig, IndexStats, SearchRequest, SearchResult, Vector, VectorId, VectorRecord,
18};
19
20pub enum UpsertResult {
21 Inserted,
22 Updated,
23}
24
25pub struct Storage {
26 pub vectors: MmapVectorStore,
27 pub wal: WriteAheadLog,
28 pub metadata: MetadataStore,
29 pub index: AnyIndex,
30 pub sparse: SparseIndex,
31 pub hybrid: HybridEngine,
32 pub planner: QueryPlanner,
33 pub config: CollectionConfig,
34 data_dir: PathBuf,
35}
36
37impl Storage {
38 pub fn create(data_dir: &Path, config: &CollectionConfig) -> Result<Self> {
39 std::fs::create_dir_all(data_dir)
40 .map_err(|e| VecDbError::StorageError(format!("create_dir_all failed: {e}")))?;
41
42 let vectors_path = data_dir.join(format!("{}.vectors", config.name));
43 let wal_path = data_dir.join(format!("{}.wal", config.name));
44 let metadata_path = data_dir.join(format!("{}.db", config.name));
45 let sparse_path = data_dir.join(format!("{}.sparse.json", config.name));
46
47 let vectors = MmapVectorStore::create(&vectors_path, config.dimension, 1024)?;
48 let wal = WriteAheadLog::open(&wal_path)?;
49 let metadata = MetadataStore::open(&metadata_path)?;
50 metadata.save_collection(config)?;
51 metadata.ensure_payload_indexes(&config.indexed_payload_fields)?;
52
53 let index = AnyIndex::from_config(config);
54 let sparse = SparseIndex::create(&sparse_path);
55 let hybrid = HybridEngine::default();
56 let planner = QueryPlanner::new(config.clone());
57
58 tracing::info!("Storage created for collection '{}'", config.name);
59
60 Ok(Self {
61 vectors,
62 wal,
63 metadata,
64 index,
65 sparse,
66 hybrid,
67 planner,
68 config: config.clone(),
69 data_dir: data_dir.to_path_buf(),
70 })
71 }
72
73 pub fn open(data_dir: &Path, collection_name: &str) -> Result<Self> {
74 let vectors_path = data_dir.join(format!("{collection_name}.vectors"));
75 let wal_path = data_dir.join(format!("{collection_name}.wal"));
76 let metadata_path = data_dir.join(format!("{collection_name}.db"));
77 let sparse_path = data_dir.join(format!("{collection_name}.sparse.json"));
78
79 let vectors = MmapVectorStore::open(&vectors_path)?;
80 let wal = WriteAheadLog::open(&wal_path)?;
81 let metadata = MetadataStore::open(&metadata_path)?;
82 let config = metadata.load_collection(collection_name)?;
83 metadata.ensure_payload_indexes(&config.indexed_payload_fields)?;
84
85 let (index, saved_index_exists) =
86 AnyIndex::load_or_create(data_dir, collection_name, &config);
87
88 let sparse = SparseIndex::open(&sparse_path)?;
89 let hybrid = HybridEngine::default();
90 let planner = QueryPlanner::new(config.clone());
91
92 let mut storage = Self {
93 vectors,
94 wal,
95 metadata,
96 index,
97 sparse,
98 hybrid,
99 planner,
100 config,
101 data_dir: data_dir.to_path_buf(),
102 };
103
104 let recovered = storage.recover_from_wal()?;
105
106 if !saved_index_exists || recovered > 0 {
108 storage.rebuild_index()?;
109 }
110
111 tracing::info!(
112 "Storage opened for collection '{}', {} vectors ({} recovered from WAL)",
113 collection_name,
114 storage.vectors.len(),
115 recovered
116 );
117
118 Ok(storage)
119 }
120
121 pub fn upsert(&mut self, record: VectorRecord) -> Result<UpsertResult> {
122 if record.vector.len() != self.config.dimension {
123 return Err(VecDbError::DimensionMismatch {
124 expected: self.config.dimension,
125 got: record.vector.len(),
126 });
127 }
128
129 let id = record.id.clone();
130 let existing = self.metadata.get(&id);
131
132 let (mmap_index, result) = match existing {
133 Ok((idx, _)) => (idx, UpsertResult::Updated),
134 Err(VecDbError::NotFound { .. }) => {
135 let idx = self.vectors.append(&record.vector)?;
136 (idx, UpsertResult::Inserted)
137 }
138 Err(e) => return Err(e),
139 };
140
141 self.wal.append(&WalEntry::Insert {
143 id: id.clone(),
144 mmap_index,
145 record: record.clone(),
146 })?;
147
148 if matches!(result, UpsertResult::Updated) {
150 self.vectors.overwrite(mmap_index, &record.vector)?;
151 }
152
153 self.metadata.upsert(&id, mmap_index, &record)?;
155
156 self.index
158 .insert(record.id.clone(), record.vector.clone())?;
159
160 if let Some(ref text) = record.text {
162 if !text.is_empty() {
163 self.sparse.index_document(&id, text)?;
164 }
165 }
166
167 Ok(result)
168 }
169
170 pub fn bulk_upsert(&mut self, records: Vec<VectorRecord>) -> Result<usize> {
179 for r in &records {
180 if r.vector.len() != self.config.dimension {
181 return Err(VecDbError::DimensionMismatch {
182 expected: self.config.dimension,
183 got: r.vector.len(),
184 });
185 }
186 }
187
188 let mut wal_entries = Vec::with_capacity(records.len());
189 let mut items: Vec<(String, usize, VectorRecord)> = Vec::with_capacity(records.len());
190 for record in records {
191 let id = record.id.clone();
192 let mmap_index = self.vectors.append(&record.vector)?;
193 wal_entries.push(WalEntry::Insert {
194 id: id.clone(),
195 mmap_index,
196 record: record.clone(),
197 });
198 items.push((id, mmap_index, record));
199 }
200
201 self.wal.append_batch(&wal_entries)?;
202 drop(wal_entries);
203
204 let refs: Vec<(String, usize, &VectorRecord)> = items
205 .iter()
206 .map(|(id, idx, rec)| (id.clone(), *idx, rec))
207 .collect();
208 self.metadata.upsert_batch(&refs)?;
209 drop(refs);
210 let count = items.len();
211 drop(items);
212
213 self.rebuild_index()?;
215 self.checkpoint()?;
217 Ok(count)
218 }
219
220 pub fn delete(&mut self, id: &VectorId) -> Result<()> {
221 self.wal.append(&WalEntry::Delete { id: id.clone() })?;
222 self.metadata.delete(id)?;
223 let _ = self.index.delete(id);
225 let _ = self.sparse.remove_document(id);
227 Ok(())
228 }
229
230 pub fn search(&self, query: &crate::types::Vector, k: usize) -> Result<Vec<SearchResult>> {
232 let hits = self.index.search(query, k)?;
233 let mut results = Vec::with_capacity(hits.len());
234 for (id, score) in hits {
235 match self.metadata.get(&id) {
236 Ok((_, rec)) => results.push(SearchResult {
237 id,
238 score,
239 dense_score: Some(score),
240 sparse_score: None,
241 payload: rec.payload,
242 text: rec.text,
243 }),
244 Err(_) => continue, }
246 }
247 Ok(results)
248 }
249
250 pub fn search_sparse(&self, query: &str, k: usize) -> Result<Vec<SearchResult>> {
252 let hits = self.sparse.search(query, k)?;
253 let mut results = Vec::with_capacity(hits.len());
254 for (id, score) in hits {
255 match self.metadata.get(&id) {
256 Ok((_, rec)) => results.push(SearchResult {
257 id,
258 score,
259 dense_score: None,
260 sparse_score: Some(score),
261 payload: rec.payload,
262 text: rec.text,
263 }),
264 Err(_) => continue,
265 }
266 }
267 Ok(results)
268 }
269
270 pub fn execute_sql(&self, sql: &str) -> Result<Vec<SearchResult>> {
276 use crate::planner::sql::{AstConverter, SqlParser};
277
278 let vector_count = self.metadata.count_active()?;
279
280 let stmt = SqlParser::parse(sql)?;
282 let query_vector: Option<Vector> = AstConverter::extract_query_vector(&stmt);
283
284 let converter = AstConverter::new(QueryPlanner::new(self.config.clone()));
286 let plan = converter.convert(stmt, vector_count)?;
287
288 tracing::debug!("SQL plan:\n{}", self.planner.explain(&plan));
289 PlanExecutor::new(self).execute(&plan, query_vector.as_ref(), None)
290 }
291
292 pub fn execute_search(&self, request: &SearchRequest) -> Result<Vec<SearchResult>> {
298 let vector_count = self.metadata.count_active()?;
299 let plan = self.planner.plan_search(request, vector_count)?;
300 tracing::debug!("Query plan:\n{}", self.planner.explain(&plan));
301 let executor = PlanExecutor::new(self);
302 executor.execute(
303 &plan,
304 request.vector.as_ref(),
305 request.query_text.as_deref(),
306 )
307 }
308
309 pub fn search_dense(&self, query_vector: &Vector, k: usize) -> Result<Vec<SearchResult>> {
314 if query_vector.len() != self.config.dimension {
315 return Err(VecDbError::DimensionMismatch {
316 expected: self.config.dimension,
317 got: query_vector.len(),
318 });
319 }
320 let hits = self.index.search(query_vector, k)?;
321 let mut results = Vec::with_capacity(hits.len());
322 for (id, score) in hits {
323 match self.metadata.get(&id) {
324 Ok((_, rec)) => results.push(SearchResult {
325 id,
326 score,
327 dense_score: Some(score),
328 sparse_score: None,
329 payload: rec.payload,
330 text: rec.text,
331 }),
332 Err(_) => continue,
333 }
334 }
335 Ok(results)
336 }
337
338 pub fn search_dense_filtered(
350 &self,
351 query: &Vector,
352 k: usize,
353 filter: &serde_json::Value,
354 ) -> Result<Vec<SearchResult>> {
355 if query.len() != self.config.dimension {
356 return Err(VecDbError::DimensionMismatch {
357 expected: self.config.dimension,
358 got: query.len(),
359 });
360 }
361
362 let allowed = self
363 .metadata
364 .filter_ids_indexed(filter, &self.config.indexed_payload_fields)?;
365 if allowed.is_empty() {
366 return Ok(vec![]);
367 }
368 let n = self.metadata.count_active()?;
369 let bruteforce_threshold = (n / 4).max(8192);
373 let metric = &self.config.metric;
374
375 if allowed.len() <= bruteforce_threshold {
376 let mut scored: Vec<(VectorId, f32)> = Vec::with_capacity(allowed.len());
377 for (id, mmap_index) in &allowed {
378 let v = self.vectors.get(*mmap_index)?;
379 let dist = crate::index::compute_distance(query, &v, metric);
380 scored.push((id.clone(), crate::index::to_score(dist, metric)));
381 }
382 scored.sort_unstable_by(|a, b| {
383 b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
384 });
385 scored.truncate(k);
386
387 let mut results = Vec::with_capacity(scored.len());
388 for (id, score) in scored {
389 if let Ok((_, rec)) = self.metadata.get(&id) {
390 results.push(SearchResult {
391 id,
392 score,
393 dense_score: Some(score),
394 sparse_score: None,
395 payload: rec.payload,
396 text: rec.text,
397 });
398 }
399 }
400 return Ok(results);
401 }
402
403 let allowed_set: std::collections::HashSet<VectorId> =
405 allowed.into_iter().map(|(id, _)| id).collect();
406 let oversample = (k * 4).max(k + 50);
407 let hits = self.index.search(query, oversample)?;
408 let mut results = Vec::with_capacity(k);
409 for (id, score) in hits {
410 if !allowed_set.contains(&id) {
411 continue;
412 }
413 if let Ok((_, rec)) = self.metadata.get(&id) {
414 results.push(SearchResult {
415 id,
416 score,
417 dense_score: Some(score),
418 sparse_score: None,
419 payload: rec.payload,
420 text: rec.text,
421 });
422 }
423 if results.len() >= k {
424 break;
425 }
426 }
427 Ok(results)
428 }
429
430 pub fn search_hybrid(
440 &self,
441 query_vector: Option<&Vector>,
442 query_text: Option<&str>,
443 k: usize,
444 alpha: f32,
445 strategy: FusionStrategy,
446 ) -> Result<Vec<SearchResult>> {
447 if query_vector.is_none() && query_text.is_none() {
449 return Err(VecDbError::InvalidQuery(
450 "hybrid search requires at least one of query_vector or query_text".into(),
451 ));
452 }
453 if let Some(qv) = query_vector {
454 if qv.len() != self.config.dimension {
455 return Err(VecDbError::DimensionMismatch {
456 expected: self.config.dimension,
457 got: qv.len(),
458 });
459 }
460 }
461
462 let oversample_k = k * 5;
463
464 let dense_results: Vec<(VectorId, f32)> = if let Some(qv) = query_vector {
466 self.index.search(qv, oversample_k)?
467 } else {
468 vec![]
469 };
470
471 let sparse_results: Vec<(VectorId, f32)> = if let Some(qt) = query_text {
473 if !dense_results.is_empty() {
474 let candidate_ids: Vec<VectorId> =
476 dense_results.iter().map(|(id, _)| id.clone()).collect();
477 self.sparse.score(qt, &candidate_ids)?
478 } else {
479 self.sparse.score_all(qt, oversample_k)?
481 }
482 } else {
483 vec![]
484 };
485
486 let engine = HybridEngine::new(alpha, strategy, 5);
488 let mut fused = engine.fuse(dense_results, sparse_results, k)?;
489
490 for result in &mut fused {
492 match self.metadata.get(&result.id) {
493 Ok((_, rec)) => {
494 result.payload = rec.payload;
495 result.text = rec.text;
496 }
497 Err(_) => {
498 tracing::warn!(
499 "hybrid search: metadata not found for id '{}', keeping null payload",
500 result.id
501 );
502 }
503 }
504 }
505
506 Ok(fused)
507 }
508
509 pub fn rebuild_index(&mut self) -> Result<()> {
512 let active = self.metadata.list_active()?;
513 let mut pairs: Vec<(VectorId, crate::types::Vector)> = Vec::with_capacity(active.len());
514 for (id, mmap_idx) in &active {
515 let vec = self.vectors.get(*mmap_idx)?;
516 pairs.push((id.clone(), vec));
517 }
518 let count = pairs.len();
519 self.index.build(pairs)?;
520
521 self.sparse = SparseIndex::create(
523 &self
524 .data_dir
525 .join(format!("{}.sparse.json", self.config.name)),
526 );
527 for (id, _) in &active {
528 if let Ok((_, rec)) = self.metadata.get(id) {
529 if let Some(ref text) = rec.text {
530 if !text.is_empty() {
531 let _ = self.sparse.index_document(id, text);
532 }
533 }
534 }
535 }
536
537 tracing::info!("Index rebuilt from {} vectors", count);
538 Ok(())
539 }
540
541 pub fn save_indexes(&self) -> Result<()> {
543 self.index
544 .save_for_collection(&self.data_dir, &self.config.name)?;
545 self.sparse.save()?;
546 Ok(())
547 }
548
549 pub fn recover_from_wal(&mut self) -> Result<usize> {
550 let entries = self.wal.replay()?;
551 let mut applied = 0usize;
552
553 for entry in &entries {
554 match entry {
555 WalEntry::Insert {
556 id,
557 mmap_index,
558 record,
559 } => match self.metadata.get(id) {
560 Ok((existing_idx, _)) if existing_idx == *mmap_index => {
561 }
563 _ => {
564 self.metadata.upsert(id, *mmap_index, record)?;
565 applied += 1;
566 }
567 },
568 WalEntry::Delete { id } => match self.metadata.get(id) {
569 Ok(_) => {
570 self.metadata.delete(id)?;
571 applied += 1;
572 }
573 Err(VecDbError::NotFound { .. }) => {}
574 Err(e) => return Err(e),
575 },
576 WalEntry::Checkpoint { .. } => {}
577 }
578 }
579
580 tracing::info!("WAL recovery applied {} entries", applied);
581 Ok(applied)
582 }
583
584 pub fn checkpoint(&mut self) -> Result<()> {
585 self.wal.checkpoint()?;
586 self.wal.truncate_after_checkpoint()?;
587 tracing::info!(
588 "Storage checkpoint complete for collection '{}'",
589 self.config.name
590 );
591 Ok(())
592 }
593
594 pub fn stats(&self) -> Result<IndexStats> {
595 let vector_count = self.metadata.count_active()?;
596 let vectors_path = self.data_dir.join(format!("{}.vectors", self.config.name));
597 let disk_bytes = std::fs::metadata(&vectors_path)
598 .map(|m| m.len())
599 .unwrap_or(0);
600
601 Ok(IndexStats {
602 collection: self.config.name.clone(),
603 vector_count,
604 index_type: self.config.index_type.clone(),
605 dimension: self.config.dimension,
606 disk_bytes,
607 memory_bytes: 0,
608 })
609 }
610}
611
612#[cfg(test)]
613mod tests {
614 use super::*;
615 use chrono::Utc;
616 use serde_json::json;
617 use tempfile::tempdir;
618
619 use crate::types::{CollectionConfig, Vector, VectorRecord};
620
621 fn dummy_record(id: &str, vector: Vector) -> VectorRecord {
622 let now = Utc::now();
623 VectorRecord {
624 id: id.to_string(),
625 vector,
626 payload: json!({ "id": id }),
627 text: Some(format!("text for {id}")),
628 created_at: now,
629 updated_at: now,
630 }
631 }
632
633 #[test]
634 fn test_mmap_create_and_open() {
635 let dir = tempdir().unwrap();
636 let path = dir.path().join("test.vectors");
637
638 {
639 let mut store = MmapVectorStore::create(&path, 4, 10).unwrap();
640 store.append(&vec![1.0, 2.0, 3.0, 4.0]).unwrap();
641 store.append(&vec![5.0, 6.0, 7.0, 8.0]).unwrap();
642 store.append(&vec![9.0, 10.0, 11.0, 12.0]).unwrap();
643 assert_eq!(store.len(), 3);
644 }
645
646 let store = MmapVectorStore::open(&path).unwrap();
647 assert_eq!(store.len(), 3);
648 assert_eq!(store.get(0).unwrap(), vec![1.0f32, 2.0, 3.0, 4.0]);
649 assert_eq!(store.get(1).unwrap(), vec![5.0f32, 6.0, 7.0, 8.0]);
650 assert_eq!(store.get(2).unwrap(), vec![9.0f32, 10.0, 11.0, 12.0]);
651 }
652
653 #[test]
654 fn test_mmap_dimension_mismatch() {
655 let dir = tempdir().unwrap();
656 let path = dir.path().join("test.vectors");
657 let mut store = MmapVectorStore::create(&path, 4, 10).unwrap();
658 let result = store.append(&vec![1.0, 2.0, 3.0]);
659 assert!(matches!(
660 result,
661 Err(VecDbError::DimensionMismatch {
662 expected: 4,
663 got: 3
664 })
665 ));
666 }
667
668 #[test]
669 fn test_mmap_grow() {
670 let dir = tempdir().unwrap();
671 let path = dir.path().join("test.vectors");
672 let mut store = MmapVectorStore::create(&path, 2, 2).unwrap();
673 store.append(&vec![1.0f32, 2.0]).unwrap();
674 store.append(&vec![3.0f32, 4.0]).unwrap();
675 store.append(&vec![5.0f32, 6.0]).unwrap(); assert_eq!(store.len(), 3);
677 assert_eq!(store.get(0).unwrap(), vec![1.0f32, 2.0]);
678 assert_eq!(store.get(1).unwrap(), vec![3.0f32, 4.0]);
679 assert_eq!(store.get(2).unwrap(), vec![5.0f32, 6.0]);
680 }
681
682 #[test]
683 fn test_wal_append_and_replay() {
684 let dir = tempdir().unwrap();
685 let path = dir.path().join("test.wal");
686 let mut wal = WriteAheadLog::open(&path).unwrap();
687
688 wal.append(&WalEntry::Insert {
689 id: "doc1".into(),
690 mmap_index: 0,
691 record: dummy_record("doc1", vec![1.0f32, 2.0]),
692 })
693 .unwrap();
694 wal.append(&WalEntry::Insert {
695 id: "doc2".into(),
696 mmap_index: 1,
697 record: dummy_record("doc2", vec![3.0f32, 4.0]),
698 })
699 .unwrap();
700 wal.append(&WalEntry::Insert {
701 id: "doc3".into(),
702 mmap_index: 2,
703 record: dummy_record("doc3", vec![5.0f32, 6.0]),
704 })
705 .unwrap();
706 wal.append(&WalEntry::Delete { id: "doc1".into() }).unwrap();
707
708 let entries = wal.replay().unwrap();
709 assert_eq!(entries.len(), 4);
710 assert!(matches!(&entries[0], WalEntry::Insert { id, .. } if id == "doc1"));
711 assert!(matches!(&entries[3], WalEntry::Delete { id } if id == "doc1"));
712 }
713
714 #[test]
715 fn test_wal_checkpoint_and_truncate() {
716 let dir = tempdir().unwrap();
717 let path = dir.path().join("test.wal");
718 let mut wal = WriteAheadLog::open(&path).unwrap();
719
720 let r = dummy_record("x", vec![1.0f32]);
721 for i in 0..3usize {
722 wal.append(&WalEntry::Insert {
723 id: format!("doc{i}"),
724 mmap_index: i,
725 record: r.clone(),
726 })
727 .unwrap();
728 }
729 wal.checkpoint().unwrap();
730 for i in 3..5usize {
731 wal.append(&WalEntry::Insert {
732 id: format!("doc{i}"),
733 mmap_index: i,
734 record: r.clone(),
735 })
736 .unwrap();
737 }
738 wal.truncate_after_checkpoint().unwrap();
739
740 let entries = wal.replay().unwrap();
741 assert_eq!(entries.len(), 2);
742 }
743
744 #[test]
745 fn test_metadata_upsert_and_get() {
746 let dir = tempdir().unwrap();
747 let path = dir.path().join("test.db");
748 let store = MetadataStore::open(&path).unwrap();
749
750 let record = VectorRecord {
751 id: "doc1".into(),
752 vector: vec![],
753 payload: json!({ "title": "hello" }),
754 text: Some("hello world".into()),
755 created_at: Utc::now(),
756 updated_at: Utc::now(),
757 };
758
759 store.upsert("doc1", 0, &record).unwrap();
760 let (mmap_index, retrieved) = store.get("doc1").unwrap();
761
762 assert_eq!(mmap_index, 0);
763 assert_eq!(retrieved.id, "doc1");
764 assert_eq!(retrieved.text.as_deref(), Some("hello world"));
765 assert_eq!(retrieved.payload["title"], "hello");
766 }
767
768 #[test]
769 fn test_metadata_soft_delete() {
770 let dir = tempdir().unwrap();
771 let path = dir.path().join("test.db");
772 let store = MetadataStore::open(&path).unwrap();
773
774 store
775 .upsert("doc2", 0, &dummy_record("doc2", vec![]))
776 .unwrap();
777 store.delete("doc2").unwrap();
778
779 assert!(matches!(
780 store.get("doc2"),
781 Err(VecDbError::NotFound { .. })
782 ));
783 assert_eq!(store.count_active().unwrap(), 0);
784 }
785
786 #[test]
787 fn test_storage_full_roundtrip() {
788 let dir = tempdir().unwrap();
789 let config = CollectionConfig::new("testcol", 3);
790
791 {
792 let mut storage = Storage::create(dir.path(), &config).unwrap();
793 for i in 0..5u32 {
794 let r = dummy_record(
795 &format!("doc{i}"),
796 vec![i as f32, i as f32 + 1.0, i as f32 + 2.0],
797 );
798 storage.upsert(r).unwrap();
799 }
800 assert_eq!(storage.stats().unwrap().vector_count, 5);
801 storage.delete(&"doc0".to_string()).unwrap();
802 assert_eq!(storage.stats().unwrap().vector_count, 4);
803 }
804
805 let mut storage = Storage::open(dir.path(), "testcol").unwrap();
806 assert_eq!(storage.stats().unwrap().vector_count, 4);
807 storage.checkpoint().unwrap();
808 }
809
810 #[test]
811 fn test_wal_recovery() {
812 let dir = tempdir().unwrap();
813 let config = CollectionConfig::new("waltest", 2);
814
815 {
816 let mut storage = Storage::create(dir.path(), &config).unwrap();
817 storage
818 .upsert(dummy_record("r1", vec![1.0f32, 2.0]))
819 .unwrap();
820 storage
821 .upsert(dummy_record("r2", vec![3.0f32, 4.0]))
822 .unwrap();
823 storage
824 .upsert(dummy_record("r3", vec![5.0f32, 6.0]))
825 .unwrap();
826 }
828
829 let storage = Storage::open(dir.path(), "waltest").unwrap();
830 assert!(storage.metadata.get("r1").is_ok());
831 assert!(storage.metadata.get("r2").is_ok());
832 assert!(storage.metadata.get("r3").is_ok());
833 }
834
835 #[test]
836 fn test_sparse_search_after_upsert() {
837 let dir = tempdir().unwrap();
838 let config = CollectionConfig::new("sparsecol", 3);
839 let mut storage = Storage::create(dir.path(), &config).unwrap();
840
841 let now = Utc::now();
842 storage
843 .upsert(VectorRecord {
844 id: "v1".to_string(),
845 vector: vec![1.0, 0.0, 0.0],
846 payload: json!({}),
847 text: Some("rust vector database storage engine".to_string()),
848 created_at: now,
849 updated_at: now,
850 })
851 .unwrap();
852 storage
853 .upsert(VectorRecord {
854 id: "v2".to_string(),
855 vector: vec![0.0, 1.0, 0.0],
856 payload: json!({}),
857 text: Some("python machine learning numpy".to_string()),
858 created_at: now,
859 updated_at: now,
860 })
861 .unwrap();
862
863 let results = storage.search_sparse("rust database", 5).unwrap();
864 assert!(!results.is_empty());
865 assert_eq!(results[0].id, "v1");
866 assert!(results[0].sparse_score.is_some());
867 assert!(results[0].dense_score.is_none());
868 }
869
870 #[test]
871 fn test_sparse_delete_removes_from_index() {
872 let dir = tempdir().unwrap();
873 let config = CollectionConfig::new("delsparse", 2);
874 let mut storage = Storage::create(dir.path(), &config).unwrap();
875
876 let now = Utc::now();
877 storage
878 .upsert(VectorRecord {
879 id: "x1".to_string(),
880 vector: vec![1.0, 0.0],
881 payload: json!({}),
882 text: Some("neural network deep learning".to_string()),
883 created_at: now,
884 updated_at: now,
885 })
886 .unwrap();
887
888 storage.delete(&"x1".to_string()).unwrap();
889
890 let results = storage.search_sparse("neural", 5).unwrap();
891 assert!(results.is_empty());
892 }
893
894 #[test]
897 fn test_storage_search_hybrid_both_signals() {
898 let dir = tempdir().unwrap();
899 let config = CollectionConfig::new("hybcol", 4);
900 let mut storage = Storage::create(dir.path(), &config).unwrap();
901
902 let texts = [
903 "machine learning algorithms",
904 "deep neural networks",
905 "random forest trees",
906 "gradient boosting ensemble",
907 "support vector machines",
908 ];
909 let now = Utc::now();
910 for (i, text) in texts.iter().enumerate() {
911 storage
912 .upsert(VectorRecord {
913 id: format!("hyb{i}"),
914 vector: vec![i as f32, i as f32 + 1.0, i as f32 + 0.5, 1.0],
915 payload: json!({ "i": i }),
916 text: Some(text.to_string()),
917 created_at: now,
918 updated_at: now,
919 })
920 .unwrap();
921 }
922
923 let query_vec = vec![0.0_f32, 1.0, 0.5, 1.0];
924 let results = storage
925 .search_hybrid(
926 Some(&query_vec),
927 Some("machine learning"),
928 3,
929 0.7,
930 crate::hybrid::FusionStrategy::WeightedSum,
931 )
932 .unwrap();
933
934 assert!(
935 results.len() <= 3,
936 "expected <= 3 results, got {}",
937 results.len()
938 );
939 for r in &results {
940 assert!(
941 storage.metadata.get(&r.id).is_ok(),
942 "id {} not in metadata",
943 r.id
944 );
945 assert!(r.score >= 0.0, "score {} < 0.0", r.score);
946 }
947 for i in 1..results.len() {
949 assert!(
950 results[i - 1].score >= results[i].score,
951 "results not sorted at index {i}"
952 );
953 }
954 }
955
956 #[test]
957 fn test_storage_search_hybrid_dense_only() {
958 let dir = tempdir().unwrap();
959 let config = CollectionConfig::new("denonly", 4);
960 let mut storage = Storage::create(dir.path(), &config).unwrap();
961
962 let now = Utc::now();
963 for i in 0..5usize {
964 storage
965 .upsert(VectorRecord {
966 id: format!("d{i}"),
967 vector: vec![i as f32, 1.0, 0.0, 0.0],
968 payload: json!({}),
969 text: None, created_at: now,
971 updated_at: now,
972 })
973 .unwrap();
974 }
975
976 let query_vec = vec![2.0_f32, 1.0, 0.0, 0.0];
977 let results = storage
978 .search_hybrid(
979 Some(&query_vec),
980 None,
981 3,
982 0.7,
983 crate::hybrid::FusionStrategy::WeightedSum,
984 )
985 .unwrap();
986
987 assert!(!results.is_empty(), "dense-only hybrid must return results");
988 }
989
990 #[test]
991 fn test_storage_search_hybrid_sparse_only() {
992 let dir = tempdir().unwrap();
993 let config = CollectionConfig::new("sparonly", 4);
994 let mut storage = Storage::create(dir.path(), &config).unwrap();
995
996 let now = Utc::now();
997 let texts = [
998 "neural network deep learning",
999 "gradient descent optimization",
1000 "convolutional neural network image",
1001 "recurrent network sequence model",
1002 "transformer attention mechanism",
1003 ];
1004 for (i, text) in texts.iter().enumerate() {
1005 storage
1006 .upsert(VectorRecord {
1007 id: format!("sp{i}"),
1008 vector: vec![i as f32, 0.0, 0.0, 1.0],
1009 payload: json!({}),
1010 text: Some(text.to_string()),
1011 created_at: now,
1012 updated_at: now,
1013 })
1014 .unwrap();
1015 }
1016
1017 let results = storage
1018 .search_hybrid(
1019 None,
1020 Some("neural network"),
1021 3,
1022 0.7,
1023 crate::hybrid::FusionStrategy::WeightedSum,
1024 )
1025 .unwrap();
1026
1027 assert!(
1028 !results.is_empty(),
1029 "sparse-only hybrid must return results"
1030 );
1031 }
1032
1033 #[test]
1034 fn test_storage_search_hybrid_no_inputs_errors() {
1035 let dir = tempdir().unwrap();
1036 let config = CollectionConfig::new("errtest", 4);
1037 let storage = Storage::create(dir.path(), &config).unwrap();
1038
1039 let result = storage.search_hybrid(
1040 None,
1041 None,
1042 3,
1043 0.7,
1044 crate::hybrid::FusionStrategy::WeightedSum,
1045 );
1046 assert!(
1047 matches!(result, Err(VecDbError::InvalidQuery(_))),
1048 "expected InvalidQuery error"
1049 );
1050 }
1051
1052 #[test]
1053 fn test_storage_search_dense_method() {
1054 let dir = tempdir().unwrap();
1055 let config = CollectionConfig::new("denmeth", 4);
1056 let mut storage = Storage::create(dir.path(), &config).unwrap();
1057
1058 let now = Utc::now();
1059 for i in 0..5usize {
1060 storage
1061 .upsert(VectorRecord {
1062 id: format!("v{i}"),
1063 vector: vec![i as f32, i as f32 + 1.0, 0.0, 1.0],
1064 payload: json!({ "i": i }),
1065 text: Some(format!("text {i}")),
1066 created_at: now,
1067 updated_at: now,
1068 })
1069 .unwrap();
1070 }
1071
1072 let query_vec = vec![2.0_f32, 3.0, 0.0, 1.0];
1073 let results = storage.search_dense(&query_vec, 3).unwrap();
1074
1075 assert!(!results.is_empty(), "search_dense must return results");
1076 for r in &results {
1077 assert!(r.dense_score.is_some(), "dense_score must be Some");
1078 assert!(r.sparse_score.is_none(), "sparse_score must be None");
1079 }
1080 }
1081
1082 #[test]
1083 fn test_save_indexes_and_reload() {
1084 let dir = tempdir().unwrap();
1085 let config = CollectionConfig::new("saveidx", 3);
1086
1087 {
1088 let mut storage = Storage::create(dir.path(), &config).unwrap();
1089 let now = Utc::now();
1090 storage
1091 .upsert(VectorRecord {
1092 id: "s1".to_string(),
1093 vector: vec![1.0, 2.0, 3.0],
1094 payload: json!({}),
1095 text: Some("inverted index bm25 scoring".to_string()),
1096 created_at: now,
1097 updated_at: now,
1098 })
1099 .unwrap();
1100 storage.save_indexes().unwrap();
1101 }
1102
1103 let storage = Storage::open(dir.path(), "saveidx").unwrap();
1104 let results = storage.search_sparse("bm25", 5).unwrap();
1105 assert!(!results.is_empty());
1106 assert_eq!(results[0].id, "s1");
1107 }
1108}