use std::cell::RefCell;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::sync::Arc;
use durability::{Directory, PersistenceError, PersistenceResult};
use segstore::{SegmentedStore, Store};
use crate::{AxisBox, IndexParams, Region, RegionIndex, SearchParams};
struct BoxBacking;
impl Store for BoxBacking {
type Id = u32;
type Item = AxisBox;
type Segment = Vec<(u32, AxisBox)>;
fn build_segment(&self, batch: &[(u32, AxisBox)]) -> Vec<(u32, AxisBox)> {
batch.to_vec()
}
fn merge_segments(
&self,
segs: &[Vec<(u32, AxisBox)>],
live: &dyn Fn(&u32) -> bool,
) -> Vec<(u32, AxisBox)> {
segs.iter()
.flatten()
.filter(|(id, _)| live(id))
.cloned()
.collect()
}
fn segment_len(&self, seg: &Vec<(u32, AxisBox)>) -> usize {
seg.len()
}
fn live_len(&self, seg: &Vec<(u32, AxisBox)>, live: &dyn Fn(&u32) -> bool) -> Option<usize> {
Some(seg.iter().filter(|(id, _)| live(id)).count())
}
}
struct Cache {
by_ptr: HashMap<usize, Option<RegionIndex<AxisBox>>>,
}
pub struct UpdatableIndex {
inner: SegmentedStore<BoxBacking>,
dim: usize,
m: usize,
m_max: usize,
ef_construction: usize,
cache: RefCell<Cache>,
}
impl UpdatableIndex {
pub fn open(
dir: Arc<dyn Directory>,
flush_threshold: usize,
dim: usize,
params: IndexParams,
) -> PersistenceResult<Self> {
Ok(Self {
inner: SegmentedStore::open(dir, BoxBacking, flush_threshold)?,
dim,
m: params.m,
m_max: params.m_max,
ef_construction: params.ef_construction,
cache: RefCell::new(Cache {
by_ptr: HashMap::new(),
}),
})
}
pub fn add(&mut self, id: u32, region: AxisBox) -> PersistenceResult<()> {
if region.dim() != self.dim {
return Err(PersistenceError::InvalidConfig(format!(
"region dimension {} does not match index dimension {}",
region.dim(),
self.dim
)));
}
self.inner.add(id, region)?;
Ok(())
}
pub fn delete(&mut self, id: u32) -> PersistenceResult<()> {
self.inner.delete(id)?;
self.cache.borrow_mut().by_ptr.clear();
Ok(())
}
pub fn compact(&mut self) -> PersistenceResult<()> {
self.inner.compact()?;
Ok(())
}
pub fn checkpoint(&mut self) -> PersistenceResult<()> {
self.inner.checkpoint()
}
pub fn reclaim(&mut self, min_live_ratio: f64) -> PersistenceResult<()> {
self.inner.reclaim_tombstones(min_live_ratio)?;
Ok(())
}
pub fn space_amplification(&self) -> Option<f64> {
self.inner.space_amplification()
}
pub fn search(&self, query: &[f32], k: usize, params: SearchParams) -> Vec<(u32, f32)> {
let SearchParams { ef, overretrieve } = params;
let sp = || SearchParams { ef, overretrieve };
let mut cand: Vec<(u32, f32)> = Vec::new();
{
let segs = self.inner.segments();
let mut cache = self.cache.borrow_mut();
let current: std::collections::HashSet<usize> =
segs.iter().map(|a| Arc::as_ptr(a) as usize).collect();
cache.by_ptr.retain(|key, _| current.contains(key));
for seg in segs {
let key = Arc::as_ptr(seg) as usize;
cache
.by_ptr
.entry(key)
.or_insert_with(|| self.build_live_index(&seg[..]));
}
for idx in cache.by_ptr.values().flatten() {
cand.extend(idx.search(query, k, sp()).unwrap_or_default());
}
}
let buffered = self.inner.buffer().to_vec();
if let Some(idx) = self.build_live_index(&buffered) {
cand.extend(idx.search(query, k, sp()).unwrap_or_default());
}
cand.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
cand.truncate(k);
cand
}
fn build_live_index(&self, batch: &[(u32, AxisBox)]) -> Option<RegionIndex<AxisBox>> {
let params = IndexParams {
m: self.m,
m_max: self.m_max,
ef_construction: self.ef_construction,
};
let mut idx = match RegionIndex::<AxisBox>::new(self.dim, params) {
Ok(i) => i,
Err(_) => return None,
};
let mut any = false;
for (id, region) in batch {
if self.inner.is_live(id) && idx.add(*id, region.clone()).is_ok() {
any = true;
}
}
if !any || idx.build().is_err() {
return None;
}
Some(idx)
}
}
#[cfg(test)]
mod tests {
use super::*;
use durability::MemoryDirectory;
fn b(lo: f32, hi: f32) -> AxisBox {
AxisBox::new(vec![lo, lo], vec![hi, hi])
}
#[test]
fn add_delete_compact_recover_through_real_region_index() {
let dir = MemoryDirectory::arc();
{
let mut store =
UpdatableIndex::open(dir.clone(), 2, 2, IndexParams::default()).unwrap();
store.add(0, b(0.0, 1.0)).unwrap(); store.add(1, b(5.0, 6.0)).unwrap(); store.add(2, b(10.0, 11.0)).unwrap();
let top: Vec<u32> = store
.search(&[0.5, 0.5], 1, SearchParams::default())
.into_iter()
.map(|(id, _)| id)
.collect();
assert_eq!(top, vec![0], "point inside box 0 retrieves region 0");
let again: Vec<u32> = store
.search(&[0.5, 0.5], 1, SearchParams::default())
.into_iter()
.map(|(id, _)| id)
.collect();
assert_eq!(again, vec![0], "cached query is stable");
store.delete(0).unwrap();
let top: Vec<u32> = store
.search(&[0.5, 0.5], 1, SearchParams::default())
.into_iter()
.map(|(id, _)| id)
.collect();
assert_eq!(top, vec![1], "after deleting 0, nearest region is 1");
store.compact().unwrap();
assert_eq!(
store
.search(&[0.5, 0.5], 1, SearchParams::default())
.first()
.map(|(id, _)| *id),
Some(1)
);
}
let store = UpdatableIndex::open(dir, 2, 2, IndexParams::default()).unwrap();
let top: Vec<u32> = store
.search(&[0.5, 0.5], 1, SearchParams::default())
.into_iter()
.map(|(id, _)| id)
.collect();
assert_eq!(top, vec![1], "recovery preserves the search");
}
}