use std::num::NonZeroUsize;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use lru::LruCache;
use parking_lot::Mutex;
use roaring::RoaringTreemap;
use crate::error::Result;
use crate::lexical::query::matcher::Matcher;
pub(crate) fn drain_matcher(mut matcher: Box<dyn Matcher>) -> Result<RoaringTreemap> {
let mut bitmap = RoaringTreemap::new();
while !matcher.is_exhausted() {
let doc_id = matcher.doc_id();
if doc_id == u64::MAX {
break;
}
bitmap.insert(doc_id);
if !matcher.next()? {
break;
}
}
Ok(bitmap)
}
#[derive(Debug)]
pub struct QueryFilterCache {
inner: Option<Mutex<LruCache<String, Arc<RoaringTreemap>>>>,
hits: AtomicU64,
misses: AtomicU64,
}
impl QueryFilterCache {
pub fn new(capacity: usize) -> Self {
let inner = NonZeroUsize::new(capacity).map(|c| Mutex::new(LruCache::new(c)));
QueryFilterCache {
inner,
hits: AtomicU64::new(0),
misses: AtomicU64::new(0),
}
}
pub fn is_enabled(&self) -> bool {
self.inner.is_some()
}
pub fn get(&self, key: &str) -> Option<Arc<RoaringTreemap>> {
let hit = self
.inner
.as_ref()
.and_then(|inner| inner.lock().get(key).cloned());
if hit.is_some() {
self.hits.fetch_add(1, Ordering::Relaxed);
} else {
self.misses.fetch_add(1, Ordering::Relaxed);
}
hit
}
pub fn put(&self, key: String, value: Arc<RoaringTreemap>) {
if let Some(inner) = self.inner.as_ref() {
inner.lock().put(key, value);
}
}
pub fn len(&self) -> usize {
self.inner.as_ref().map_or(0, |inner| inner.lock().len())
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn stats(&self) -> QueryFilterCacheStats {
QueryFilterCacheStats {
hits: self.hits.load(Ordering::Relaxed),
misses: self.misses.load(Ordering::Relaxed),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QueryFilterCacheStats {
pub hits: u64,
pub misses: u64,
}
#[cfg(test)]
mod tests {
use super::*;
fn set(ids: &[u64]) -> Arc<RoaringTreemap> {
Arc::new(ids.iter().copied().collect())
}
#[test]
fn put_then_get_returns_same_set() {
let cache = QueryFilterCache::new(4);
cache.put("k".to_string(), set(&[1, 2, 3]));
let got = cache.get("k").expect("entry should be present");
assert_eq!(got.iter().collect::<Vec<_>>(), vec![1, 2, 3]);
assert_eq!(cache.stats().hits, 1);
assert_eq!(cache.stats().misses, 0);
}
#[test]
fn miss_increments_miss_counter() {
let cache = QueryFilterCache::new(4);
assert!(cache.get("absent").is_none());
assert_eq!(cache.stats().misses, 1);
assert_eq!(cache.stats().hits, 0);
}
#[test]
fn capacity_zero_disables_cache() {
let cache = QueryFilterCache::new(0);
assert!(!cache.is_enabled());
cache.put("k".to_string(), set(&[1]));
assert!(cache.get("k").is_none());
assert!(cache.is_empty());
}
#[test]
fn lru_evicts_least_recently_used() {
let cache = QueryFilterCache::new(2);
cache.put("a".to_string(), set(&[1]));
cache.put("b".to_string(), set(&[2]));
assert!(cache.get("a").is_some());
cache.put("c".to_string(), set(&[3]));
assert!(cache.get("a").is_some(), "a was recently used");
assert!(cache.get("c").is_some(), "c was just inserted");
assert!(cache.get("b").is_none(), "b should have been evicted");
assert_eq!(cache.len(), 2);
}
}