use super::InvertedPartition;
use std::collections::HashMap;
use std::sync::Arc;
pub trait Scorer: Send + Sync {
fn query_weight(&self, token: &str) -> f32;
fn doc_weight(&self, freq: u32, doc_tokens: u32) -> f32;
fn doc_weight_upper_bound(&self) -> Option<f32> {
None
}
fn doc_weight_cache_key(&self) -> Option<u64> {
None
}
fn doc_norm(&self, doc_tokens: u32) -> Option<f32> {
let _ = doc_tokens;
None
}
}
impl<T: Scorer + ?Sized> Scorer for Arc<T> {
fn query_weight(&self, token: &str) -> f32 {
self.as_ref().query_weight(token)
}
fn doc_weight(&self, freq: u32, doc_tokens: u32) -> f32 {
self.as_ref().doc_weight(freq, doc_tokens)
}
fn doc_weight_upper_bound(&self) -> Option<f32> {
self.as_ref().doc_weight_upper_bound()
}
fn doc_weight_cache_key(&self) -> Option<u64> {
self.as_ref().doc_weight_cache_key()
}
fn doc_norm(&self, doc_tokens: u32) -> Option<f32> {
self.as_ref().doc_norm(doc_tokens)
}
}
#[inline]
pub(super) fn bm25_doc_weight_with_norm(freq: u32, doc_norm: f32) -> f32 {
let freq = freq as f32;
(K1 + 1.0) * freq / (freq + doc_norm)
}
pub const K1: f32 = 1.2;
pub const B: f32 = 0.75;
#[inline]
fn bm25_doc_norm(doc_tokens: u32, avg_doc_length: f32) -> f32 {
let doc_tokens = doc_tokens as f32;
K1 * (1.0 - B + B * doc_tokens / avg_doc_length)
}
#[derive(Debug, Clone)]
pub struct MemBM25Scorer {
pub total_tokens: u64,
pub num_docs: usize,
pub token_docs: HashMap<String, usize>,
}
impl MemBM25Scorer {
pub fn new(total_tokens: u64, num_docs: usize, token_docs: HashMap<String, usize>) -> Self {
Self {
total_tokens,
num_docs,
token_docs,
}
}
pub fn update(&mut self, doc_token_count: &HashMap<String, usize>, num_tokens: u64) {
self.total_tokens += num_tokens;
self.num_docs += 1;
for (token, count) in doc_token_count {
if let Some(old_count) = self.token_docs.get_mut(token) {
*old_count += *count;
} else {
log::warn!("Token {} not found in token_docs", token);
}
}
}
pub fn num_docs(&self) -> usize {
self.num_docs
}
pub fn avg_doc_length(&self) -> f32 {
self.total_tokens as f32 / self.num_docs as f32
}
pub fn num_docs_containing_token(&self, token: &str) -> usize {
match self.token_docs.get(token) {
Some(nq) => *nq,
None => 0,
}
}
}
impl Scorer for MemBM25Scorer {
fn query_weight(&self, token: &str) -> f32 {
let token_docs = self.num_docs_containing_token(token);
if token_docs == 0 {
return 0.0;
}
idf(token_docs, self.num_docs)
}
fn doc_weight(&self, freq: u32, doc_tokens: u32) -> f32 {
let doc_norm = bm25_doc_norm(doc_tokens, self.avg_doc_length());
bm25_doc_weight_with_norm(freq, doc_norm)
}
fn doc_norm(&self, doc_tokens: u32) -> Option<f32> {
Some(bm25_doc_norm(doc_tokens, self.avg_doc_length()))
}
fn doc_weight_upper_bound(&self) -> Option<f32> {
Some(K1 + 1.0)
}
fn doc_weight_cache_key(&self) -> Option<u64> {
Some(u64::from(self.avg_doc_length().to_bits()))
}
}
pub struct IndexBM25Scorer<'a> {
partitions: Vec<&'a InvertedPartition>,
num_docs: usize,
avg_doc_length: f32,
}
impl<'a> IndexBM25Scorer<'a> {
pub fn new(partitions: impl Iterator<Item = &'a InvertedPartition>) -> Self {
let partitions = partitions.collect::<Vec<_>>();
let num_docs = partitions.iter().map(|p| p.docs.len()).sum();
let total_tokens: u64 = partitions
.iter()
.map(|p| {
p.docs.total_tokens_cached().expect(
"IndexBM25Scorer::new requires each partition's total_tokens to be \
cached; call `ensure_loaded` / `ensure_num_tokens_loaded` / \
`total_tokens_num` first",
)
})
.sum();
let avgdl = total_tokens as f32 / num_docs as f32;
Self {
partitions,
num_docs,
avg_doc_length: avgdl,
}
}
pub fn num_docs_containing_token(&self, token: &str) -> usize {
self.partitions
.iter()
.map(|part| {
if let Some(token_id) = part.tokens.get(token) {
part.inverted_list.posting_len(token_id)
} else {
0
}
})
.sum()
}
}
impl Scorer for IndexBM25Scorer<'_> {
fn query_weight(&self, token: &str) -> f32 {
let token_docs = self.num_docs_containing_token(token);
if token_docs == 0 {
return 0.0;
}
idf(token_docs, self.num_docs)
}
fn doc_weight(&self, freq: u32, doc_tokens: u32) -> f32 {
let doc_norm = bm25_doc_norm(doc_tokens, self.avg_doc_length);
bm25_doc_weight_with_norm(freq, doc_norm)
}
fn doc_norm(&self, doc_tokens: u32) -> Option<f32> {
Some(bm25_doc_norm(doc_tokens, self.avg_doc_length))
}
fn doc_weight_upper_bound(&self) -> Option<f32> {
Some(K1 + 1.0)
}
fn doc_weight_cache_key(&self) -> Option<u64> {
Some(u64::from(self.avg_doc_length.to_bits()))
}
}
#[inline]
pub fn idf(token_docs: usize, num_docs: usize) -> f32 {
let num_docs = num_docs as f32;
((num_docs - token_docs as f32 + 0.5) / (token_docs as f32 + 0.5) + 1.0).ln()
}