1use std::collections::{BTreeMap, HashMap};
12
13use crate::bm25::bm25_score;
14use crate::docblobs::put_varint;
15use crate::positions::walk;
16use crate::segment::TextSegment;
17
18pub struct FrozenBucket {
22 pub terms: BTreeMap<Vec<u8>, Vec<u8>>,
24 pub fwd: BTreeMap<Vec<u8>, Vec<u8>>,
28 pub n_docs: u64,
30 pub total_len: u64,
32}
33
34pub struct ColdEntry {
36 pub key: Vec<u8>,
38 pub tf: u32,
40 pub dl: u32,
42 pub positions: Vec<u8>,
45}
46
47pub fn encode_posting(docs: &[ColdEntry]) -> Vec<u8> {
50 let mut out = Vec::new();
51 put_varint(&mut out, docs.len() as u32);
52 for d in docs {
53 put_varint(&mut out, d.key.len() as u32);
54 out.extend_from_slice(&d.key);
55 put_varint(&mut out, d.tf);
56 put_varint(&mut out, d.dl);
57 put_varint(&mut out, d.positions.len() as u32);
58 out.extend_from_slice(&d.positions);
59 }
60 out
61}
62
63pub fn posting_df(payload: &[u8]) -> Option<u32> {
65 read_varint(payload, &mut 0)
66}
67
68pub struct FwdRecord {
71 pub dl: u32,
73 pub terms: Vec<Vec<u8>>,
75 pub values: Vec<Option<Vec<u8>>>,
78}
79
80pub fn encode_fwd(dl: u32, terms: &[&[u8]], values: &[Option<&[u8]>]) -> Vec<u8> {
87 let mut out = Vec::new();
88 put_varint(&mut out, dl);
89 put_varint(&mut out, terms.len() as u32);
90 for t in terms {
91 put_varint(&mut out, t.len() as u32);
92 out.extend_from_slice(t);
93 }
94 put_varint(&mut out, values.len() as u32);
95 for v in values {
96 match v {
97 None => put_varint(&mut out, 0),
98 Some(b) => {
99 put_varint(&mut out, 1);
100 put_varint(&mut out, b.len() as u32);
101 out.extend_from_slice(b);
102 }
103 }
104 }
105 out
106}
107
108pub(crate) fn entries_fit(n: usize, payload_len: usize) -> usize {
119 n.min(payload_len)
120}
121
122pub fn decode_fwd(payload: &[u8]) -> Option<FwdRecord> {
124 let mut at = 0usize;
125 let dl = read_varint(payload, &mut at)?;
126 let n = read_varint(payload, &mut at)? as usize;
127 let mut terms = Vec::with_capacity(entries_fit(n, payload.len()));
128 for _ in 0..n {
129 let klen = read_varint(payload, &mut at)? as usize;
130 terms.push(payload.get(at..at + klen)?.to_vec());
131 at += klen;
132 }
133 let nv = read_varint(payload, &mut at)? as usize;
134 let mut values = Vec::with_capacity(entries_fit(nv, payload.len()));
135 for _ in 0..nv {
136 values.push(match read_varint(payload, &mut at)? {
137 0 => None,
138 1 => {
139 let vlen = read_varint(payload, &mut at)? as usize;
140 let v = payload.get(at..at + vlen)?.to_vec();
141 at += vlen;
142 Some(v)
143 }
144 _ => return None,
145 });
146 }
147 (at == payload.len()).then_some(FwdRecord { dl, terms, values })
148}
149
150pub fn decode_posting(payload: &[u8]) -> Option<Vec<ColdEntry>> {
153 let mut at = 0usize;
154 let n = read_varint(payload, &mut at)? as usize;
155 let mut out = Vec::with_capacity(entries_fit(n, payload.len()));
156 for _ in 0..n {
157 let klen = read_varint(payload, &mut at)? as usize;
158 let key = payload.get(at..at + klen)?.to_vec();
159 at += klen;
160 let tf = read_varint(payload, &mut at)?;
161 let dl = read_varint(payload, &mut at)?;
162 let plen = read_varint(payload, &mut at)? as usize;
163 let positions = payload.get(at..at + plen)?.to_vec();
164 at += plen;
165 out.push(ColdEntry { key, tf, dl, positions });
166 }
167 (at == payload.len()).then_some(out)
168}
169
170pub fn score_cold(
176 payload: &[u8],
177 term: &[u8],
178 stats: &crate::CorpusStats,
179 dead: &dyn Fn(&[u8]) -> bool,
180 acc: &mut HashMap<Vec<u8>, f64>,
181) -> Option<()> {
182 let entries = decode_posting(payload)?;
183 let df = f64::from(*stats.df.get(term).unwrap_or(&(entries.len() as u32)));
184 for e in entries {
185 if dead(&e.key) {
186 continue;
187 }
188 let s = bm25_score(f64::from(e.tf), df, stats.n_docs, f64::from(e.dl), stats.avgdl);
189 *acc.entry(e.key).or_insert(0.0) += s;
190 }
191 Some(())
192}
193
194pub fn score_cold_phrase(
204 payloads: &[Vec<u8>],
205 toks: &[Vec<u8>],
206 stats: &crate::CorpusStats,
207 dead: &dyn Fn(&[u8]) -> bool,
208 acc: &mut HashMap<Vec<u8>, f64>,
209) -> Option<()> {
210 if payloads.len() != toks.len() || toks.is_empty() {
211 return None;
212 }
213 let per_tok: Vec<HashMap<Vec<u8>, ColdEntry>> = payloads
214 .iter()
215 .map(|p| decode_posting(p).map(|es| es.into_iter().map(|e| (e.key.clone(), e)).collect()))
216 .collect::<Option<_>>()?;
217 let distinct = crate::segment::distinct_tokens(toks);
218 for (key, first) in &per_tok[0] {
219 if dead(key) || !per_tok[1..].iter().all(|m| m.contains_key(key)) {
220 continue;
221 }
222 let adjacent = walk(&first.positions).any(|start| {
223 toks.iter()
224 .enumerate()
225 .skip(1)
226 .all(|(i, _)| walk(&per_tok[i][key].positions).any(|p| p == start + i as u32))
227 });
228 if !adjacent {
229 continue;
230 }
231 let dl = f64::from(first.dl);
232 let mut score = 0.0;
233 for t in &distinct {
234 let Some(pos) = toks.iter().position(|tt| tt == t) else { continue };
235 let e = &per_tok[pos][key];
236 let df = f64::from(*stats.df.get(t).unwrap_or(&(per_tok[pos].len() as u32)));
237 score += bm25_score(f64::from(e.tf), df, stats.n_docs, dl, stats.avgdl);
238 }
239 *acc.entry(key.clone()).or_insert(0.0) += score;
240 }
241 Some(())
242}
243
244pub fn highlight_fields(fields: &[Vec<u8>], query: &[u8]) -> Vec<(usize, Vec<(usize, usize)>)> {
250 let (bare, phrases, prefixes) = crate::parse_clauses(query);
251 let terms: std::collections::HashSet<&[u8]> = bare.iter().map(Vec::as_slice).collect();
252 let mut out = Vec::new();
253 for (fi, text) in fields.iter().enumerate() {
254 let mut spans =
255 crate::segment::field_spans(&crate::tokenize_spans(text), &terms, &phrases, &prefixes);
256 if !spans.is_empty() {
257 spans.sort_unstable();
258 spans.dedup();
259 out.push((fi, spans));
260 }
261 }
262 out
263}
264
265fn read_varint(b: &[u8], at: &mut usize) -> Option<u32> {
266 let mut cur = 0u32;
267 let mut shift = 0u32;
268 loop {
269 let byte = *b.get(*at)?;
270 *at += 1;
271 cur |= u32::from(byte & 0x7f) << shift;
272 if byte & 0x80 == 0 {
273 return Some(cur);
274 }
275 shift += 7;
276 if shift > 28 {
277 return None;
278 }
279 }
280}
281
282impl TextSegment {
283 pub fn freeze_docs(&mut self, keys: &[Vec<u8>]) -> Option<FrozenBucket> {
290 let mut terms: BTreeMap<Vec<u8>, Vec<ColdEntry>> = BTreeMap::new();
291 let mut fwd: BTreeMap<Vec<u8>, Vec<u8>> = BTreeMap::new();
292 let mut n_docs = 0u64;
293 let mut total_len = 0u64;
294 for key in keys {
295 let Some((id, dl, tf_map)) = self.doc_terms(key) else { continue };
296 n_docs += 1;
297 total_len += u64::from(dl);
298 let mut doc_terms: Vec<&[u8]> = tf_map.keys().map(Vec::as_slice).collect();
299 doc_terms.sort_unstable();
300 let values = self.doc_values_of(id);
301 fwd.insert(key.clone(), encode_fwd(dl, &doc_terms, &values));
302 for (t, tf) in &tf_map {
303 let positions = self.positions_blob(t, id).map(<[u8]>::to_vec).unwrap_or_default();
304 terms.entry(t.clone()).or_default().push(ColdEntry {
305 key: key.clone(),
306 tf: *tf,
307 dl,
308 positions,
309 });
310 }
311 }
312 if n_docs == 0 {
313 return None;
314 }
315 for key in keys {
317 self.apply_doc(key, None, &[]);
318 }
319 let terms = terms
320 .into_iter()
321 .map(|(t, entries)| {
322 let payload = encode_posting(&entries);
323 (t, payload)
324 })
325 .collect();
326 Some(FrozenBucket { terms, fwd, n_docs, total_len })
327 }
328}
329
330#[cfg(test)]
331#[path = "cold_tests.rs"]
332mod tests;
333
334#[cfg(test)]
335mod bound_tests {
336 #[test]
343 fn a_count_from_a_payload_cannot_size_an_allocation() {
344 use super::entries_fit;
345 assert_eq!(entries_fit(3, 1024), 3, "an honest count is used as-is");
346 assert_eq!(
347 entries_fit(u32::MAX as usize, 40),
348 40,
349 "4.29e9 entries over forty bytes reserves the ceiling, not 103 GB"
350 );
351 for len in [0usize, 1, 64, 4096] {
354 assert_eq!(entries_fit(len, len), len, "len at {len} still fits exactly");
355 }
356
357 let mut payload = vec![0xffu8, 0xff, 0xff, 0xff, 0x0f]; payload.push(0x00);
361 assert!(super::decode_posting(&payload).is_none());
362 }
363}