use crate::codec::{frame_value, unframe_value};
use crate::error::{storage_err, TopoError};
use crate::ids::NodeId;
use crate::quant::{cosine_q, is_zero, quantize};
use crate::slots::node_ulid;
use crate::vector_store::{read_qvec_by_slot, vector_prefix, OrderedScore};
use redb::{ReadableTable, Table, TableDefinition};
use serde::{Deserialize, Serialize};
use std::cmp::Reverse;
use std::collections::{BinaryHeap, HashMap, HashSet};
use std::rc::Rc;
pub(crate) const HNSW_META: TableDefinition<&[u8], &[u8]> = TableDefinition::new("hnsw_meta");
pub(crate) const HNSW_LINKS: TableDefinition<&[u8], &[u8]> = TableDefinition::new("hnsw_links");
pub(crate) const HNSW_META_FORMAT_V0: u8 = 0;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct HnswParams {
pub version: u32,
pub m: u32,
pub m0: u32,
pub ef_construction: u32,
pub level_cap: u8,
pub build_threshold: u64,
pub rebuild_num: u32,
pub rebuild_den: u32,
}
impl Default for HnswParams {
fn default() -> Self {
HnswParams {
version: 3,
m: 16,
m0: 32,
ef_construction: 128,
level_cap: 16,
build_threshold: 1024,
rebuild_num: 3,
rebuild_den: 10,
}
}
}
impl HnswParams {
pub(crate) fn validate(&self) -> Result<(), TopoError> {
if self.m < 2 || !self.m.is_power_of_two() {
return Err(TopoError::Rejected(format!(
"hnsw m must be a power of two >= 2, got {}",
self.m
)));
}
if self.m0 < self.m {
return Err(TopoError::Rejected("hnsw m0 must be >= m".into()));
}
if self.ef_construction < self.m {
return Err(TopoError::Rejected(
"hnsw ef_construction must be >= m".into(),
));
}
if self.rebuild_den == 0 || self.rebuild_num >= self.rebuild_den {
return Err(TopoError::Rejected(
"hnsw rebuild ratio must be a proper fraction".into(),
));
}
if self.build_threshold < 2 {
return Err(TopoError::Rejected(
"hnsw build_threshold must be >= 2".into(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct ClusterMeta {
pub format: u8,
pub built: bool,
pub entry_slot: u64,
pub entry_level: u8,
pub graph_len: u64,
pub stale: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct LinkRow {
pub tomb: bool,
pub neighbors: Vec<u64>,
}
pub(crate) fn meta_key(model: u32, scope: u32) -> [u8; 8] {
let mut k = [0u8; 8];
k[..4].copy_from_slice(&model.to_be_bytes());
k[4..].copy_from_slice(&scope.to_be_bytes());
k
}
pub(crate) fn link_prefix(model: u32, scope: u32) -> [u8; 8] {
meta_key(model, scope)
}
pub(crate) fn link_key(model: u32, scope: u32, slot: u64, level: u8) -> [u8; 17] {
let mut k = [0u8; 17];
k[..8].copy_from_slice(&meta_key(model, scope));
k[8..16].copy_from_slice(&slot.to_be_bytes());
k[16] = level;
k
}
fn splitmix64(mut x: u64) -> u64 {
x = x.wrapping_add(0x9E3779B97F4A7C15);
let mut z = x;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
z ^ (z >> 31)
}
pub(crate) fn level_for(id: NodeId, m: u32, level_cap: u8) -> u8 {
let v = id.as_u128();
let h = splitmix64(splitmix64((v >> 64) as u64) ^ (v as u64));
let bits_per_level = m.trailing_zeros(); let level = (h.leading_zeros() / bits_per_level) as u8;
level.min(level_cap)
}
pub(crate) fn read_meta(
table: &impl ReadableTable<&'static [u8], &'static [u8]>,
model: u32,
scope: u32,
) -> Result<Option<ClusterMeta>, TopoError> {
let key = meta_key(model, scope);
match table.get(key.as_slice()).map_err(storage_err)? {
None => Ok(None),
Some(value) => {
let bytes = value.value();
let meta: ClusterMeta =
postcard::from_bytes(bytes).map_err(|e| TopoError::Encoding(e.to_string()))?;
if meta.format != HNSW_META_FORMAT_V0 {
return Err(TopoError::Encoding(format!(
"unknown hnsw meta format 0x{:02X}",
meta.format
)));
}
Ok(Some(meta))
}
}
}
pub(crate) fn write_meta(
table: &mut Table<'_, &'static [u8], &'static [u8]>,
model: u32,
scope: u32,
meta: &ClusterMeta,
) -> Result<(), TopoError> {
let key = meta_key(model, scope);
let bytes = postcard::to_allocvec(meta).map_err(|e| TopoError::Encoding(e.to_string()))?;
table
.insert(key.as_slice(), bytes.as_slice())
.map_err(storage_err)?;
Ok(())
}
pub(crate) fn read_links(
table: &impl ReadableTable<&'static [u8], &'static [u8]>,
model: u32,
scope: u32,
slot: u64,
level: u8,
) -> Result<Option<LinkRow>, TopoError> {
let key = link_key(model, scope, slot, level);
match table.get(key.as_slice()).map_err(storage_err)? {
None => Ok(None),
Some(value) => {
let raw = unframe_value(value.value())?;
let row: LinkRow =
postcard::from_bytes(&raw).map_err(|e| TopoError::Encoding(e.to_string()))?;
Ok(Some(row))
}
}
}
pub(crate) fn write_links(
table: &mut Table<'_, &'static [u8], &'static [u8]>,
model: u32,
scope: u32,
slot: u64,
level: u8,
row: &LinkRow,
) -> Result<(), TopoError> {
let key = link_key(model, scope, slot, level);
let raw = postcard::to_allocvec(row).map_err(|e| TopoError::Encoding(e.to_string()))?;
let framed = frame_value(raw);
table
.insert(key.as_slice(), framed.as_slice())
.map_err(storage_err)?;
Ok(())
}
pub(crate) struct GraphReader<'a, V, R>
where
V: ReadableTable<&'static [u8], &'static [u8]>,
R: ReadableTable<&'static [u8], &'static [u8]>,
{
pub vectors: &'a V,
pub refs: &'a R,
pub model: u32,
pub scope: u32,
}
pub(crate) struct VecCache {
map: HashMap<u64, Option<Rc<Vec<i8>>>>,
}
impl VecCache {
pub(crate) fn new() -> Self {
VecCache {
map: HashMap::new(),
}
}
pub(crate) fn get<V, R>(
&mut self,
reader: &GraphReader<'_, V, R>,
slot: u64,
) -> Result<Option<Rc<Vec<i8>>>, TopoError>
where
V: ReadableTable<&'static [u8], &'static [u8]>,
R: ReadableTable<&'static [u8], &'static [u8]>,
{
if let Some(hit) = self.map.get(&slot) {
return Ok(hit.clone());
}
let resolved = match read_qvec_by_slot(reader.vectors, reader.refs, slot)? {
Some((m, s, _scale, codes)) if m == reader.model && s == reader.scope => {
Some(Rc::new(codes))
}
_ => None,
};
self.map.insert(slot, resolved.clone());
Ok(resolved)
}
}
fn search_layer<V, R>(
links: &impl ReadableTable<&'static [u8], &'static [u8]>,
reader: &GraphReader<'_, V, R>,
cache: &mut VecCache,
entry_pts: &[u64],
query: &[i8],
ef: usize,
level: u8,
) -> Result<Vec<(OrderedScore, u64)>, TopoError>
where
V: ReadableTable<&'static [u8], &'static [u8]>,
R: ReadableTable<&'static [u8], &'static [u8]>,
{
let mut visited: HashSet<u64> = HashSet::new();
let mut candidates: BinaryHeap<(OrderedScore, Reverse<u64>)> = BinaryHeap::new();
let mut results: BinaryHeap<Reverse<(OrderedScore, Reverse<u64>)>> = BinaryHeap::new();
let seed = |visited: &mut HashSet<u64>,
candidates: &mut BinaryHeap<(OrderedScore, Reverse<u64>)>,
results: &mut BinaryHeap<Reverse<(OrderedScore, Reverse<u64>)>>,
cache: &mut VecCache,
slot: u64|
-> Result<(), TopoError> {
if !visited.insert(slot) {
return Ok(());
}
let Some(row) = read_links(links, reader.model, reader.scope, slot, level)? else {
return Ok(());
};
let scoreable = match cache.get(reader, slot)? {
Some(v) => cosine_q(query, &v),
None => None,
};
match scoreable {
Some(score) => {
let os = OrderedScore(score);
candidates.push((os, Reverse(slot)));
if !row.tomb {
results.push(Reverse((os, Reverse(slot))));
if results.len() > ef.max(1) {
results.pop();
}
}
}
None => {
candidates.push((OrderedScore(f32::INFINITY), Reverse(slot)));
}
}
Ok(())
};
for &slot in entry_pts {
seed(&mut visited, &mut candidates, &mut results, cache, slot)?;
}
while let Some(&(cand_score, Reverse(cand_slot))) = candidates.peek() {
if results.len() >= ef {
if let Some(&Reverse((worst_score, _))) = results.peek() {
if cand_score < worst_score {
break;
}
}
}
candidates.pop();
let Some(row) = read_links(links, reader.model, reader.scope, cand_slot, level)? else {
continue;
};
for &nbr_slot in &row.neighbors {
seed(&mut visited, &mut candidates, &mut results, cache, nbr_slot)?;
}
}
let mut out: Vec<(OrderedScore, u64)> = results
.into_iter()
.map(|Reverse((score, Reverse(slot)))| (score, slot))
.collect();
out.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.cmp(&b.1)));
if out.len() > ef {
out.truncate(ef);
}
Ok(out)
}
fn select_neighbors<F>(
candidates: &[(OrderedScore, u64)],
max_m: usize,
mut resolve: F,
) -> Result<Vec<u64>, TopoError>
where
F: FnMut(u64) -> Result<Option<Rc<Vec<i8>>>, TopoError>,
{
let mut kept: Vec<(u64, Rc<Vec<i8>>)> = Vec::with_capacity(max_m.min(candidates.len()));
let mut pruned: Vec<u64> = Vec::new();
for &(score, slot) in candidates {
if kept.len() >= max_m {
break;
}
let Some(v) = resolve(slot)? else {
continue;
};
let diverse = kept.iter().all(|(_, kv)| match cosine_q(&v, kv) {
Some(sim_to_kept) => OrderedScore(sim_to_kept) < score,
None => true,
});
if diverse {
kept.push((slot, v));
} else {
pruned.push(slot);
}
}
let mut members: HashSet<u64> = kept.iter().map(|&(slot, _)| slot).collect();
for slot in pruned {
if members.len() >= max_m {
break;
}
members.insert(slot);
}
Ok(candidates
.iter()
.map(|&(_, slot)| slot)
.filter(|slot| members.contains(slot))
.collect())
}
fn prune_neighbor<V, R>(
links: &mut Table<'_, &'static [u8], &'static [u8]>,
reader: &GraphReader<'_, V, R>,
cache: &mut VecCache,
neighbor_slot: u64,
new_slot: u64,
level: u8,
max_m: usize,
) -> Result<(), TopoError>
where
V: ReadableTable<&'static [u8], &'static [u8]>,
R: ReadableTable<&'static [u8], &'static [u8]>,
{
let mut row =
read_links(links, reader.model, reader.scope, neighbor_slot, level)?.unwrap_or(LinkRow {
tomb: false,
neighbors: Vec::new(),
});
if !row.neighbors.contains(&new_slot) {
row.neighbors.push(new_slot);
}
if row.neighbors.len() > max_m {
match cache.get(reader, neighbor_slot)? {
Some(nv) => {
let mut scored: Vec<(OrderedScore, u64)> = Vec::with_capacity(row.neighbors.len());
for &cand in &row.neighbors {
if let Some(cv) = cache.get(reader, cand)? {
if let Some(score) = cosine_q(&nv, &cv) {
scored.push((OrderedScore(score), cand));
}
}
}
scored.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.cmp(&b.1)));
row.neighbors = select_neighbors(&scored, max_m, |s| cache.get(reader, s))?;
}
None => {
row.neighbors.sort_unstable();
row.neighbors.truncate(max_m);
}
}
}
write_links(
links,
reader.model,
reader.scope,
neighbor_slot,
level,
&row,
)
}
pub(crate) fn insert<V, R>(
links: &mut Table<'_, &'static [u8], &'static [u8]>,
meta: &mut Table<'_, &'static [u8], &'static [u8]>,
reader: &GraphReader<'_, V, R>,
params: &HnswParams,
slot: u64,
id: NodeId,
vector: &[f32],
) -> Result<(), TopoError>
where
V: ReadableTable<&'static [u8], &'static [u8]>,
R: ReadableTable<&'static [u8], &'static [u8]>,
{
let (_, qvector) = quantize(vector);
if is_zero(&qvector) {
return Ok(()); }
let mut cache = VecCache::new();
let level = level_for(id, params.m, params.level_cap);
let cur_meta = match read_meta(meta, reader.model, reader.scope)? {
None => {
for lvl in 0..=level {
write_links(
links,
reader.model,
reader.scope,
slot,
lvl,
&LinkRow {
tomb: false,
neighbors: Vec::new(),
},
)?;
}
write_meta(
meta,
reader.model,
reader.scope,
&ClusterMeta {
format: HNSW_META_FORMAT_V0,
built: true,
entry_slot: slot,
entry_level: level,
graph_len: 1,
stale: 0,
},
)?;
return Ok(());
}
Some(m) => m,
};
let entry_level = cur_meta.entry_level;
let mut entry_slot = cur_meta.entry_slot;
let mut descend_level = entry_level;
while descend_level > level {
let hits = search_layer(
links,
reader,
&mut cache,
&[entry_slot],
&qvector,
1,
descend_level,
)?;
if let Some(&(_, best)) = hits.first() {
entry_slot = best;
}
descend_level -= 1;
}
if level > entry_level {
for lvl in (entry_level + 1)..=level {
write_links(
links,
reader.model,
reader.scope,
slot,
lvl,
&LinkRow {
tomb: false,
neighbors: Vec::new(),
},
)?;
}
}
let top = level.min(entry_level);
let mut entry_pts = vec![entry_slot];
let mut cur_level = top;
loop {
let ef_c = params.ef_construction as usize;
let candidates = search_layer(
links, reader, &mut cache, &entry_pts, &qvector, ef_c, cur_level,
)?;
let max_m = if cur_level == 0 { params.m0 } else { params.m } as usize;
let selected = select_neighbors(&candidates, max_m, |s| cache.get(reader, s))?;
write_links(
links,
reader.model,
reader.scope,
slot,
cur_level,
&LinkRow {
tomb: false,
neighbors: selected.clone(),
},
)?;
for &nbr in &selected {
prune_neighbor(links, reader, &mut cache, nbr, slot, cur_level, max_m)?;
}
entry_pts = candidates.into_iter().map(|(_, s)| s).collect();
if entry_pts.is_empty() {
entry_pts = vec![entry_slot];
}
if cur_level == 0 {
break;
}
cur_level -= 1;
}
let mut new_meta = cur_meta;
new_meta.graph_len += 1;
if level > entry_level {
new_meta.entry_slot = slot;
new_meta.entry_level = level;
}
write_meta(meta, reader.model, reader.scope, &new_meta)?;
Ok(())
}
pub(crate) fn reinsert_links<V, R>(
links: &mut Table<'_, &'static [u8], &'static [u8]>,
meta: &mut Table<'_, &'static [u8], &'static [u8]>,
reader: &GraphReader<'_, V, R>,
params: &HnswParams,
slot: u64,
vector: &[f32],
) -> Result<(), TopoError>
where
V: ReadableTable<&'static [u8], &'static [u8]>,
R: ReadableTable<&'static [u8], &'static [u8]>,
{
let Some(cur_meta) = read_meta(meta, reader.model, reader.scope)? else {
return Ok(()); };
if read_links(links, reader.model, reader.scope, slot, 0)?.is_none() {
return Ok(()); }
let (_, qvector) = quantize(vector);
if is_zero(&qvector) {
tombstone(links, meta, reader.model, reader.scope, slot)?;
return Ok(());
}
let mut own_level: u8 = 0;
while read_links(links, reader.model, reader.scope, slot, own_level + 1)?.is_some() {
own_level += 1;
}
let entry_level = cur_meta.entry_level;
let mut entry_slot = cur_meta.entry_slot;
let level = own_level.min(entry_level);
let mut cache = VecCache::new();
let mut descend_level = entry_level;
while descend_level > level {
let hits = search_layer(
links,
reader,
&mut cache,
&[entry_slot],
&qvector,
1,
descend_level,
)?;
if let Some(&(_, best)) = hits.iter().find(|&&(_, s)| s != slot) {
entry_slot = best;
}
descend_level -= 1;
}
let mut entry_pts = vec![entry_slot];
let mut cur_level = level;
loop {
let ef_c = params.ef_construction as usize;
let candidates = search_layer(
links, reader, &mut cache, &entry_pts, &qvector, ef_c, cur_level,
)?;
let filtered: Vec<(OrderedScore, u64)> =
candidates.into_iter().filter(|&(_, s)| s != slot).collect();
let max_m = if cur_level == 0 { params.m0 } else { params.m } as usize;
let selected = select_neighbors(&filtered, max_m, |s| cache.get(reader, s))?;
write_links(
links,
reader.model,
reader.scope,
slot,
cur_level,
&LinkRow {
tomb: false,
neighbors: selected,
},
)?;
entry_pts = filtered.into_iter().map(|(_, s)| s).collect();
if entry_pts.is_empty() {
entry_pts = vec![entry_slot];
}
if cur_level == 0 {
break;
}
cur_level -= 1;
}
let mut new_meta = cur_meta;
new_meta.stale += 1;
write_meta(meta, reader.model, reader.scope, &new_meta)?;
Ok(())
}
pub(crate) fn tombstone(
links: &mut Table<'_, &'static [u8], &'static [u8]>,
meta: &mut Table<'_, &'static [u8], &'static [u8]>,
model: u32,
scope: u32,
slot: u64,
) -> Result<bool, TopoError> {
let Some(mut row) = read_links(links, model, scope, slot, 0)? else {
return Ok(false);
};
if row.tomb {
return Ok(false);
}
row.tomb = true;
write_links(links, model, scope, slot, 0, &row)?;
if let Some(mut m) = read_meta(meta, model, scope)? {
m.stale += 1;
write_meta(meta, model, scope, &m)?;
}
Ok(true)
}
pub(crate) fn ef_search(k: usize) -> usize {
(4 * k).max(64)
}
pub(crate) fn search<V, R>(
links: &impl ReadableTable<&'static [u8], &'static [u8]>,
meta_row: &ClusterMeta,
reader: &GraphReader<'_, V, R>,
query: &[f32],
ef: usize,
k: usize,
) -> Result<Vec<(u64, f32)>, TopoError>
where
V: ReadableTable<&'static [u8], &'static [u8]>,
R: ReadableTable<&'static [u8], &'static [u8]>,
{
let ef_eff = ef.max(k);
let (_, qquery) = quantize(query);
let mut cache = VecCache::new();
let mut entry_slot = meta_row.entry_slot;
let mut cur_level = meta_row.entry_level;
while cur_level > 0 {
let hits = search_layer(
links,
reader,
&mut cache,
&[entry_slot],
&qquery,
1,
cur_level,
)?;
if let Some(&(_, best)) = hits.first() {
entry_slot = best;
}
cur_level -= 1;
}
let hits = search_layer(links, reader, &mut cache, &[entry_slot], &qquery, ef_eff, 0)?;
Ok(hits
.into_iter()
.map(|(score, slot)| (slot, score.0))
.collect())
}
pub(crate) fn cluster_vector_count(
vectors: &impl ReadableTable<&'static [u8], &'static [u8]>,
model: u32,
scope: u32,
) -> Result<u64, TopoError> {
let prefix = vector_prefix(model, scope);
let mut start = prefix.to_vec();
start.extend_from_slice(&0u64.to_be_bytes());
let mut end = prefix.to_vec();
end.extend_from_slice(&u64::MAX.to_be_bytes());
let mut count = 0u64;
for entry in vectors
.range(start.as_slice()..=end.as_slice())
.map_err(storage_err)?
{
entry.map_err(storage_err)?;
count += 1;
}
Ok(count)
}
pub(crate) fn clusters(
vectors: &impl ReadableTable<&'static [u8], &'static [u8]>,
) -> Result<Vec<(u32, u32)>, TopoError> {
let mut out: Vec<(u32, u32)> = Vec::new();
for entry in vectors.iter().map_err(storage_err)? {
let (key_guard, _) = entry.map_err(storage_err)?;
let key = key_guard.value();
let model = u32::from_be_bytes(
key[0..4]
.try_into()
.map_err(|_| TopoError::Encoding("bad vector_key length".into()))?,
);
let scope = u32::from_be_bytes(
key[4..8]
.try_into()
.map_err(|_| TopoError::Encoding("bad vector_key length".into()))?,
);
if out.last() != Some(&(model, scope)) {
out.push((model, scope));
}
}
Ok(out)
}
#[allow(clippy::too_many_arguments)] pub(crate) fn build_cluster<V, R, VI, NI>(
links: &mut Table<'_, &'static [u8], &'static [u8]>,
meta: &mut Table<'_, &'static [u8], &'static [u8]>,
vectors_iter_source: &VI,
reader: &GraphReader<'_, V, R>,
node_ids: &NI,
params: &HnswParams,
model: u32,
scope: u32,
) -> Result<(), TopoError>
where
V: ReadableTable<&'static [u8], &'static [u8]>,
R: ReadableTable<&'static [u8], &'static [u8]>,
VI: ReadableTable<&'static [u8], &'static [u8]>,
NI: ReadableTable<&'static [u8], &'static [u8]>,
{
let prefix = link_prefix(model, scope);
let mut start = prefix.to_vec();
start.extend_from_slice(&[0u8; 9]);
let mut end = prefix.to_vec();
end.extend_from_slice(&[0xFFu8; 9]);
let mut stale_keys: Vec<Vec<u8>> = Vec::new();
for entry in links
.range(start.as_slice()..=end.as_slice())
.map_err(storage_err)?
{
let (k, _v) = entry.map_err(storage_err)?;
stale_keys.push(k.value().to_vec());
}
for k in stale_keys {
links.remove(k.as_slice()).map_err(storage_err)?;
}
meta.remove(meta_key(model, scope).as_slice())
.map_err(storage_err)?;
let vprefix = vector_prefix(model, scope);
let mut vstart = vprefix.to_vec();
vstart.extend_from_slice(&0u64.to_be_bytes());
let mut vend = vprefix.to_vec();
vend.extend_from_slice(&u64::MAX.to_be_bytes());
for entry in vectors_iter_source
.range(vstart.as_slice()..=vend.as_slice())
.map_err(storage_err)?
{
let (key_guard, value_guard) = entry.map_err(storage_err)?;
let key = key_guard.value();
let slot_bytes: [u8; 8] = key[8..16]
.try_into()
.map_err(|_| TopoError::Encoding("bad vector_key length".into()))?;
let slot = u64::from_be_bytes(slot_bytes);
let raw = unframe_value(value_guard.value())?;
let (scale, codes): (f32, Vec<i8>) =
postcard::from_bytes(&raw).map_err(|e| TopoError::Encoding(e.to_string()))?;
drop(key_guard);
drop(value_guard);
if is_zero(&codes) {
continue; }
let Some(id) = node_ulid(node_ids, slot)? else {
continue; };
let vector = crate::quant::dequantize(scale, &codes);
insert(links, meta, reader, params, slot, id, &vector)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::NodeId;
#[test]
fn level_distribution_and_pins() {
let l0 = level_for(NodeId::from_u128(10), 16, 16);
let l1 = level_for(NodeId::from_u128(11), 16, 16);
assert_eq!(l0, level_for(NodeId::from_u128(10), 16, 16));
assert_eq!(l1, level_for(NodeId::from_u128(11), 16, 16));
let mut counts = [0usize; 17];
for i in 0..4096u128 {
counts[level_for(NodeId::from_u128(i), 16, 16) as usize] += 1;
}
assert!(
counts[0] > 3500,
"level 0 should be ~15/16 of 4096, got {}",
counts[0]
);
assert!(
counts[1] > 100,
"level 1 should be ~1/16 of 4096, got {}",
counts[1]
);
for i in 0..4096u128 {
assert!(level_for(NodeId::from_u128(i), 16, 3) <= 3);
}
}
#[test]
fn keys_are_prefix_ordered() {
let p = link_prefix(7, 9);
let k = link_key(7, 9, 42, 3);
assert_eq!(&k[..8], &p[..]);
assert!(link_key(7, 9, 1, 5) < link_key(7, 9, 2, 0));
assert!(link_key(7, 9, 2, 0) < link_key(7, 9, 2, 1));
assert_eq!(meta_key(7, 9), p);
}
#[test]
fn ef_search_is_max_of_4k_and_64() {
assert_eq!(ef_search(0), 64);
assert_eq!(ef_search(1), 64);
assert_eq!(ef_search(10), 64); assert_eq!(ef_search(16), 64); assert_eq!(ef_search(17), 68); assert_eq!(ef_search(1000), 4000);
}
#[test]
fn params_roundtrip_and_validate() {
let p = HnswParams::default();
p.validate().unwrap();
let bytes = postcard::to_allocvec(&p).unwrap();
assert_eq!(postcard::from_bytes::<HnswParams>(&bytes).unwrap(), p);
assert!(
HnswParams {
m: 12,
..HnswParams::default()
}
.validate()
.is_err(),
"m must be a power of two"
);
assert!(HnswParams {
rebuild_num: 10,
rebuild_den: 10,
..HnswParams::default()
}
.validate()
.is_err());
}
#[test]
fn link_row_roundtrip_via_tables() {
let dir = tempfile::tempdir().unwrap();
let db = redb::Database::create(dir.path().join("t.redb")).unwrap();
let tx = db.begin_write().unwrap();
{
let mut links = tx.open_table(HNSW_LINKS).unwrap();
let mut meta = tx.open_table(HNSW_META).unwrap();
write_links(
&mut links,
1,
2,
3,
0,
&LinkRow {
tomb: false,
neighbors: vec![5, 9, 1],
},
)
.unwrap();
write_meta(
&mut meta,
1,
2,
&ClusterMeta {
format: HNSW_META_FORMAT_V0,
built: true,
entry_slot: 3,
entry_level: 0,
graph_len: 1,
stale: 0,
},
)
.unwrap();
assert_eq!(
read_links(&links, 1, 2, 3, 0).unwrap().unwrap().neighbors,
vec![5, 9, 1]
);
assert!(
read_links(&links, 1, 2, 4, 0).unwrap().is_none(),
"missing key is Ok(None)"
);
assert_eq!(read_meta(&meta, 1, 2).unwrap().unwrap().entry_slot, 3);
}
tx.commit().unwrap();
}
use crate::slots::{alloc_node_slot, NODE_IDS, NODE_SLOTS};
use crate::storage::META as SLOT_ALLOC_META;
use crate::vector_store::{put_vector, EMBEDDING_REF, VECTORS};
use redb::Database;
struct VecRng(u64);
impl VecRng {
fn next_f32(&mut self) -> f32 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^= z >> 31;
(z as f32 / u64::MAX as f32) * 2.0 - 1.0
}
}
fn seed_vectors(n: usize, dim: usize, seed: u64) -> Vec<Vec<f32>> {
let mut rng = VecRng(seed);
(0..n)
.map(|_| (0..dim).map(|_| rng.next_f32()).collect())
.collect()
}
fn open_db() -> (tempfile::TempDir, Database) {
let dir = tempfile::tempdir().unwrap();
let db = Database::create(dir.path().join("t.redb")).unwrap();
(dir, db)
}
fn brute_force(
entries: &[(u64, Vec<f32>)],
tombstoned: &HashSet<u64>,
query: &[f32],
k: usize,
) -> Vec<(u64, f32)> {
let (_, qquery) = quantize(query);
let mut scored: Vec<(u64, f32)> = entries
.iter()
.filter(|(slot, _)| !tombstoned.contains(slot))
.filter_map(|(slot, v)| cosine_q(&qquery, &quantize(v).1).map(|s| (*slot, s)))
.collect();
scored.sort_by(|a, b| {
OrderedScore(b.1)
.cmp(&OrderedScore(a.1))
.then_with(|| a.0.cmp(&b.0))
});
scored.truncate(k);
scored
}
fn insert_incrementally(
db: &Database,
model: u32,
scope: u32,
vectors: &[Vec<f32>],
params: &HnswParams,
) {
let tx = db.begin_write().unwrap();
{
let mut vtab = tx.open_table(VECTORS).unwrap();
let mut rtab = tx.open_table(EMBEDDING_REF).unwrap();
let mut links = tx.open_table(HNSW_LINKS).unwrap();
let mut meta = tx.open_table(HNSW_META).unwrap();
for (slot, v) in vectors.iter().enumerate() {
put_vector(&mut vtab, &mut rtab, model, scope, slot as u64, v).unwrap();
let reader = GraphReader {
vectors: &vtab,
refs: &rtab,
model,
scope,
};
insert(
&mut links,
&mut meta,
&reader,
params,
slot as u64,
NodeId::from_u128(slot as u128 + 1),
v,
)
.unwrap();
}
}
tx.commit().unwrap();
}
fn search_cluster(
db: &Database,
model: u32,
scope: u32,
query: &[f32],
ef: usize,
k: usize,
) -> Vec<(u64, f32)> {
let tx = db.begin_read().unwrap();
let links = tx.open_table(HNSW_LINKS).unwrap();
let meta_tab = tx.open_table(HNSW_META).unwrap();
let vtab = tx.open_table(VECTORS).unwrap();
let rtab = tx.open_table(EMBEDDING_REF).unwrap();
let meta_row = read_meta(&meta_tab, model, scope).unwrap().unwrap();
let reader = GraphReader {
vectors: &vtab,
refs: &rtab,
model,
scope,
};
search(&links, &meta_row, &reader, query, ef, k).unwrap()
}
fn collect_link_rows(
links: &impl ReadableTable<&'static [u8], &'static [u8]>,
model: u32,
scope: u32,
) -> Vec<(u64, u8, LinkRow)> {
let prefix = link_prefix(model, scope);
let mut start = prefix.to_vec();
start.extend_from_slice(&[0u8; 9]);
let mut end = prefix.to_vec();
end.extend_from_slice(&[0xFFu8; 9]);
let mut out = Vec::new();
for entry in links.range(start.as_slice()..=end.as_slice()).unwrap() {
let (k, v) = entry.unwrap();
let key = k.value();
let slot = u64::from_be_bytes(key[8..16].try_into().unwrap());
let level = key[16];
let raw = unframe_value(v.value()).unwrap();
let row: LinkRow = postcard::from_bytes(&raw).unwrap();
out.push((slot, level, row));
}
out
}
#[test]
fn insert_then_search_finds_exact_neighbors_when_ef_covers_all() {
let dim = 8;
let n = 64;
let model = 1;
let scope = 1;
let params = HnswParams::default();
let vectors = seed_vectors(n, dim, 0x5EED_0001);
let (_dir, db) = open_db();
insert_incrementally(&db, model, scope, &vectors, ¶ms);
let entries: Vec<(u64, Vec<f32>)> = vectors
.iter()
.enumerate()
.map(|(slot, v)| (slot as u64, v.clone()))
.collect();
let (ef, k) = (64usize, 10usize);
let queries = seed_vectors(5, dim, 0x5EED_0002);
for q in &queries {
let got = search_cluster(&db, model, scope, q, ef, k);
let want = brute_force(&entries, &HashSet::new(), q, ef.max(k));
assert_eq!(
got, want,
"ef >= n must make HNSW search exactly equal brute force"
);
}
}
#[test]
fn search_excludes_tombstones_but_routes_through_them() {
let dim = 8;
let n = 32;
let model = 1;
let scope = 1;
let params = HnswParams::default();
let vectors = seed_vectors(n, dim, 0x5EED_0003);
let (_dir, db) = open_db();
insert_incrementally(&db, model, scope, &vectors, ¶ms);
let entries: Vec<(u64, Vec<f32>)> = vectors
.iter()
.enumerate()
.map(|(slot, v)| (slot as u64, v.clone()))
.collect();
let query = seed_vectors(1, dim, 0x5EED_0004).remove(0);
let top1 = brute_force(&entries, &HashSet::new(), &query, 1);
let tombstoned_slot = top1[0].0;
let tx = db.begin_write().unwrap();
{
let mut links = tx.open_table(HNSW_LINKS).unwrap();
let mut meta = tx.open_table(HNSW_META).unwrap();
let newly = tombstone(&mut links, &mut meta, model, scope, tombstoned_slot).unwrap();
assert!(
newly,
"the slot must have been present and not already a tomb"
);
let again = tombstone(&mut links, &mut meta, model, scope, tombstoned_slot).unwrap();
assert!(!again);
}
tx.commit().unwrap();
let mut tombstoned = HashSet::new();
tombstoned.insert(tombstoned_slot);
let (ef, k) = (32usize, 10usize);
let want = brute_force(&entries, &tombstoned, &query, ef.max(k));
let got = search_cluster(&db, model, scope, &query, ef, k);
assert_eq!(
got, want,
"tombstoned slot must be excluded from results but the rest must \
still be exactly the brute-force top-k (it still routed)"
);
assert!(got.iter().all(|&(slot, _)| slot != tombstoned_slot));
}
#[test]
fn zero_norm_vectors_never_enter_the_graph() {
let dim = 8;
let model = 1;
let scope = 1;
let params = HnswParams::default();
let vectors = seed_vectors(5, dim, 0x5EED_0005);
let (_dir, db) = open_db();
insert_incrementally(&db, model, scope, &vectors, ¶ms);
let zero_slot = vectors.len() as u64;
let zero_vec = vec![0.0f32; dim];
let tx = db.begin_write().unwrap();
{
let mut vtab = tx.open_table(VECTORS).unwrap();
let mut rtab = tx.open_table(EMBEDDING_REF).unwrap();
let mut links = tx.open_table(HNSW_LINKS).unwrap();
let mut meta = tx.open_table(HNSW_META).unwrap();
let before = read_meta(&meta, model, scope).unwrap().unwrap();
put_vector(&mut vtab, &mut rtab, model, scope, zero_slot, &zero_vec).unwrap();
let reader = GraphReader {
vectors: &vtab,
refs: &rtab,
model,
scope,
};
insert(
&mut links,
&mut meta,
&reader,
¶ms,
zero_slot,
NodeId::from_u128(999),
&zero_vec,
)
.unwrap();
let after = read_meta(&meta, model, scope).unwrap().unwrap();
assert_eq!(
before, after,
"zero-norm insert must be a total no-op on meta (graph_len unchanged)"
);
assert!(
read_links(&links, model, scope, zero_slot, 0)
.unwrap()
.is_none(),
"zero-norm insert must write no link row"
);
}
tx.commit().unwrap();
let query = seed_vectors(1, dim, 0x5EED_0006).remove(0);
let got = search_cluster(&db, model, scope, &query, 10, 5);
assert!(
got.iter().all(|&(slot, _)| slot != zero_slot),
"search must never return the zero-norm slot"
);
}
#[test]
fn build_cluster_is_equivalent_to_incremental_inserts() {
let dim = 8;
let n = 48;
let model_a = 1;
let model_b = 2;
let scope = 1;
let params = HnswParams::default();
let vectors = seed_vectors(n, dim, 0x5EED_0007);
let ids: Vec<NodeId> = (0..n).map(|i| NodeId::from_u128(i as u128 + 1)).collect();
let (_dir, db) = open_db();
{
let tx = db.begin_write().unwrap();
{
let mut vtab = tx.open_table(VECTORS).unwrap();
let mut rtab = tx.open_table(EMBEDDING_REF).unwrap();
let mut links = tx.open_table(HNSW_LINKS).unwrap();
let mut meta = tx.open_table(HNSW_META).unwrap();
for (slot, v) in vectors.iter().enumerate() {
put_vector(&mut vtab, &mut rtab, model_a, scope, slot as u64, v).unwrap();
let reader = GraphReader {
vectors: &vtab,
refs: &rtab,
model: model_a,
scope,
};
insert(
&mut links,
&mut meta,
&reader,
¶ms,
slot as u64,
ids[slot],
v,
)
.unwrap();
}
}
tx.commit().unwrap();
}
{
let tx = db.begin_write().unwrap();
{
let mut vtab = tx.open_table(VECTORS).unwrap();
let mut rtab = tx.open_table(EMBEDDING_REF).unwrap();
let mut slot_meta = tx.open_table(SLOT_ALLOC_META).unwrap();
let mut node_slots = tx.open_table(NODE_SLOTS).unwrap();
let mut node_ids = tx.open_table(NODE_IDS).unwrap();
for (slot, v) in vectors.iter().enumerate() {
put_vector(&mut vtab, &mut rtab, model_b, scope, slot as u64, v).unwrap();
let alloc_slot =
alloc_node_slot(&mut slot_meta, &mut node_slots, &mut node_ids, ids[slot])
.unwrap();
assert_eq!(
alloc_slot, slot as u64,
"this test's id scheme must allocate slots in the same order as VECTORS"
);
}
let mut links = tx.open_table(HNSW_LINKS).unwrap();
let mut meta = tx.open_table(HNSW_META).unwrap();
let reader = GraphReader {
vectors: &vtab,
refs: &rtab,
model: model_b,
scope,
};
build_cluster(
&mut links, &mut meta, &vtab, &reader, &node_ids, ¶ms, model_b, scope,
)
.unwrap();
}
tx.commit().unwrap();
}
let tx = db.begin_read().unwrap();
let links = tx.open_table(HNSW_LINKS).unwrap();
let meta_tab = tx.open_table(HNSW_META).unwrap();
let rows_a = collect_link_rows(&links, model_a, scope);
let rows_b = collect_link_rows(&links, model_b, scope);
assert_eq!(
rows_a, rows_b,
"build_cluster must reproduce incremental-insert HNSW_LINKS rows \
bit for bit, modulo the cluster prefix"
);
let meta_a = read_meta(&meta_tab, model_a, scope).unwrap().unwrap();
let meta_b = read_meta(&meta_tab, model_b, scope).unwrap().unwrap();
assert_eq!(
meta_a, meta_b,
"build_cluster must reproduce identical meta"
);
}
#[test]
fn zero_norm_query_yields_empty_result() {
let dim = 8;
let n = 32;
let model = 1;
let scope = 1;
let params = HnswParams::default();
let vectors = seed_vectors(n, dim, 0x5EED_0008);
let (_dir, db) = open_db();
insert_incrementally(&db, model, scope, &vectors, ¶ms);
let zero_query = vec![0.0f32; dim];
let got = search_cluster(&db, model, scope, &zero_query, 16, 5);
assert!(
got.is_empty(),
"a zero-norm query must yield an empty result, not an error or a scored hit"
);
}
#[test]
fn tombstoning_the_entry_point_still_routes_correctly() {
let dim = 8;
let n = 40;
let model = 1;
let scope = 1;
let params = HnswParams::default();
let vectors = seed_vectors(n, dim, 0x5EED_0009);
let (_dir, db) = open_db();
insert_incrementally(&db, model, scope, &vectors, ¶ms);
let entry_slot = {
let tx = db.begin_read().unwrap();
let meta_tab = tx.open_table(HNSW_META).unwrap();
read_meta(&meta_tab, model, scope)
.unwrap()
.unwrap()
.entry_slot
};
let entries: Vec<(u64, Vec<f32>)> = vectors
.iter()
.enumerate()
.map(|(slot, v)| (slot as u64, v.clone()))
.collect();
let tx = db.begin_write().unwrap();
{
let mut links = tx.open_table(HNSW_LINKS).unwrap();
let mut meta = tx.open_table(HNSW_META).unwrap();
let newly = tombstone(&mut links, &mut meta, model, scope, entry_slot).unwrap();
assert!(newly, "the entry slot must have had a live level-0 row");
}
tx.commit().unwrap();
let mut tombstoned = HashSet::new();
tombstoned.insert(entry_slot);
let query = seed_vectors(1, dim, 0x5EED_000A).remove(0);
let (ef, k) = (40usize, 10usize);
let want = brute_force(&entries, &tombstoned, &query, ef.max(k));
let got = search_cluster(&db, model, scope, &query, ef, k);
assert_eq!(
got, want,
"tombstoning the entry point must still leave the rest of the graph \
reachable (search still starts its greedy descend FROM the tombstoned \
entry, it just never RANKS it)"
);
assert!(got.iter().all(|&(slot, _)| slot != entry_slot));
}
#[test]
fn reinsert_links_rewires_own_row_without_touching_neighbors() {
let dim = 8;
let n = 40;
let model = 1;
let scope = 1;
let params = HnswParams::default();
let vectors = seed_vectors(n, dim, 0x5EED_000B);
let (_dir, db) = open_db();
insert_incrementally(&db, model, scope, &vectors, ¶ms);
let target_slot: u64 = 3;
let before_meta = {
let tx = db.begin_read().unwrap();
let meta_tab = tx.open_table(HNSW_META).unwrap();
read_meta(&meta_tab, model, scope).unwrap().unwrap()
};
let before_rows: Vec<(u64, u8, LinkRow)> = {
let tx = db.begin_read().unwrap();
let links = tx.open_table(HNSW_LINKS).unwrap();
collect_link_rows(&links, model, scope)
.into_iter()
.filter(|(slot, _, _)| *slot != target_slot)
.collect()
};
let new_vector = seed_vectors(1, dim, 0x5EED_000C).remove(0);
let tx = db.begin_write().unwrap();
{
let mut vtab = tx.open_table(VECTORS).unwrap();
let mut rtab = tx.open_table(EMBEDDING_REF).unwrap();
put_vector(&mut vtab, &mut rtab, model, scope, target_slot, &new_vector).unwrap();
let mut links = tx.open_table(HNSW_LINKS).unwrap();
let mut meta = tx.open_table(HNSW_META).unwrap();
let reader = GraphReader {
vectors: &vtab,
refs: &rtab,
model,
scope,
};
reinsert_links(
&mut links,
&mut meta,
&reader,
¶ms,
target_slot,
&new_vector,
)
.unwrap();
}
tx.commit().unwrap();
let after_meta = {
let tx = db.begin_read().unwrap();
let meta_tab = tx.open_table(HNSW_META).unwrap();
read_meta(&meta_tab, model, scope).unwrap().unwrap()
};
assert_eq!(
after_meta.stale,
before_meta.stale + 1,
"reinsert_links must bump stale by exactly 1"
);
assert_eq!(
after_meta.graph_len, before_meta.graph_len,
"reinsert_links must never touch graph_len"
);
let after_rows: Vec<(u64, u8, LinkRow)> = {
let tx = db.begin_read().unwrap();
let links = tx.open_table(HNSW_LINKS).unwrap();
collect_link_rows(&links, model, scope)
.into_iter()
.filter(|(slot, _, _)| *slot != target_slot)
.collect()
};
assert_eq!(
before_rows, after_rows,
"reinsert_links must not touch any OTHER slot's link rows"
);
let tx = db.begin_read().unwrap();
let links = tx.open_table(HNSW_LINKS).unwrap();
let target_rows: Vec<(u64, u8, LinkRow)> = collect_link_rows(&links, model, scope)
.into_iter()
.filter(|(slot, _, _)| *slot == target_slot)
.collect();
assert!(!target_rows.is_empty());
for (_, _, row) in &target_rows {
assert!(!row.tomb, "a live re-embed's row must not be tombstoned");
assert!(
!row.neighbors.contains(&target_slot),
"a slot must never list itself as its own neighbor"
);
}
let got = search_cluster(&db, model, scope, &new_vector, 40, 1);
assert_eq!(got[0].0, target_slot);
}
#[test]
fn reinsert_links_on_unbuilt_cluster_is_a_no_op() {
let model = 9;
let scope = 9;
let params = HnswParams::default();
let (_dir, db) = open_db();
let tx = db.begin_write().unwrap();
{
let mut vtab = tx.open_table(VECTORS).unwrap();
let mut rtab = tx.open_table(EMBEDDING_REF).unwrap();
let mut links = tx.open_table(HNSW_LINKS).unwrap();
let mut meta = tx.open_table(HNSW_META).unwrap();
let v = vec![1.0f32, 0.0, 0.0, 0.0];
put_vector(&mut vtab, &mut rtab, model, scope, 0, &v).unwrap();
let reader = GraphReader {
vectors: &vtab,
refs: &rtab,
model,
scope,
};
reinsert_links(&mut links, &mut meta, &reader, ¶ms, 0, &v).unwrap();
assert!(
read_meta(&meta, model, scope).unwrap().is_none(),
"an unbuilt cluster must stay unbuilt after reinsert_links"
);
}
tx.commit().unwrap();
}
#[test]
fn cluster_vector_count_matches_row_count() {
let dim = 4;
let model = 1;
let scope = 1;
let vectors = seed_vectors(5, dim, 0x5EED_000D);
let (_dir, db) = open_db();
let tx = db.begin_write().unwrap();
{
let mut vtab = tx.open_table(VECTORS).unwrap();
let mut rtab = tx.open_table(EMBEDDING_REF).unwrap();
for (slot, v) in vectors.iter().enumerate() {
put_vector(&mut vtab, &mut rtab, model, scope, slot as u64, v).unwrap();
}
}
tx.commit().unwrap();
let tx = db.begin_read().unwrap();
let vtab = tx.open_table(VECTORS).unwrap();
assert_eq!(cluster_vector_count(&vtab, model, scope).unwrap(), 5);
assert_eq!(cluster_vector_count(&vtab, model, 2).unwrap(), 0);
}
fn unit2(deg: f32) -> Vec<f32> {
let r = deg.to_radians();
vec![r.cos(), r.sin()]
}
fn table_resolver(
table: &[Vec<i8>],
) -> impl FnMut(u64) -> Result<Option<Rc<Vec<i8>>>, TopoError> + '_ {
move |slot: u64| Ok(table.get(slot as usize).cloned().map(Rc::new))
}
fn scored_candidates(query: &[i8], table: &[Vec<i8>]) -> Vec<(OrderedScore, u64)> {
let mut out: Vec<(OrderedScore, u64)> = table
.iter()
.enumerate()
.map(|(slot, v)| (OrderedScore(cosine_q(query, v).unwrap()), slot as u64))
.collect();
out.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.cmp(&b.1)));
out
}
#[test]
fn heuristic_selection_prefers_diverse_over_clumped() {
let table: Vec<Vec<i8>> = [unit2(5.0), unit2(8.0), unit2(-25.0)]
.iter()
.map(|v| quantize(v).1)
.collect();
let query = quantize(&unit2(0.0)).1;
let selected = select_neighbors(
&scored_candidates(&query, &table),
2,
table_resolver(&table),
)
.unwrap();
assert_eq!(
selected,
vec![0, 2],
"keep closest + diverse, prune the clump"
);
}
#[test]
fn heuristic_backfills_pruned_in_candidate_order() {
let table: Vec<Vec<i8>> = [unit2(5.0), unit2(8.0), unit2(10.0), unit2(-25.0)]
.iter()
.map(|v| quantize(v).1)
.collect();
let query = quantize(&unit2(0.0)).1;
let selected = select_neighbors(
&scored_candidates(&query, &table),
3,
table_resolver(&table),
)
.unwrap();
assert_eq!(
selected,
vec![0, 1, 3],
"a + backfilled b + diverse c; d stays pruned"
);
}
#[test]
fn heuristic_duplicate_candidates_keep_lowest_slot_first() {
let table: Vec<Vec<i8>> = [unit2(5.0), unit2(5.0)]
.iter()
.map(|v| quantize(v).1)
.collect();
let query = quantize(&unit2(0.0)).1;
let selected = select_neighbors(
&scored_candidates(&query, &table),
2,
table_resolver(&table),
)
.unwrap();
assert_eq!(
selected,
vec![0, 1],
"slot 0 kept on the tie, slot 1 backfilled"
);
let selected_one = select_neighbors(
&scored_candidates(&query, &table),
1,
table_resolver(&table),
)
.unwrap();
assert_eq!(
selected_one,
vec![0],
"no backfill room: the duplicate stays pruned"
);
}
#[test]
fn insert_wires_diverse_neighbors_not_closest_clump() {
let model = 1;
let scope = 1;
let params = HnswParams {
m: 2,
m0: 2,
ef_construction: 8,
..HnswParams::default()
};
let vectors = vec![unit2(5.0), unit2(8.0), unit2(-25.0), unit2(0.0)];
let (_dir, db) = open_db();
insert_incrementally(&db, model, scope, &vectors, ¶ms);
let tx = db.begin_read().unwrap();
let links = tx.open_table(HNSW_LINKS).unwrap();
let row = read_links(&links, model, scope, 3, 0).unwrap().unwrap();
assert_eq!(
row.neighbors,
vec![0, 2],
"diverse {{a, c}}, not the {{a, b}} clump"
);
}
#[test]
fn default_params_version_is_bumped_for_heuristic_selection() {
assert_eq!(HnswParams::default().version, 3);
}
#[test]
fn vec_cache_matches_direct_reads_and_caches_misses() {
let model = 1;
let scope = 1;
let (_dir, db) = open_db();
let v0 = unit2(5.0);
let codes0 = quantize(&v0).1;
let tx = db.begin_write().unwrap();
{
let mut vtab = tx.open_table(VECTORS).unwrap();
let mut rtab = tx.open_table(EMBEDDING_REF).unwrap();
put_vector(&mut vtab, &mut rtab, model, scope, 0, &v0).unwrap();
put_vector(&mut vtab, &mut rtab, 9, 9, 1, &unit2(8.0)).unwrap();
let reader = GraphReader {
vectors: &vtab,
refs: &rtab,
model,
scope,
};
let mut cache = VecCache::new();
assert_eq!(
cache.get(&reader, 0).unwrap().as_deref(),
Some(&codes0),
"in-cluster slot resolves to the stored vector's codes"
);
assert_eq!(
cache.get(&reader, 1).unwrap(),
None,
"cross-cluster slot is unresolvable in this reader's cluster"
);
assert_eq!(cache.get(&reader, 2).unwrap(), None, "absent slot is None");
}
tx.commit().unwrap();
let tx = db.begin_write().unwrap();
{
let mut vtab = tx.open_table(VECTORS).unwrap();
let mut rtab = tx.open_table(EMBEDDING_REF).unwrap();
let mut cache = VecCache::new();
{
let reader = GraphReader {
vectors: &vtab,
refs: &rtab,
model,
scope,
};
assert_eq!(cache.get(&reader, 0).unwrap().as_deref(), Some(&codes0));
}
crate::vector_store::remove_vector(&mut vtab, &mut rtab, 0).unwrap();
{
let reader = GraphReader {
vectors: &vtab,
refs: &rtab,
model,
scope,
};
assert_eq!(
cache.get(&reader, 0).unwrap().as_deref(),
Some(&codes0),
"cached hit survives row removal — served from memory"
);
let mut fresh = VecCache::new();
assert_eq!(
fresh.get(&reader, 0).unwrap(),
None,
"a fresh cache sees the removal"
);
}
}
tx.commit().unwrap();
}
}