use alloc::vec::Vec;
#[cfg(feature = "counters")]
use core::cell::Cell;
use plugmem_arena::{Arena, ArenaCfg, ShardMode, Slot, key};
use crate::error::Error;
use crate::id::FactId;
use crate::index::postings::PostingStore;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DocLenSlot {
pub fact: FactId,
pub len: u16,
}
impl Slot for DocLenSlot {
const SIZE: usize = 8;
const KEY_LEN: usize = 4;
fn write(&self, out: &mut [u8]) {
key::write_u32(out, self.fact.0);
out[4..6].copy_from_slice(&self.len.to_be_bytes());
out[6..8].copy_from_slice(&[0, 0]);
}
fn read(bytes: &[u8]) -> Self {
Self {
fact: FactId(key::read_u32(bytes)),
len: u16::from_be_bytes(bytes[4..6].try_into().unwrap()),
}
}
}
#[derive(Debug, Default)]
pub struct Bm25Scratch {
acc: hashbrown::HashMap<u32, f32, xxhash_rust::xxh3::Xxh3Builder>,
top: Vec<(f32, u32)>,
}
impl Bm25Scratch {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Debug)]
pub struct Bm25Index<'a> {
postings: PostingStore<'a, true>,
doc_len: Arena<'a, DocLenSlot>,
total_docs: u64,
total_len: u64,
#[cfg(feature = "counters")]
decoded: Cell<u64>,
}
impl<'a> Bm25Index<'a> {
pub fn new(shards: usize, max_bytes: usize) -> Result<Self, Error> {
Ok(Self {
postings: PostingStore::new(shards, max_bytes)?,
doc_len: Arena::new(
ArenaCfg::new(shards, ShardMode::Uniform).with_max_bytes(max_bytes),
)?,
total_docs: 0,
total_len: 0,
#[cfg(feature = "counters")]
decoded: Cell::new(0),
})
}
pub fn index_doc(&mut self, fact: FactId, term_tfs: &[(u32, u8)]) -> Result<(), Error> {
let mut len = 0u32;
for &(term, tf) in term_tfs {
self.postings.push(term, fact, tf)?;
len += u32::from(tf);
}
self.doc_len.insert(&DocLenSlot {
fact,
len: u16::try_from(len).unwrap_or(u16::MAX),
})?;
self.total_docs += 1;
self.total_len += u64::from(len);
Ok(())
}
pub fn df(&self, term: u32) -> u32 {
self.postings.count(term)
}
pub fn docs(&self) -> u64 {
self.total_docs
}
pub fn idf(&self, df: u32) -> f32 {
let n = self.total_docs as f32;
let df = df as f32;
libm::logf(1.0 + (n - df + 0.5) / (df + 0.5))
}
pub fn search(
&self,
(k1, b): (f32, f32),
terms: &[u32],
k: usize,
live: &mut dyn FnMut(FactId) -> bool,
scratch: &mut Bm25Scratch,
out: &mut Vec<(FactId, f32)>,
) {
out.clear();
if self.total_docs == 0 || k == 0 {
return;
}
scratch.acc.clear();
let avg_len = self.total_len as f32 / self.total_docs as f32;
#[cfg(feature = "counters")]
let mut decoded = 0u64;
for &term in terms {
let df = self.postings.count(term);
if df == 0 {
continue;
}
let idf = self.idf(df);
for (fact, tf) in self.postings.entries(term) {
#[cfg(feature = "counters")]
{
decoded += 1;
}
let Some(doc) = self.doc_len.get(&fact.0.to_be_bytes()) else {
continue;
};
let tf = f32::from(tf);
let norm =
tf * (k1 + 1.0) / (tf + k1 * (1.0 - b + b * f32::from(doc.len) / avg_len));
*scratch.acc.entry(fact.0).or_insert(0.0) += idf * norm;
}
}
#[cfg(feature = "counters")]
self.decoded.set(self.decoded.get() + decoded);
scratch.top.clear();
for (&id, &score) in &scratch.acc {
if live(FactId(id)) {
scratch.top.push((score, id));
}
}
scratch
.top
.sort_unstable_by(|a, b| b.0.total_cmp(&a.0).then(a.1.cmp(&b.1)));
for &(score, id) in scratch.top.iter().take(k) {
out.push((FactId(id), score));
}
}
pub fn pool_bytes(&self) -> usize {
self.postings.pool_bytes() + self.doc_len.pool_bytes()
}
pub fn total_len(&self) -> u64 {
self.total_len
}
pub(crate) fn postings(&self) -> &PostingStore<'a, true> {
&self.postings
}
pub(crate) fn doc_len_arena(&self) -> &Arena<'a, DocLenSlot> {
&self.doc_len
}
pub(crate) fn from_parts(
postings: PostingStore<'a, true>,
doc_len: Arena<'a, DocLenSlot>,
total_docs: u64,
total_len: u64,
) -> Self {
Self {
postings,
doc_len,
total_docs,
total_len,
#[cfg(feature = "counters")]
decoded: Cell::new(0),
}
}
#[cfg(feature = "counters")]
pub fn decoded(&self) -> u64 {
self.decoded.get()
}
#[cfg(feature = "counters")]
pub fn reset_decoded(&self) {
self.decoded.set(0);
}
}