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()
}
}
pub struct UpdatableIndex {
inner: SegmentedStore<GramBacking>,
k: usize,
}
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,
})
}
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())
}
pub fn delete(&mut self, id: u32) -> PersistenceResult<()> {
self.inner.delete(id)
}
pub fn compact(&mut self) -> PersistenceResult<()> {
self.inner.compact()
}
pub fn checkpoint(&mut self) -> PersistenceResult<()> {
self.inner.checkpoint()
}
pub fn candidates(&self, text: &str) -> Vec<u32> {
let mut out: Vec<u32> = Vec::new();
for seg in self.inner.segments() {
out.extend(self.candidates_in(seg, text));
}
let buffered = self.inner.buffer().to_vec();
out.extend(self.candidates_in(&buffered, text));
out.sort_unstable();
out.dedup();
out
}
fn candidates_in(&self, batch: &[(u32, String)], text: &str) -> Vec<u32> {
let mut ix = GramDex::new();
let mut any = false;
for (id, doc) in batch {
if self.inner.is_live(id) && ix.add_document_char_kgrams(*id, doc, self.k).is_ok() {
any = true;
}
}
if !any {
return Vec::new();
}
ix.candidates_union_char_kgrams(text, self.k)
.unwrap_or_default()
}
}
#[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), "exact match is a candidate");
assert!(c.contains(&2), "yellow shares trigrams with mellow");
store.delete(2).unwrap();
let c = store.candidates("mellow");
assert!(!c.contains(&2), "deleted doc drops out of candidates");
assert!(c.contains(&3));
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");
}
}