ruvector_core/
vector_db.rs1use crate::error::Result;
4use crate::index::flat::FlatIndex;
5
6#[cfg(feature = "hnsw")]
7use crate::index::hnsw::HnswIndex;
8
9use crate::index::VectorIndex;
10use crate::types::*;
11use parking_lot::RwLock;
12use std::sync::Arc;
13
14#[cfg(feature = "storage")]
16use crate::storage::VectorStorage;
17
18#[cfg(not(feature = "storage"))]
19use crate::storage_memory::MemoryStorage as VectorStorage;
20
21pub struct VectorDB {
23 storage: Arc<VectorStorage>,
24 index: Arc<RwLock<Box<dyn VectorIndex>>>,
25 options: DbOptions,
26}
27
28impl VectorDB {
29 pub fn new(options: DbOptions) -> Result<Self> {
31 #[cfg(feature = "storage")]
32 let storage = Arc::new(VectorStorage::new(
33 &options.storage_path,
34 options.dimensions,
35 )?);
36
37 #[cfg(not(feature = "storage"))]
38 let storage = Arc::new(VectorStorage::new(options.dimensions)?);
39
40 let index: Box<dyn VectorIndex> = if let Some(hnsw_config) = &options.hnsw_config {
42 #[cfg(feature = "hnsw")]
43 {
44 Box::new(HnswIndex::new(
45 options.dimensions,
46 options.distance_metric,
47 hnsw_config.clone(),
48 )?)
49 }
50 #[cfg(not(feature = "hnsw"))]
51 {
52 tracing::warn!("HNSW requested but not available (WASM build), using flat index");
54 Box::new(FlatIndex::new(options.dimensions, options.distance_metric))
55 }
56 } else {
57 Box::new(FlatIndex::new(options.dimensions, options.distance_metric))
58 };
59
60 Ok(Self {
61 storage,
62 index: Arc::new(RwLock::new(index)),
63 options,
64 })
65 }
66
67 pub fn with_dimensions(dimensions: usize) -> Result<Self> {
69 let mut options = DbOptions::default();
70 options.dimensions = dimensions;
71 Self::new(options)
72 }
73
74 pub fn insert(&self, entry: VectorEntry) -> Result<VectorId> {
76 let id = self.storage.insert(&entry)?;
77
78 let mut index = self.index.write();
80 index.add(id.clone(), entry.vector)?;
81
82 Ok(id)
83 }
84
85 pub fn insert_batch(&self, entries: Vec<VectorEntry>) -> Result<Vec<VectorId>> {
87 let ids = self.storage.insert_batch(&entries)?;
88
89 let mut index = self.index.write();
91 let index_entries: Vec<_> = ids
92 .iter()
93 .zip(entries.iter())
94 .map(|(id, entry)| (id.clone(), entry.vector.clone()))
95 .collect();
96
97 index.add_batch(index_entries)?;
98
99 Ok(ids)
100 }
101
102 pub fn search(&self, query: SearchQuery) -> Result<Vec<SearchResult>> {
104 let index = self.index.read();
105 let mut results = index.search(&query.vector, query.k)?;
106
107 for result in &mut results {
109 if let Ok(Some(entry)) = self.storage.get(&result.id) {
110 result.vector = Some(entry.vector);
111 result.metadata = entry.metadata;
112 }
113 }
114
115 if let Some(filter) = &query.filter {
117 results.retain(|r| {
118 if let Some(metadata) = &r.metadata {
119 filter
120 .iter()
121 .all(|(key, value)| metadata.get(key).map_or(false, |v| v == value))
122 } else {
123 false
124 }
125 });
126 }
127
128 Ok(results)
129 }
130
131 pub fn delete(&self, id: &str) -> Result<bool> {
133 let deleted_storage = self.storage.delete(id)?;
134
135 if deleted_storage {
136 let mut index = self.index.write();
137 let _ = index.remove(&id.to_string())?;
138 }
139
140 Ok(deleted_storage)
141 }
142
143 pub fn get(&self, id: &str) -> Result<Option<VectorEntry>> {
145 self.storage.get(id)
146 }
147
148 pub fn len(&self) -> Result<usize> {
150 self.storage.len()
151 }
152
153 pub fn is_empty(&self) -> Result<bool> {
155 self.storage.is_empty()
156 }
157
158 pub fn options(&self) -> &DbOptions {
160 &self.options
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use std::path::Path;
168 use tempfile::tempdir;
169
170 #[test]
171 fn test_vector_db_creation() -> Result<()> {
172 let dir = tempdir().unwrap();
173 let mut options = DbOptions::default();
174 options.storage_path = dir.path().join("test.db").to_string_lossy().to_string();
175 options.dimensions = 3;
176
177 let db = VectorDB::new(options)?;
178 assert!(db.is_empty()?);
179
180 Ok(())
181 }
182
183 #[test]
184 fn test_insert_and_search() -> Result<()> {
185 let dir = tempdir().unwrap();
186 let mut options = DbOptions::default();
187 options.storage_path = dir.path().join("test.db").to_string_lossy().to_string();
188 options.dimensions = 3;
189 options.distance_metric = DistanceMetric::Euclidean; options.hnsw_config = None; let db = VectorDB::new(options)?;
193
194 db.insert(VectorEntry {
196 id: Some("v1".to_string()),
197 vector: vec![1.0, 0.0, 0.0],
198 metadata: None,
199 })?;
200
201 db.insert(VectorEntry {
202 id: Some("v2".to_string()),
203 vector: vec![0.0, 1.0, 0.0],
204 metadata: None,
205 })?;
206
207 db.insert(VectorEntry {
208 id: Some("v3".to_string()),
209 vector: vec![0.0, 0.0, 1.0],
210 metadata: None,
211 })?;
212
213 let results = db.search(SearchQuery {
215 vector: vec![1.0, 0.0, 0.0],
216 k: 2,
217 filter: None,
218 ef_search: None,
219 })?;
220
221 assert!(results.len() >= 1);
222 assert_eq!(results[0].id, "v1", "First result should be exact match");
223 assert!(
224 results[0].score < 0.01,
225 "Exact match should have ~0 distance"
226 );
227
228 Ok(())
229 }
230}