embedvec/
lib.rs

1//! # embedvec — High-Performance Embedded Vector Database
2//!
3//! [![crates.io](https://img.shields.io/crates/v/embedvec.svg)](https://crates.io/crates/embedvec)
4//! [![docs.rs](https://docs.rs/embedvec/badge.svg)](https://docs.rs/embedvec)
5//! [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6//!
7//! **Fast, lightweight, in-process vector database** with HNSW indexing, SIMD-accelerated
8//! distance calculations, metadata filtering, E8 lattice quantization, and optional
9//! persistence via Sled or RocksDB.
10//!
11//! ## Why embedvec?
12//!
13//! - **Pure Rust** — No C++ dependencies (unless using RocksDB), safe and portable
14//! - **Blazing Fast** — AVX2/FMA SIMD acceleration, optimized HNSW with O(1) lookups
15//! - **Memory Efficient** — E8 quantization provides 4-6× compression with <5% recall loss
16//! - **Flexible Persistence** — Choose between Sled (pure Rust) or RocksDB (high performance)
17//! - **Production Ready** — Async API, metadata filtering, batch operations
18//!
19//! ## Benchmarks (768-dim vectors, 10k dataset)
20//!
21//! | Operation | Time | Throughput |
22//! |-----------|------|------------|
23//! | **Search (ef=32)** | 3.0 ms | 3,300 queries/sec |
24//! | **Search (ef=64)** | 4.9 ms | 2,000 queries/sec |
25//! | **Search (ef=128)** | 16.1 ms | 620 queries/sec |
26//! | **Insert (768-dim)** | 25.5 ms/100 | 3,900 vectors/sec |
27//! | **Distance (cosine)** | 122 ns/pair | 8.2M ops/sec |
28//! | **Distance (dot)** | 91 ns/pair | 11M ops/sec |
29//!
30//! *Benchmarks on AMD Ryzen 9 / Intel i9, AVX2 enabled. Run `cargo bench` to reproduce.*
31//!
32//! ## Features
33//!
34//! | Feature | Description |
35//! |---------|-------------|
36//! | **HNSW Indexing** | Hierarchical Navigable Small World graph for O(log n) ANN search |
37//! | **SIMD Distance** | AVX2/FMA accelerated cosine, euclidean, dot product |
38//! | **E8 Quantization** | Lattice-based compression (4-6× memory reduction) |
39//! | **Metadata Filtering** | Composable filters: eq, gt, lt, contains, AND/OR/NOT |
40//! | **Dual Persistence** | Sled (pure Rust) or RocksDB (high performance) |
41//! | **Async API** | Tokio-compatible async operations |
42//! | **Python Bindings** | PyO3-based interop (feature-gated) |
43//!
44//! ## Quick Start
45//!
46//! ```rust,no_run
47//! use embedvec::{Distance, EmbedVec, FilterExpr};
48//!
49//! #[tokio::main]
50//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
51//!     // Create in-memory database
52//!     let mut db = EmbedVec::new(768, Distance::Cosine, 32, 200).await?;
53//!     
54//!     // Add vectors with metadata
55//!     db.add(&vec![0.1; 768], serde_json::json!({"doc_id": "123", "category": "tech"})).await?;
56//!     
57//!     // Search with optional filter
58//!     let filter = FilterExpr::eq("category", "tech");
59//!     let results = db.search(&vec![0.15; 768], 10, 100, Some(filter)).await?;
60//!     
61//!     for hit in results {
62//!         println!("id: {}, score: {:.4}", hit.id, hit.score);
63//!     }
64//!     Ok(())
65//! }
66//! ```
67//!
68//! ## With Persistence
69//!
70//! ```rust,no_run
71//! use embedvec::{Distance, EmbedVec, BackendConfig, BackendType};
72//!
73//! #[tokio::main]
74//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
75//!     // Sled backend (default, pure Rust)
76//!     let db = EmbedVec::with_persistence("/tmp/vectors.db", 768, Distance::Cosine, 32, 200).await?;
77//!     
78//!     // Or RocksDB for higher performance (requires persistence-rocksdb feature)
79//!     // let config = BackendConfig::new("/tmp/vectors.db").backend(BackendType::RocksDb);
80//!     // let db = EmbedVec::with_backend(config, 768, Distance::Cosine, 32, 200).await?;
81//!     Ok(())
82//! }
83//! ```
84//!
85//! ## E8 Quantization (4-6× Memory Savings)
86//!
87//! ```rust,no_run
88//! use embedvec::{EmbedVec, Distance, Quantization};
89//!
90//! #[tokio::main]
91//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
92//!     let db = EmbedVec::builder()
93//!         .dimension(768)
94//!         .metric(Distance::Cosine)
95//!         .quantization(Quantization::e8_default())  // ~1.25 bits/dim
96//!         .build()
97//!         .await?;
98//!     Ok(())
99//! }
100//! ```
101//!
102//! ## Feature Flags
103//!
104//! | Flag | Description | Default |
105//! |------|-------------|---------|
106//! | `persistence-sled` | Sled persistence backend | ✓ |
107//! | `persistence-rocksdb` | RocksDB persistence backend | ✗ |
108//! | `async` | Tokio async API | ✓ |
109//! | `python` | PyO3 Python bindings | ✗ |
110//! | `simd` | Explicit SIMD (auto-detected) | ✗ |
111//!
112//! ## Memory Usage (768-dim vectors)
113//!
114//! | Mode | Bits/Dim | Memory/Vector | 1M Vectors |
115//! |------|----------|---------------|------------|
116//! | Raw (f32) | 32 | 3.1 KB | ~3.1 GB |
117//! | E8 10-bit | ~1.25 | ~220 B | ~220 MB |
118//!
119//! ## License
120//!
121//! MIT OR Apache-2.0
122
123#![warn(missing_docs)]
124#![warn(rustdoc::missing_crate_level_docs)]
125
126// WASM global allocator
127#[cfg(feature = "wasm")]
128#[global_allocator]
129static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
130
131pub mod distance;
132pub mod e8;
133pub mod error;
134pub mod filter;
135pub mod hnsw;
136pub mod metadata;
137#[cfg(any(feature = "persistence-sled", feature = "persistence-rocksdb"))]
138pub mod persistence;
139pub mod quantization;
140pub mod storage;
141
142#[cfg(feature = "python")]
143pub mod python;
144
145// Re-exports for convenient API access
146pub use distance::Distance;
147pub use e8::{E8Codec, HadamardTransform};
148pub use error::{EmbedVecError, Result};
149pub use filter::FilterExpr;
150pub use hnsw::HnswIndex;
151pub use metadata::Metadata;
152#[cfg(any(feature = "persistence-sled", feature = "persistence-rocksdb"))]
153pub use persistence::{BackendConfig, BackendType, PersistenceBackend};
154pub use quantization::Quantization;
155pub use storage::VectorStorage;
156
157use ordered_float::OrderedFloat;
158use parking_lot::RwLock;
159use serde::{Deserialize, Serialize};
160use std::sync::Arc;
161
162/// Search result hit containing vector ID, similarity score, and payload
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct Hit {
165    /// Unique identifier of the vector
166    pub id: usize,
167    /// Similarity/distance score (interpretation depends on metric)
168    pub score: f32,
169    /// Associated metadata payload
170    pub payload: Metadata,
171}
172
173impl Hit {
174    /// Create a new Hit
175    pub fn new(id: usize, score: f32, payload: Metadata) -> Self {
176        Self { id, score, payload }
177    }
178}
179
180/// Builder for configuring EmbedVec instances
181#[derive(Debug, Clone)]
182pub struct EmbedVecBuilder {
183    dimension: usize,
184    distance: Distance,
185    m: usize,
186    ef_construction: usize,
187    quantization: Quantization,
188    #[cfg(any(feature = "persistence-sled", feature = "persistence-rocksdb"))]
189    persistence_config: Option<persistence::BackendConfig>,
190}
191
192impl EmbedVecBuilder {
193    /// Create a new builder with required dimension
194    pub fn new(dimension: usize) -> Self {
195        Self {
196            dimension,
197            distance: Distance::Cosine,
198            m: 16,
199            ef_construction: 200,
200            quantization: Quantization::None,
201            #[cfg(any(feature = "persistence-sled", feature = "persistence-rocksdb"))]
202            persistence_config: None,
203        }
204    }
205
206    /// Set the dimension of vectors
207    pub fn dimension(mut self, dim: usize) -> Self {
208        self.dimension = dim;
209        self
210    }
211
212    /// Set the distance metric
213    pub fn metric(mut self, distance: Distance) -> Self {
214        self.distance = distance;
215        self
216    }
217
218    /// Set HNSW M parameter (connections per layer)
219    pub fn m(mut self, m: usize) -> Self {
220        self.m = m;
221        self
222    }
223
224    /// Set HNSW ef_construction parameter
225    pub fn ef_construction(mut self, ef: usize) -> Self {
226        self.ef_construction = ef;
227        self
228    }
229
230    /// Set quantization mode
231    pub fn quantization(mut self, quant: Quantization) -> Self {
232        self.quantization = quant;
233        self
234    }
235
236    /// Set persistence path for on-disk storage (uses Sled by default)
237    #[cfg(any(feature = "persistence-sled", feature = "persistence-rocksdb"))]
238    pub fn persistence(mut self, path: impl Into<String>) -> Self {
239        self.persistence_config = Some(persistence::BackendConfig::new(path));
240        self
241    }
242    
243    /// Set persistence with full backend configuration
244    #[cfg(any(feature = "persistence-sled", feature = "persistence-rocksdb"))]
245    pub fn persistence_config(mut self, config: persistence::BackendConfig) -> Self {
246        self.persistence_config = Some(config);
247        self
248    }
249
250    /// Build the EmbedVec instance
251    #[cfg(feature = "async")]
252    pub async fn build(self) -> Result<EmbedVec> {
253        EmbedVec::from_builder(self).await
254    }
255
256    /// Build the EmbedVec instance (sync version)
257    #[cfg(not(feature = "async"))]
258    pub fn build(self) -> Result<EmbedVec> {
259        EmbedVec::new_internal(
260            self.dimension,
261            self.distance,
262            self.m,
263            self.ef_construction,
264            self.quantization,
265            #[cfg(any(feature = "persistence-sled", feature = "persistence-rocksdb"))]
266            self.persistence_config,
267        )
268    }
269}
270
271/// Main embedded vector database struct
272///
273/// Provides HNSW-based approximate nearest neighbor search with optional
274/// E8 lattice quantization for memory efficiency.
275pub struct EmbedVec {
276    /// Vector dimension
277    dimension: usize,
278    /// Distance metric
279    distance: Distance,
280    /// HNSW index
281    pub index: Arc<RwLock<HnswIndex>>,
282    /// Vector storage (raw or quantized)
283    pub storage: Arc<RwLock<VectorStorage>>,
284    /// Metadata storage
285    pub metadata: Arc<RwLock<Vec<Metadata>>>,
286    /// Quantization configuration
287    quantization: Quantization,
288    /// E8 codec (if quantization enabled)
289    e8_codec: Option<E8Codec>,
290    /// Persistence backend (sled or rocksdb)
291    #[cfg(any(feature = "persistence-sled", feature = "persistence-rocksdb"))]
292    backend: Option<Box<dyn persistence::PersistenceBackend>>,
293}
294
295impl EmbedVec {
296    /// Create a new in-memory EmbedVec instance
297    ///
298    /// # Arguments
299    /// * `dim` - Vector dimension (e.g., 768 for many LLM embeddings)
300    /// * `distance` - Distance metric (Cosine, Euclidean, DotProduct)
301    /// * `m` - HNSW M parameter (connections per layer, typically 16-64)
302    /// * `ef_construction` - HNSW construction parameter (typically 100-500)
303    ///
304    /// # Example
305    /// ```rust,no_run
306    /// use embedvec::{EmbedVec, Distance};
307    ///
308    /// #[tokio::main]
309    /// async fn main() {
310    ///     let db = EmbedVec::new(768, Distance::Cosine, 32, 200).await.unwrap();
311    /// }
312    /// ```
313    #[cfg(all(feature = "async", any(feature = "persistence-sled", feature = "persistence-rocksdb")))]
314    pub async fn new(
315        dim: usize,
316        distance: Distance,
317        m: usize,
318        ef_construction: usize,
319    ) -> Result<Self> {
320        Self::new_internal(dim, distance, m, ef_construction, Quantization::None, None)
321    }
322    
323    #[cfg(all(feature = "async", not(any(feature = "persistence-sled", feature = "persistence-rocksdb"))))]
324    pub async fn new(
325        dim: usize,
326        distance: Distance,
327        m: usize,
328        ef_construction: usize,
329    ) -> Result<Self> {
330        Self::new_internal(dim, distance, m, ef_construction, Quantization::None)
331    }
332
333    /// Create a new EmbedVec with persistence (uses Sled by default)
334    #[cfg(all(feature = "async", any(feature = "persistence-sled", feature = "persistence-rocksdb")))]
335    pub async fn with_persistence(
336        path: impl AsRef<std::path::Path>,
337        dim: usize,
338        distance: Distance,
339        m: usize,
340        ef_construction: usize,
341    ) -> Result<Self> {
342        let path_str = path.as_ref().to_string_lossy().to_string();
343        let config = persistence::BackendConfig::new(path_str);
344        Self::new_internal(
345            dim,
346            distance,
347            m,
348            ef_construction,
349            Quantization::None,
350            Some(config),
351        )
352    }
353    
354    /// Create a new EmbedVec with a specific persistence backend
355    #[cfg(all(feature = "async", any(feature = "persistence-sled", feature = "persistence-rocksdb")))]
356    pub async fn with_backend(
357        config: persistence::BackendConfig,
358        dim: usize,
359        distance: Distance,
360        m: usize,
361        ef_construction: usize,
362    ) -> Result<Self> {
363        Self::new_internal(
364            dim,
365            distance,
366            m,
367            ef_construction,
368            Quantization::None,
369            Some(config),
370        )
371    }
372
373    /// Create EmbedVec from builder configuration
374    #[cfg(feature = "async")]
375    async fn from_builder(builder: EmbedVecBuilder) -> Result<Self> {
376        Self::new_internal(
377            builder.dimension,
378            builder.distance,
379            builder.m,
380            builder.ef_construction,
381            builder.quantization,
382            #[cfg(any(feature = "persistence-sled", feature = "persistence-rocksdb"))]
383            builder.persistence_config,
384        )
385    }
386
387    /// Internal constructor (public for Python bindings)
388    #[cfg(any(feature = "persistence-sled", feature = "persistence-rocksdb"))]
389    pub fn new_internal(
390        dim: usize,
391        distance: Distance,
392        m: usize,
393        ef_construction: usize,
394        quantization: Quantization,
395        persistence_config: Option<persistence::BackendConfig>,
396    ) -> Result<Self> {
397        if dim == 0 {
398            return Err(EmbedVecError::InvalidDimension(dim));
399        }
400
401        let index = HnswIndex::new(m, ef_construction, distance);
402        let storage = VectorStorage::new(dim, quantization.clone());
403
404        let e8_codec = match &quantization {
405            Quantization::None => None,
406            Quantization::E8 {
407                bits_per_block,
408                use_hadamard,
409                random_seed,
410            } => Some(E8Codec::new(dim, *bits_per_block, *use_hadamard, *random_seed)),
411        };
412
413        let backend = if let Some(config) = persistence_config {
414            Some(persistence::create_backend(&config)?)
415        } else {
416            None
417        };
418
419        Ok(Self {
420            dimension: dim,
421            distance,
422            index: Arc::new(RwLock::new(index)),
423            storage: Arc::new(RwLock::new(storage)),
424            metadata: Arc::new(RwLock::new(Vec::new())),
425            quantization,
426            e8_codec,
427            backend,
428        })
429    }
430    
431    /// Internal constructor without persistence
432    #[cfg(not(any(feature = "persistence-sled", feature = "persistence-rocksdb")))]
433    pub fn new_internal(
434        dim: usize,
435        distance: Distance,
436        m: usize,
437        ef_construction: usize,
438        quantization: Quantization,
439    ) -> Result<Self> {
440        if dim == 0 {
441            return Err(EmbedVecError::InvalidDimension(dim));
442        }
443
444        let index = HnswIndex::new(m, ef_construction, distance);
445        let storage = VectorStorage::new(dim, quantization.clone());
446
447        let e8_codec = match &quantization {
448            Quantization::None => None,
449            Quantization::E8 {
450                bits_per_block,
451                use_hadamard,
452                random_seed,
453            } => Some(E8Codec::new(dim, *bits_per_block, *use_hadamard, *random_seed)),
454        };
455
456        Ok(Self {
457            dimension: dim,
458            distance,
459            index: Arc::new(RwLock::new(index)),
460            storage: Arc::new(RwLock::new(storage)),
461            metadata: Arc::new(RwLock::new(Vec::new())),
462            quantization,
463            e8_codec,
464        })
465    }
466
467    /// Get a builder for configuring EmbedVec
468    pub fn builder() -> EmbedVecBuilder {
469        EmbedVecBuilder::new(768) // Default dimension
470    }
471
472    /// Add a single vector with metadata
473    ///
474    /// # Arguments
475    /// * `vector` - The embedding vector (must match configured dimension)
476    /// * `payload` - Associated metadata (JSON-compatible)
477    ///
478    /// # Returns
479    /// The assigned vector ID
480    #[cfg(feature = "async")]
481    pub async fn add(&mut self, vector: &[f32], payload: impl Into<Metadata>) -> Result<usize> {
482        self.add_internal(vector, payload.into())
483    }
484
485    /// Add multiple vectors with metadata in batch
486    ///
487    /// # Arguments
488    /// * `vectors` - Slice of embedding vectors
489    /// * `payloads` - Associated metadata for each vector
490    #[cfg(feature = "async")]
491    pub async fn add_many(
492        &mut self,
493        vectors: &[Vec<f32>],
494        payloads: Vec<impl Into<Metadata>>,
495    ) -> Result<()> {
496        if vectors.len() != payloads.len() {
497            return Err(EmbedVecError::MismatchedLengths {
498                vectors: vectors.len(),
499                payloads: payloads.len(),
500            });
501        }
502
503        for (vector, payload) in vectors.iter().zip(payloads.into_iter()) {
504            self.add_internal(vector, payload.into())?;
505        }
506
507        Ok(())
508    }
509
510    /// Internal add implementation (public for Python bindings)
511    pub fn add_internal(&mut self, vector: &[f32], payload: Metadata) -> Result<usize> {
512        if vector.len() != self.dimension {
513            return Err(EmbedVecError::DimensionMismatch {
514                expected: self.dimension,
515                got: vector.len(),
516            });
517        }
518
519        // Normalize if using cosine distance
520        let processed_vector = if self.distance == Distance::Cosine {
521            normalize_vector(vector)
522        } else {
523            vector.to_vec()
524        };
525
526        // Store vector (quantized or raw)
527        let id = {
528            let mut storage = self.storage.write();
529            storage.add(&processed_vector, self.e8_codec.as_ref())?
530        };
531
532        // Store metadata
533        {
534            let mut meta = self.metadata.write();
535            if id >= meta.len() {
536                meta.resize(id + 1, Metadata::default());
537            }
538            meta[id] = payload;
539        }
540
541        // Add to HNSW index
542        {
543            let mut index = self.index.write();
544            let storage = self.storage.read();
545            index.insert(id, &processed_vector, &storage, self.e8_codec.as_ref())?;
546        }
547
548        Ok(id)
549    }
550
551    /// Search for nearest neighbors
552    ///
553    /// # Arguments
554    /// * `query` - Query vector
555    /// * `k` - Number of results to return
556    /// * `ef_search` - Search parameter (higher = better recall, slower)
557    /// * `filter` - Optional metadata filter expression
558    ///
559    /// # Returns
560    /// Vector of Hit results sorted by similarity
561    #[cfg(feature = "async")]
562    pub async fn search(
563        &self,
564        query: &[f32],
565        k: usize,
566        ef_search: usize,
567        filter: Option<FilterExpr>,
568    ) -> Result<Vec<Hit>> {
569        self.search_internal(query, k, ef_search, filter)
570    }
571
572    /// Internal search implementation (public for Python bindings)
573    pub fn search_internal(
574        &self,
575        query: &[f32],
576        k: usize,
577        ef_search: usize,
578        filter: Option<FilterExpr>,
579    ) -> Result<Vec<Hit>> {
580        if query.len() != self.dimension {
581            return Err(EmbedVecError::DimensionMismatch {
582                expected: self.dimension,
583                got: query.len(),
584            });
585        }
586
587        // Normalize query if using cosine distance
588        let processed_query = if self.distance == Distance::Cosine {
589            normalize_vector(query)
590        } else {
591            query.to_vec()
592        };
593
594        // Search HNSW index
595        let candidates = {
596            let index = self.index.read();
597            let storage = self.storage.read();
598            index.search(
599                &processed_query,
600                k,
601                ef_search,
602                &storage,
603                self.e8_codec.as_ref(),
604            )?
605        };
606
607        // Apply filter and collect results
608        let metadata = self.metadata.read();
609        let mut results: Vec<Hit> = candidates
610            .into_iter()
611            .filter_map(|(id, score)| {
612                let payload = metadata.get(id)?.clone();
613
614                // Apply filter if present
615                if let Some(ref f) = filter {
616                    if !f.matches(&payload) {
617                        return None;
618                    }
619                }
620
621                Some(Hit::new(id, score, payload))
622            })
623            .take(k)
624            .collect();
625
626        // Sort by score (lower is better for distance metrics)
627        results.sort_by_key(|h| OrderedFloat(h.score));
628
629        Ok(results)
630    }
631
632    /// Get the number of vectors in the database
633    #[cfg(feature = "async")]
634    pub async fn len(&self) -> usize {
635        self.storage.read().len()
636    }
637
638    /// Check if the database is empty
639    #[cfg(feature = "async")]
640    pub async fn is_empty(&self) -> bool {
641        self.storage.read().is_empty()
642    }
643
644    /// Clear all vectors and metadata
645    #[cfg(feature = "async")]
646    pub async fn clear(&mut self) -> Result<()> {
647        {
648            let mut storage = self.storage.write();
649            storage.clear();
650        }
651        {
652            let mut metadata = self.metadata.write();
653            metadata.clear();
654        }
655        {
656            let mut index = self.index.write();
657            index.clear();
658        }
659        Ok(())
660    }
661
662    /// Flush changes to disk (if persistence enabled)
663    #[cfg(all(feature = "async", feature = "persistence"))]
664    pub async fn flush(&mut self) -> Result<()> {
665        if let Some(ref db) = self.db {
666            db.flush()
667                .map_err(|e| EmbedVecError::PersistenceError(e.to_string()))?;
668        }
669        Ok(())
670    }
671
672    /// Get current quantization mode
673    pub fn quantization(&self) -> &Quantization {
674        &self.quantization
675    }
676
677    /// Set quantization mode (requires re-indexing)
678    #[cfg(feature = "async")]
679    pub async fn set_quantization(&mut self, quant: Quantization) -> Result<()> {
680        self.quantization = quant.clone();
681        self.e8_codec = match &quant {
682            Quantization::None => None,
683            Quantization::E8 {
684                bits_per_block,
685                use_hadamard,
686                random_seed,
687            } => Some(E8Codec::new(
688                self.dimension,
689                *bits_per_block,
690                *use_hadamard,
691                *random_seed,
692            )),
693        };
694
695        // Re-quantize existing vectors
696        let mut storage = self.storage.write();
697        storage.set_quantization(quant, self.e8_codec.as_ref())?;
698
699        Ok(())
700    }
701
702    /// Get vector dimension
703    pub fn dimension(&self) -> usize {
704        self.dimension
705    }
706
707    /// Get distance metric
708    pub fn distance(&self) -> Distance {
709        self.distance
710    }
711}
712
713/// Normalize a vector to unit length
714fn normalize_vector(v: &[f32]) -> Vec<f32> {
715    let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
716    if norm > 1e-10 {
717        v.iter().map(|x| x / norm).collect()
718    } else {
719        v.to_vec()
720    }
721}
722
723#[cfg(test)]
724mod tests {
725    use super::*;
726
727    #[tokio::test]
728    async fn test_basic_operations() {
729        let mut db = EmbedVec::new(4, Distance::Cosine, 16, 100).await.unwrap();
730
731        let id = db
732            .add(&[1.0, 0.0, 0.0, 0.0], serde_json::json!({"test": "value"}))
733            .await
734            .unwrap();
735        assert_eq!(id, 0);
736
737        let results = db.search(&[1.0, 0.0, 0.0, 0.0], 1, 50, None).await.unwrap();
738        assert_eq!(results.len(), 1);
739        assert_eq!(results[0].id, 0);
740    }
741
742    #[tokio::test]
743    async fn test_dimension_mismatch() {
744        let mut db = EmbedVec::new(4, Distance::Cosine, 16, 100).await.unwrap();
745
746        let result = db
747            .add(&[1.0, 0.0, 0.0], serde_json::json!({}))
748            .await;
749        assert!(result.is_err());
750    }
751}