use wide::f32x4;
pub const K1: f32 = 1.2;
pub const B: f32 = 0.75;
const IDF_SMOOTHING: f64 = 0.5;
const SCORE_SIMD_LANES: usize = 4;
#[inline]
pub fn idf(n_docs: u64, df: u64) -> f32 {
debug_assert!(df <= n_docs, "df ({df}) > n_docs ({n_docs})");
let n = n_docs as f64;
let df = df as f64;
let arg = 1.0 + (n - df + IDF_SMOOTHING) / (df + IDF_SMOOTHING);
arg.ln() as f32
}
#[inline(always)]
pub fn score(idf_t: f32, tf: u32, dl: u32, avgdl: f32) -> f32 {
let tf = tf as f32;
let norm = if avgdl > 0.0 {
1.0 - B + B * (dl as f32) / avgdl
} else {
1.0
};
let denom = tf + K1 * norm;
if denom == 0.0 {
return 0.0;
}
idf_t * tf * (K1 + 1.0) / denom
}
#[inline(always)]
pub fn score_with_dl_norm_k1(idf_x_k1p1: f32, tf: u32, dl_norm_k1: f32) -> f32 {
let tf = tf as f32;
idf_x_k1p1 * tf / (tf + dl_norm_k1)
}
#[inline(always)]
pub fn score_simd_x4(
idfs_x_k1p1: [f32; SCORE_SIMD_LANES],
tfs: [f32; SCORE_SIMD_LANES],
dl_norm_k1: f32,
) -> f32 {
let idf_v = f32x4::from(idfs_x_k1p1);
let tf_v = f32x4::from(tfs);
let denom = tf_v + f32x4::splat(dl_norm_k1);
let num = idf_v * tf_v;
let scores = num / denom;
scores.reduce_add()
}
#[inline]
pub fn block_upper_bound(idf_t: f32, max_tf: u32, min_dl: u32, avgdl: f32) -> f32 {
score(idf_t, max_tf, min_dl, avgdl)
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f32, b: f32, eps: f32) -> bool {
(a - b).abs() < eps
}
#[test]
fn idf_is_non_negative_for_all_valid_inputs() {
let cases = [
(1u64, 0u64),
(1, 1),
(10, 0),
(10, 1),
(10, 5),
(10, 10),
(1_000_000, 0),
(1_000_000, 1),
(1_000_000, 500_000),
(1_000_000, 1_000_000),
];
for (n, df) in cases {
let i = idf(n, df);
assert!(i >= 0.0, "idf({n},{df}) = {i} should be >= 0");
assert!(i.is_finite(), "idf({n},{df}) = {i} should be finite");
}
}
#[test]
fn idf_is_monotonic_in_df() {
let n = 1_000_000u64;
let dfs = [1u64, 10, 100, 1_000, 10_000, 100_000, 500_000];
let mut prev = f32::INFINITY;
for df in dfs {
let cur = idf(n, df);
assert!(
cur < prev,
"idf at df={df} ({cur}) should be < idf at smaller df ({prev})"
);
prev = cur;
}
}
#[test]
fn idf_reaches_zero_at_full_corpus() {
let i = idf(1_000_000, 1_000_000);
assert!(i > 0.0 && i < 1e-5, "idf at df=N ({i}) should be ≈ 0+");
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "df")]
fn idf_debug_panics_on_df_greater_than_n_docs() {
let _ = idf(10, 11);
}
#[test]
fn score_is_non_negative() {
let i = idf(1_000_000, 1_000);
for tf in [1, 2, 5, 10, 100] {
for dl in [1, 10, 100, 1_000, 10_000] {
for avgdl in [10.0, 100.0, 1_000.0] {
let s = score(i, tf, dl, avgdl);
assert!(
s >= 0.0,
"score(i={i}, tf={tf}, dl={dl}, avgdl={avgdl}) = {s}"
);
assert!(s.is_finite());
}
}
}
}
#[test]
fn score_grows_with_tf() {
let i = idf(1_000_000, 100);
let s1 = score(i, 1, 200, 200.0);
let s2 = score(i, 5, 200, 200.0);
let s3 = score(i, 100, 200, 200.0);
assert!(s1 < s2 && s2 < s3);
}
#[test]
fn score_saturates_with_tf() {
let i = idf(1_000_000, 100);
let s_low = score(i, 1, 200, 200.0);
let s_mid = score(i, 10, 200, 200.0);
let s_high = score(i, 1_000, 200, 200.0);
assert!(s_high > s_mid && s_mid > s_low);
assert!(
s_high < 2.0 * s_mid,
"tf should saturate, not scale linearly"
);
}
#[test]
fn score_decreases_with_doc_length() {
let i = idf(1_000_000, 100);
let s_short = score(i, 3, 50, 200.0);
let s_long = score(i, 3, 800, 200.0);
assert!(s_short > s_long);
}
#[test]
fn score_at_avgdl_uses_unit_norm() {
let i = 2.0_f32;
let tf = 5;
let avgdl = 200.0;
let dl = 200;
let expected = i * (tf as f32) * (K1 + 1.0) / ((tf as f32) + K1);
let actual = score(i, tf, dl, avgdl);
assert!(
approx(actual, expected, 1e-5),
"expected {expected}, got {actual}"
);
}
#[test]
fn score_handles_degenerate_avgdl_zero() {
let s = score(1.0, 1, 100, 0.0);
assert!(s.is_finite());
assert!(s >= 0.0);
}
#[test]
fn score_at_b_zero_drops_length_norm() {
let i = 2.0_f32;
let s_at_avgdl = score(i, 5, 200, 200.0);
let s_short = score(i, 5, 1, 200.0);
assert!(s_short > s_at_avgdl);
}
#[test]
fn score_at_b_one_extreme() {
let i = 2.0_f32;
let s_zero_dl = score(i, 5, 0, 200.0);
let s_one_dl = score(i, 5, 1, 200.0);
assert!(s_zero_dl > s_one_dl);
}
#[test]
fn upper_bound_at_extreme_inputs_matches_score() {
let i = idf(1_000_000, 1_000);
for max_tf in [1, 5, 50] {
for min_dl in [1, 100, 1_000] {
for avgdl in [50.0, 500.0] {
let ub = block_upper_bound(i, max_tf, min_dl, avgdl);
let s = score(i, max_tf, min_dl, avgdl);
assert!(approx(ub, s, 1e-6));
}
}
}
}
#[test]
fn upper_bound_is_real_upper_bound() {
let i = idf(1_000_000, 1_000);
let max_tf = 10;
let min_dl = 50;
let avgdl = 200.0;
let ub = block_upper_bound(i, max_tf, min_dl, avgdl);
for tf in 1..=max_tf {
for dl in min_dl..=(min_dl * 5) {
let s = score(i, tf, dl, avgdl);
assert!(
s <= ub + 1e-6,
"score(tf={tf}, dl={dl}) = {s} should be ≤ ub {ub}"
);
}
}
}
#[test]
fn lucene_defaults_match() {
assert!(approx(K1, 1.2, 1e-6));
assert!(approx(B, 0.75, 1e-6));
}
#[test]
fn simd_x4_equals_scalar_sum() {
let dl = 200u32;
let avgdl = 200.0;
let k1_norm = K1 * (1.0 - B + B * dl as f32 / avgdl);
let triples: [(f32, u32); 4] = [(1.5, 1), (1.7, 2), (2.0, 1), (1.2, 3)];
let scalar: f32 = triples
.iter()
.map(|(idf, tf)| score(*idf, *tf, dl, avgdl))
.sum();
let idfs_x_k1p1 = [
triples[0].0 * (K1 + 1.0),
triples[1].0 * (K1 + 1.0),
triples[2].0 * (K1 + 1.0),
triples[3].0 * (K1 + 1.0),
];
let tfs = [
triples[0].1 as f32,
triples[1].1 as f32,
triples[2].1 as f32,
triples[3].1 as f32,
];
let simd = score_simd_x4(idfs_x_k1p1, tfs, k1_norm);
assert!((scalar - simd).abs() < 1e-4, "simd={simd} scalar={scalar}");
}
}