#![warn(missing_docs)]
#![warn(rustdoc::missing_crate_level_docs)]
#[cfg(feature = "wasm")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
pub mod distance;
pub mod e8;
pub mod error;
pub mod filter;
pub mod h4;
pub mod hnsw;
pub mod metadata;
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
pub mod persistence;
pub mod quantization;
pub mod storage;
#[cfg(feature = "python")]
pub mod python;
pub use distance::Distance;
pub use e8::{E8Codec, HadamardTransform};
pub use error::{EmbedVecError, Result};
pub use filter::FilterExpr;
pub use h4::{H4Codec, hadamard4_inplace};
pub use hnsw::HnswIndex;
pub use metadata::Metadata;
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
pub use persistence::{BackendConfig, BackendType, PersistenceBackend};
pub use quantization::Quantization;
pub use storage::VectorStorage;
use ordered_float::OrderedFloat;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Hit {
pub id: usize,
pub score: f32,
pub payload: Metadata,
}
impl Hit {
pub fn new(id: usize, score: f32, payload: Metadata) -> Self {
Self { id, score, payload }
}
}
#[derive(Debug, Clone)]
pub struct EmbedVecBuilder {
dimension: usize,
distance: Distance,
m: usize,
ef_construction: usize,
quantization: Quantization,
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
persistence_config: Option<persistence::BackendConfig>,
}
impl EmbedVecBuilder {
pub fn new(dimension: usize) -> Self {
Self {
dimension,
distance: Distance::Cosine,
m: 16,
ef_construction: 200,
quantization: Quantization::None,
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
persistence_config: None,
}
}
pub fn dimension(mut self, dim: usize) -> Self {
self.dimension = dim;
self
}
pub fn metric(mut self, distance: Distance) -> Self {
self.distance = distance;
self
}
pub fn m(mut self, m: usize) -> Self {
self.m = m;
self
}
pub fn ef_construction(mut self, ef: usize) -> Self {
self.ef_construction = ef;
self
}
pub fn quantization(mut self, quant: Quantization) -> Self {
self.quantization = quant;
self
}
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
pub fn persistence(mut self, path: impl Into<String>) -> Self {
self.persistence_config = Some(persistence::BackendConfig::new(path));
self
}
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
pub fn persistence_config(mut self, config: persistence::BackendConfig) -> Self {
self.persistence_config = Some(config);
self
}
#[cfg(feature = "async")]
pub async fn build(self) -> Result<EmbedVec> {
EmbedVec::from_builder(self).await
}
#[cfg(not(feature = "async"))]
pub fn build(self) -> Result<EmbedVec> {
EmbedVec::new_internal(
self.dimension,
self.distance,
self.m,
self.ef_construction,
self.quantization,
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
self.persistence_config,
)
}
}
pub struct EmbedVec {
dimension: usize,
distance: Distance,
pub index: Arc<RwLock<HnswIndex>>,
pub storage: Arc<RwLock<VectorStorage>>,
pub metadata: Arc<RwLock<Vec<Metadata>>>,
quantization: Quantization,
e8_codec: Option<E8Codec>,
h4_codec: Option<H4Codec>,
deleted: Arc<RwLock<HashSet<usize>>>,
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
backend: Option<Box<dyn persistence::PersistenceBackend>>,
}
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
const PERSIST_HEADER_KEY: &[u8] = b"__embedvec_header__";
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
const PERSIST_REC_PREFIX: &[u8] = b"rec:";
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
const PERSIST_VERSION: u32 = 1;
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
#[derive(Serialize, Deserialize)]
struct PersistHeader {
version: u32,
dimension: usize,
distance: Distance,
quantization: Quantization,
m: usize,
ef_construction: usize,
#[serde(default)]
high_water_mark: usize,
}
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
#[derive(Serialize, Deserialize)]
struct PersistedRecord {
stored: storage::StoredVector,
meta: Metadata,
}
impl EmbedVec {
#[cfg(all(feature = "async", any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb")))]
pub async fn new(
dim: usize,
distance: Distance,
m: usize,
ef_construction: usize,
) -> Result<Self> {
Self::new_internal(dim, distance, m, ef_construction, Quantization::None, None)
}
#[cfg(all(feature = "async", not(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))))]
pub async fn new(
dim: usize,
distance: Distance,
m: usize,
ef_construction: usize,
) -> Result<Self> {
Self::new_internal(dim, distance, m, ef_construction, Quantization::None)
}
#[cfg(all(feature = "async", any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb")))]
pub async fn with_persistence(
path: impl AsRef<std::path::Path>,
dim: usize,
distance: Distance,
m: usize,
ef_construction: usize,
) -> Result<Self> {
let path_str = path.as_ref().to_string_lossy().to_string();
let config = persistence::BackendConfig::new(path_str);
Self::new_internal(
dim,
distance,
m,
ef_construction,
Quantization::None,
Some(config),
)
}
#[cfg(all(feature = "async", any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb")))]
pub async fn with_backend(
config: persistence::BackendConfig,
dim: usize,
distance: Distance,
m: usize,
ef_construction: usize,
) -> Result<Self> {
Self::new_internal(
dim,
distance,
m,
ef_construction,
Quantization::None,
Some(config),
)
}
#[cfg(feature = "async")]
async fn from_builder(builder: EmbedVecBuilder) -> Result<Self> {
Self::new_internal(
builder.dimension,
builder.distance,
builder.m,
builder.ef_construction,
builder.quantization,
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
builder.persistence_config,
)
}
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
pub fn new_internal(
dim: usize,
distance: Distance,
m: usize,
ef_construction: usize,
quantization: Quantization,
persistence_config: Option<persistence::BackendConfig>,
) -> Result<Self> {
if dim == 0 {
return Err(EmbedVecError::InvalidDimension(dim));
}
let mut index = HnswIndex::new(m, ef_construction, distance);
let storage = VectorStorage::new(dim, quantization.clone());
let e8_codec = match &quantization {
Quantization::E8 { bits_per_block, use_hadamard, random_seed } =>
Some(E8Codec::new(dim, *bits_per_block, *use_hadamard, *random_seed)),
_ => None,
};
let h4_codec = match &quantization {
Quantization::H4 { use_hadamard, random_seed } =>
Some(H4Codec::new(dim, *use_hadamard, *random_seed)),
_ => None,
};
index.set_h4_codec(h4_codec.clone());
let backend = if let Some(config) = persistence_config {
Some(persistence::create_backend(&config)?)
} else {
None
};
let mut db = Self {
dimension: dim,
distance,
index: Arc::new(RwLock::new(index)),
storage: Arc::new(RwLock::new(storage)),
metadata: Arc::new(RwLock::new(Vec::new())),
quantization,
e8_codec,
h4_codec,
deleted: Arc::new(RwLock::new(HashSet::new())),
backend,
};
db.load_from_backend()?;
Ok(db)
}
#[cfg(not(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb")))]
pub fn new_internal(
dim: usize,
distance: Distance,
m: usize,
ef_construction: usize,
quantization: Quantization,
) -> Result<Self> {
if dim == 0 {
return Err(EmbedVecError::InvalidDimension(dim));
}
let mut index = HnswIndex::new(m, ef_construction, distance);
let storage = VectorStorage::new(dim, quantization.clone());
let e8_codec = match &quantization {
Quantization::E8 { bits_per_block, use_hadamard, random_seed } =>
Some(E8Codec::new(dim, *bits_per_block, *use_hadamard, *random_seed)),
_ => None,
};
let h4_codec = match &quantization {
Quantization::H4 { use_hadamard, random_seed } =>
Some(H4Codec::new(dim, *use_hadamard, *random_seed)),
_ => None,
};
index.set_h4_codec(h4_codec.clone());
Ok(Self {
dimension: dim,
distance,
index: Arc::new(RwLock::new(index)),
storage: Arc::new(RwLock::new(storage)),
metadata: Arc::new(RwLock::new(Vec::new())),
quantization,
e8_codec,
h4_codec,
deleted: Arc::new(RwLock::new(HashSet::new())),
})
}
pub fn builder() -> EmbedVecBuilder {
EmbedVecBuilder::new(768) }
#[cfg(feature = "async")]
pub async fn add(&mut self, vector: &[f32], payload: impl Into<Metadata>) -> Result<usize> {
self.add_internal(vector, payload.into())
}
#[cfg(feature = "async")]
pub async fn add_many(
&mut self,
vectors: &[Vec<f32>],
payloads: Vec<impl Into<Metadata>>,
) -> Result<()> {
if vectors.len() != payloads.len() {
return Err(EmbedVecError::MismatchedLengths {
vectors: vectors.len(),
payloads: payloads.len(),
});
}
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
let mut new_ids: Vec<usize> = Vec::with_capacity(vectors.len());
for (vector, payload) in vectors.iter().zip(payloads.into_iter()) {
let _id = self.insert_in_memory(vector, payload.into())?;
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
new_ids.push(_id);
}
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
self.persist_records(&new_ids)?;
Ok(())
}
pub fn add_internal(&mut self, vector: &[f32], payload: Metadata) -> Result<usize> {
let id = self.insert_in_memory(vector, payload)?;
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
self.persist_record(id)?;
Ok(id)
}
fn insert_in_memory(&mut self, vector: &[f32], payload: Metadata) -> Result<usize> {
if vector.len() != self.dimension {
return Err(EmbedVecError::DimensionMismatch {
expected: self.dimension,
got: vector.len(),
});
}
let processed_vector = if self.distance == Distance::Cosine {
normalize_vector(vector)
} else {
vector.to_vec()
};
let id = {
let mut storage = self.storage.write();
storage.add(&processed_vector, self.e8_codec.as_ref(), self.h4_codec.as_ref())?
};
{
let mut meta = self.metadata.write();
if id >= meta.len() {
meta.resize(id + 1, Metadata::default());
}
meta[id] = payload;
}
{
let mut index = self.index.write();
let storage = self.storage.read();
index.insert(id, &processed_vector, &storage, self.e8_codec.as_ref())?;
}
Ok(id)
}
#[cfg(feature = "async")]
pub async fn delete(&mut self, id: usize) -> Result<bool> {
self.delete_internal(id)
}
#[cfg(feature = "async")]
pub async fn delete_many(&mut self, ids: &[usize]) -> Result<usize> {
let mut removed = 0;
for &id in ids {
if self.delete_internal(id)? {
removed += 1;
}
}
Ok(removed)
}
pub fn delete_internal(&mut self, id: usize) -> Result<bool> {
if id >= self.storage.read().len() {
return Ok(false);
}
if !self.deleted.write().insert(id) {
return Ok(false);
}
{
let mut meta = self.metadata.write();
if let Some(slot) = meta.get_mut(id) {
*slot = Metadata::default();
}
}
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
{
if let Some(backend) = &self.backend {
backend.delete(&Self::rec_key(id))?;
}
}
Ok(true)
}
#[cfg(feature = "async")]
pub async fn search(
&self,
query: &[f32],
k: usize,
ef_search: usize,
filter: Option<FilterExpr>,
) -> Result<Vec<Hit>> {
self.search_internal(query, k, ef_search, filter)
}
pub fn search_internal(
&self,
query: &[f32],
k: usize,
ef_search: usize,
filter: Option<FilterExpr>,
) -> Result<Vec<Hit>> {
if query.len() != self.dimension {
return Err(EmbedVecError::DimensionMismatch {
expected: self.dimension,
got: query.len(),
});
}
let processed_query = if self.distance == Distance::Cosine {
normalize_vector(query)
} else {
query.to_vec()
};
let fetch_k = ef_search.max(k);
let candidates = {
let index = self.index.read();
let storage = self.storage.read();
index.search(
&processed_query,
fetch_k,
ef_search,
&storage,
self.e8_codec.as_ref(),
)?
};
let metadata = self.metadata.read();
let deleted = self.deleted.read();
let mut results: Vec<Hit> = candidates
.into_iter()
.filter_map(|(id, score)| {
if deleted.contains(&id) {
return None;
}
let payload = metadata.get(id)?.clone();
if let Some(ref f) = filter {
if !f.matches(&payload) {
return None;
}
}
Some(Hit::new(id, score, payload))
})
.take(k)
.collect();
results.sort_by_key(|h| OrderedFloat(h.score));
Ok(results)
}
#[cfg(feature = "async")]
pub async fn search_many(
&self,
queries: &[Vec<f32>],
k: usize,
ef_search: usize,
filter: Option<FilterExpr>,
) -> Result<Vec<Vec<Hit>>> {
self.search_many_internal(queries, k, ef_search, filter)
}
pub fn search_many_internal(
&self,
queries: &[Vec<f32>],
k: usize,
ef_search: usize,
filter: Option<FilterExpr>,
) -> Result<Vec<Vec<Hit>>> {
use rayon::prelude::*;
queries
.par_iter()
.map(|q| self.search_internal(q, k, ef_search, filter.clone()))
.collect()
}
pub fn payload(&self, id: usize) -> Option<Metadata> {
if self.deleted.read().contains(&id) {
return None;
}
self.metadata.read().get(id).cloned()
}
pub fn contains_id(&self, id: usize) -> bool {
id < self.storage.read().len() && !self.deleted.read().contains(&id)
}
pub fn entries(&self) -> Vec<(usize, Metadata)> {
let metadata = self.metadata.read();
let deleted = self.deleted.read();
metadata
.iter()
.enumerate()
.filter(|(id, _)| !deleted.contains(id))
.map(|(id, m)| (id, m.clone()))
.collect()
}
pub fn live_count(&self) -> usize {
self.storage
.read()
.len()
.saturating_sub(self.deleted.read().len())
}
#[cfg(feature = "async")]
pub async fn len(&self) -> usize {
self.live_count()
}
#[cfg(feature = "async")]
pub async fn is_empty(&self) -> bool {
self.len().await == 0
}
#[cfg(feature = "async")]
pub async fn clear(&mut self) -> Result<()> {
self.clear_sync()
}
pub fn clear_sync(&mut self) -> Result<()> {
{
let mut storage = self.storage.write();
storage.clear();
}
{
let mut metadata = self.metadata.write();
metadata.clear();
}
{
let mut index = self.index.write();
index.clear();
}
{
let mut deleted = self.deleted.write();
deleted.clear();
}
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
self.clear_backend()?;
Ok(())
}
#[cfg(all(feature = "async", any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb")))]
pub async fn flush(&mut self) -> Result<()> {
self.persist_header()?;
if let Some(ref backend) = self.backend {
backend.flush()?;
}
Ok(())
}
pub fn quantization(&self) -> &Quantization {
&self.quantization
}
#[cfg(feature = "async")]
pub async fn set_quantization(&mut self, quant: Quantization) -> Result<()> {
self.e8_codec = match &quant {
Quantization::E8 { bits_per_block, use_hadamard, random_seed } =>
Some(E8Codec::new(self.dimension, *bits_per_block, *use_hadamard, *random_seed)),
_ => None,
};
self.h4_codec = match &quant {
Quantization::H4 { use_hadamard, random_seed } =>
Some(H4Codec::new(self.dimension, *use_hadamard, *random_seed)),
_ => None,
};
self.quantization = quant.clone();
self.index.write().set_h4_codec(self.h4_codec.clone());
{
let mut storage = self.storage.write();
storage.set_quantization(quant, self.e8_codec.as_ref(), self.h4_codec.as_ref())?;
}
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
self.persist_all()?;
Ok(())
}
pub fn dimension(&self) -> usize {
self.dimension
}
pub fn distance(&self) -> Distance {
self.distance
}
}
#[cfg(any(feature = "persistence-fjall", feature = "persistence-sled", feature = "persistence-rocksdb"))]
impl EmbedVec {
fn rec_key(id: usize) -> Vec<u8> {
format!("rec:{:020}", id).into_bytes()
}
fn encode_record(&self, id: usize) -> Result<Vec<u8>> {
let stored = self
.storage
.read()
.get_stored(id)
.cloned()
.ok_or(EmbedVecError::VectorNotFound(id))?;
let meta = self.metadata.read().get(id).cloned().unwrap_or_default();
let record = PersistedRecord { stored, meta };
serde_json::to_vec(&record)
.map_err(|e| EmbedVecError::SerializationError(e.to_string()))
}
fn persist_header(&self) -> Result<()> {
let backend = match &self.backend {
Some(b) => b,
None => return Ok(()),
};
let header = PersistHeader {
version: PERSIST_VERSION,
dimension: self.dimension,
distance: self.distance,
quantization: self.quantization.clone(),
m: self.index.read().m(),
ef_construction: self.index.read().ef_construction(),
high_water_mark: self.storage.read().len(),
};
let bytes = serde_json::to_vec(&header)
.map_err(|e| EmbedVecError::SerializationError(e.to_string()))?;
backend.set(PERSIST_HEADER_KEY, &bytes)
}
fn persist_record(&self, id: usize) -> Result<()> {
let backend = match &self.backend {
Some(b) => b,
None => return Ok(()),
};
let value = self.encode_record(id)?;
backend.set(&Self::rec_key(id), &value)
}
fn persist_records(&self, ids: &[usize]) -> Result<()> {
let backend = match &self.backend {
Some(b) => b,
None => return Ok(()),
};
if ids.is_empty() {
return Ok(());
}
let mut batch: Vec<(Vec<u8>, Vec<u8>)> = Vec::with_capacity(ids.len());
for &id in ids {
batch.push((Self::rec_key(id), self.encode_record(id)?));
}
backend.set_batch(&batch)
}
fn persist_all(&self) -> Result<()> {
if self.backend.is_none() {
return Ok(());
}
self.persist_header()?;
let n = self.storage.read().len();
let ids: Vec<usize> = (0..n).collect();
self.persist_records(&ids)
}
fn clear_backend(&self) -> Result<()> {
let backend = match &self.backend {
Some(b) => b,
None => return Ok(()),
};
let keys: Vec<Vec<u8>> = backend
.scan_prefix(PERSIST_REC_PREFIX)?
.into_iter()
.map(|(k, _)| k)
.collect();
for key in keys {
backend.delete(&key)?;
}
Ok(())
}
fn load_from_backend(&mut self) -> Result<()> {
let header_bytes = match &self.backend {
Some(backend) => backend.get(PERSIST_HEADER_KEY)?,
None => return Ok(()),
};
let header: PersistHeader = match header_bytes {
Some(bytes) => serde_json::from_slice(&bytes)
.map_err(|e| EmbedVecError::SerializationError(e.to_string()))?,
None => return self.persist_header(),
};
if header.dimension != self.dimension {
return Err(EmbedVecError::DimensionMismatch {
expected: header.dimension,
got: self.dimension,
});
}
self.distance = header.distance;
self.quantization = header.quantization.clone();
self.e8_codec = match &self.quantization {
Quantization::E8 { bits_per_block, use_hadamard, random_seed } =>
Some(E8Codec::new(self.dimension, *bits_per_block, *use_hadamard, *random_seed)),
_ => None,
};
self.h4_codec = match &self.quantization {
Quantization::H4 { use_hadamard, random_seed } =>
Some(H4Codec::new(self.dimension, *use_hadamard, *random_seed)),
_ => None,
};
*self.storage.write() = VectorStorage::new(self.dimension, self.quantization.clone());
let mut rebuilt = HnswIndex::new(header.m, header.ef_construction, self.distance);
rebuilt.set_h4_codec(self.h4_codec.clone());
*self.index.write() = rebuilt;
self.metadata.write().clear();
self.deleted.write().clear();
let records = match &self.backend {
Some(backend) => backend.scan_prefix(PERSIST_REC_PREFIX)?,
None => return Ok(()),
};
let mut deleted_ids: HashSet<usize> = HashSet::new();
{
let mut storage = self.storage.write();
let mut metadata = self.metadata.write();
let mut index = self.index.write();
let mut next_id = 0usize;
for (key, value) in records {
let id: usize = std::str::from_utf8(&key[PERSIST_REC_PREFIX.len()..])
.ok()
.and_then(|s| s.parse().ok())
.ok_or_else(|| {
EmbedVecError::PersistenceError(format!(
"invalid record key: {}",
String::from_utf8_lossy(&key)
))
})?;
while next_id < id {
storage.push_stored(crate::storage::StoredVector::Tombstone);
metadata.push(Metadata::default());
deleted_ids.insert(next_id);
next_id += 1;
}
let record: PersistedRecord = serde_json::from_slice(&value)
.map_err(|e| EmbedVecError::SerializationError(e.to_string()))?;
let decoded = record
.stored
.to_f32(self.e8_codec.as_ref(), self.h4_codec.as_ref());
storage.push_stored(record.stored);
metadata.push(record.meta);
index.insert(id, &decoded, &*storage, self.e8_codec.as_ref())?;
next_id += 1;
}
while next_id < header.high_water_mark {
storage.push_stored(crate::storage::StoredVector::Tombstone);
metadata.push(Metadata::default());
deleted_ids.insert(next_id);
next_id += 1;
}
}
*self.deleted.write() = deleted_ids;
Ok(())
}
}
fn normalize_vector(v: &[f32]) -> Vec<f32> {
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 1e-10 {
v.iter().map(|x| x / norm).collect()
} else {
v.to_vec()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_basic_operations() {
let mut db = EmbedVec::new(4, Distance::Cosine, 16, 100).await.unwrap();
let id = db
.add(&[1.0, 0.0, 0.0, 0.0], serde_json::json!({"test": "value"}))
.await
.unwrap();
assert_eq!(id, 0);
let results = db.search(&[1.0, 0.0, 0.0, 0.0], 1, 50, None).await.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, 0);
}
#[tokio::test]
async fn test_dimension_mismatch() {
let mut db = EmbedVec::new(4, Distance::Cosine, 16, 100).await.unwrap();
let result = db
.add(&[1.0, 0.0, 0.0], serde_json::json!({}))
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_h4_search_recall_multi() {
let dim = 128;
let n = 16;
let mut db = EmbedVec::builder()
.dimension(dim)
.metric(Distance::Cosine)
.quantization(Quantization::h4_default())
.build()
.await
.unwrap();
let mut seed = 0x1234_5678u64;
let mut rng = || {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
(seed as f32 / u64::MAX as f32) * 2.0 - 1.0
};
let mut vectors = Vec::new();
for i in 0..n {
let v: Vec<f32> = (0..dim).map(|_| rng()).collect();
db.add(&v, serde_json::json!({ "i": i })).await.unwrap();
vectors.push(v);
}
let mut self_hits = 0;
for (i, v) in vectors.iter().enumerate() {
let hits = db.search(v, 1, 64, None).await.unwrap();
if hits[0].id == i {
self_hits += 1;
}
}
assert!(
self_hits >= n - 1,
"H4 self-recall too low: {self_hits}/{n} (index not decoding H4?)"
);
let h0 = db.search(&vectors[0], 1, 64, None).await.unwrap()[0].id;
let h_last = db.search(&vectors[n - 1], 1, 64, None).await.unwrap()[0].id;
assert_ne!(h0, h_last, "distinct H4 queries returned the same top hit");
}
#[tokio::test]
async fn test_search_many_and_entries() {
let mut db = EmbedVec::new(4, Distance::Cosine, 16, 100).await.unwrap();
let a = db.add(&[1.0, 0.0, 0.0, 0.0], serde_json::json!({"doc": "a"})).await.unwrap();
let b = db.add(&[0.0, 1.0, 0.0, 0.0], serde_json::json!({"doc": "b"})).await.unwrap();
let _c = db.add(&[0.0, 0.0, 1.0, 0.0], serde_json::json!({"doc": "c"})).await.unwrap();
let queries = vec![vec![1.0, 0.0, 0.0, 0.0], vec![0.0, 1.0, 0.0, 0.0]];
let results = db.search_many(&queries, 1, 50, None).await.unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0][0].id, a);
assert_eq!(results[1][0].id, b);
let entries = db.entries();
assert_eq!(entries.len(), 3);
assert_eq!(db.payload(a).unwrap()["doc"], "a");
assert!(db.contains_id(b));
db.delete(b).await.unwrap();
assert_eq!(db.entries().len(), 2);
assert!(db.payload(b).is_none());
assert!(!db.contains_id(b));
}
#[tokio::test]
async fn test_delete_in_memory() {
let mut db = EmbedVec::new(4, Distance::Cosine, 16, 100).await.unwrap();
let a = db.add(&[1.0, 0.0, 0.0, 0.0], serde_json::json!({"v": "a"})).await.unwrap();
let b = db.add(&[0.0, 1.0, 0.0, 0.0], serde_json::json!({"v": "b"})).await.unwrap();
let c = db.add(&[0.0, 0.0, 1.0, 0.0], serde_json::json!({"v": "c"})).await.unwrap();
assert_eq!(db.len().await, 3);
assert!(db.delete(b).await.unwrap());
assert_eq!(db.len().await, 2);
assert!(!db.delete(b).await.unwrap());
assert!(!db.delete(999).await.unwrap());
let hits = db.search(&[0.0, 1.0, 0.0, 0.0], 3, 50, None).await.unwrap();
assert!(hits.iter().all(|h| h.id != b));
assert_eq!(db.search(&[1.0, 0.0, 0.0, 0.0], 1, 50, None).await.unwrap()[0].id, a);
assert_eq!(db.search(&[0.0, 0.0, 1.0, 0.0], 1, 50, None).await.unwrap()[0].id, c);
let d = db.add(&[0.0, 0.0, 0.0, 1.0], serde_json::json!({"v": "d"})).await.unwrap();
assert_eq!(d, 3);
assert_ne!(d, b);
assert_eq!(db.len().await, 3);
}
#[tokio::test]
async fn test_h4_quantization_end_to_end() {
let mut db = EmbedVec::builder()
.dimension(8)
.metric(Distance::Cosine)
.quantization(Quantization::h4_default())
.build()
.await
.unwrap();
let id = db
.add(&[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], serde_json::json!({"lattice": "h4"}))
.await
.unwrap();
assert_eq!(id, 0);
let results = db
.search(&[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 1, 50, None)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, 0);
}
}