use crate::storage::Vector;
use anyhow::{anyhow, Context, Result};
use rkyv::{
check_archived_root, ser::serializers::AllocSerializer, ser::Serializer, AlignedVec,
Deserialize,
};
use rocksdb::{BlockBasedOptions, Cache, Options, WriteBatch, DB};
use std::path::Path;
pub struct VectorIndex {
db: DB,
}
impl VectorIndex {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
let mut opts = Options::default();
opts.create_if_missing(true);
opts.set_compression_type(rocksdb::DBCompressionType::Zstd);
opts.optimize_level_style_compaction(64 * 1024 * 1024);
let cache = Cache::new_lru_cache(64 * 1024 * 1024);
let mut block_opts = BlockBasedOptions::default();
block_opts.set_block_cache(&cache);
opts.set_block_based_table_factory(&block_opts);
opts.set_write_buffer_size(16 * 1024 * 1024);
opts.set_max_write_buffer_number(2);
let db = DB::open(&opts, path).context("open vector index")?;
Ok(Self { db })
}
pub fn put_batch(&self, vectors: &[Vector]) -> Result<()> {
if vectors.is_empty() {
return Ok(());
}
let mut batch = WriteBatch::default();
for v in vectors {
let mut ser = AllocSerializer::<256>::default();
ser.serialize_value(v)
.context("serialize vector for index")?;
let bytes: AlignedVec = ser.into_serializer().into_inner();
batch.put(v.id.to_le_bytes(), bytes);
}
self.db.write(batch).context("write vector index batch")
}
#[allow(dead_code)]
pub fn delete_batch(&self, ids: &[u64]) -> Result<()> {
if ids.is_empty() {
return Ok(());
}
let mut batch = WriteBatch::default();
for id in ids {
batch.delete(id.to_le_bytes());
}
self.db.write(batch).context("delete vector index batch")
}
pub fn get_many(&self, ids: &[u64]) -> Result<Vec<(u64, Vector)>> {
let mut out = Vec::with_capacity(ids.len());
for id in ids {
if let Some(raw) = self
.db
.get(id.to_le_bytes())
.with_context(|| format!("read id {} from vector index", id))?
{
let archived = check_archived_root::<Vector>(&raw)
.map_err(|e| anyhow!("validate archived vector: {:?}", e))?;
let vec: Vector = archived.deserialize(&mut rkyv::Infallible)?;
out.push((*id, vec));
}
}
Ok(out)
}
pub fn first_existing(&self, ids: &[u64]) -> Result<Option<u64>> {
for id in ids {
if self.exists(*id)? {
return Ok(Some(*id));
}
}
Ok(None)
}
pub fn exists(&self, id: u64) -> Result<bool> {
let key = id.to_le_bytes();
if !self.db.key_may_exist(key) {
return Ok(false);
}
Ok(self
.db
.get(key)
.with_context(|| format!("check existence of id {}", id))?
.is_some())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn index_round_trip() {
let dir = tempfile::tempdir().expect("tempdir");
let index = VectorIndex::open(dir.path()).expect("open index");
let v1 = Vector::new(1, vec![1.0, 2.0, 3.0]);
let v2 = Vector::new(2, vec![4.0, 5.0, 6.0]);
index.put_batch(&[v1.clone(), v2.clone()]).expect("put");
let got = index
.get_many(&[1, 2, 3])
.expect("get")
.into_iter()
.map(|(_, v)| (v.id, v.data))
.collect::<Vec<_>>();
assert_eq!(got.len(), 2);
assert!(got.contains(&(v1.id, v1.data)));
assert!(got.contains(&(v2.id, v2.data)));
}
#[test]
fn exists_returns_false_for_missing_id() {
let dir = tempfile::tempdir().expect("tempdir");
let index = VectorIndex::open(dir.path()).expect("open index");
assert!(!index.exists(42).expect("exists check"));
assert!(!index.exists(0).expect("exists check"));
assert!(!index.exists(u64::MAX).expect("exists check"));
}
#[test]
fn exists_returns_true_for_inserted_id() {
let dir = tempfile::tempdir().expect("tempdir");
let index = VectorIndex::open(dir.path()).expect("open index");
let v1 = Vector::new(1, vec![1.0, 2.0, 3.0]);
let v2 = Vector::new(2, vec![4.0, 5.0, 6.0]);
index.put_batch(&[v1, v2]).expect("put");
assert!(index.exists(1).expect("exists check"));
assert!(index.exists(2).expect("exists check"));
assert!(!index.exists(3).expect("exists check"));
}
#[test]
fn first_existing_uses_exists() {
let dir = tempfile::tempdir().expect("tempdir");
let index = VectorIndex::open(dir.path()).expect("open index");
let v = Vector::new(5, vec![1.0]);
index.put_batch(&[v]).expect("put");
assert_eq!(index.first_existing(&[1, 2, 3]).expect("check"), None);
assert_eq!(index.first_existing(&[1, 5, 3]).expect("check"), Some(5));
assert_eq!(index.first_existing(&[5, 1, 2]).expect("check"), Some(5));
}
}