use crate::db::Db;
use crate::error::{storage_err, TopoError};
use crate::ids::{NodeId, ScopeSet};
use crate::state::NodeRecord;
use crate::storage::{read_node_by_slot, NODES};
use crate::vector_store::{search_scan, OrderedScore, EMBEDDING_REF, VECTORS};
#[derive(Debug, Clone)]
pub struct VectorQuery {
pub scopes: ScopeSet,
pub model: String,
pub vector: Vec<f32>,
pub k: usize,
pub candidates: Option<Vec<NodeId>>,
}
impl Db {
pub fn search_vector(&self, q: &VectorQuery) -> Result<Vec<(NodeRecord, f32)>, TopoError> {
if q.k == 0 || q.vector.is_empty() {
return Err(TopoError::Rejected(
"vector search requires k > 0 and a non-empty query vector".into(),
));
}
let storage = self.storage();
let tx = storage.db.begin_read().map_err(storage_err)?;
let dicts = storage.dicts.read().expect("dict lock poisoned");
let scope_registry = storage
.scope_registry
.read()
.expect("scope registry lock poisoned");
let hits = search_scan(&tx, &dicts, &scope_registry, q)?;
let nodes = tx.open_table(NODES).map_err(storage_err)?;
let vectors = tx.open_table(VECTORS).map_err(storage_err)?;
let refs = tx.open_table(EMBEDDING_REF).map_err(storage_err)?;
let mut out: Vec<(NodeRecord, f32)> = Vec::with_capacity(hits.len());
for (slot, score) in hits {
if let Some(rec) =
read_node_by_slot(&nodes, &vectors, &refs, &dicts, &scope_registry, slot)?
{
if q.scopes.contains(rec.scope) {
out.push((rec, score));
}
}
}
out.sort_by(|a, b| {
OrderedScore(b.1)
.cmp(&OrderedScore(a.1))
.then_with(|| a.0.id.cmp(&b.0.id))
});
out.truncate(q.k);
self.bump(out.iter().map(|(n, _)| n.id));
Ok(out)
}
}