use super::*;
fn stopwords(words: &[&str]) -> Stopwords {
Stopwords::parse(&words.join("\n"))
}
fn token_text(tokens: &[&str]) -> Vec<String> {
tokens.iter().map(|t| t.to_string()).collect()
}
fn rescore(weight: f32) -> f32 {
(1.0 + weight).ln().powf(ALPHA)
}
#[test]
fn a_word_split_into_two_pieces_sums_its_attention_across_the_span() {
let stopwords = stopwords(&[]);
let tokens = token_text(&["un", "##happy"]);
let embedding = pool_row(&[0.3, 0.2], &[0, 1], &tokens, &stopwords);
let expected_index = bm25_hash(&stem("unhappy"));
assert_eq!(embedding.indices, vec![expected_index]);
let want = rescore(0.5);
assert!(
(embedding.values[0] - want).abs() < 1e-6,
"{} vs {want} (a mean over the span would give {})",
embedding.values[0],
rescore(0.25)
);
}
#[test]
fn a_stopword_contributes_nothing() {
let stopwords = stopwords(&["the"]);
let tokens = token_text(&["the"]);
let embedding = pool_row(&[0.9], &[0], &tokens, &stopwords);
assert!(embedding.is_empty());
}
#[test]
fn a_bare_ascii_punctuation_character_contributes_nothing() {
let stopwords = stopwords(&[]);
let tokens = token_text(&["."]);
let embedding = pool_row(&[0.5], &[0], &tokens, &stopwords);
assert!(embedding.is_empty());
}
#[test]
fn special_tokens_contribute_nothing() {
let stopwords = stopwords(&[]);
let tokens = token_text(&["[CLS]", "cat", "[SEP]"]);
let embedding = pool_row(&[0.7, 0.2, 0.6], &[0, 1, 2], &tokens, &stopwords);
assert_eq!(embedding.indices, vec![bm25_hash(&stem("cat"))]);
}
#[test]
fn a_word_with_embedded_punctuation_survives_whole_rather_than_being_split() {
let stopwords = stopwords(&[]);
let tokens = token_text(&["cat!dog"]);
let embedding = pool_row(&[0.4], &[0], &tokens, &stopwords);
assert_eq!(
embedding.len(),
1,
"must be one survivor, not resplit into two"
);
assert_eq!(embedding.indices, vec![bm25_hash(&stem("cat!dog"))]);
assert_ne!(
embedding.indices,
vec![bm25_hash(&stem("cat"))],
"must not have been cleaned down to its first word-character run"
);
}
#[test]
fn duplicate_stems_within_one_row_fold_by_maximum_not_sum() {
let stopwords = stopwords(&[]);
let tokens = token_text(&["run", "running", "run"]);
let embedding = pool_row(&[0.3, 0.9, 0.1], &[0, 1, 2], &tokens, &stopwords);
assert_eq!(embedding.indices, vec![bm25_hash(&stem("run"))]);
let want = rescore(0.9);
assert!(
(embedding.values[0] - want).abs() < 1e-6,
"{} vs {want} (summing all three would give {})",
embedding.values[0],
rescore(1.3)
);
}
#[test]
fn the_rescore_formula_is_log1p_to_the_alpha_power() {
let stopwords = stopwords(&[]);
let tokens = token_text(&["xyzzy"]);
let embedding = pool_row(&[1.0], &[0], &tokens, &stopwords);
let want = 2.0f32.ln().powf(0.5);
assert!(
(embedding.values[0] - want).abs() < 1e-6,
"{} vs {want}",
embedding.values[0]
);
}
#[test]
fn the_query_path_weighs_every_unique_stem_at_one_regardless_of_repetition() {
let stopwords = stopwords(&[]);
let tokens = token_text(&["cat", "dog", "cat"]);
let embedding = pool_query_row(&[0, 1, 0], &tokens, &stopwords);
let mut indices = embedding.indices.clone();
indices.sort_unstable();
let mut expected = vec![bm25_hash(&stem("cat")), bm25_hash(&stem("dog"))];
expected.sort_unstable();
assert_eq!(indices, expected);
assert!(
embedding.values.iter().all(|&v| v == 1.0),
"{:?}",
embedding.values
);
}
#[test]
fn the_query_path_drops_stopwords_specials_and_bare_punctuation() {
let stopwords = stopwords(&["the"]);
let tokens = token_text(&["[CLS]", "the", ".", "fox", "[SEP]"]);
let embedding = pool_query_row(&[0, 1, 2, 3, 4], &tokens, &stopwords);
assert_eq!(embedding.indices, vec![bm25_hash(&stem("fox"))]);
}