use super::{compact_chunks, Embedding, EntangledHVec, StoredChunk, StoredDocument, VectorState};
use std::collections::BTreeMap;
#[test]
fn rebuilt_terms_preserve_complete_document_bytes_and_legacy_frequencies() {
for count in [0, 1, 10, 11, 12, 127, 128, 129, 4095, 4096, 4097] {
let mut terms = BTreeMap::new();
for index in 0..count {
terms.insert(
format!("term:{index:05}:é\0"),
match index % 3 {
0 => 0,
1 => 1,
_ => u32::MAX,
},
);
}
let mut document = StoredDocument {
id: "document:é".into(),
source_uri: Some("source:é".into()),
version: "1".into(),
metadata: serde_json::json!({"nested":[null, false, "é"]}),
chunks: vec![StoredChunk {
id: "chunk:é".into(),
text: Some("é\0 running".into()),
text_at: None,
start_byte: 0,
end_byte: 11,
vector: EntangledHVec::new_deterministic(1024, count as u64),
embedding: Some(Embedding::from_f32(vec![1.0, -0.0, 0.0])),
terms: terms.into(),
word_count: 0,
stemmed: count % 2 == 0,
vector_state: VectorState::Encoded,
}],
};
let expected = serde_json::to_vec(&document).unwrap();
compact_chunks(&mut document.chunks);
assert_eq!(serde_json::to_vec(&document).unwrap(), expected);
compact_chunks(&mut document.chunks);
assert_eq!(serde_json::to_vec(&document).unwrap(), expected);
let restored: StoredDocument = serde_json::from_slice(&expected).unwrap();
assert_eq!(serde_json::to_vec(&restored).unwrap(), expected);
}
}
#[test]
fn memoized_terms_match_the_reference_tokenizer() {
fn reference(text: &str, stem: bool) -> std::collections::BTreeMap<String, u32> {
let stemmer = rust_stemmers::Stemmer::create(rust_stemmers::Algorithm::English);
let mut out = std::collections::BTreeMap::new();
for word in text
.split(|c: char| !c.is_alphanumeric())
.filter(|s| !s.is_empty())
{
let lower = word.to_lowercase();
let key = if stem {
stemmer.stem(&lower).into_owned()
} else {
lower
};
*out.entry(key).or_insert(0) += 1;
}
out
}
let texts = [
"Running runners RUN quickly; the runner's running-shoes ran.",
"ΟΔΟΣ Οδός ΣΟΦΟΣ naïve Café CAFÉ straße STRASSE 42 x2 İstanbul",
"",
"!!! ,,, ---",
"Running runners RUN quickly; the runner's running-shoes ran.",
];
for stem in [false, true] {
for text in texts {
assert_eq!(
super::terms(text, stem),
reference(text, stem),
"stem={stem} text={text:?}"
);
}
}
}
#[test]
fn interned_chunk_terms_match_terms_with_title_weighting() {
fn expected(text: &str, title_weight: u32) -> (Vec<(String, u32)>, usize) {
let mut terms = super::terms(text, true);
if title_weight > 1 {
for (term, count) in super::terms(text.lines().next().unwrap_or_default(), true) {
*terms.entry(term).or_insert(0) += count * (title_weight - 1);
}
}
let words = terms.values().map(|&n| n as usize).sum();
(terms.into_iter().collect(), words)
}
fn actual(text: &str, title_weight: u32) -> (Vec<(String, u32)>, usize) {
let (terms, words) = super::chunk_terms(text, title_weight, true);
terms.assert_valid_storage();
let pairs = (&terms)
.into_iter()
.map(|(k, &v)| (k.to_owned(), v))
.collect();
(pairs, words)
}
let many: String = (0..70_000).map(|i| format!("w{i} ")).collect();
let texts = [
"Running runners RUN quickly;\nthe runner's running-shoes ran. Running",
"ΟΔΟΣ Οδός ΣΟΦΟΣ naïve\r\nCafé CAFÉ straße STRASSE 42 x2 İstanbul",
"",
"!!! ,,, ---\nwords after an empty title",
"abcdefgh abcdefghi abcdefgg abcdefg abcdefghij zzzzzzzzz a aa\u{0}aa",
&many,
"Running runners RUN quickly;\nthe runner's running-shoes ran. Running",
];
for title_weight in [1, 2, 16] {
for text in texts {
assert_eq!(actual(text, title_weight), expected(text, title_weight));
}
let mut memo = super::StemMemo::default();
let _installed = memo.install();
for text in texts.iter().chain(&texts) {
for words in text.split_inclusive(' ').collect::<Vec<_>>().chunks(1000) {
let chunk = words.concat();
assert_eq!(actual(&chunk, title_weight), expected(&chunk, title_weight));
}
}
}
}
#[test]
fn word_map_keys_words_across_the_inline_limit() {
let mut words: Vec<String> = (1..=34)
.flat_map(|len| {
[
"a".repeat(len),
format!("{}b", "a".repeat(len - 1)),
"é".repeat(len / 2 + 1),
]
})
.collect();
words.sort();
words.dedup();
let mut map = super::WordMap::default();
for (id, word) in words.iter().enumerate() {
assert_eq!(map.get(word), None, "{word:?}");
map.insert(word, id as u32);
}
assert_eq!(map.len(), words.len());
for (id, word) in words.iter().enumerate() {
assert_eq!(map.get(word), Some(id as u32), "{word:?}");
}
assert_eq!(map.get(&"a".repeat(35)), None);
}
#[test]
fn pooled_interner_counts_later_batches_like_a_fresh_one() {
let texts = [
"Running runners RUN quickly;\nthe runner's running-shoes ran. Running",
"ΟΔΟΣ Οδός ΣΟΦΟΣ naïve\r\nCafé CAFÉ straße STRASSE 42 x2 İstanbul",
"abcdefghijklmnop abcdefghijklmnopq éééééééé ééééééééx Abcdefghijklmnopq",
];
let table = |text: &str| {
let (terms, words) = super::chunk_terms(text, 2, true);
let pairs: Vec<(String, u32)> = (&terms)
.into_iter()
.map(|(k, &v)| (k.to_owned(), v))
.collect();
(pairs, words)
};
let fresh: Vec<_> = texts.iter().map(|text| table(text)).collect();
let pool = super::StemMemoPool::default();
for batch in 0..3 {
let mut memo = pool.take();
let _installed = memo.install();
if batch == 1 {
for start in (0..=super::STEM_MEMO_CAPACITY).step_by(1000) {
let chunk: String = (start..start + 1000).map(|i| format!("w{i} ")).collect();
super::chunk_terms(&chunk, 1, true);
}
}
for (text, expected) in texts.iter().zip(&fresh) {
assert_eq!(&table(text), expected, "batch {batch}");
}
}
let warm = pool.take();
let cold = pool.take();
assert!(warm.memo.0.words.len() > 0);
assert_eq!(cold.memo.0.words.len(), 0);
drop(cold);
drop(warm);
assert_eq!(pool.idle.lock().len(), 2);
assert!(pool.take().memo.0.words.len() > 0);
pool.set_keep(false);
assert!(pool.idle.lock().is_empty());
{
let mut memo = pool.take();
let _installed = memo.install();
assert_eq!(&table(texts[0]), &fresh[0]);
}
assert!(pool.idle.lock().is_empty());
pool.set_keep(true);
drop(pool.take());
assert_eq!(pool.idle.lock().len(), 1);
}
#[test]
fn trim_keeps_the_largest_interners_within_the_word_bound() {
let cap = super::STEM_MEMO_CAPACITY;
let memo = |words: usize| {
let mut memo = super::StemMemo::default();
for i in 0..words {
memo.0.words.insert(&format!("w{i}"), 0);
}
memo
};
for (sizes, kept) in [
(vec![], vec![]),
(vec![3, 7], vec![7, 3]),
(vec![cap + 5], vec![cap + 5]),
(vec![1, cap / 2, cap / 2], vec![cap / 2, cap / 2]),
(vec![cap / 2, 1, cap / 2 - 1], vec![cap / 2, cap / 2 - 1, 1]),
(vec![cap / 2 + 1, cap / 2, 10], vec![cap / 2 + 1]),
] {
let pool = super::StemMemoPool::default();
pool.idle.lock().extend(sizes.iter().map(|&n| memo(n)));
pool.trim();
let left: Vec<usize> = pool.idle.lock().iter().map(|m| m.0.words.len()).collect();
assert_eq!(left, kept, "sizes {sizes:?}");
}
}
#[test]
fn chunk_boundaries_match_char_whitespace_on_mixed_text() {
let text = "α\u{a0}beta \u{85}gamma\u{2003}δ\tε\u{3000}zeta\u{b}eta θ";
let reference: Vec<&str> = text
.split(char::is_whitespace)
.filter(|w| !w.is_empty())
.collect();
let input: super::DocumentInput = serde_json::from_value(serde_json::json!({
"id": "d", "text": text, "chunkWords": 1, "overlapWords": 0
}))
.unwrap();
let chunks: Vec<String> = super::chunk_document(&input)
.unwrap()
.into_iter()
.map(|c| c.text)
.collect();
assert_eq!(chunks, reference);
}
#[test]
fn interned_positions_match_the_reference_analyzer() {
let long = format!("{} tail", "Repeat repeats repeated ".repeat(400));
let texts = [
("Running runs ran; the RUNNER ran\nand ran again", 3),
("ÜBER über Σοφία ΣΟΦΊΑΣ naïve café CAFÉ 東京 東京", 1),
("title line\nbody title body", 2),
("", 1),
("--- ...", 1),
(long.as_str(), 1),
];
for stem in [true, false] {
let mut interner = super::Interner::default();
for whole_document in [false, true] {
for (text, title_weight) in texts {
let (terms, _) =
interner.chunk_terms(text, title_weight, stem, Some(whole_document));
let reference =
super::positions::Positions::from_text(text, stem, &terms, whole_document);
assert_eq!(
serde_json::to_value(terms.positions.as_deref().unwrap()).unwrap(),
serde_json::to_value(&reference).unwrap(),
"{text:?} stem {stem}"
);
let (plain, _) = interner.chunk_terms(text, title_weight, stem, None);
assert!(plain.positions.is_none());
}
}
}
}