ruvector_core/
vector_db.rs

1//! Main VectorDB interface
2
3use 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// Import appropriate storage backend based on features
15#[cfg(feature = "storage")]
16use crate::storage::VectorStorage;
17
18#[cfg(not(feature = "storage"))]
19use crate::storage_memory::MemoryStorage as VectorStorage;
20
21/// Main vector database
22pub struct VectorDB {
23    storage: Arc<VectorStorage>,
24    index: Arc<RwLock<Box<dyn VectorIndex>>>,
25    options: DbOptions,
26}
27
28impl VectorDB {
29    /// Create a new vector database with the given options
30    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        // Choose index based on configuration and available features
41        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                // Fall back to flat index if HNSW is not available
53                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    /// Create with default options
68    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    /// Insert a vector entry
75    pub fn insert(&self, entry: VectorEntry) -> Result<VectorId> {
76        let id = self.storage.insert(&entry)?;
77
78        // Add to index
79        let mut index = self.index.write();
80        index.add(id.clone(), entry.vector)?;
81
82        Ok(id)
83    }
84
85    /// Insert multiple vectors in a batch
86    pub fn insert_batch(&self, entries: Vec<VectorEntry>) -> Result<Vec<VectorId>> {
87        let ids = self.storage.insert_batch(&entries)?;
88
89        // Add to index
90        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    /// Search for similar vectors
103    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        // Enrich results with full data if needed
108        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        // Apply metadata filters if specified
116        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    /// Delete a vector by ID
132    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    /// Get a vector by ID
144    pub fn get(&self, id: &str) -> Result<Option<VectorEntry>> {
145        self.storage.get(id)
146    }
147
148    /// Get the number of vectors
149    pub fn len(&self) -> Result<usize> {
150        self.storage.len()
151    }
152
153    /// Check if database is empty
154    pub fn is_empty(&self) -> Result<bool> {
155        self.storage.is_empty()
156    }
157
158    /// Get database options
159    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; // Use Euclidean for clearer test
190        options.hnsw_config = None; // Use flat index for testing
191
192        let db = VectorDB::new(options)?;
193
194        // Insert vectors
195        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        // Search for exact match
214        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}