use std::collections::{HashMap, VecDeque};
use std::path::PathBuf;
use std::sync::{Arc, Mutex, OnceLock};
use roaring::RoaringBitmap;
use crate::index::overlay::OverlayView;
use crate::index::segment::MmapSegment;
use crate::path::PathIndex;
pub(crate) const POSTING_BITMAP_CACHE_MAX_BYTES: usize = 256 * 1024 * 1024;
struct PostingCache {
map: HashMap<u64, (Arc<RoaringBitmap>, usize)>,
order: VecDeque<u64>,
bytes: usize,
}
impl PostingCache {
fn new() -> Self {
Self {
map: HashMap::new(),
order: VecDeque::new(),
bytes: 0,
}
}
fn get(&self, key: u64) -> Option<Arc<RoaringBitmap>> {
self.map.get(&key).map(|(bm, _)| Arc::clone(bm))
}
fn insert_with_budget(
&mut self,
key: u64,
bitmap: Arc<RoaringBitmap>,
budget: usize,
) -> Arc<RoaringBitmap> {
if let Some((existing, _)) = self.map.get(&key) {
return Arc::clone(existing);
}
let size = bitmap.serialized_size();
while self.bytes + size > budget {
let Some(old) = self.order.pop_front() else {
break; };
if let Some((_, old_size)) = self.map.remove(&old) {
self.bytes -= old_size;
}
}
self.bytes += size;
self.order.push_back(key);
self.map.insert(key, (Arc::clone(&bitmap), size));
bitmap
}
fn insert(&mut self, key: u64, bitmap: Arc<RoaringBitmap>) -> Arc<RoaringBitmap> {
self.insert_with_budget(key, bitmap, POSTING_BITMAP_CACHE_MAX_BYTES)
}
}
pub struct BaseSegments {
pub segments: Vec<MmapSegment>,
pub base_ids: Vec<u32>,
pub base_doc_paths: Vec<Option<PathBuf>>,
pub path_doc_ids: HashMap<PathBuf, Vec<u32>>,
pub(crate) base_doc_to_file_id: OnceLock<Arc<Vec<u32>>>,
}
impl BaseSegments {
pub(crate) fn base_doc_to_file_id(&self, path_index: &crate::path::PathIndex) -> Arc<Vec<u32>> {
Arc::clone(self.base_doc_to_file_id.get_or_init(|| {
let mut map = vec![u32::MAX; self.base_doc_paths.len()];
for (gid, path) in self.base_doc_paths.iter().enumerate() {
if let Some(path) = path {
if let Some(fid) = path_index.file_id(path) {
map[gid] = fid;
}
}
}
Arc::new(map)
}))
}
}
pub struct IndexSnapshot {
pub base: Arc<BaseSegments>,
pub overlay: OverlayView,
pub delete_set: RoaringBitmap,
pub path_index: PathIndex,
pub overlay_doc_to_file_id: HashMap<u32, u32>,
all_doc_ids_cache: OnceLock<RoaringBitmap>,
posting_bitmap_cache: OnceLock<Mutex<PostingCache>>,
pub(crate) glob_cache: OnceLock<Mutex<HashMap<String, RoaringBitmap>>>,
pub scan_threshold: f64,
}
impl IndexSnapshot {
pub fn base_doc_to_file_id(&self) -> Arc<Vec<u32>> {
self.base.base_doc_to_file_id(&self.path_index)
}
pub fn base_segments(&self) -> &[MmapSegment] {
&self.base.segments
}
pub fn segment_base_ids(&self) -> &[u32] {
&self.base.base_ids
}
pub fn all_doc_ids(&self) -> &RoaringBitmap {
self.all_doc_ids_cache.get_or_init(|| {
let mut bm = RoaringBitmap::new();
for (seg_idx, seg) in self.base.segments.iter().enumerate() {
let base = self.base.base_ids.get(seg_idx).copied().unwrap_or(0);
for local in 0..seg.doc_count {
let global = base + local;
if !self.delete_set.contains(global) {
bm.insert(global);
}
}
}
for doc in &self.overlay.docs {
bm.insert(doc.doc_id);
}
bm
})
}
fn posting_bitmap_cache(&self) -> &Mutex<PostingCache> {
self.posting_bitmap_cache
.get_or_init(|| Mutex::new(PostingCache::new()))
}
pub(crate) fn cached_posting_bitmap(&self, gram_hash: u64) -> Option<Arc<RoaringBitmap>> {
let cache = self
.posting_bitmap_cache()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
cache.get(gram_hash)
}
pub(crate) fn store_posting_bitmap(
&self,
gram_hash: u64,
bitmap: Arc<RoaringBitmap>,
) -> Arc<RoaringBitmap> {
let mut cache = self
.posting_bitmap_cache()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
cache.insert(gram_hash, bitmap)
}
#[cfg(test)]
pub(crate) fn clone_for_test(&self) -> IndexSnapshot {
IndexSnapshot {
base: Arc::clone(&self.base),
overlay: self.overlay.clone(),
delete_set: self.delete_set.clone(),
path_index: self.path_index.clone(),
overlay_doc_to_file_id: self.overlay_doc_to_file_id.clone(),
scan_threshold: self.scan_threshold,
all_doc_ids_cache: OnceLock::new(),
posting_bitmap_cache: OnceLock::new(),
glob_cache: OnceLock::new(),
}
}
#[cfg(test)]
pub(crate) fn with_scan_threshold(&self, threshold: f64) -> IndexSnapshot {
IndexSnapshot {
scan_threshold: threshold,
..self.clone_for_test()
}
}
#[cfg(test)]
pub(crate) fn posting_bitmap_cache_len(&self) -> usize {
self.posting_bitmap_cache
.get()
.map(|cache| {
cache
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.map
.len()
})
.unwrap_or(0)
}
}
pub fn new_snapshot(
base: Arc<BaseSegments>,
overlay: crate::index::overlay::OverlayView,
delete_set: roaring::RoaringBitmap,
path_index: crate::path::PathIndex,
overlay_doc_to_file_id: HashMap<u32, u32>,
scan_threshold: f64,
) -> IndexSnapshot {
IndexSnapshot {
base,
overlay,
delete_set,
path_index,
overlay_doc_to_file_id,
scan_threshold,
all_doc_ids_cache: OnceLock::new(),
posting_bitmap_cache: OnceLock::new(),
glob_cache: OnceLock::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bitmap(id: u32) -> Arc<RoaringBitmap> {
Arc::new(RoaringBitmap::from_iter([id]))
}
#[test]
fn posting_cache_evicts_oldest_first_under_byte_budget() {
let mut cache = PostingCache::new();
let one = bitmap(0).serialized_size();
let budget = one * 2;
cache.insert_with_budget(0, bitmap(0), budget);
cache.insert_with_budget(1, bitmap(1), budget);
cache.insert_with_budget(2, bitmap(2), budget);
assert!(cache.get(0).is_none(), "oldest entry must be evicted");
assert!(
cache.get(1).is_some(),
"recent entry must survive the cliff"
);
assert!(cache.get(2).is_some(), "newest entry must be present");
assert!(cache.bytes <= budget, "byte total stays bounded");
}
#[test]
fn posting_cache_keeps_new_entry_even_if_alone_over_budget() {
let mut cache = PostingCache::new();
cache.insert_with_budget(7, bitmap(7), 0);
assert!(cache.get(7).is_some());
}
#[test]
fn posting_cache_dedups_and_returns_existing() {
let mut cache = PostingCache::new();
let first = cache.insert(3, bitmap(3));
let second = cache.insert(3, bitmap(3));
assert!(Arc::ptr_eq(&first, &second), "duplicate returns stored Arc");
assert_eq!(cache.map.len(), 1);
}
}