use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;
use durability::{Directory, PersistenceResult};
use segstore::{SegmentedStore, Store};
use crate::{BlockingConfig, MinHashTextLSH};
struct TextBacking;
impl Store for TextBacking {
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())
}
}
type Block = (MinHashTextLSH, Vec<u32>);
struct Cache {
by_ptr: HashMap<usize, Option<Block>>,
}
pub struct UpdatableIndex {
inner: SegmentedStore<TextBacking>,
config: BlockingConfig,
cache: RefCell<Cache>,
}
impl UpdatableIndex {
pub fn open(
dir: Arc<dyn Directory>,
flush_threshold: usize,
config: BlockingConfig,
) -> PersistenceResult<Self> {
Ok(Self {
inner: SegmentedStore::open(dir, TextBacking, flush_threshold)?,
config,
cache: RefCell::new(Cache {
by_ptr: HashMap::new(),
}),
})
}
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 near_duplicates(&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 block in cache.by_ptr.values().flatten() {
out.extend(query_block(block, text));
}
}
let buffered = self.inner.buffer().to_vec();
if let Some(block) = self.build_live_index(&buffered) {
out.extend(query_block(&block, text));
}
out.sort_unstable();
out.dedup();
out
}
fn build_live_index(&self, batch: &[(u32, String)]) -> Option<Block> {
let mut lsh = match MinHashTextLSH::new(self.config.clone()) {
Ok(l) => l,
Err(_) => return None,
};
let mut ids: Vec<u32> = Vec::new();
for (id, doc) in batch {
if self.inner.is_live(id) {
lsh.insert_text(id.to_string(), doc);
ids.push(*id);
}
}
if ids.is_empty() {
return None;
}
Some((lsh, ids))
}
}
fn query_block((lsh, ids): &Block, text: &str) -> Vec<u32> {
lsh.query(text)
.into_iter()
.filter_map(|i| ids.get(i).copied())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use durability::MemoryDirectory;
const A: &str = "the quick brown fox jumps over the lazy dog";
const B: &str = "lorem ipsum dolor sit amet consectetur adipiscing elit";
#[test]
fn add_delete_compact_recover_through_real_lsh() {
let dir = MemoryDirectory::arc();
{
let mut store =
UpdatableIndex::open(dir.clone(), 2, BlockingConfig::default()).unwrap();
store.add(1, A).unwrap();
store.add(2, A).unwrap(); store.add(3, B).unwrap();
let dups = store.near_duplicates(A);
assert!(
dups.contains(&1) && dups.contains(&2),
"identical docs are near-duplicates"
);
assert!(!dups.contains(&3), "unrelated doc is not");
assert_eq!(store.near_duplicates(A), dups, "cached query is stable");
store.delete(2).unwrap();
assert!(
!store.near_duplicates(A).contains(&2),
"delete invalidates the cache; deleted doc drops out"
);
store.compact().unwrap();
let dups = store.near_duplicates(A);
assert!(
dups.contains(&1) && !dups.contains(&2),
"compaction preserves the result"
);
}
let store = UpdatableIndex::open(dir, 2, BlockingConfig::default()).unwrap();
let dups = store.near_duplicates(A);
assert!(
dups.contains(&1) && !dups.contains(&2),
"recovery preserves the result"
);
}
}