1use std::path::{Path, PathBuf};
18
19use chrono::Utc;
20use serde_json::Value;
21
22use crate::errors::{Result, VecDbError};
23use crate::hybrid::FusionStrategy;
24use crate::storage::{Storage, UpsertResult};
25use crate::types::{CollectionConfig, IndexStats, SearchResult, Vector, VectorId, VectorRecord};
26
27#[derive(Debug, Clone)]
31pub struct Db {
32 root: PathBuf,
33}
34
35impl Db {
36 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
41 let root = path.as_ref().to_path_buf();
42 std::fs::create_dir_all(&root).map_err(|e| {
43 VecDbError::StorageError(format!("create_dir_all({root:?}) failed: {e}"))
44 })?;
45 Ok(Self { root })
46 }
47
48 pub fn path(&self) -> &Path {
50 &self.root
51 }
52
53 pub fn create_collection(
56 &self,
57 name: impl Into<String>,
58 dimension: usize,
59 ) -> Result<Collection> {
60 self.create_collection_with(CollectionConfig::new(name, dimension))
61 }
62
63 pub fn create_collection_with(&self, config: CollectionConfig) -> Result<Collection> {
65 if self.collection_exists(&config.name) {
66 return Err(VecDbError::CollectionAlreadyExists(config.name));
67 }
68 let storage = Storage::create(&self.root, &config)?;
69 Ok(Collection { storage })
70 }
71
72 pub fn create_collection_quantized(
75 &self,
76 name: impl Into<String>,
77 dimension: usize,
78 ) -> Result<Collection> {
79 self.create_collection_with(
80 CollectionConfig::new(name, dimension)
81 .with_quantization(crate::types::Quantization::ScalarInt8),
82 )
83 }
84
85 pub fn create_collection_binary(
89 &self,
90 name: impl Into<String>,
91 dimension: usize,
92 ) -> Result<Collection> {
93 self.create_collection_with(
94 CollectionConfig::new(name, dimension)
95 .with_quantization(crate::types::Quantization::Binary),
96 )
97 }
98
99 pub fn collection(&self, name: &str) -> Result<Collection> {
101 if !self.collection_exists(name) {
102 return Err(VecDbError::CollectionNotFound(name.to_string()));
103 }
104 let storage = Storage::open(&self.root, name)?;
105 Ok(Collection { storage })
106 }
107
108 pub fn open_or_create(&self, name: &str, dimension: usize) -> Result<Collection> {
111 if self.collection_exists(name) {
112 self.collection(name)
113 } else {
114 self.create_collection(name, dimension)
115 }
116 }
117
118 pub fn collection_exists(&self, name: &str) -> bool {
120 self.root.join(format!("{name}.db")).exists()
121 }
122
123 pub fn list_collections(&self) -> Result<Vec<String>> {
125 let mut names = Vec::new();
126 let entries = std::fs::read_dir(&self.root)
127 .map_err(|e| VecDbError::StorageError(format!("read_dir failed: {e}")))?;
128 for entry in entries.flatten() {
129 let path = entry.path();
130 if path.extension().map(|e| e == "db").unwrap_or(false) {
131 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
132 names.push(stem.to_string());
133 }
134 }
135 }
136 names.sort();
137 Ok(names)
138 }
139
140 pub fn drop_collection(&self, name: &str) -> Result<()> {
142 if !self.collection_exists(name) {
143 return Err(VecDbError::CollectionNotFound(name.to_string()));
144 }
145 for suffix in &[
146 ".db",
147 ".wal",
148 ".vectors",
149 ".hnsw.json",
150 ".ivf.json",
151 ".sq.json",
152 ".bq.json",
153 ".sparse.json",
154 ] {
155 let path = self.root.join(format!("{name}{suffix}"));
156 if path.exists() {
157 std::fs::remove_file(&path).map_err(|e| {
158 VecDbError::StorageError(format!("remove {path:?} failed: {e}"))
159 })?;
160 }
161 }
162 Ok(())
163 }
164}
165
166pub struct Collection {
171 storage: Storage,
172}
173
174impl Collection {
175 pub fn insert(
178 &mut self,
179 id: impl Into<VectorId>,
180 vector: Vector,
181 payload: Value,
182 ) -> Result<UpsertResult> {
183 self.upsert(id, vector, payload, None)
184 }
185
186 pub fn insert_text(
189 &mut self,
190 id: impl Into<VectorId>,
191 vector: Vector,
192 payload: Value,
193 text: impl Into<String>,
194 ) -> Result<UpsertResult> {
195 self.upsert(id, vector, payload, Some(text.into()))
196 }
197
198 fn upsert(
199 &mut self,
200 id: impl Into<VectorId>,
201 vector: Vector,
202 payload: Value,
203 text: Option<String>,
204 ) -> Result<UpsertResult> {
205 let now = Utc::now();
206 self.storage.upsert(VectorRecord {
207 id: id.into(),
208 vector,
209 payload,
210 text,
211 created_at: now,
212 updated_at: now,
213 })
214 }
215
216 pub fn upsert_record(&mut self, record: VectorRecord) -> Result<UpsertResult> {
218 self.storage.upsert(record)
219 }
220
221 pub fn insert_batch(&mut self, items: Vec<(VectorId, Vector, Value)>) -> Result<usize> {
225 let now = Utc::now();
226 let records = items
227 .into_iter()
228 .map(|(id, vector, payload)| VectorRecord {
229 id,
230 vector,
231 payload,
232 text: None,
233 created_at: now,
234 updated_at: now,
235 })
236 .collect();
237 self.storage.bulk_upsert(records)
238 }
239
240 pub fn delete(&mut self, id: &str) -> Result<()> {
242 self.storage.delete(&id.to_string())
243 }
244
245 pub fn query(&self, vector: &[f32], k: usize) -> Result<Vec<SearchResult>> {
247 self.storage.search_dense(&vector.to_vec(), k)
248 }
249
250 pub fn query_filtered(
258 &self,
259 vector: &[f32],
260 k: usize,
261 filter: &Value,
262 ) -> Result<Vec<SearchResult>> {
263 self.storage
264 .search_dense_filtered(&vector.to_vec(), k, filter)
265 }
266
267 pub fn query_text(&self, text: &str, k: usize) -> Result<Vec<SearchResult>> {
269 self.storage.search_sparse(text, k)
270 }
271
272 pub fn query_hybrid(
275 &self,
276 vector: Option<&[f32]>,
277 text: Option<&str>,
278 k: usize,
279 alpha: f32,
280 ) -> Result<Vec<SearchResult>> {
281 let owned = vector.map(|v| v.to_vec());
282 self.storage
283 .search_hybrid(owned.as_ref(), text, k, alpha, FusionStrategy::WeightedSum)
284 }
285
286 pub fn sql(&self, sql: &str) -> Result<Vec<SearchResult>> {
288 self.storage.execute_sql(sql)
289 }
290
291 pub fn len(&self) -> Result<usize> {
293 Ok(self.storage.stats()?.vector_count)
294 }
295
296 pub fn is_empty(&self) -> Result<bool> {
298 Ok(self.len()? == 0)
299 }
300
301 pub fn stats(&self) -> Result<IndexStats> {
303 self.storage.stats()
304 }
305
306 pub fn name(&self) -> &str {
308 &self.storage.config.name
309 }
310
311 pub fn flush(&mut self) -> Result<()> {
314 self.storage.save_indexes()?;
315 self.storage.checkpoint()
316 }
317}
318
319impl Drop for Collection {
320 fn drop(&mut self) {
321 let _ = self.storage.save_indexes();
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use serde_json::json;
331 use tempfile::tempdir;
332
333 #[test]
334 fn open_create_insert_query() {
335 let dir = tempdir().unwrap();
336 let db = Db::open(dir.path()).unwrap();
337 let mut c = db.create_collection("docs", 3).unwrap();
338 c.insert("a", vec![1.0, 0.0, 0.0], json!({ "n": 1 }))
339 .unwrap();
340 c.insert("b", vec![0.0, 1.0, 0.0], json!({ "n": 2 }))
341 .unwrap();
342 let hits = c.query(&[1.0, 0.0, 0.0], 1).unwrap();
343 assert_eq!(hits[0].id, "a");
344 }
345
346 #[test]
347 fn persist_and_reopen() {
348 let dir = tempdir().unwrap();
349 {
350 let db = Db::open(dir.path()).unwrap();
351 let mut c = db.create_collection("k", 2).unwrap();
352 c.insert("x", vec![1.0, 2.0], json!({})).unwrap();
353 c.flush().unwrap();
354 }
355 let db = Db::open(dir.path()).unwrap();
356 let c = db.collection("k").unwrap();
357 assert_eq!(c.len().unwrap(), 1);
358 assert_eq!(c.query(&[1.0, 2.0], 1).unwrap()[0].id, "x");
359 }
360
361 #[test]
362 fn open_or_create_is_idempotent() {
363 let dir = tempdir().unwrap();
364 let db = Db::open(dir.path()).unwrap();
365 {
366 let mut c = db.open_or_create("c", 2).unwrap();
367 c.insert("1", vec![0.0, 1.0], json!({})).unwrap();
368 c.flush().unwrap();
369 }
370 let c = db.open_or_create("c", 2).unwrap();
371 assert_eq!(c.len().unwrap(), 1);
372 }
373
374 #[test]
375 fn create_existing_errors() {
376 let dir = tempdir().unwrap();
377 let db = Db::open(dir.path()).unwrap();
378 let _c = db.create_collection("dup", 2).unwrap();
379 drop(_c);
380 assert!(matches!(
381 db.create_collection("dup", 2),
382 Err(VecDbError::CollectionAlreadyExists(_))
383 ));
384 }
385
386 #[test]
387 fn list_and_drop() {
388 let dir = tempdir().unwrap();
389 let db = Db::open(dir.path()).unwrap();
390 db.create_collection("a", 2).unwrap().flush().unwrap();
391 db.create_collection("b", 2).unwrap().flush().unwrap();
392 assert_eq!(db.list_collections().unwrap(), vec!["a", "b"]);
393 db.drop_collection("a").unwrap();
394 assert_eq!(db.list_collections().unwrap(), vec!["b"]);
395 assert!(!db.collection_exists("a"));
396 }
397
398 #[test]
399 fn hybrid_and_text_query() {
400 let dir = tempdir().unwrap();
401 let db = Db::open(dir.path()).unwrap();
402 let mut c = db.create_collection("h", 3).unwrap();
403 c.insert_text("d1", vec![1.0, 0.0, 0.0], json!({}), "rust vector database")
404 .unwrap();
405 c.insert_text(
406 "d2",
407 vec![0.0, 1.0, 0.0],
408 json!({}),
409 "python machine learning",
410 )
411 .unwrap();
412 let text_hits = c.query_text("rust database", 5).unwrap();
413 assert_eq!(text_hits[0].id, "d1");
414 let hybrid = c
415 .query_hybrid(Some(&[1.0, 0.0, 0.0]), Some("rust"), 2, 0.5)
416 .unwrap();
417 assert!(!hybrid.is_empty());
418 }
419}