const MIN_PAGES: u32 = 3;
const MIN_WEIGHT: f32 = 0.15;
pub struct TermWeights {
df: Vec<u32>,
pages: u32,
weights: Vec<f32>,
}
fn idf(n: u32, df: u32) -> f32 {
let n = n as f32;
(((n + 1.0) / (df as f32 + 0.5)).ln() / (n + 1.0).ln()).clamp(MIN_WEIGHT, 1.0)
}
impl TermWeights {
pub fn new(terms: usize) -> Self {
Self {
df: vec![0; terms],
pages: 0,
weights: vec![1.0; terms],
}
}
pub fn observe(&mut self, mask: u64) {
self.pages += 1;
for (i, d) in self.df.iter_mut().enumerate() {
if mask & (1 << i) != 0 {
*d += 1;
}
}
if self.pages < MIN_PAGES {
return;
}
for (w, &d) in self.weights.iter_mut().zip(&self.df) {
*w = idf(self.pages, d);
}
}
pub fn coverage(&self, mask: u64) -> f32 {
let total: f32 = self.weights.iter().sum();
if total <= 0.0 {
return 0.0;
}
let hit: f32 = self
.weights
.iter()
.enumerate()
.filter(|(i, _)| mask & (1 << i) != 0)
.map(|(_, w)| w)
.sum();
hit / total
}
pub fn current(&self) -> &[f32] {
&self.weights
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn common_terms_lose_weight_after_a_few_pages() {
let mut w = TermWeights::new(3);
for page in 0..6u64 {
w.observe(0b001 | if page % 3 == 0 { 0b010 } else { 0 });
}
let ws = w.current();
assert!(ws[0] < ws[1] && ws[1] < ws[2], "{ws:?}");
assert_eq!(ws[2], 1.0);
assert!(ws[0] >= MIN_WEIGHT);
assert!(w.coverage(0b001) < w.coverage(0b100));
assert_eq!(w.coverage(0b111), 1.0);
}
#[test]
fn before_enough_pages_all_terms_are_equal() {
let mut w = TermWeights::new(2);
w.observe(0b01);
w.observe(0b01);
assert_eq!(w.current(), &[1.0, 1.0]);
assert_eq!(w.coverage(0b01), 0.5);
}
}