use crate::codec::{frame_value, unframe_value};
use crate::dict::{DictKind, Dicts};
use crate::error::{storage_err, TopoError};
use crate::scopes::ScopeRegistry;
use crate::slots::{node_slot, NODE_SLOTS};
use crate::vector::VectorQuery;
use redb::{ReadTransaction, ReadableTable, Table, TableDefinition};
use std::cmp::Reverse;
use std::collections::{BinaryHeap, HashSet};
pub(crate) const VECTORS: TableDefinition<&[u8], &[u8]> = TableDefinition::new("vectors");
pub(crate) const EMBEDDING_REF: TableDefinition<&[u8], &[u8]> =
TableDefinition::new("embedding_ref");
fn slot_key(slot: u64) -> [u8; 8] {
slot.to_be_bytes()
}
pub(crate) fn vector_key(model: u32, scope: u32, slot: u64) -> [u8; 16] {
let mut k = [0u8; 16];
k[0..4].copy_from_slice(&model.to_be_bytes());
k[4..8].copy_from_slice(&scope.to_be_bytes());
k[8..16].copy_from_slice(&slot.to_be_bytes());
k
}
pub(crate) fn vector_prefix(model: u32, scope: u32) -> [u8; 8] {
let mut k = [0u8; 8];
k[0..4].copy_from_slice(&model.to_be_bytes());
k[4..8].copy_from_slice(&scope.to_be_bytes());
k
}
fn encode_ref(model: u32, scope: u32) -> Result<Vec<u8>, TopoError> {
postcard::to_allocvec(&(model, scope)).map_err(|e| TopoError::Encoding(e.to_string()))
}
pub(crate) fn decode_ref(bytes: &[u8]) -> Result<(u32, u32), TopoError> {
postcard::from_bytes(bytes).map_err(|e| TopoError::Encoding(e.to_string()))
}
pub(crate) fn put_vector(
vectors: &mut Table<'_, &'static [u8], &'static [u8]>,
refs: &mut Table<'_, &'static [u8], &'static [u8]>,
model: u32,
scope: u32,
slot: u64,
v: &[f32],
) -> Result<(), TopoError> {
let rk = slot_key(slot);
let old: Option<(u32, u32)> = match refs.get(rk.as_slice()).map_err(storage_err)? {
Some(g) => Some(decode_ref(g.value())?),
None => None,
};
if let Some((old_model, old_scope)) = old {
if old_model != model {
vectors
.remove(vector_key(old_model, old_scope, slot).as_slice())
.map_err(storage_err)?;
} else {
debug_assert_eq!(
old_scope, scope,
"node scope is immutable; a same-model re-embed can never move scopes"
);
}
}
let raw = postcard::to_allocvec(v).map_err(|e| TopoError::Encoding(e.to_string()))?;
let framed = frame_value(raw);
vectors
.insert(vector_key(model, scope, slot).as_slice(), framed.as_slice())
.map_err(storage_err)?;
refs.insert(rk.as_slice(), encode_ref(model, scope)?.as_slice())
.map_err(storage_err)?;
Ok(())
}
pub(crate) fn remove_vector(
vectors: &mut Table<'_, &'static [u8], &'static [u8]>,
refs: &mut Table<'_, &'static [u8], &'static [u8]>,
slot: u64,
) -> Result<(), TopoError> {
let rk = slot_key(slot);
let old: Option<(u32, u32)> = match refs.get(rk.as_slice()).map_err(storage_err)? {
Some(g) => Some(decode_ref(g.value())?),
None => None,
};
if let Some((model, scope)) = old {
vectors
.remove(vector_key(model, scope, slot).as_slice())
.map_err(storage_err)?;
refs.remove(rk.as_slice()).map_err(storage_err)?;
}
Ok(())
}
pub(crate) fn read_vector_by_slot(
vectors: &impl ReadableTable<&'static [u8], &'static [u8]>,
refs: &impl ReadableTable<&'static [u8], &'static [u8]>,
slot: u64,
) -> Result<Option<(u32, u32, Vec<f32>)>, TopoError> {
let rk = slot_key(slot);
let Some(g) = refs.get(rk.as_slice()).map_err(storage_err)? else {
return Ok(None);
};
let (model, scope) = decode_ref(g.value())?;
drop(g);
match vectors
.get(vector_key(model, scope, slot).as_slice())
.map_err(storage_err)?
{
Some(v) => {
let raw = unframe_value(v.value())?;
let vec: Vec<f32> =
postcard::from_bytes(&raw).map_err(|e| TopoError::Encoding(e.to_string()))?;
Ok(Some((model, scope, vec)))
}
None => Err(TopoError::Encoding(
"read_vector_by_slot: embedding_ref present but vectors row missing".into(),
)),
}
}
fn cosine(a: &[f32], b: &[f32]) -> Option<f32> {
let (mut dot, mut na, mut nb) = (0.0f32, 0.0f32, 0.0f32);
for (x, y) in a.iter().zip(b) {
dot += x * y;
na += x * x;
nb += y * y;
}
if na == 0.0 || nb == 0.0 {
return None;
}
Some(dot / (na.sqrt() * nb.sqrt()))
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct OrderedScore(pub(crate) f32);
impl Eq for OrderedScore {}
impl PartialOrd for OrderedScore {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for OrderedScore {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0.total_cmp(&other.0)
}
}
pub(crate) fn push_topk(
heap: &mut BinaryHeap<Reverse<(OrderedScore, u64)>>,
score: f32,
slot: u64,
k: usize,
) {
if k == 0 {
return;
}
heap.push(Reverse((OrderedScore(score), slot)));
while heap.len() > k {
let Reverse((min_score, _)) = *heap.peek().expect("heap.len() > k >= 1: non-empty");
let mut group = Vec::new();
while let Some(&Reverse((next_score, _))) = heap.peek() {
if next_score != min_score {
break;
}
group.push(heap.pop().expect("just peeked"));
}
if heap.len() >= k {
} else {
heap.extend(group);
break;
}
}
}
pub(crate) fn search_scan(
tx: &ReadTransaction,
dicts: &Dicts,
scope_registry: &ScopeRegistry,
q: &VectorQuery,
) -> Result<Vec<(u64, f32)>, TopoError> {
let Some(model_id) = dicts.id_of(DictKind::Model, &q.model) else {
return Ok(Vec::new());
};
let vectors = tx.open_table(VECTORS).map_err(storage_err)?;
let refs = tx.open_table(EMBEDDING_REF).map_err(storage_err)?;
let mut heap: BinaryHeap<Reverse<(OrderedScore, u64)>> = BinaryHeap::new();
if let Some(candidates) = &q.candidates {
let node_slots = tx.open_table(NODE_SLOTS).map_err(storage_err)?;
let allowed_scopes: HashSet<u32> = q
.scopes
.iter_scopes()
.filter_map(|s| scope_registry.id_of(s))
.collect();
let distinct: HashSet<_> = candidates.iter().copied().collect();
for id in distinct {
let Some(slot) = node_slot(&node_slots, id)? else {
continue;
};
let Some((row_model, row_scope, vector)) = read_vector_by_slot(&vectors, &refs, slot)?
else {
continue;
};
if row_model != model_id || !allowed_scopes.contains(&row_scope) {
continue;
}
if vector.len() != q.vector.len() {
continue;
}
if let Some(score) = cosine(&vector, &q.vector) {
push_topk(&mut heap, score, slot, q.k);
}
}
} else {
for scope in q.scopes.iter_scopes() {
let Some(scope_id) = scope_registry.id_of(scope) else {
continue;
};
let prefix = vector_prefix(model_id, scope_id);
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());
for entry in vectors
.range(start.as_slice()..=end.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 vector: Vec<f32> =
postcard::from_bytes(&raw).map_err(|e| TopoError::Encoding(e.to_string()))?;
if vector.len() != q.vector.len() {
continue;
}
if let Some(score) = cosine(&vector, &q.vector) {
push_topk(&mut heap, score, slot, q.k);
}
}
}
}
Ok(heap
.into_iter()
.map(|Reverse((score, slot))| (slot, score.0))
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use redb::Database;
fn open() -> (tempfile::TempDir, Database) {
let dir = tempfile::tempdir().unwrap();
let db = Database::create(dir.path().join("t.redb")).unwrap();
(dir, db)
}
fn cluster_rows(
vectors: &impl ReadableTable<&'static [u8], &'static [u8]>,
model: u32,
scope: u32,
) -> Vec<[u8; 16]> {
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());
vectors
.range(start.as_slice()..=end.as_slice())
.unwrap()
.map(|entry| {
let (k, _) = entry.unwrap();
k.value().try_into().unwrap()
})
.collect()
}
#[test]
fn put_read_round_trips() {
let (_dir, db) = open();
let tx = db.begin_write().unwrap();
{
let mut vectors = tx.open_table(VECTORS).unwrap();
let mut refs = tx.open_table(EMBEDDING_REF).unwrap();
put_vector(&mut vectors, &mut refs, 1, 2, 7, &[1.0, 2.0, 3.0]).unwrap();
}
tx.commit().unwrap();
let tx = db.begin_read().unwrap();
let vectors = tx.open_table(VECTORS).unwrap();
let refs = tx.open_table(EMBEDDING_REF).unwrap();
let (model, scope, vector) = read_vector_by_slot(&vectors, &refs, 7).unwrap().unwrap();
assert_eq!((model, scope), (1, 2));
assert_eq!(vector, vec![1.0, 2.0, 3.0]);
}
#[test]
fn reembed_same_model_overwrites_in_place_no_orphan() {
let (_dir, db) = open();
let tx = db.begin_write().unwrap();
{
let mut vectors = tx.open_table(VECTORS).unwrap();
let mut refs = tx.open_table(EMBEDDING_REF).unwrap();
put_vector(&mut vectors, &mut refs, 1, 2, 7, &[1.0, 2.0]).unwrap();
put_vector(&mut vectors, &mut refs, 1, 2, 7, &[9.0, 9.0]).unwrap();
let rows = cluster_rows(&vectors, 1, 2);
assert_eq!(
rows.len(),
1,
"same-model re-embed must not leave an orphan row"
);
assert_eq!(rows[0], vector_key(1, 2, 7));
}
tx.commit().unwrap();
let tx = db.begin_read().unwrap();
let vectors = tx.open_table(VECTORS).unwrap();
let refs = tx.open_table(EMBEDDING_REF).unwrap();
let (model, scope, vector) = read_vector_by_slot(&vectors, &refs, 7).unwrap().unwrap();
assert_eq!((model, scope), (1, 2));
assert_eq!(vector, vec![9.0, 9.0]);
}
#[test]
fn reembed_under_new_model_deletes_old_models_row_and_updates_ref() {
let (_dir, db) = open();
let tx = db.begin_write().unwrap();
{
let mut vectors = tx.open_table(VECTORS).unwrap();
let mut refs = tx.open_table(EMBEDDING_REF).unwrap();
put_vector(&mut vectors, &mut refs, 1, 2, 7, &[1.0, 2.0]).unwrap();
put_vector(&mut vectors, &mut refs, 5, 2, 7, &[3.0, 4.0]).unwrap();
assert!(
cluster_rows(&vectors, 1, 2).is_empty(),
"old model's cluster must be empty after a cross-model re-embed"
);
let rows = cluster_rows(&vectors, 5, 2);
assert_eq!(rows, vec![vector_key(5, 2, 7)]);
}
tx.commit().unwrap();
let tx = db.begin_read().unwrap();
let vectors = tx.open_table(VECTORS).unwrap();
let refs = tx.open_table(EMBEDDING_REF).unwrap();
let (model, scope, vector) = read_vector_by_slot(&vectors, &refs, 7).unwrap().unwrap();
assert_eq!((model, scope), (5, 2));
assert_eq!(vector, vec![3.0, 4.0]);
}
#[test]
fn remove_vector_clears_both_tables() {
let (_dir, db) = open();
let tx = db.begin_write().unwrap();
{
let mut vectors = tx.open_table(VECTORS).unwrap();
let mut refs = tx.open_table(EMBEDDING_REF).unwrap();
put_vector(&mut vectors, &mut refs, 1, 2, 7, &[1.0, 2.0]).unwrap();
remove_vector(&mut vectors, &mut refs, 7).unwrap();
assert!(cluster_rows(&vectors, 1, 2).is_empty());
assert!(refs.get(slot_key(7).as_slice()).unwrap().is_none());
}
tx.commit().unwrap();
let tx = db.begin_read().unwrap();
let vectors = tx.open_table(VECTORS).unwrap();
let refs = tx.open_table(EMBEDDING_REF).unwrap();
assert!(read_vector_by_slot(&vectors, &refs, 7).unwrap().is_none());
let tx = db.begin_write().unwrap();
{
let mut vectors = tx.open_table(VECTORS).unwrap();
let mut refs = tx.open_table(EMBEDDING_REF).unwrap();
remove_vector(&mut vectors, &mut refs, 999).unwrap();
}
tx.commit().unwrap();
}
#[test]
fn read_vector_by_slot_never_embedded_is_none() {
let (_dir, db) = open();
let tx = db.begin_write().unwrap();
{
tx.open_table(VECTORS).unwrap();
tx.open_table(EMBEDDING_REF).unwrap();
}
tx.commit().unwrap();
let tx = db.begin_read().unwrap();
let vectors = tx.open_table(VECTORS).unwrap();
let refs = tx.open_table(EMBEDDING_REF).unwrap();
assert!(read_vector_by_slot(&vectors, &refs, 42).unwrap().is_none());
}
fn sort_and_truncate(entries: &[(f32, u64)], k: usize) -> Vec<(f32, u64)> {
let mut out = entries.to_vec();
out.sort_by(|a, b| {
OrderedScore(b.0)
.cmp(&OrderedScore(a.0))
.then_with(|| a.1.cmp(&b.1))
});
out.truncate(k);
out
}
fn heap_topk(entries: &[(f32, u64)], k: usize) -> Vec<(f32, u64)> {
let mut heap: BinaryHeap<Reverse<(OrderedScore, u64)>> = BinaryHeap::new();
for &(score, slot) in entries {
push_topk(&mut heap, score, slot, k);
}
let raw: Vec<(f32, u64)> = heap
.into_iter()
.map(|Reverse((score, slot))| (score.0, slot))
.collect();
sort_and_truncate(&raw, k)
}
#[test]
fn heap_retains_all_ties_at_the_boundary_conservatively() {
let mut heap: BinaryHeap<Reverse<(OrderedScore, u64)>> = BinaryHeap::new();
for slot in [10u64, 5, 20] {
push_topk(&mut heap, 1.0, slot, 2);
}
let mut slots: Vec<u64> = heap.into_iter().map(|Reverse((_, slot))| slot).collect();
slots.sort_unstable();
assert_eq!(
slots,
vec![5, 10, 20],
"all boundary ties must be retained, not just k"
);
}
#[test]
fn heap_drops_ties_once_enough_strictly_better_elements_arrive() {
let mut heap: BinaryHeap<Reverse<(OrderedScore, u64)>> = BinaryHeap::new();
for slot in [1u64, 2, 3] {
push_topk(&mut heap, 1.0, slot, 1); }
push_topk(&mut heap, 5.0, 4, 1); let slots: Vec<u64> = heap.into_iter().map(|Reverse((_, slot))| slot).collect();
assert_eq!(
slots,
vec![4],
"a strictly-better element must fully displace a moot tie"
);
}
proptest! {
#[test]
fn streaming_heap_topk_matches_sort_and_truncate(
entries in proptest::collection::vec((-1000.0f32..1000.0f32, 0u64..10_000u64), 0..200),
k in 1usize..20,
) {
prop_assert_eq!(heap_topk(&entries, k), sort_and_truncate(&entries, k));
}
}
}