use super::{TermId, TextIndex};
use std::cmp::Ordering;
use std::collections::BinaryHeap;
pub const K1: f64 = 1.2;
pub const B: f64 = 0.75;
pub fn idf(total_docs: usize, df: usize) -> f64 {
let n = total_docs as f64;
let df = df as f64;
(1.0 + (n - df + 0.5) / (df + 0.5)).ln()
}
#[derive(Clone, Copy, Debug)]
pub struct QueryTerm {
pub term: TermId,
pub idf: f64,
}
#[derive(Clone, Debug, Default)]
pub struct PreparedQuery {
terms: Vec<QueryTerm>,
}
impl PreparedQuery {
pub fn terms(&self) -> &[QueryTerm] {
&self.terms
}
pub fn is_empty(&self) -> bool {
self.terms.is_empty()
}
}
#[derive(Clone, Copy, Debug)]
pub struct ScoredDoc {
pub slot: u32,
pub score: f64,
}
impl PartialEq for ScoredDoc {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Eq for ScoredDoc {}
impl PartialOrd for ScoredDoc {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ScoredDoc {
fn cmp(&self, other: &Self) -> Ordering {
other
.score
.total_cmp(&self.score)
.then(self.slot.cmp(&other.slot))
}
}
impl TextIndex {
pub fn prepare_query(&self, query: &str) -> PreparedQuery {
let total = self.total_docs();
let mut terms: Vec<QueryTerm> = Vec::new();
for token in super::analyze(query) {
let Some(term) = self.term_id(&token) else {
continue;
};
if terms.iter().any(|seen| seen.term == term) {
continue;
}
terms.push(QueryTerm {
term,
idf: idf(total, self.postings_of(term).len()),
});
}
PreparedQuery { terms }
}
pub fn score(&self, slot: u32, query: &PreparedQuery) -> f64 {
let Some(doc) = self.docs.get(&slot) else {
return 0.0;
};
let avgdl = self.avgdl();
if avgdl <= 0.0 {
return 0.0;
}
let norm = K1 * (1.0 - B + B * (f64::from(doc.len) / avgdl));
let mut total = 0.0;
for term in &query.terms {
let tf = doc.term_freq(term.term);
if tf == 0 {
continue;
}
let tf = f64::from(tf);
total += term.idf * (tf * (K1 + 1.0)) / (tf + norm);
}
total
}
pub fn top_k(&self, query: &PreparedQuery, k: usize) -> Vec<ScoredDoc> {
if k == 0 || query.is_empty() {
return Vec::new();
}
let mut candidates: Vec<u32> = Vec::new();
for term in &query.terms {
candidates.extend(self.postings_of(term.term).iter().map(|p| p.slot));
}
candidates.sort_unstable();
candidates.dedup();
let mut heap: BinaryHeap<ScoredDoc> = BinaryHeap::with_capacity(k + 1);
for slot in candidates {
heap.push(ScoredDoc {
slot,
score: self.score(slot, query),
});
if heap.len() > k {
heap.pop();
}
}
heap.into_sorted_vec()
}
}