use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;
use durability::{Directory, PersistenceResult};
use segstore::{SegmentedStore, Store};
use crate::GramDex;
struct GramBacking;
impl Store for GramBacking {
type Id = u32;
type Item = String;
type Segment = Vec<(u32, String)>;
fn build_segment(&self, batch: &[(u32, String)]) -> Vec<(u32, String)> {
batch.to_vec()
}
fn merge_segments(
&self,
segs: &[Vec<(u32, String)>],
live: &dyn Fn(&u32) -> bool,
) -> Vec<(u32, String)> {
segs.iter()
.flatten()
.filter(|(id, _)| live(id))
.cloned()
.collect()
}
fn segment_len(&self, seg: &Vec<(u32, String)>) -> usize {
seg.len()
}
fn live_len(&self, seg: &Vec<(u32, String)>, live: &dyn Fn(&u32) -> bool) -> Option<usize> {
Some(seg.iter().filter(|(id, _)| live(id)).count())
}
}
struct Cache {
by_ptr: HashMap<usize, Option<GramDex>>,
}
pub struct UpdatableIndex {
inner: SegmentedStore<GramBacking>,
k: usize,
cache: RefCell<Cache>,
}
impl UpdatableIndex {
pub fn open(
dir: Arc<dyn Directory>,
flush_threshold: usize,
k: usize,
) -> PersistenceResult<Self> {
Ok(Self {
inner: SegmentedStore::open(dir, GramBacking, flush_threshold)?,
k,
cache: RefCell::new(Cache {
by_ptr: HashMap::new(),
}),
})
}
pub fn k(&self) -> usize {
self.k
}
pub fn add(&mut self, id: u32, text: impl Into<String>) -> PersistenceResult<()> {
self.inner.add(id, text.into())?;
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 candidates(&self, text: &str) -> Vec<u32> {
let mut out: Vec<u32> = 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 ix in cache.by_ptr.values().flatten() {
out.extend(
ix.candidates_union_char_kgrams(text, self.k)
.unwrap_or_default(),
);
}
}
let buffered: Vec<(u32, String)> = self.inner.buffer().to_vec();
if let Some(ix) = self.build_live_index(&buffered) {
out.extend(
ix.candidates_union_char_kgrams(text, self.k)
.unwrap_or_default(),
);
}
out.sort_unstable();
out.dedup();
out
}
fn build_live_index(&self, items: &[(u32, String)]) -> Option<GramDex> {
let mut ix = GramDex::new();
let mut any = false;
for (id, doc) in items {
if self.inner.is_live(id) && ix.add_document_char_kgrams(*id, doc, self.k).is_ok() {
any = true;
}
}
if !any {
return None;
}
Some(ix)
}
}
#[cfg(test)]
mod tests {
use super::*;
use durability::MemoryDirectory;
#[test]
fn add_delete_compact_recover_with_configurable_k() {
let dir = MemoryDirectory::arc();
{
let mut store = UpdatableIndex::open(dir.clone(), 2, 3).unwrap();
store.add(1, "hello").unwrap();
store.add(2, "yellow").unwrap();
store.add(3, "mellow").unwrap();
let c = store.candidates("mellow");
assert!(c.contains(&3) && c.contains(&2));
assert_eq!(store.candidates("mellow"), c, "cached query is stable");
store.delete(2).unwrap();
assert!(
!store.candidates("mellow").contains(&2),
"delete invalidates the cache"
);
store.compact().unwrap();
let c = store.candidates("mellow");
assert!(
c.contains(&3) && !c.contains(&2),
"compaction preserves the result"
);
}
let store = UpdatableIndex::open(dir, 2, 3).unwrap();
let c = store.candidates("mellow");
assert!(
c.contains(&3) && !c.contains(&2),
"recovery preserves the result"
);
}
#[test]
fn larger_k_is_stricter() {
let dir = MemoryDirectory::arc();
let mut store = UpdatableIndex::open(dir, 4, 5).unwrap();
store.add(1, "hello").unwrap();
store.add(2, "yellow").unwrap();
let c = store.candidates("mellow");
assert!(c.contains(&2), "yellow shares the 5-gram ellow");
assert!(!c.contains(&1), "hello shares no 5-gram with mellow at k=5");
}
}