use std::collections::{BTreeMap, HashMap};
use crate::bm25::bm25_score;
use crate::positions::walk;
use crate::segment::TextSegment;
#[path = "cold_codec.rs"]
mod cold_codec;
pub use cold_codec::{
ColdEntry, FwdRecord, decode_fwd, decode_posting, encode_fwd, encode_posting, posting_df,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub struct FrozenBucket {
pub terms: BTreeMap<Vec<u8>, Vec<u8>>,
pub fwd: BTreeMap<Vec<u8>, Vec<u8>>,
pub n_docs: u64,
pub total_len: u64,
}
pub fn score_cold(
payload: &[u8],
term: &[u8],
stats: &crate::CorpusStats,
dead: &dyn Fn(&[u8]) -> bool,
acc: &mut HashMap<Vec<u8>, f64>,
) -> Option<()> {
let entries = decode_posting(payload)?;
let df = f64::from(*stats.df.get(term).unwrap_or(&(entries.len() as u32)));
for e in entries {
if dead(&e.key) {
continue;
}
let s = bm25_score(f64::from(e.tf), df, stats.n_docs, f64::from(e.dl), stats.avgdl);
*acc.entry(e.key).or_insert(0.0) += s;
}
Some(())
}
pub fn score_cold_phrase(
payloads: &[Vec<u8>],
toks: &[Vec<u8>],
stats: &crate::CorpusStats,
dead: &dyn Fn(&[u8]) -> bool,
acc: &mut HashMap<Vec<u8>, f64>,
) -> Option<()> {
if payloads.len() != toks.len() || toks.is_empty() {
return None;
}
let per_tok: Vec<HashMap<Vec<u8>, ColdEntry>> = payloads
.iter()
.map(|p| decode_posting(p).map(|es| es.into_iter().map(|e| (e.key.clone(), e)).collect()))
.collect::<Option<_>>()?;
let distinct = crate::segment::distinct_tokens(toks);
for (key, first) in &per_tok[0] {
if dead(key) || !per_tok[1..].iter().all(|m| m.contains_key(key)) {
continue;
}
let adjacent = walk(&first.positions).any(|start| {
toks.iter()
.enumerate()
.skip(1)
.all(|(i, _)| walk(&per_tok[i][key].positions).any(|p| p == start + i as u32))
});
if !adjacent {
continue;
}
let dl = f64::from(first.dl);
let mut score = 0.0;
for t in &distinct {
let Some(pos) = toks.iter().position(|tt| tt == t) else { continue };
let e = &per_tok[pos][key];
let df = f64::from(*stats.df.get(t).unwrap_or(&(per_tok[pos].len() as u32)));
score += bm25_score(f64::from(e.tf), df, stats.n_docs, dl, stats.avgdl);
}
*acc.entry(key.clone()).or_insert(0.0) += score;
}
Some(())
}
pub fn highlight_fields(fields: &[Vec<u8>], query: &[u8]) -> Vec<(usize, Vec<(usize, usize)>)> {
let (bare, phrases, prefixes) = crate::parse_clauses(query);
let terms: std::collections::HashSet<&[u8]> = bare.iter().map(Vec::as_slice).collect();
let mut out = Vec::new();
for (fi, text) in fields.iter().enumerate() {
let mut spans =
crate::segment::field_spans(&crate::tokenize_spans(text), &terms, &phrases, &prefixes);
if !spans.is_empty() {
spans.sort_unstable();
spans.dedup();
out.push((fi, spans));
}
}
out
}
impl TextSegment {
pub fn freeze_docs(&mut self, keys: &[Vec<u8>]) -> Option<FrozenBucket> {
let mut terms: BTreeMap<Vec<u8>, Vec<ColdEntry>> = BTreeMap::new();
let mut fwd: BTreeMap<Vec<u8>, Vec<u8>> = BTreeMap::new();
let mut n_docs = 0u64;
let mut total_len = 0u64;
for key in keys {
let Some((id, dl, tf_map)) = self.doc_terms(key) else { continue };
n_docs += 1;
total_len += u64::from(dl);
let mut doc_terms: Vec<&[u8]> = tf_map.keys().map(Vec::as_slice).collect();
doc_terms.sort_unstable();
let values = self.doc_values_of(id);
fwd.insert(key.clone(), encode_fwd(dl, &doc_terms, &values));
for (t, tf) in &tf_map {
let positions = self.positions_blob(t, id).map(<[u8]>::to_vec).unwrap_or_default();
terms.entry(t.clone()).or_default().push(ColdEntry {
key: key.clone(),
tf: *tf,
dl,
positions,
});
}
}
if n_docs == 0 {
return None;
}
for key in keys {
self.apply_doc(key, None, &[]);
}
let terms = terms
.into_iter()
.map(|(t, entries)| {
let payload = encode_posting(&entries);
(t, payload)
})
.collect();
Some(FrozenBucket { terms, fwd, n_docs, total_len })
}
}
#[cfg(test)]
#[path = "cold_tests.rs"]
mod tests;