Skip to main content

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 and H4 lattice quantization, and optional
9//! persistence via Fjall (default), 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/H4 quantization provides 4-16× compression with <5% recall loss
16//! - **Flexible Persistence** — Fjall (default, pure Rust LSM), 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** | 8D lattice compression (4-6× memory reduction) |
39//! | **H4 Quantization** | 4D 600-cell compression (~15× memory reduction) |
40//! | **Metadata Filtering** | Composable filters: eq, gt, lt, contains, AND/OR/NOT |
41//! | **Flexible Persistence** | Fjall (default, pure Rust LSM), Sled (pure Rust), or RocksDB (high performance) |
42//! | **Async API** | Tokio-compatible async operations |
43//! | **Python Bindings** | PyO3-based interop (feature-gated) |
44//!
45//! ## Quick Start
46//!
47//! ```rust,no_run
48//! use embedvec::{Distance, EmbedVec, FilterExpr};
49//!
50//! #[tokio::main]
51//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
52//!     // Create in-memory database
53//!     let mut db = EmbedVec::new(768, Distance::Cosine, 32, 200).await?;
54//!     
55//!     // Add vectors with metadata
56//!     db.add(&vec![0.1; 768], serde_json::json!({"doc_id": "123", "category": "tech"})).await?;
57//!     
58//!     // Search with optional filter
59//!     let filter = FilterExpr::eq("category", "tech");
60//!     let results = db.search(&vec![0.15; 768], 10, 100, Some(filter)).await?;
61//!     
62//!     for hit in results {
63//!         println!("id: {}, score: {:.4}", hit.id, hit.score);
64//!     }
65//!     Ok(())
66//! }
67//! ```
68//!
69//! ## With Persistence
70//!
71//! ```rust,no_run
72//! use embedvec::{Distance, EmbedVec, BackendConfig, BackendType};
73//!
74//! #[tokio::main]
75//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
76//!     // Fjall backend (default, pure Rust LSM-tree)
77//!     let db = EmbedVec::with_persistence("/tmp/vectors.db", 768, Distance::Cosine, 32, 200).await?;
78//!     Ok(())
79//! }
80//! ```
81//!
82//! ## E8 Quantization (4-6× Memory Savings)
83//!
84//! ```rust,no_run
85//! use embedvec::{EmbedVec, Distance, Quantization};
86//!
87//! #[tokio::main]
88//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
89//!     let db = EmbedVec::builder()
90//!         .dimension(768)
91//!         .metric(Distance::Cosine)
92//!         .quantization(Quantization::e8_default())  // ~1.25 bits/dim
93//!         .build()
94//!         .await?;
95//!     Ok(())
96//! }
97//! ```
98//!
99//! ## H4 Quantization (~15× Memory Savings)
100//!
101//! ```rust,no_run
102//! use embedvec::{EmbedVec, Distance, Quantization};
103//!
104//! #[tokio::main]
105//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
106//!     let db = EmbedVec::builder()
107//!         .dimension(768)
108//!         .metric(Distance::Cosine)
109//!         .quantization(Quantization::h4_default())  // ~1.73 bits/dim, 4D 600-cell
110//!         .build()
111//!         .await?;
112//!     Ok(())
113//! }
114//! ```
115//!
116//! ## Feature Flags
117//!
118//! | Flag | Description | Default |
119//! |------|-------------|---------|
120//! | `persistence-fjall` | Fjall persistence backend (pure Rust LSM) | ✓ |
121//! | `persistence-sled` | Sled persistence backend | ✗ |
122//! | `persistence-rocksdb` | RocksDB persistence backend | ✗ |
123//! | `async` | Tokio async API | ✓ |
124//! | `python` | PyO3 Python bindings | ✗ |
125//! | `simd` | Explicit SIMD (auto-detected) | ✗ |
126//!
127//! ## Memory Usage (768-dim vectors)
128//!
129//! | Mode | Bits/Dim | Memory/Vector | 1M Vectors |
130//! |------|----------|---------------|------------|
131//! | Raw (f32) | 32 | 3.1 KB | ~3.1 GB |
132//! | E8 10-bit | ~1.25 | ~220 B | ~220 MB |
133//! | H4 | ~1.73 | ~196 B | ~196 MB |
134//!
135//! ## License
136//!
137//! MIT OR Apache-2.0
138
139#![warn(missing_docs)]
140#![warn(rustdoc::missing_crate_level_docs)]
141
142// WASM global allocator
143#[cfg(feature = "wasm")]
144#[global_allocator]
145static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
146
147pub mod distance;
148pub mod e8;
149pub mod error;
150pub mod filter;
151pub mod h4;
152pub mod hnsw;
153pub mod metadata;
154#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
155pub mod persistence;
156pub mod quantization;
157pub mod storage;
158
159#[cfg(feature = "python")]
160pub mod python;
161
162// Re-exports for convenient API access
163pub use distance::Distance;
164pub use e8::{E8Codec, HadamardTransform};
165pub use error::{EmbedVecError, Result};
166pub use filter::FilterExpr;
167pub use h4::{H4Codec, hadamard4_inplace};
168pub use hnsw::HnswIndex;
169pub use metadata::Metadata;
170#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
171pub use persistence::{BackendConfig, BackendType, PersistenceBackend};
172pub use quantization::Quantization;
173pub use storage::VectorStorage;
174
175use ordered_float::OrderedFloat;
176use parking_lot::RwLock;
177use serde::{Deserialize, Serialize};
178use std::collections::HashSet;
179use std::sync::Arc;
180
181/// Search result hit containing vector ID, similarity score, and payload
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct Hit {
184    /// Unique identifier of the vector
185    pub id: usize,
186    /// Similarity/distance score (interpretation depends on metric)
187    pub score: f32,
188    /// Associated metadata payload
189    pub payload: Metadata,
190}
191
192impl Hit {
193    /// Create a new Hit
194    pub fn new(id: usize, score: f32, payload: Metadata) -> Self {
195        Self { id, score, payload }
196    }
197}
198
199/// Builder for configuring EmbedVec instances
200#[derive(Debug, Clone)]
201pub struct EmbedVecBuilder {
202    dimension: usize,
203    distance: Distance,
204    m: usize,
205    ef_construction: usize,
206    quantization: Quantization,
207    #[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
208    persistence_config: Option<persistence::BackendConfig>,
209}
210
211impl EmbedVecBuilder {
212    /// Create a new builder with required dimension
213    pub fn new(dimension: usize) -> Self {
214        Self {
215            dimension,
216            distance: Distance::Cosine,
217            m: 16,
218            ef_construction: 200,
219            quantization: Quantization::None,
220            #[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
221            persistence_config: None,
222        }
223    }
224
225    /// Set the dimension of vectors
226    pub fn dimension(mut self, dim: usize) -> Self {
227        self.dimension = dim;
228        self
229    }
230
231    /// Set the distance metric
232    pub fn metric(mut self, distance: Distance) -> Self {
233        self.distance = distance;
234        self
235    }
236
237    /// Set HNSW M parameter (connections per layer)
238    pub fn m(mut self, m: usize) -> Self {
239        self.m = m;
240        self
241    }
242
243    /// Set HNSW ef_construction parameter
244    pub fn ef_construction(mut self, ef: usize) -> Self {
245        self.ef_construction = ef;
246        self
247    }
248
249    /// Set quantization mode
250    pub fn quantization(mut self, quant: Quantization) -> Self {
251        self.quantization = quant;
252        self
253    }
254
255    /// Set persistence path for on-disk storage (uses Fjall by default)
256    #[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
257    pub fn persistence(mut self, path: impl Into<String>) -> Self {
258        self.persistence_config = Some(persistence::BackendConfig::new(path));
259        self
260    }
261
262    /// Set persistence with full backend configuration
263    #[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
264    pub fn persistence_config(mut self, config: persistence::BackendConfig) -> Self {
265        self.persistence_config = Some(config);
266        self
267    }
268
269    /// Build the EmbedVec instance
270    #[cfg(feature = "async")]
271    pub async fn build(self) -> Result<EmbedVec> {
272        EmbedVec::from_builder(self).await
273    }
274
275    /// Build the EmbedVec instance (sync version)
276    #[cfg(not(feature = "async"))]
277    pub fn build(self) -> Result<EmbedVec> {
278        EmbedVec::new_internal(
279            self.dimension,
280            self.distance,
281            self.m,
282            self.ef_construction,
283            self.quantization,
284            #[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
285            self.persistence_config,
286        )
287    }
288}
289
290/// Main embedded vector database struct
291///
292/// Provides HNSW-based approximate nearest neighbor search with optional
293/// E8 or H4 lattice quantization for memory efficiency.
294pub struct EmbedVec {
295    /// Vector dimension
296    dimension: usize,
297    /// Distance metric
298    distance: Distance,
299    /// HNSW index
300    pub index: Arc<RwLock<HnswIndex>>,
301    /// Vector storage (raw, E8-quantized, or H4-quantized)
302    pub storage: Arc<RwLock<VectorStorage>>,
303    /// Metadata storage
304    pub metadata: Arc<RwLock<Vec<Metadata>>>,
305    /// Quantization configuration
306    quantization: Quantization,
307    /// E8 codec (present when quantization = E8)
308    e8_codec: Option<E8Codec>,
309    /// H4 codec (present when quantization = H4)
310    h4_codec: Option<H4Codec>,
311    /// Soft-deleted vector ids — excluded from search results and `len()`.
312    /// Their slots stay in storage (as graph waypoints) until the next reopen.
313    deleted: Arc<RwLock<HashSet<usize>>>,
314    /// Persistence backend (fjall, sled, or rocksdb)
315    #[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
316    backend: Option<Box<dyn persistence::PersistenceBackend>>,
317}
318
319// =============================================================================
320// On-disk persistence format
321// =============================================================================
322
323/// Backend key holding the self-describing database header.
324#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
325const PERSIST_HEADER_KEY: &[u8] = b"__embedvec_header__";
326
327/// Backend key prefix for per-vector records. Ids are zero-padded so a
328/// lexicographic prefix scan returns records in ascending id order.
329#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
330const PERSIST_REC_PREFIX: &[u8] = b"rec:";
331
332/// On-disk format version (bump on incompatible layout changes).
333#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
334const PERSIST_VERSION: u32 = 1;
335
336/// Self-describing header so a persisted database reopens with the exact
337/// configuration it was created with, regardless of constructor arguments.
338#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
339#[derive(Serialize, Deserialize)]
340struct PersistHeader {
341    version: u32,
342    dimension: usize,
343    distance: Distance,
344    quantization: Quantization,
345    m: usize,
346    ef_construction: usize,
347    /// Highest id slot ever allocated (incl. deleted) at last flush — lets a
348    /// reopen recreate trailing tombstones so deleted ids are never reused.
349    #[serde(default)]
350    high_water_mark: usize,
351}
352
353/// One persisted vector: its (compact) stored form plus its metadata.
354#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
355#[derive(Serialize, Deserialize)]
356struct PersistedRecord {
357    stored: storage::StoredVector,
358    meta: Metadata,
359}
360
361impl EmbedVec {
362    /// Create a new in-memory EmbedVec instance
363    ///
364    /// # Arguments
365    /// * `dim` - Vector dimension (e.g., 768 for many LLM embeddings)
366    /// * `distance` - Distance metric (Cosine, Euclidean, DotProduct)
367    /// * `m` - HNSW M parameter (connections per layer, typically 16-64)
368    /// * `ef_construction` - HNSW construction parameter (typically 100-500)
369    ///
370    /// # Example
371    /// ```rust,no_run
372    /// use embedvec::{EmbedVec, Distance};
373    ///
374    /// #[tokio::main]
375    /// async fn main() {
376    ///     let db = EmbedVec::new(768, Distance::Cosine, 32, 200).await.unwrap();
377    /// }
378    /// ```
379    #[cfg(all(feature = "async", any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb")))]
380    pub async fn new(
381        dim: usize,
382        distance: Distance,
383        m: usize,
384        ef_construction: usize,
385    ) -> Result<Self> {
386        Self::new_internal(dim, distance, m, ef_construction, Quantization::None, None)
387    }
388
389    #[cfg(all(feature = "async", not(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))))]
390    pub async fn new(
391        dim: usize,
392        distance: Distance,
393        m: usize,
394        ef_construction: usize,
395    ) -> Result<Self> {
396        Self::new_internal(dim, distance, m, ef_construction, Quantization::None)
397    }
398
399    /// Create a new EmbedVec with persistence (uses Fjall by default)
400    #[cfg(all(feature = "async", any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb")))]
401    pub async fn with_persistence(
402        path: impl AsRef<std::path::Path>,
403        dim: usize,
404        distance: Distance,
405        m: usize,
406        ef_construction: usize,
407    ) -> Result<Self> {
408        let path_str = path.as_ref().to_string_lossy().to_string();
409        let config = persistence::BackendConfig::new(path_str);
410        Self::new_internal(
411            dim,
412            distance,
413            m,
414            ef_construction,
415            Quantization::None,
416            Some(config),
417        )
418    }
419
420    /// Create a new EmbedVec with a specific persistence backend
421    #[cfg(all(feature = "async", any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb")))]
422    pub async fn with_backend(
423        config: persistence::BackendConfig,
424        dim: usize,
425        distance: Distance,
426        m: usize,
427        ef_construction: usize,
428    ) -> Result<Self> {
429        Self::new_internal(
430            dim,
431            distance,
432            m,
433            ef_construction,
434            Quantization::None,
435            Some(config),
436        )
437    }
438
439    /// Create EmbedVec from builder configuration
440    #[cfg(feature = "async")]
441    async fn from_builder(builder: EmbedVecBuilder) -> Result<Self> {
442        Self::new_internal(
443            builder.dimension,
444            builder.distance,
445            builder.m,
446            builder.ef_construction,
447            builder.quantization,
448            #[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
449            builder.persistence_config,
450        )
451    }
452
453    /// Internal constructor (public for Python bindings) — with persistence
454    #[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
455    pub fn new_internal(
456        dim: usize,
457        distance: Distance,
458        m: usize,
459        ef_construction: usize,
460        quantization: Quantization,
461        persistence_config: Option<persistence::BackendConfig>,
462    ) -> Result<Self> {
463        if dim == 0 {
464            return Err(EmbedVecError::InvalidDimension(dim));
465        }
466
467        let mut index = HnswIndex::new(m, ef_construction, distance);
468        let storage = VectorStorage::new(dim, quantization.clone());
469
470        let e8_codec = match &quantization {
471            Quantization::E8 { bits_per_block, use_hadamard, random_seed } =>
472                Some(E8Codec::new(dim, *bits_per_block, *use_hadamard, *random_seed)),
473            _ => None,
474        };
475
476        let h4_codec = match &quantization {
477            Quantization::H4 { use_hadamard, random_seed } =>
478                Some(H4Codec::new(dim, *use_hadamard, *random_seed)),
479            _ => None,
480        };
481
482        // The index needs the H4 codec to decode H4 vectors during distance
483        // computation (E8 is threaded per-call; raw uses the zero-copy path).
484        index.set_h4_codec(h4_codec.clone());
485
486        let backend = if let Some(config) = persistence_config {
487            Some(persistence::create_backend(&config)?)
488        } else {
489            None
490        };
491
492        let mut db = Self {
493            dimension: dim,
494            distance,
495            index: Arc::new(RwLock::new(index)),
496            storage: Arc::new(RwLock::new(storage)),
497            metadata: Arc::new(RwLock::new(Vec::new())),
498            quantization,
499            e8_codec,
500            h4_codec,
501            deleted: Arc::new(RwLock::new(HashSet::new())),
502            backend,
503        };
504
505        // Reload any previously persisted vectors (and adopt their config), or
506        // record the header for a fresh store so it reopens consistently.
507        db.load_from_backend()?;
508
509        Ok(db)
510    }
511
512    /// Internal constructor without persistence
513    #[cfg(not(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb")))]
514    pub fn new_internal(
515        dim: usize,
516        distance: Distance,
517        m: usize,
518        ef_construction: usize,
519        quantization: Quantization,
520    ) -> Result<Self> {
521        if dim == 0 {
522            return Err(EmbedVecError::InvalidDimension(dim));
523        }
524
525        let mut index = HnswIndex::new(m, ef_construction, distance);
526        let storage = VectorStorage::new(dim, quantization.clone());
527
528        let e8_codec = match &quantization {
529            Quantization::E8 { bits_per_block, use_hadamard, random_seed } =>
530                Some(E8Codec::new(dim, *bits_per_block, *use_hadamard, *random_seed)),
531            _ => None,
532        };
533
534        let h4_codec = match &quantization {
535            Quantization::H4 { use_hadamard, random_seed } =>
536                Some(H4Codec::new(dim, *use_hadamard, *random_seed)),
537            _ => None,
538        };
539
540        // The index needs the H4 codec to decode H4 vectors during distance
541        // computation (E8 is threaded per-call; raw uses the zero-copy path).
542        index.set_h4_codec(h4_codec.clone());
543
544        Ok(Self {
545            dimension: dim,
546            distance,
547            index: Arc::new(RwLock::new(index)),
548            storage: Arc::new(RwLock::new(storage)),
549            metadata: Arc::new(RwLock::new(Vec::new())),
550            quantization,
551            e8_codec,
552            h4_codec,
553            deleted: Arc::new(RwLock::new(HashSet::new())),
554        })
555    }
556
557    /// Get a builder for configuring EmbedVec
558    pub fn builder() -> EmbedVecBuilder {
559        EmbedVecBuilder::new(768) // Default dimension
560    }
561
562    /// Add a single vector with metadata
563    ///
564    /// # Arguments
565    /// * `vector` - The embedding vector (must match configured dimension)
566    /// * `payload` - Associated metadata (JSON-compatible)
567    ///
568    /// # Returns
569    /// The assigned vector ID
570    #[cfg(feature = "async")]
571    pub async fn add(&mut self, vector: &[f32], payload: impl Into<Metadata>) -> Result<usize> {
572        self.add_internal(vector, payload.into())
573    }
574
575    /// Add multiple vectors with metadata in batch
576    ///
577    /// # Arguments
578    /// * `vectors` - Slice of embedding vectors
579    /// * `payloads` - Associated metadata for each vector
580    #[cfg(feature = "async")]
581    pub async fn add_many(
582        &mut self,
583        vectors: &[Vec<f32>],
584        payloads: Vec<impl Into<Metadata>>,
585    ) -> Result<()> {
586        if vectors.len() != payloads.len() {
587            return Err(EmbedVecError::MismatchedLengths {
588                vectors: vectors.len(),
589                payloads: payloads.len(),
590            });
591        }
592
593        #[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
594        let mut new_ids: Vec<usize> = Vec::with_capacity(vectors.len());
595
596        for (vector, payload) in vectors.iter().zip(payloads.into_iter()) {
597            let _id = self.insert_in_memory(vector, payload.into())?;
598            #[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
599            new_ids.push(_id);
600        }
601
602        // Persist the whole batch in a single atomic write (fast on Fjall/Sled).
603        #[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
604        self.persist_records(&new_ids)?;
605
606        Ok(())
607    }
608
609    /// Internal add implementation (public for Python bindings)
610    pub fn add_internal(&mut self, vector: &[f32], payload: Metadata) -> Result<usize> {
611        let id = self.insert_in_memory(vector, payload)?;
612
613        // Persist the single record (no-op when no backend is configured).
614        #[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
615        self.persist_record(id)?;
616
617        Ok(id)
618    }
619
620    /// Insert a vector + metadata into the in-memory storage, metadata table,
621    /// and HNSW index. Does not touch the persistence backend.
622    fn insert_in_memory(&mut self, vector: &[f32], payload: Metadata) -> Result<usize> {
623        if vector.len() != self.dimension {
624            return Err(EmbedVecError::DimensionMismatch {
625                expected: self.dimension,
626                got: vector.len(),
627            });
628        }
629
630        // Normalize if using cosine distance
631        let processed_vector = if self.distance == Distance::Cosine {
632            normalize_vector(vector)
633        } else {
634            vector.to_vec()
635        };
636
637        // Store vector (quantized or raw)
638        let id = {
639            let mut storage = self.storage.write();
640            storage.add(&processed_vector, self.e8_codec.as_ref(), self.h4_codec.as_ref())?
641        };
642
643        // Store metadata
644        {
645            let mut meta = self.metadata.write();
646            if id >= meta.len() {
647                meta.resize(id + 1, Metadata::default());
648            }
649            meta[id] = payload;
650        }
651
652        // Add to HNSW index (HNSW currently uses E8 codec path; H4 vectors inserted raw)
653        {
654            let mut index = self.index.write();
655            let storage = self.storage.read();
656            index.insert(id, &processed_vector, &storage, self.e8_codec.as_ref())?;
657        }
658
659        Ok(id)
660    }
661
662    /// Delete a vector by its id.
663    ///
664    /// Soft delete: the id is excluded from future searches and from `len()`,
665    /// its metadata is cleared, and its record is removed from the persistence
666    /// backend. The vector stays in the in-memory graph as a routing waypoint
667    /// until the next reopen, when the index is rebuilt without it. Ids of
668    /// surviving vectors remain stable and deleted ids are not reused.
669    ///
670    /// Returns `true` if the id existed and was newly deleted, or `false` if it
671    /// was out of range or already deleted.
672    #[cfg(feature = "async")]
673    pub async fn delete(&mut self, id: usize) -> Result<bool> {
674        self.delete_internal(id)
675    }
676
677    /// Delete many vectors by id. Returns the number actually removed.
678    #[cfg(feature = "async")]
679    pub async fn delete_many(&mut self, ids: &[usize]) -> Result<usize> {
680        let mut removed = 0;
681        for &id in ids {
682            if self.delete_internal(id)? {
683                removed += 1;
684            }
685        }
686        Ok(removed)
687    }
688
689    /// Internal delete implementation (public for Python bindings).
690    pub fn delete_internal(&mut self, id: usize) -> Result<bool> {
691        // Out of range -> nothing to delete.
692        if id >= self.storage.read().len() {
693            return Ok(false);
694        }
695        // Mark deleted; bail out if it was already deleted.
696        if !self.deleted.write().insert(id) {
697            return Ok(false);
698        }
699        // Clear metadata so a stale payload can't match a filter or be returned.
700        {
701            let mut meta = self.metadata.write();
702            if let Some(slot) = meta.get_mut(id) {
703                *slot = Metadata::default();
704            }
705        }
706        // Remove from disk (the in-memory slot survives until the next reopen).
707        #[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
708        {
709            if let Some(backend) = &self.backend {
710                backend.delete(&Self::rec_key(id))?;
711            }
712        }
713        Ok(true)
714    }
715
716    /// Search for nearest neighbors
717    ///
718    /// # Arguments
719    /// * `query` - Query vector
720    /// * `k` - Number of results to return
721    /// * `ef_search` - Search parameter (higher = better recall, slower)
722    /// * `filter` - Optional metadata filter expression
723    ///
724    /// # Returns
725    /// Vector of Hit results sorted by similarity
726    #[cfg(feature = "async")]
727    pub async fn search(
728        &self,
729        query: &[f32],
730        k: usize,
731        ef_search: usize,
732        filter: Option<FilterExpr>,
733    ) -> Result<Vec<Hit>> {
734        self.search_internal(query, k, ef_search, filter)
735    }
736
737    /// Internal search implementation (public for Python bindings)
738    pub fn search_internal(
739        &self,
740        query: &[f32],
741        k: usize,
742        ef_search: usize,
743        filter: Option<FilterExpr>,
744    ) -> Result<Vec<Hit>> {
745        if query.len() != self.dimension {
746            return Err(EmbedVecError::DimensionMismatch {
747                expected: self.dimension,
748                got: query.len(),
749            });
750        }
751
752        // Normalize query if using cosine distance
753        let processed_query = if self.distance == Distance::Cosine {
754            normalize_vector(query)
755        } else {
756            query.to_vec()
757        };
758
759        // Over-fetch up to the full search beam so soft-deletes and metadata
760        // filters still leave enough live candidates to return k results.
761        let fetch_k = ef_search.max(k);
762
763        // Search HNSW index (uses E8 codec for distance; H4 decoded via storage on retrieval)
764        let candidates = {
765            let index = self.index.read();
766            let storage = self.storage.read();
767            index.search(
768                &processed_query,
769                fetch_k,
770                ef_search,
771                &storage,
772                self.e8_codec.as_ref(),
773            )?
774        };
775
776        // Apply deletes + filter and collect results
777        let metadata = self.metadata.read();
778        let deleted = self.deleted.read();
779        let mut results: Vec<Hit> = candidates
780            .into_iter()
781            .filter_map(|(id, score)| {
782                // Skip soft-deleted vectors.
783                if deleted.contains(&id) {
784                    return None;
785                }
786
787                let payload = metadata.get(id)?.clone();
788
789                // Apply filter if present
790                if let Some(ref f) = filter {
791                    if !f.matches(&payload) {
792                        return None;
793                    }
794                }
795
796                Some(Hit::new(id, score, payload))
797            })
798            .take(k)
799            .collect();
800
801        // Sort by score (lower is better for distance metrics)
802        results.sort_by_key(|h| OrderedFloat(h.score));
803
804        Ok(results)
805    }
806
807    /// Search many query vectors at once, returning one result list per query.
808    ///
809    /// Queries are run in parallel (Rayon). Equivalent to calling `search` in a
810    /// loop, but uses all cores — useful for batched retrieval (e.g. a
811    /// LangChain/LlamaIndex retriever issuing several queries).
812    #[cfg(feature = "async")]
813    pub async fn search_many(
814        &self,
815        queries: &[Vec<f32>],
816        k: usize,
817        ef_search: usize,
818        filter: Option<FilterExpr>,
819    ) -> Result<Vec<Vec<Hit>>> {
820        self.search_many_internal(queries, k, ef_search, filter)
821    }
822
823    /// Internal batch search (public for Python bindings).
824    pub fn search_many_internal(
825        &self,
826        queries: &[Vec<f32>],
827        k: usize,
828        ef_search: usize,
829        filter: Option<FilterExpr>,
830    ) -> Result<Vec<Vec<Hit>>> {
831        use rayon::prelude::*;
832        queries
833            .par_iter()
834            .map(|q| self.search_internal(q, k, ef_search, filter.clone()))
835            .collect()
836    }
837
838    /// Get the metadata payload for a live id (`None` if out of range or deleted).
839    pub fn payload(&self, id: usize) -> Option<Metadata> {
840        if self.deleted.read().contains(&id) {
841            return None;
842        }
843        self.metadata.read().get(id).cloned()
844    }
845
846    /// Whether `id` refers to a live (non-deleted) vector.
847    pub fn contains_id(&self, id: usize) -> bool {
848        id < self.storage.read().len() && !self.deleted.read().contains(&id)
849    }
850
851    /// All live `(id, payload)` pairs.
852    ///
853    /// Lets callers rebuild an external key→id index (e.g. a document-id map for
854    /// a LangChain/LlamaIndex adapter) after reopening a persisted store, since
855    /// ids are stable across reload.
856    pub fn entries(&self) -> Vec<(usize, Metadata)> {
857        let metadata = self.metadata.read();
858        let deleted = self.deleted.read();
859        metadata
860            .iter()
861            .enumerate()
862            .filter(|(id, _)| !deleted.contains(id))
863            .map(|(id, m)| (id, m.clone()))
864            .collect()
865    }
866
867    /// Number of live (non-deleted) vectors (synchronous; for Python bindings).
868    pub fn live_count(&self) -> usize {
869        self.storage
870            .read()
871            .len()
872            .saturating_sub(self.deleted.read().len())
873    }
874
875    /// Get the number of live (non-deleted) vectors in the database
876    #[cfg(feature = "async")]
877    pub async fn len(&self) -> usize {
878        self.live_count()
879    }
880
881    /// Check if the database has no live vectors
882    #[cfg(feature = "async")]
883    pub async fn is_empty(&self) -> bool {
884        self.len().await == 0
885    }
886
887    /// Clear all vectors and metadata
888    #[cfg(feature = "async")]
889    pub async fn clear(&mut self) -> Result<()> {
890        self.clear_sync()
891    }
892
893    /// Synchronous clear (public for Python bindings).
894    pub fn clear_sync(&mut self) -> Result<()> {
895        {
896            let mut storage = self.storage.write();
897            storage.clear();
898        }
899        {
900            let mut metadata = self.metadata.write();
901            metadata.clear();
902        }
903        {
904            let mut index = self.index.write();
905            index.clear();
906        }
907        {
908            let mut deleted = self.deleted.write();
909            deleted.clear();
910        }
911
912        // Remove persisted vector records (the header is kept so the store's
913        // configuration survives an empty reopen).
914        #[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
915        self.clear_backend()?;
916
917        Ok(())
918    }
919
920    /// Flush changes to disk (if persistence enabled)
921    #[cfg(all(feature = "async", any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb")))]
922    pub async fn flush(&mut self) -> Result<()> {
923        // Record the current high-water mark so that ids (including trailing
924        // deleted ones) stay stable across a flush + reopen.
925        self.persist_header()?;
926        if let Some(ref backend) = self.backend {
927            backend.flush()?;
928        }
929        Ok(())
930    }
931
932    /// Get current quantization mode
933    pub fn quantization(&self) -> &Quantization {
934        &self.quantization
935    }
936
937    /// Set quantization mode (re-quantizes all existing vectors)
938    #[cfg(feature = "async")]
939    pub async fn set_quantization(&mut self, quant: Quantization) -> Result<()> {
940        self.e8_codec = match &quant {
941            Quantization::E8 { bits_per_block, use_hadamard, random_seed } =>
942                Some(E8Codec::new(self.dimension, *bits_per_block, *use_hadamard, *random_seed)),
943            _ => None,
944        };
945        self.h4_codec = match &quant {
946            Quantization::H4 { use_hadamard, random_seed } =>
947                Some(H4Codec::new(self.dimension, *use_hadamard, *random_seed)),
948            _ => None,
949        };
950        self.quantization = quant.clone();
951
952        // Keep the index's H4 codec in sync with the new quantization so its
953        // distance computations decode H4 vectors correctly.
954        self.index.write().set_h4_codec(self.h4_codec.clone());
955
956        // Re-quantize existing vectors (scoped so the write lock is released
957        // before any persistence re-encode below re-reads storage).
958        {
959            let mut storage = self.storage.write();
960            storage.set_quantization(quant, self.e8_codec.as_ref(), self.h4_codec.as_ref())?;
961        }
962
963        // The on-disk encoding changed for every vector — rewrite header + records.
964        #[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
965        self.persist_all()?;
966
967        Ok(())
968    }
969
970    /// Get vector dimension
971    pub fn dimension(&self) -> usize {
972        self.dimension
973    }
974
975    /// Get distance metric
976    pub fn distance(&self) -> Distance {
977        self.distance
978    }
979}
980
981// =============================================================================
982// Persistence: save/load vectors + metadata + index through the backend
983// =============================================================================
984
985#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
986impl EmbedVec {
987    /// Backend key for a record id (zero-padded so prefix scans stay id-ordered).
988    fn rec_key(id: usize) -> Vec<u8> {
989        format!("rec:{:020}", id).into_bytes()
990    }
991
992    /// Serialize the record (stored vector + metadata) for a given id.
993    fn encode_record(&self, id: usize) -> Result<Vec<u8>> {
994        let stored = self
995            .storage
996            .read()
997            .get_stored(id)
998            .cloned()
999            .ok_or(EmbedVecError::VectorNotFound(id))?;
1000        let meta = self.metadata.read().get(id).cloned().unwrap_or_default();
1001        let record = PersistedRecord { stored, meta };
1002        serde_json::to_vec(&record)
1003            .map_err(|e| EmbedVecError::SerializationError(e.to_string()))
1004    }
1005
1006    /// Write the self-describing header for the current configuration.
1007    fn persist_header(&self) -> Result<()> {
1008        let backend = match &self.backend {
1009            Some(b) => b,
1010            None => return Ok(()),
1011        };
1012        let header = PersistHeader {
1013            version: PERSIST_VERSION,
1014            dimension: self.dimension,
1015            distance: self.distance,
1016            quantization: self.quantization.clone(),
1017            m: self.index.read().m(),
1018            ef_construction: self.index.read().ef_construction(),
1019            high_water_mark: self.storage.read().len(),
1020        };
1021        let bytes = serde_json::to_vec(&header)
1022            .map_err(|e| EmbedVecError::SerializationError(e.to_string()))?;
1023        backend.set(PERSIST_HEADER_KEY, &bytes)
1024    }
1025
1026    /// Persist a single record.
1027    fn persist_record(&self, id: usize) -> Result<()> {
1028        let backend = match &self.backend {
1029            Some(b) => b,
1030            None => return Ok(()),
1031        };
1032        let value = self.encode_record(id)?;
1033        backend.set(&Self::rec_key(id), &value)
1034    }
1035
1036    /// Persist many records in one atomic batch.
1037    fn persist_records(&self, ids: &[usize]) -> Result<()> {
1038        let backend = match &self.backend {
1039            Some(b) => b,
1040            None => return Ok(()),
1041        };
1042        if ids.is_empty() {
1043            return Ok(());
1044        }
1045        let mut batch: Vec<(Vec<u8>, Vec<u8>)> = Vec::with_capacity(ids.len());
1046        for &id in ids {
1047            batch.push((Self::rec_key(id), self.encode_record(id)?));
1048        }
1049        backend.set_batch(&batch)
1050    }
1051
1052    /// Rewrite the header and every record (used after a quantization change).
1053    fn persist_all(&self) -> Result<()> {
1054        if self.backend.is_none() {
1055            return Ok(());
1056        }
1057        self.persist_header()?;
1058        let n = self.storage.read().len();
1059        let ids: Vec<usize> = (0..n).collect();
1060        self.persist_records(&ids)
1061    }
1062
1063    /// Delete all persisted vector records (keeps the header).
1064    fn clear_backend(&self) -> Result<()> {
1065        let backend = match &self.backend {
1066            Some(b) => b,
1067            None => return Ok(()),
1068        };
1069        let keys: Vec<Vec<u8>> = backend
1070            .scan_prefix(PERSIST_REC_PREFIX)?
1071            .into_iter()
1072            .map(|(k, _)| k)
1073            .collect();
1074        for key in keys {
1075            backend.delete(&key)?;
1076        }
1077        Ok(())
1078    }
1079
1080    /// Reload persisted vectors on open.
1081    ///
1082    /// If a header exists, its configuration is adopted (on-disk vectors are
1083    /// encoded against it), all records are loaded, and the HNSW index is
1084    /// rebuilt. If no header exists, the store is treated as fresh and the
1085    /// current configuration is recorded for consistent future reopens.
1086    ///
1087    /// Gaps in the id sequence (from deletes) are refilled with tombstones, and
1088    /// trailing deletes are restored up to the persisted high-water mark, so
1089    /// surviving ids stay stable and deleted ids are never reused.
1090    fn load_from_backend(&mut self) -> Result<()> {
1091        let header_bytes = match &self.backend {
1092            Some(backend) => backend.get(PERSIST_HEADER_KEY)?,
1093            None => return Ok(()),
1094        };
1095
1096        let header: PersistHeader = match header_bytes {
1097            Some(bytes) => serde_json::from_slice(&bytes)
1098                .map_err(|e| EmbedVecError::SerializationError(e.to_string()))?,
1099            // Fresh store: record the configuration so reopening is consistent.
1100            None => return self.persist_header(),
1101        };
1102
1103        if header.dimension != self.dimension {
1104            return Err(EmbedVecError::DimensionMismatch {
1105                expected: header.dimension,
1106                got: self.dimension,
1107            });
1108        }
1109
1110        // Adopt the persisted configuration — on-disk vectors are encoded against it.
1111        self.distance = header.distance;
1112        self.quantization = header.quantization.clone();
1113        self.e8_codec = match &self.quantization {
1114            Quantization::E8 { bits_per_block, use_hadamard, random_seed } =>
1115                Some(E8Codec::new(self.dimension, *bits_per_block, *use_hadamard, *random_seed)),
1116            _ => None,
1117        };
1118        self.h4_codec = match &self.quantization {
1119            Quantization::H4 { use_hadamard, random_seed } =>
1120                Some(H4Codec::new(self.dimension, *use_hadamard, *random_seed)),
1121            _ => None,
1122        };
1123        *self.storage.write() = VectorStorage::new(self.dimension, self.quantization.clone());
1124        let mut rebuilt = HnswIndex::new(header.m, header.ef_construction, self.distance);
1125        rebuilt.set_h4_codec(self.h4_codec.clone());
1126        *self.index.write() = rebuilt;
1127        self.metadata.write().clear();
1128        self.deleted.write().clear();
1129
1130        // Records come back in ascending id order (zero-padded keys). Deleted
1131        // ids appear as gaps; fill those slots with tombstones so surviving ids
1132        // stay stable (id == position). Tombstones never enter the HNSW graph.
1133        let records = match &self.backend {
1134            Some(backend) => backend.scan_prefix(PERSIST_REC_PREFIX)?,
1135            None => return Ok(()),
1136        };
1137
1138        let mut deleted_ids: HashSet<usize> = HashSet::new();
1139        {
1140            let mut storage = self.storage.write();
1141            let mut metadata = self.metadata.write();
1142            let mut index = self.index.write();
1143            let mut next_id = 0usize;
1144
1145            for (key, value) in records {
1146                let id: usize = std::str::from_utf8(&key[PERSIST_REC_PREFIX.len()..])
1147                    .ok()
1148                    .and_then(|s| s.parse().ok())
1149                    .ok_or_else(|| {
1150                        EmbedVecError::PersistenceError(format!(
1151                            "invalid record key: {}",
1152                            String::from_utf8_lossy(&key)
1153                        ))
1154                    })?;
1155
1156                // Fill any gap before this id with tombstones (deleted slots).
1157                while next_id < id {
1158                    storage.push_stored(crate::storage::StoredVector::Tombstone);
1159                    metadata.push(Metadata::default());
1160                    deleted_ids.insert(next_id);
1161                    next_id += 1;
1162                }
1163
1164                let record: PersistedRecord = serde_json::from_slice(&value)
1165                    .map_err(|e| EmbedVecError::SerializationError(e.to_string()))?;
1166                // Decode for the index before moving the stored form into storage.
1167                let decoded = record
1168                    .stored
1169                    .to_f32(self.e8_codec.as_ref(), self.h4_codec.as_ref());
1170                storage.push_stored(record.stored);
1171                metadata.push(record.meta);
1172                index.insert(id, &decoded, &*storage, self.e8_codec.as_ref())?;
1173                next_id += 1;
1174            }
1175
1176            // Recreate trailing deleted slots up to the recorded high-water mark.
1177            while next_id < header.high_water_mark {
1178                storage.push_stored(crate::storage::StoredVector::Tombstone);
1179                metadata.push(Metadata::default());
1180                deleted_ids.insert(next_id);
1181                next_id += 1;
1182            }
1183        }
1184
1185        *self.deleted.write() = deleted_ids;
1186
1187        Ok(())
1188    }
1189}
1190
1191/// Normalize a vector to unit length
1192fn normalize_vector(v: &[f32]) -> Vec<f32> {
1193    let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
1194    if norm > 1e-10 {
1195        v.iter().map(|x| x / norm).collect()
1196    } else {
1197        v.to_vec()
1198    }
1199}
1200
1201#[cfg(test)]
1202mod tests {
1203    use super::*;
1204
1205    #[tokio::test]
1206    async fn test_basic_operations() {
1207        let mut db = EmbedVec::new(4, Distance::Cosine, 16, 100).await.unwrap();
1208
1209        let id = db
1210            .add(&[1.0, 0.0, 0.0, 0.0], serde_json::json!({"test": "value"}))
1211            .await
1212            .unwrap();
1213        assert_eq!(id, 0);
1214
1215        let results = db.search(&[1.0, 0.0, 0.0, 0.0], 1, 50, None).await.unwrap();
1216        assert_eq!(results.len(), 1);
1217        assert_eq!(results[0].id, 0);
1218    }
1219
1220    #[tokio::test]
1221    async fn test_dimension_mismatch() {
1222        let mut db = EmbedVec::new(4, Distance::Cosine, 16, 100).await.unwrap();
1223
1224        let result = db
1225            .add(&[1.0, 0.0, 0.0], serde_json::json!({}))
1226            .await;
1227        assert!(result.is_err());
1228    }
1229
1230    #[tokio::test]
1231    async fn test_h4_search_recall_multi() {
1232        // Regression test for the H4 codec not being threaded into the HNSW
1233        // index: H4 vectors used to decode to zeros during distance computation,
1234        // so every candidate was equidistant and search returned garbage. With
1235        // distinct vectors, each should now retrieve itself.
1236        let dim = 128;
1237        let n = 16;
1238        let mut db = EmbedVec::builder()
1239            .dimension(dim)
1240            .metric(Distance::Cosine)
1241            .quantization(Quantization::h4_default())
1242            .build()
1243            .await
1244            .unwrap();
1245
1246        // Well-separated pseudo-random vectors.
1247        let mut seed = 0x1234_5678u64;
1248        let mut rng = || {
1249            seed ^= seed << 13;
1250            seed ^= seed >> 7;
1251            seed ^= seed << 17;
1252            (seed as f32 / u64::MAX as f32) * 2.0 - 1.0
1253        };
1254        let mut vectors = Vec::new();
1255        for i in 0..n {
1256            let v: Vec<f32> = (0..dim).map(|_| rng()).collect();
1257            db.add(&v, serde_json::json!({ "i": i })).await.unwrap();
1258            vectors.push(v);
1259        }
1260
1261        // Each vector should retrieve itself as the nearest neighbor.
1262        let mut self_hits = 0;
1263        for (i, v) in vectors.iter().enumerate() {
1264            let hits = db.search(v, 1, 64, None).await.unwrap();
1265            if hits[0].id == i {
1266                self_hits += 1;
1267            }
1268        }
1269        assert!(
1270            self_hits >= n - 1,
1271            "H4 self-recall too low: {self_hits}/{n} (index not decoding H4?)"
1272        );
1273
1274        // Anti-garbage: distinct queries return distinct nearest neighbors.
1275        let h0 = db.search(&vectors[0], 1, 64, None).await.unwrap()[0].id;
1276        let h_last = db.search(&vectors[n - 1], 1, 64, None).await.unwrap()[0].id;
1277        assert_ne!(h0, h_last, "distinct H4 queries returned the same top hit");
1278    }
1279
1280    #[tokio::test]
1281    async fn test_search_many_and_entries() {
1282        let mut db = EmbedVec::new(4, Distance::Cosine, 16, 100).await.unwrap();
1283        let a = db.add(&[1.0, 0.0, 0.0, 0.0], serde_json::json!({"doc": "a"})).await.unwrap();
1284        let b = db.add(&[0.0, 1.0, 0.0, 0.0], serde_json::json!({"doc": "b"})).await.unwrap();
1285        let _c = db.add(&[0.0, 0.0, 1.0, 0.0], serde_json::json!({"doc": "c"})).await.unwrap();
1286
1287        // Batch search: each query returns its own ranked list.
1288        let queries = vec![vec![1.0, 0.0, 0.0, 0.0], vec![0.0, 1.0, 0.0, 0.0]];
1289        let results = db.search_many(&queries, 1, 50, None).await.unwrap();
1290        assert_eq!(results.len(), 2);
1291        assert_eq!(results[0][0].id, a);
1292        assert_eq!(results[1][0].id, b);
1293
1294        // entries() / payload() expose live (id, payload) for external id maps.
1295        let entries = db.entries();
1296        assert_eq!(entries.len(), 3);
1297        assert_eq!(db.payload(a).unwrap()["doc"], "a");
1298        assert!(db.contains_id(b));
1299
1300        // After delete, entries/payload reflect liveness.
1301        db.delete(b).await.unwrap();
1302        assert_eq!(db.entries().len(), 2);
1303        assert!(db.payload(b).is_none());
1304        assert!(!db.contains_id(b));
1305    }
1306
1307    #[tokio::test]
1308    async fn test_delete_in_memory() {
1309        let mut db = EmbedVec::new(4, Distance::Cosine, 16, 100).await.unwrap();
1310        let a = db.add(&[1.0, 0.0, 0.0, 0.0], serde_json::json!({"v": "a"})).await.unwrap();
1311        let b = db.add(&[0.0, 1.0, 0.0, 0.0], serde_json::json!({"v": "b"})).await.unwrap();
1312        let c = db.add(&[0.0, 0.0, 1.0, 0.0], serde_json::json!({"v": "c"})).await.unwrap();
1313        assert_eq!(db.len().await, 3);
1314
1315        // Delete b.
1316        assert!(db.delete(b).await.unwrap());
1317        assert_eq!(db.len().await, 2);
1318
1319        // Deleting again, or an out-of-range id, returns false.
1320        assert!(!db.delete(b).await.unwrap());
1321        assert!(!db.delete(999).await.unwrap());
1322
1323        // b is excluded from search; a and c are still found.
1324        let hits = db.search(&[0.0, 1.0, 0.0, 0.0], 3, 50, None).await.unwrap();
1325        assert!(hits.iter().all(|h| h.id != b));
1326        assert_eq!(db.search(&[1.0, 0.0, 0.0, 0.0], 1, 50, None).await.unwrap()[0].id, a);
1327        assert_eq!(db.search(&[0.0, 0.0, 1.0, 0.0], 1, 50, None).await.unwrap()[0].id, c);
1328
1329        // A new add gets a fresh id — b's id is not reused.
1330        let d = db.add(&[0.0, 0.0, 0.0, 1.0], serde_json::json!({"v": "d"})).await.unwrap();
1331        assert_eq!(d, 3);
1332        assert_ne!(d, b);
1333        assert_eq!(db.len().await, 3);
1334    }
1335
1336    #[tokio::test]
1337    async fn test_h4_quantization_end_to_end() {
1338        let mut db = EmbedVec::builder()
1339            .dimension(8)
1340            .metric(Distance::Cosine)
1341            .quantization(Quantization::h4_default())
1342            .build()
1343            .await
1344            .unwrap();
1345
1346        let id = db
1347            .add(&[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], serde_json::json!({"lattice": "h4"}))
1348            .await
1349            .unwrap();
1350        assert_eq!(id, 0);
1351
1352        let results = db
1353            .search(&[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 1, 50, None)
1354            .await
1355            .unwrap();
1356        assert_eq!(results.len(), 1);
1357        assert_eq!(results[0].id, 0);
1358    }
1359}