use std::collections::HashMap;
use super::{CorpusStats, TextSegment};
use crate::bm25::bm25_score;
#[derive(Clone, Copy)]
pub(crate) struct Scope<'a> {
pub(crate) stats: Option<&'a CorpusStats>,
pub(crate) n_docs: f64,
pub(crate) avgdl: f64,
pub(crate) want: &'a [usize],
}
impl Scope<'_> {
pub(crate) fn scoped(&self) -> bool {
!self.want.is_empty()
}
fn df(&self, t: &[u8], local: usize) -> f64 {
self.stats.and_then(|s| s.df.get(t)).map(|&d| f64::from(d)).unwrap_or(local as f64)
}
}
impl TextSegment {
pub(crate) fn scope_stats(&self, stats: Option<&CorpusStats>, want: &[usize]) -> (f64, f64) {
if want.is_empty() || stats.is_some() {
return self.corpus_stats(stats);
}
let n = self.docs.len() as f64;
(n, self.total_len_in(want) as f64 / n)
}
pub fn total_len_in(&self, fields: &[usize]) -> u64 {
match self.fields.as_ref() {
Some(fs) if !fields.is_empty() => fs.total_len_in(fields),
_ => self.total_len,
}
}
pub(crate) fn add_typo(
&self,
t: &[u8],
budget: u32,
scores: &mut HashMap<u32, f64>,
sc: &Scope,
) {
if budget == 0 {
self.add_term(t, scores, sc);
return;
}
for cand in self.expand_typo(t, budget) {
self.add_term(cand, scores, sc);
}
}
pub(crate) fn add_prefix(&self, pfx: &[u8], scores: &mut HashMap<u32, f64>, sc: &Scope) {
for t in self.expand_prefix(pfx) {
self.add_term(t, scores, sc);
}
}
pub(crate) fn add_term(&self, t: &[u8], scores: &mut HashMap<u32, f64>, sc: &Scope) {
if sc.scoped() {
self.add_term_scoped(t, scores, sc);
return;
}
let Some(list) = self.postings.get(t) else { return };
let df = sc.df(t, list.len());
for (tf, bands) in list.tf_groups() {
for (_b, band) in bands.iter() {
for &id in band {
let dl = f64::from(self.id_dl[id as usize]);
*scores.entry(id).or_insert(0.0) +=
bm25_score(f64::from(tf), df, sc.n_docs, dl, sc.avgdl);
}
}
}
}
fn add_term_scoped(&self, t: &[u8], scores: &mut HashMap<u32, f64>, sc: &Scope) {
let Some(fs) = self.fields.as_ref() else { return };
let docs = fs.docs_in(t, sc.want);
let df = sc.df(t, docs.len());
for (id, tf) in docs {
let dl = f64::from(fs.doc_len_in(id, sc.want));
*scores.entry(id).or_insert(0.0) +=
bm25_score(f64::from(tf), df, sc.n_docs, dl, sc.avgdl);
}
}
pub(crate) fn clause_score(&self, distinct: &[Vec<u8>], id: u32, sc: &Scope) -> f64 {
if sc.scoped() {
return self.clause_score_scoped(distinct, id, sc);
}
let dl = f64::from(self.id_dl[id as usize]);
let mut score = 0.0;
for t in distinct {
let Some(list) = self.postings.get(t) else { continue };
let Some(tf) = list.get(id) else { continue };
score += bm25_score(f64::from(tf), sc.df(t, list.len()), sc.n_docs, dl, sc.avgdl);
}
score
}
fn clause_score_scoped(&self, distinct: &[Vec<u8>], id: u32, sc: &Scope) -> f64 {
let Some(fs) = self.fields.as_ref() else { return 0.0 };
let dl = f64::from(fs.doc_len_in(id, sc.want));
let mut score = 0.0;
for t in distinct {
let docs = fs.docs_in(t, sc.want);
let Some(&(_, tf)) = docs.iter().find(|(d, _)| *d == id) else { continue };
score += bm25_score(f64::from(tf), sc.df(t, docs.len()), sc.n_docs, dl, sc.avgdl);
}
score
}
pub(crate) fn expand_prefix(&self, pfx: &[u8]) -> Vec<&[u8]> {
let mut e: Vec<&[u8]> = match pfx.first() {
None => self.postings.keys().map(Vec::as_slice).collect(),
Some(&head) => self
.postings
.keys()
.map(Vec::as_slice)
.filter(|t| t.first() == Some(&head) && t.starts_with(pfx))
.collect(),
};
e.sort_unstable();
e
}
pub(crate) fn expand_typo(&self, t: &[u8], budget: u32) -> Vec<&[u8]> {
let mut e: Vec<&[u8]> = self
.postings
.keys()
.map(Vec::as_slice)
.filter(|cand| crate::edit::edit_within(t, cand, budget).is_some())
.collect();
e.sort_unstable();
e
}
pub(crate) fn rarest_anchor<'a>(&self, toks: &'a [Vec<u8>]) -> Option<&'a [u8]> {
let mut best: Option<(&'a [u8], usize)> = None;
for t in toks {
let df = self.postings.get(t)?.len();
if best.is_none_or(|(_, b)| df < b) {
best = Some((t, df));
}
}
best.map(|(t, _)| t)
}
pub(crate) fn phrase_in_scope(&self, id: u32, start: u32, len: u32, want: &[usize]) -> bool {
let Some(fs) = self.fields.as_ref() else { return false };
let lens = fs.doc_lens(id);
let mut base = 0u32;
for (f, &n) in lens.iter().enumerate() {
if want.contains(&f) && start >= base && start + len <= base + n {
return true;
}
base += n;
}
false
}
}