use std::borrow::{Borrow, Cow};
use std::collections::hash_map::Entry;
use std::hash::Hash;
use std::sync::Arc;
use foldhash::HashMap as FastMap;
use super::file_table::FileId;
pub(super) trait FilePosting: Sized + Clone {
fn file(&self) -> FileId;
fn normalize(_hits: &mut Vec<Self>) {}
fn append(hits: &mut Vec<Self>, incoming: impl IntoIterator<Item = Self>) {
hits.extend(incoming);
}
fn remove(hits: &mut Vec<Self>, file: FileId) {
hits.retain(|hit| hit.file() != file);
}
}
impl FilePosting for FileId {
fn file(&self) -> FileId {
*self
}
fn normalize(hits: &mut Vec<Self>) {
hits.sort_unstable();
hits.dedup();
}
fn append(hits: &mut Vec<Self>, incoming: impl IntoIterator<Item = Self>) {
for file in incoming {
if let Err(position) = hits.binary_search(&file) {
hits.insert(position, file);
}
}
}
fn remove(hits: &mut Vec<Self>, file: FileId) {
if let Ok(position) = hits.binary_search(&file) {
hits.remove(position);
}
}
}
const TOMBSTONE_FLOOR: usize = 256;
const DELTA_FLOOR: usize = 4096;
#[derive(Debug, Clone)]
pub(super) struct FilePostings<K, H> {
base: Arc<FastMap<K, Vec<H>>>,
stale: FileSet,
forward: FastMap<K, Vec<H>>,
reverse: FastMap<FileId, Vec<K>>,
}
impl<K, H> Default for FilePostings<K, H> {
fn default() -> Self {
Self {
base: Arc::default(),
stale: FileSet::default(),
forward: FastMap::default(),
reverse: FastMap::default(),
}
}
}
impl<K: Eq + Hash + Clone, H: FilePosting> FilePostings<K, H> {
pub fn get<Q: Eq + Hash + ?Sized>(&self, key: &Q) -> Cow<'_, [H]>
where
K: Borrow<Q>,
{
let base = self.base.get(key).map_or(&[][..], Vec::as_slice);
let delta = self.forward.get(key).map_or(&[][..], Vec::as_slice);
let base_live =
self.stale.is_empty() || !base.iter().any(|hit| self.stale.contains(hit.file()));
if base_live && delta.is_empty() {
return Cow::Borrowed(base);
}
if base.is_empty() {
return Cow::Borrowed(delta);
}
let mut hits: Vec<H> = base
.iter()
.filter(|hit| !self.stale.contains(hit.file()))
.cloned()
.collect();
H::append(&mut hits, delta.iter().cloned());
Cow::Owned(hits)
}
pub fn key<Q: Eq + Hash + ?Sized>(&self, key: &Q) -> Option<&K>
where
K: Borrow<Q>,
{
self.forward
.get_key_value(key)
.or_else(|| self.base.get_key_value(key))
.map(|(key, _)| key)
}
pub fn rows(&self) -> Vec<(&K, Cow<'_, [H]>)> {
let mut rows: Vec<(&K, Cow<'_, [H]>)> = self
.base
.keys()
.map(|key| (key, self.get(key)))
.filter(|(_, hits)| !hits.is_empty())
.collect();
rows.extend(
self.forward
.iter()
.filter(|(key, _)| !self.base.contains_key(*key))
.map(|(key, hits)| (key, Cow::Borrowed(hits.as_slice()))),
);
rows
}
pub fn keys(&self) -> impl Iterator<Item = &K> {
self.base.keys().chain(
self.forward
.keys()
.filter(|key| !self.base.contains_key(*key)),
)
}
pub fn len(&self) -> usize {
self.rows().len()
}
pub fn remove_file(&mut self, file: FileId) {
self.stale.insert(file);
let Some(terms) = self.reverse.remove(&file) else {
return;
};
for term in terms {
if let Some(hits) = self.forward.get_mut(&term) {
H::remove(hits, file);
if hits.is_empty() {
self.forward.remove(&term);
}
}
}
}
pub fn replace_file<T: IntoIterator<Item = H>>(
&mut self,
file: FileId,
rows: impl IntoIterator<Item = (K, T)>,
) {
self.remove_file(file);
let mut terms = Vec::new();
for (key, hits) in rows {
let entry = self.forward.entry(key);
terms.push(entry.key().clone());
let incoming = hits
.into_iter()
.inspect(|hit| debug_assert_eq!(hit.file(), file));
H::append(entry.or_default(), incoming);
}
if !terms.is_empty() {
self.reverse.insert(file, terms);
}
}
pub fn from_rows(rows: impl ExactSizeIterator<Item = (K, Vec<H>)>) -> Self {
let mut base: FastMap<K, Vec<H>> =
FastMap::with_capacity_and_hasher(rows.len(), Default::default());
for (key, hits) in rows {
match base.entry(key) {
Entry::Vacant(entry) => {
entry.insert(hits);
}
Entry::Occupied(mut entry) => {
entry.get_mut().extend(hits);
}
}
}
base.retain(|_, hits| {
H::normalize(hits);
!hits.is_empty()
});
Self {
base: Arc::new(base),
..Self::default()
}
}
pub fn compact_if_heavy(&mut self) {
let heavy_delta = self.forward.len() > DELTA_FLOOR.max(self.base.len() / 4);
if heavy_delta || self.stale.len() > TOMBSTONE_FLOOR {
self.compact();
}
}
pub fn compact(&mut self) {
let mut forward = std::mem::take(&mut self.forward);
let stale = std::mem::take(&mut self.stale);
self.reverse.clear();
let mut base = Arc::try_unwrap(std::mem::take(&mut self.base))
.unwrap_or_else(|shared| (*shared).clone());
base.retain(|key, hits| {
if !stale.is_empty() {
hits.retain(|hit| !stale.contains(hit.file()));
}
if let Some(delta) = forward.remove(key) {
H::append(hits, delta);
}
!hits.is_empty()
});
base.extend(forward);
self.base = Arc::new(base);
}
pub fn estimated_bytes(&self) -> usize {
let map_bytes = |map: &FastMap<K, Vec<H>>| {
map.capacity() * (std::mem::size_of::<K>() + std::mem::size_of::<Vec<H>>())
+ map
.values()
.map(|hits| hits.capacity() * std::mem::size_of::<H>())
.sum::<usize>()
};
map_bytes(&self.base)
+ map_bytes(&self.forward)
+ self.reverse.capacity()
* (std::mem::size_of::<FileId>() + std::mem::size_of::<Vec<K>>())
+ self
.reverse
.values()
.map(|terms| terms.capacity() * std::mem::size_of::<K>())
.sum::<usize>()
+ self.stale.bytes()
}
}
#[derive(Debug, Default, Clone)]
struct FileSet {
bits: Vec<u64>,
len: usize,
}
impl FileSet {
fn insert(&mut self, file: FileId) {
let (word, bit) = (file as usize / 64, file % 64);
if word >= self.bits.len() {
self.bits.resize(word + 1, 0);
}
if self.bits[word] & (1 << bit) == 0 {
self.bits[word] |= 1 << bit;
self.len += 1;
}
}
fn contains(&self, file: FileId) -> bool {
self.bits
.get(file as usize / 64)
.is_some_and(|word| word & (1 << (file % 64)) != 0)
}
fn is_empty(&self) -> bool {
self.len == 0
}
fn len(&self) -> usize {
self.len
}
fn bytes(&self) -> usize {
self.bits.capacity() * std::mem::size_of::<u64>()
}
}
#[cfg(test)]
#[path = "posting_store_tests.rs"]
mod tests;