use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use usearch::{Index, IndexOptions, MetricKind, ScalarKind};
#[derive(Debug, Serialize, Deserialize)]
struct StoreKeyMap {
id_to_key: HashMap<String, u64>,
next_key: u64,
dim: usize,
}
const INITIAL_CAPACITY: usize = 64;
const DEFAULT_HNSW_MAX_ELEMENTS: usize = 1_000_000;
fn hnsw_max_elements() -> usize {
std::env::var("TRUSTY_MAX_CHUNKS")
.ok()
.and_then(|v| v.parse().ok())
.filter(|&n: &usize| n > 0)
.unwrap_or(DEFAULT_HNSW_MAX_ELEMENTS)
}
fn validate_embedding(v: &[f32]) -> std::result::Result<(), &'static str> {
let mut sum_sq = 0.0f32;
for &x in v {
if !x.is_finite() {
return Err("contains a non-finite component (NaN or infinity)");
}
sum_sq += x * x;
}
if sum_sq < 1e-12 {
return Err("is an all-zero (degenerate) vector");
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct VectorHit {
pub chunk_id: String,
pub score: f32,
}
#[async_trait]
#[allow(clippy::len_without_is_empty)]
pub trait VectorStore: Send + Sync {
async fn upsert(&self, id: &str, embedding: Vec<f32>) -> Result<()>;
async fn search(&self, query: &[f32], top_k: usize) -> Result<Vec<VectorHit>>;
async fn remove(&self, id: &str) -> Result<()>;
async fn len(&self) -> Result<usize>;
async fn upsert_batch(&self, items: &[(String, Vec<f32>)]) -> Result<()> {
for (id, vec) in items {
self.upsert(id, vec.clone()).await?;
}
Ok(())
}
async fn save_to(&self, _path: &Path) -> Result<()> {
Ok(())
}
async fn rewrite_keys_to_relative(&self, _root_path: &Path) -> Result<usize> {
Ok(0)
}
}
pub struct UsearchStore {
index: Arc<RwLock<Index>>,
id_to_key: Arc<RwLock<HashMap<String, u64>>>,
key_to_id: Arc<RwLock<HashMap<u64, String>>>,
next_key: Arc<AtomicU64>,
dim: usize,
is_view: Arc<AtomicBool>,
hnsw_path: Arc<RwLock<Option<PathBuf>>>,
}
impl UsearchStore {
pub fn new(dim: usize) -> Result<Self> {
Self::with_capacity_hint(dim, INITIAL_CAPACITY)
}
pub fn with_capacity_hint(dim: usize, expected_chunks: usize) -> Result<Self> {
let (connectivity, expansion_add, expansion_search) = if expected_chunks > 50_000 {
(32, 128, 64)
} else {
(0, 0, 0)
};
let options = IndexOptions {
dimensions: dim,
metric: MetricKind::Cos,
quantization: ScalarKind::F32,
connectivity,
expansion_add,
expansion_search,
multi: false,
};
let index = Index::new(&options).map_err(|e| anyhow!("usearch Index::new failed: {e}"))?;
let initial = expected_chunks
.max(INITIAL_CAPACITY)
.min(hnsw_max_elements());
index
.reserve(initial)
.map_err(|e| anyhow!("usearch reserve failed: {e}"))?;
Ok(Self {
index: Arc::new(RwLock::new(index)),
id_to_key: Arc::new(RwLock::new(HashMap::new())),
key_to_id: Arc::new(RwLock::new(HashMap::new())),
next_key: Arc::new(AtomicU64::new(1)), dim,
is_view: Arc::new(AtomicBool::new(false)),
hnsw_path: Arc::new(RwLock::new(None)),
})
}
pub fn dim(&self) -> usize {
self.dim
}
pub async fn save(&self, hnsw_path: &Path) -> Result<()> {
if self.is_view.load(Ordering::Acquire) {
let same_path = {
let guard = self.hnsw_path.read().await;
guard.as_deref() == Some(hnsw_path)
};
if same_path {
tracing::debug!(
"usearch: skipping save for {} — index is in view mode, snapshot is clean",
hnsw_path.display()
);
return Ok(());
}
self.ensure_mutable().await?;
}
let key_map = {
let id_to_key = self.id_to_key.read().await;
StoreKeyMap {
id_to_key: id_to_key.clone(),
next_key: self.next_key.load(Ordering::Relaxed),
dim: self.dim,
}
};
if let Some(parent) = hnsw_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| anyhow!("create parent of {}: {e}", hnsw_path.display()))?;
}
let tmp_hnsw = hnsw_path.with_extension("usearch.tmp");
let tmp_hnsw_str = tmp_hnsw
.to_str()
.ok_or_else(|| anyhow!("non-utf8 path: {}", tmp_hnsw.display()))?;
{
let index = self.index.write().await;
index
.save(tmp_hnsw_str)
.map_err(|e| anyhow!("usearch save failed: {e}"))?;
}
std::fs::rename(&tmp_hnsw, hnsw_path).map_err(|e| anyhow!("rename hnsw snapshot: {e}"))?;
let sidecar = hnsw_path.with_extension("keys.json");
let sidecar_tmp = sidecar.with_extension("json.tmp");
let json =
serde_json::to_vec(&key_map).map_err(|e| anyhow!("serialize hnsw key map: {e}"))?;
std::fs::write(&sidecar_tmp, &json)
.map_err(|e| anyhow!("write hnsw key sidecar tmp: {e}"))?;
std::fs::rename(&sidecar_tmp, &sidecar)
.map_err(|e| anyhow!("rename hnsw key sidecar: {e}"))?;
Ok(())
}
pub async fn load_from(hnsw_path: &Path) -> Result<Option<Self>> {
let sidecar = hnsw_path.with_extension("keys.json");
if !hnsw_path.exists() || !sidecar.exists() {
return Ok(None);
}
let json = match std::fs::read(&sidecar) {
Ok(b) => b,
Err(e) => {
tracing::warn!(
"could not read hnsw key sidecar {}: {e} — discarding snapshot",
sidecar.display()
);
return Ok(None);
}
};
let key_map: StoreKeyMap = match serde_json::from_slice(&json) {
Ok(m) => m,
Err(e) => {
tracing::warn!(
"hnsw key sidecar {} is corrupt ({e}) — discarding snapshot",
sidecar.display()
);
return Ok(None);
}
};
let expected_chunks = key_map.id_to_key.len();
let store = Self::with_capacity_hint(key_map.dim, expected_chunks)?;
let hnsw_str = match hnsw_path.to_str() {
Some(s) => s,
None => {
tracing::warn!(
"non-utf8 hnsw path {} — discarding snapshot",
hnsw_path.display()
);
return Ok(None);
}
};
{
let index = store.index.write().await;
if let Err(e) = index.view(hnsw_str) {
tracing::warn!(
"usearch failed to view {} ({e}) — discarding snapshot",
hnsw_path.display()
);
return Ok(None);
}
}
store.is_view.store(true, Ordering::Release);
*store.hnsw_path.write().await = Some(hnsw_path.to_path_buf());
{
let mut id_map = store.id_to_key.write().await;
let mut key_map_rev = store.key_to_id.write().await;
for (id, key) in &key_map.id_to_key {
id_map.insert(id.clone(), *key);
key_map_rev.insert(*key, id.clone());
}
}
store
.next_key
.store(key_map.next_key.max(1), Ordering::Relaxed);
Ok(Some(store))
}
async fn ensure_mutable(&self) -> Result<()> {
if !self.is_view.load(Ordering::Acquire) {
return Ok(());
}
self.promote_view_to_mutable().await
}
async fn promote_view_to_mutable(&self) -> Result<()> {
let path = {
let guard = self.hnsw_path.read().await;
guard.clone()
};
let path = path.ok_or_else(|| {
anyhow!("usearch index is in view mode but has no source path to promote from")
})?;
let path_str = path
.to_str()
.ok_or_else(|| anyhow!("non-utf8 hnsw path: {}", path.display()))?
.to_string();
let index = self.index.write().await;
if !self.is_view.load(Ordering::Acquire) {
return Ok(());
}
index
.load(&path_str)
.map_err(|e| anyhow!("usearch failed to promote view → mutable load: {e}"))?;
let size = index.size();
if index.capacity() < size {
index
.reserve(size.max(INITIAL_CAPACITY))
.map_err(|e| anyhow!("usearch reserve after promote failed: {e}"))?;
}
self.is_view.store(false, Ordering::Release);
tracing::info!(
"usearch: promoted view → mutable for {} ({} vectors)",
path.display(),
size
);
Ok(())
}
fn ensure_capacity(index: &Index) -> Result<()> {
let size = index.size();
let cap = index.capacity();
let max_elem = hnsw_max_elements();
if size >= max_elem {
return Err(anyhow!(
"usearch index at TRUSTY_MAX_CHUNKS cap ({} elements) — refusing further upserts",
max_elem
));
}
if size + 1 > cap {
let mut new_cap = (cap.max(1)).saturating_mul(2);
if new_cap > max_elem {
new_cap = max_elem;
}
index
.reserve(new_cap)
.map_err(|e| anyhow!("usearch reserve grow failed: {e}"))?;
}
Ok(())
}
}
#[async_trait]
impl VectorStore for UsearchStore {
async fn upsert(&self, id: &str, embedding: Vec<f32>) -> Result<()> {
if embedding.len() != self.dim {
return Err(anyhow!(
"embedding dim mismatch: got {}, expected {}",
embedding.len(),
self.dim
));
}
self.ensure_mutable().await?;
let key = {
let mut id_to_key = self.id_to_key.write().await;
if let Some(&existing) = id_to_key.get(id) {
existing
} else {
let key = self.next_key.fetch_add(1, Ordering::Relaxed);
id_to_key.insert(id.to_string(), key);
self.key_to_id.write().await.insert(key, id.to_string());
key
}
};
let index = self.index.write().await;
if index.contains(key) {
index
.remove(key)
.map_err(|e| anyhow!("usearch remove (for upsert) failed: {e}"))?;
}
Self::ensure_capacity(&index)?;
index
.add(key, &embedding)
.map_err(|e| anyhow!("usearch add failed: {e}"))?;
Ok(())
}
async fn search(&self, query: &[f32], top_k: usize) -> Result<Vec<VectorHit>> {
if query.len() != self.dim {
return Err(anyhow!(
"query dim mismatch: got {}, expected {}",
query.len(),
self.dim
));
}
if top_k == 0 {
return Ok(Vec::new());
}
let matches = {
let index = self.index.read().await;
index
.search(query, top_k)
.map_err(|e| anyhow!("usearch search failed: {e}"))?
};
let key_to_id = self.key_to_id.read().await;
let mut hits = Vec::with_capacity(matches.keys.len());
for (key, dist) in matches.keys.iter().zip(matches.distances.iter()) {
if let Some(chunk_id) = key_to_id.get(key) {
let score = 1.0 - *dist;
hits.push(VectorHit {
chunk_id: chunk_id.clone(),
score,
});
}
}
Ok(hits)
}
async fn remove(&self, id: &str) -> Result<()> {
self.ensure_mutable().await?;
let key = {
let mut id_to_key = self.id_to_key.write().await;
match id_to_key.remove(id) {
Some(k) => k,
None => return Ok(()), }
};
self.key_to_id.write().await.remove(&key);
let index = self.index.write().await;
if index.contains(key) {
index
.remove(key)
.map_err(|e| anyhow!("usearch remove failed: {e}"))?;
}
Ok(())
}
async fn len(&self) -> Result<usize> {
Ok(self.index.read().await.size())
}
async fn save_to(&self, path: &Path) -> Result<()> {
self.save(path).await
}
async fn rewrite_keys_to_relative(&self, root_path: &Path) -> Result<usize> {
let mut id_map = self.id_to_key.write().await;
let mut key_map = self.key_to_id.write().await;
let mut rewrites: Vec<(String, String, u64)> = Vec::new();
let root_prefix = root_path.to_string_lossy();
for (id, &key) in id_map.iter() {
if !std::path::Path::new(id).is_absolute() {
continue;
}
match std::path::Path::new(id.as_str()).strip_prefix(root_path) {
Ok(_) => {
let new_id = id
.strip_prefix(root_prefix.as_ref())
.map(|s| s.trim_start_matches('/').to_string())
.unwrap_or_else(|| id.clone());
rewrites.push((id.clone(), new_id, key));
}
Err(_) => {
tracing::warn!(
id = %id,
root = %root_path.display(),
"M003: HNSW key is absolute but not under root_path; skipping"
);
}
}
}
let count = rewrites.len();
for (old_id, new_id, key) in rewrites {
id_map.remove(&old_id);
id_map.insert(new_id.clone(), key);
key_map.insert(key, new_id);
}
if count > 0 {
self.is_view.store(false, Ordering::Release);
}
Ok(count)
}
async fn upsert_batch(&self, items: &[(String, Vec<f32>)]) -> Result<()> {
if items.is_empty() {
return Ok(());
}
self.ensure_mutable().await?;
for (_, v) in items {
if v.len() != self.dim {
return Err(anyhow!(
"embedding dim mismatch: got {}, expected {}",
v.len(),
self.dim
));
}
}
let resolved_keys: Vec<u64> = {
let mut id_map = self.id_to_key.write().await;
let mut key_map = self.key_to_id.write().await;
let mut out = Vec::with_capacity(items.len());
for (id, _) in items {
let key = if let Some(&k) = id_map.get(id.as_str()) {
k
} else {
let k = self.next_key.fetch_add(1, Ordering::Relaxed);
id_map.insert(id.clone(), k);
key_map.insert(k, id.clone());
k
};
out.push(key);
}
out
};
let existing: std::collections::HashSet<u64> = {
let index = self.index.read().await;
resolved_keys
.iter()
.copied()
.filter(|k| index.contains(*k))
.collect()
};
let index = self.index.write().await;
let max_elem = hnsw_max_elements();
if index.size() >= max_elem {
return Err(anyhow!(
"usearch index at TRUSTY_MAX_CHUNKS cap ({} elements) — refusing batch upsert",
max_elem
));
}
let want = index.size().saturating_add(items.len());
if want > index.capacity() {
let mut new_cap = index.capacity().max(1);
while new_cap < want {
new_cap = new_cap.saturating_mul(2);
}
if new_cap > max_elem {
new_cap = max_elem;
}
index
.reserve(new_cap)
.map_err(|e| anyhow!("usearch reserve grow failed: {e}"))?;
}
for &key in &existing {
index
.remove(key)
.map_err(|e| anyhow!("usearch remove (for upsert) failed: {e}"))?;
}
let mut failed: Vec<(String, String)> = Vec::new();
for (key, (id, embedding)) in resolved_keys.iter().zip(items.iter()) {
if let Err(reason) = validate_embedding(embedding) {
failed.push((id.clone(), format!("embedding {reason}")));
continue;
}
if let Err(e) = index.add(*key, embedding) {
failed.push((id.clone(), e.to_string()));
}
}
drop(index);
if failed.is_empty() {
return Ok(());
}
{
let failed_ids: std::collections::HashSet<&str> =
failed.iter().map(|(id, _)| id.as_str()).collect();
let mut id_map = self.id_to_key.write().await;
let mut key_map = self.key_to_id.write().await;
for (id, key) in resolved_keys
.iter()
.zip(items.iter())
.filter(|(_, (id, _))| failed_ids.contains(id.as_str()))
.map(|(key, (id, _))| (id, key))
{
if !existing.contains(key) && id_map.get(id.as_str()) == Some(key) {
id_map.remove(id.as_str());
key_map.remove(key);
}
}
}
let succeeded = items.len() - failed.len();
for (id, err) in &failed {
tracing::warn!(
"usearch upsert_batch: skipped chunk '{id}' — add failed ({err}); \
likely a NaN or zero embedding vector. The rest of the batch was indexed."
);
}
if succeeded == 0 {
return Err(anyhow!(
"usearch upsert_batch: all {} vectors failed to add — \
systemic failure, not isolated bad input (first error: {})",
items.len(),
failed.first().map(|(_, e)| e.as_str()).unwrap_or("<none>")
));
}
tracing::warn!(
"usearch upsert_batch: {succeeded}/{} vectors indexed; {} skipped due to \
add failures (see warnings above)",
items.len(),
failed.len()
);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_upsert_and_search() {
let store = UsearchStore::new(4).expect("store init");
let v = vec![1.0f32, 0.0, 0.0, 0.0];
store.upsert("chunk:a", v.clone()).await.expect("upsert a");
store
.upsert("chunk:b", vec![0.0, 1.0, 0.0, 0.0])
.await
.expect("upsert b");
store
.upsert("chunk:c", vec![0.9, 0.1, 0.0, 0.0])
.await
.expect("upsert c");
let hits = store.search(&v, 2).await.expect("search");
assert_eq!(hits.len(), 2);
assert_eq!(hits[0].chunk_id, "chunk:a");
}
#[tokio::test]
async fn test_len() {
let store = UsearchStore::new(4).expect("store init");
assert_eq!(store.len().await.unwrap(), 0);
store.upsert("x", vec![1.0, 0.0, 0.0, 0.0]).await.unwrap();
assert_eq!(store.len().await.unwrap(), 1);
}
#[tokio::test]
async fn test_remove() {
let store = UsearchStore::new(4).expect("store init");
store
.upsert("del-me", vec![1.0, 0.0, 0.0, 0.0])
.await
.unwrap();
assert_eq!(store.len().await.unwrap(), 1);
store.remove("del-me").await.unwrap();
let hits = store.search(&[1.0, 0.0, 0.0, 0.0], 5).await.unwrap();
assert!(!hits.iter().any(|h| h.chunk_id == "del-me"));
}
#[tokio::test]
async fn test_concurrent_reads() {
let store = Arc::new(UsearchStore::new(4).expect("store init"));
store.upsert("r1", vec![1.0, 0.0, 0.0, 0.0]).await.unwrap();
store.upsert("r2", vec![0.0, 1.0, 0.0, 0.0]).await.unwrap();
let s1 = store.clone();
let s2 = store.clone();
let q = vec![1.0f32, 0.0, 0.0, 0.0];
let (r1, r2) = tokio::join!(s1.search(&q, 2), s2.search(&q, 2));
assert!(!r1.unwrap().is_empty());
assert!(!r2.unwrap().is_empty());
}
#[tokio::test]
async fn test_upsert_replaces_existing() {
let store = UsearchStore::new(4).expect("store init");
store
.upsert("same", vec![1.0, 0.0, 0.0, 0.0])
.await
.unwrap();
store
.upsert("same", vec![0.0, 1.0, 0.0, 0.0])
.await
.unwrap();
assert_eq!(store.len().await.unwrap(), 1);
let hits = store.search(&[0.0, 1.0, 0.0, 0.0], 1).await.unwrap();
assert_eq!(hits[0].chunk_id, "same");
}
#[tokio::test]
async fn test_dim_mismatch_errors() {
let store = UsearchStore::new(4).expect("store init");
assert!(store.upsert("bad", vec![1.0, 0.0]).await.is_err());
assert!(store.search(&[1.0, 0.0], 1).await.is_err());
}
#[tokio::test]
async fn test_upsert_batch_inserts_all() {
let store = UsearchStore::new(4).expect("store init");
let dirs: [[f32; 4]; 4] = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
];
let items: Vec<(String, Vec<f32>)> = (0..4)
.map(|i| (format!("k{i}"), dirs[i].to_vec()))
.collect();
store.upsert_batch(&items).await.expect("batch upsert");
assert_eq!(store.len().await.unwrap(), 4);
store.upsert_batch(&items).await.expect("re-batch upsert");
assert_eq!(store.len().await.unwrap(), 4);
let hits = store.search(&dirs[2], 1).await.unwrap();
assert_eq!(hits[0].chunk_id, "k2");
}
#[tokio::test]
async fn test_upsert_batch_empty_noop() {
let store = UsearchStore::new(4).expect("store init");
store.upsert_batch(&[]).await.unwrap();
assert_eq!(store.len().await.unwrap(), 0);
}
#[tokio::test]
async fn test_upsert_batch_dim_mismatch_errors() {
let store = UsearchStore::new(4).expect("store init");
let items = vec![("bad".to_string(), vec![1.0, 0.0])];
assert!(store.upsert_batch(&items).await.is_err());
}
#[test]
fn test_validate_embedding() {
assert!(validate_embedding(&[1.0, 0.0, 0.0, 0.0]).is_ok());
assert!(validate_embedding(&[1.0, f32::NAN, 0.0, 0.0]).is_err());
assert!(validate_embedding(&[f32::INFINITY, 0.0, 0.0, 0.0]).is_err());
assert!(validate_embedding(&[0.0, 0.0, 0.0, 0.0]).is_err());
}
#[tokio::test]
async fn test_upsert_batch_isolates_bad_vector() {
let store = UsearchStore::new(4).expect("store init");
let items: Vec<(String, Vec<f32>)> = vec![
("good-a".to_string(), vec![1.0, 0.0, 0.0, 0.0]),
("nan-vec".to_string(), vec![f32::NAN, 0.0, 0.0, 0.0]),
("good-b".to_string(), vec![0.0, 1.0, 0.0, 0.0]),
("zero-vec".to_string(), vec![0.0, 0.0, 0.0, 0.0]),
("good-c".to_string(), vec![0.0, 0.0, 1.0, 0.0]),
];
store
.upsert_batch(&items)
.await
.expect("batch with isolated bad vectors must still succeed");
assert_eq!(store.len().await.unwrap(), 3);
for (id, dir) in [
("good-a", [1.0f32, 0.0, 0.0, 0.0]),
("good-b", [0.0, 1.0, 0.0, 0.0]),
("good-c", [0.0, 0.0, 1.0, 0.0]),
] {
let hits = store.search(&dir, 1).await.unwrap();
assert_eq!(hits[0].chunk_id, id, "good vector {id} must round-trip");
}
store
.upsert("nan-vec", vec![0.0, 0.0, 0.0, 1.0])
.await
.expect("a now-healthy 'nan-vec' must upsert without a key collision");
assert_eq!(store.len().await.unwrap(), 4);
}
#[tokio::test]
async fn test_upsert_batch_all_bad_vectors_errors() {
let store = UsearchStore::new(4).expect("store init");
let items: Vec<(String, Vec<f32>)> = vec![
("nan-1".to_string(), vec![f32::NAN, 0.0, 0.0, 0.0]),
("zero-2".to_string(), vec![0.0, 0.0, 0.0, 0.0]),
];
assert!(
store.upsert_batch(&items).await.is_err(),
"an all-bad batch must surface an error"
);
assert_eq!(store.len().await.unwrap(), 0);
}
#[tokio::test]
async fn test_save_load_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("hnsw.usearch");
let store = UsearchStore::new(4).unwrap();
store
.upsert("alpha", vec![1.0, 0.0, 0.0, 0.0])
.await
.unwrap();
store
.upsert("beta", vec![0.0, 1.0, 0.0, 0.0])
.await
.unwrap();
store.save(&path).await.expect("save");
assert!(path.exists(), "hnsw file must exist after save");
assert!(
path.with_extension("keys.json").exists(),
"key sidecar must exist after save"
);
drop(store);
let loaded = UsearchStore::load_from(&path)
.await
.expect("load ok")
.expect("load returned Some");
assert_eq!(loaded.len().await.unwrap(), 2);
let hits = loaded.search(&[1.0, 0.0, 0.0, 0.0], 1).await.unwrap();
assert_eq!(hits[0].chunk_id, "alpha", "restored ids must round-trip");
}
#[tokio::test]
async fn test_load_missing_returns_none() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("nope.usearch");
let loaded = UsearchStore::load_from(&path).await.unwrap();
assert!(loaded.is_none());
}
#[tokio::test]
async fn test_load_corrupt_sidecar_returns_none() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("hnsw.usearch");
let store = UsearchStore::new(4).unwrap();
store.upsert("a", vec![1.0, 0.0, 0.0, 0.0]).await.unwrap();
store.save(&path).await.unwrap();
std::fs::write(path.with_extension("keys.json"), b"not valid json").unwrap();
let loaded = UsearchStore::load_from(&path).await.unwrap();
assert!(loaded.is_none(), "corrupt sidecar must fall back to None");
}
#[tokio::test]
async fn test_view_promotes_to_mutable_on_write() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("hnsw.usearch");
let store = UsearchStore::new(4).unwrap();
store
.upsert("alpha", vec![1.0, 0.0, 0.0, 0.0])
.await
.unwrap();
store
.upsert("beta", vec![0.0, 1.0, 0.0, 0.0])
.await
.unwrap();
store.save(&path).await.expect("save");
drop(store);
let loaded = UsearchStore::load_from(&path)
.await
.expect("load ok")
.expect("load returned Some");
assert!(
loaded.is_view.load(Ordering::Acquire),
"load_from must put the store in view mode for the memory fix"
);
let hits = loaded.search(&[1.0, 0.0, 0.0, 0.0], 1).await.unwrap();
assert_eq!(hits[0].chunk_id, "alpha");
assert!(
loaded.is_view.load(Ordering::Acquire),
"search must not promote view → mutable"
);
loaded
.upsert("gamma", vec![0.0, 0.0, 1.0, 0.0])
.await
.expect("upsert after view");
assert!(
!loaded.is_view.load(Ordering::Acquire),
"first write must promote view → mutable"
);
assert_eq!(loaded.len().await.unwrap(), 3);
loaded
.upsert("delta", vec![0.0, 0.0, 0.0, 1.0])
.await
.expect("upsert after promote");
assert_eq!(loaded.len().await.unwrap(), 4);
let hits = loaded.search(&[0.0, 0.0, 1.0, 0.0], 1).await.unwrap();
assert_eq!(hits[0].chunk_id, "gamma");
}
#[tokio::test]
async fn test_view_batch_upsert_promotes() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("hnsw.usearch");
let store = UsearchStore::new(4).unwrap();
store
.upsert_batch(&[("seed".to_string(), vec![1.0, 0.0, 0.0, 0.0])])
.await
.unwrap();
store.save(&path).await.unwrap();
drop(store);
let loaded = UsearchStore::load_from(&path).await.unwrap().unwrap();
assert!(loaded.is_view.load(Ordering::Acquire));
loaded
.upsert_batch(&[("more".to_string(), vec![0.0, 1.0, 0.0, 0.0])])
.await
.expect("batch upsert after view");
assert!(!loaded.is_view.load(Ordering::Acquire));
assert_eq!(loaded.len().await.unwrap(), 2);
}
#[tokio::test]
async fn test_capacity_growth() {
let store = UsearchStore::new(4).expect("store init");
for i in 0..50 {
let v = vec![i as f32, 0.0, 0.0, 0.0];
store.upsert(&format!("k{i}"), v).await.unwrap();
}
assert_eq!(store.len().await.unwrap(), 50);
}
}