use std::path::Path;
use std::sync::{Arc, Mutex};
use anyhow::{ensure, Context, Result};
use arc_swap::ArcSwap;
use turbovec::IdMapIndex;
pub struct PingPongIndexer {
write: Mutex<IdMapIndex>,
search: ArcSwap<IdMapIndex>,
dim: usize,
bit_width: usize,
}
impl PingPongIndexer {
pub fn new(dim: usize, bit_width: usize) -> Result<Self> {
Ok(Self {
write: Mutex::new(IdMapIndex::new(dim, bit_width)?),
search: ArcSwap::from_pointee(IdMapIndex::new(dim, bit_width)?),
dim,
bit_width,
})
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn ingest(&self, id: u64, vector: &[f32]) -> Result<()> {
ensure!(
vector.len() == self.dim,
"Vector dimension mismatch: {} != {}",
vector.len(),
self.dim
);
self.write
.lock()
.unwrap()
.add_with_ids(vector, &[id])
.with_context(|| format!("Failed to ingest vector with ID {id}"))?;
Ok(())
}
pub fn get_search_index(&self) -> Arc<IdMapIndex> {
self.search.load_full()
}
pub fn pending_len(&self) -> usize {
self.write.lock().unwrap().len()
}
pub fn seal(&self) -> Result<IdMapIndex> {
let fresh = IdMapIndex::new(self.dim, self.bit_width)?;
let mut guard = self.write.lock().unwrap();
Ok(std::mem::replace(&mut *guard, fresh))
}
pub fn publish(&self, sealed: Arc<IdMapIndex>) {
self.search.store(sealed);
}
pub fn swap_and_flush(&self, flush_path: Option<&Path>) -> Result<()> {
let sealed = self.seal()?;
sealed.prepare();
if let Some(path) = flush_path {
sealed
.write(path)
.with_context(|| format!("Failed to backup chunk to {}", path.display()))?;
}
self.search.store(Arc::new(sealed));
Ok(())
}
}