urna_runtime/bm25/
codec.rs1use std::collections::HashMap;
12
13use urna_format::encoding::{pack_u64s, unpack_u64s};
14use urna_format::error::UrnaError;
15
16use super::index::{BM25_PAYLOAD_VERSION, Bm25Index, Posting, TermEntry};
17use crate::error::RuntimeError;
18use urna_format::bytes::{le_f32, le_u32};
19
20impl Bm25Index {
21 pub fn to_bytes(&self) -> Vec<u8> {
23 let mut out = Vec::new();
24 out.extend_from_slice(&BM25_PAYLOAD_VERSION.to_le_bytes());
25 out.extend_from_slice(&self.k1.to_le_bytes());
26 out.extend_from_slice(&self.b.to_le_bytes());
27 out.extend_from_slice(&self.avgdl.to_le_bytes());
28 out.extend_from_slice(&(self.n_docs as u32).to_le_bytes());
29 out.extend_from_slice(&(self.n_terms as u32).to_le_bytes());
30
31 let dls: Vec<u64> = self.doc_lengths.iter().map(|&x| x as u64).collect();
32 write_blob(&mut out, &pack_u64s(&dls));
33
34 let mut terms: Vec<(&String, &TermEntry)> = self.terms.iter().collect();
36 terms.sort_by(|a, b| a.0.cmp(b.0));
37 let mut doc_gaps: Vec<u64> = Vec::new();
38 let mut tfs: Vec<u64> = Vec::new();
39 for (term, entry) in &terms {
40 let bs = term.as_bytes();
41 out.extend_from_slice(&(bs.len() as u32).to_le_bytes());
42 out.extend_from_slice(bs);
43 out.extend_from_slice(&entry.df.to_le_bytes());
44 let mut prev = 0u32;
45 for p in &entry.postings {
46 doc_gaps.push(p.doc.wrapping_sub(prev) as u64);
47 prev = p.doc;
48 tfs.push(p.tf as u64);
49 }
50 }
51 write_blob(&mut out, &pack_u64s(&doc_gaps));
52 write_blob(&mut out, &pack_u64s(&tfs));
53 out
54 }
55
56 pub fn from_bytes(bytes: &[u8]) -> Result<Self, RuntimeError> {
57 let mut cur = ByteCursor::new(bytes);
58 let version = cur.u32()?;
59 let k1 = cur.f32()?;
60 let b = cur.f32()?;
61 let avgdl = cur.f32()?;
62 let n_docs = cur.u32()? as usize;
63 let n_terms = cur.u32()? as usize;
64 let (doc_lengths, terms) = match version {
65 1 => decode_v1(&mut cur, n_docs, n_terms)?,
66 2 => decode_v2(&mut cur, n_docs, n_terms)?,
67 other => {
68 return Err(RuntimeError::Format(UrnaError::UnsupportedSectionVersion {
69 section_id: urna_format::layout::SECTION_BM25_INDEX,
70 version: other,
71 }));
72 }
73 };
74 if !(k1.is_finite() && b.is_finite() && avgdl.is_finite()) {
79 return Err(malformed("bm25: non-finite k1 / b / avgdl"));
80 }
81 if doc_lengths.len() != n_docs {
82 return Err(malformed("bm25: doc-length count != n_docs"));
83 }
84 for entry in terms.values() {
85 if entry.postings.iter().any(|p| p.doc as usize >= n_docs) {
86 return Err(malformed("bm25: posting doc id >= n_docs"));
87 }
88 }
89 Ok(Self {
90 k1,
91 b,
92 avgdl,
93 n_docs,
94 n_terms,
95 doc_lengths,
96 terms,
97 })
98 }
99}
100
101fn malformed(reason: impl Into<String>) -> RuntimeError {
102 RuntimeError::Format(UrnaError::MalformedSectionPayload {
103 section_id: urna_format::layout::SECTION_BM25_INDEX,
104 reason: reason.into(),
105 })
106}
107
108fn write_blob(out: &mut Vec<u8>, blob: &[u8]) {
109 out.extend_from_slice(&(blob.len() as u32).to_le_bytes());
110 out.extend_from_slice(blob);
111}
112
113const MAX_PREALLOC: usize = 1 << 20;
120
121type Decoded = (Vec<u32>, HashMap<String, TermEntry>);
122
123fn decode_v1(cur: &mut ByteCursor, n_docs: usize, n_terms: usize) -> Result<Decoded, RuntimeError> {
125 let mut doc_lengths = Vec::with_capacity(n_docs.min(MAX_PREALLOC));
126 for _ in 0..n_docs {
127 doc_lengths.push(cur.u32()?);
128 }
129 let mut terms: HashMap<String, TermEntry> = HashMap::with_capacity(n_terms.min(MAX_PREALLOC));
130 for _ in 0..n_terms {
131 let term = cur.term()?;
132 let df = cur.u32()?;
133 let mut postings = Vec::with_capacity((df as usize).min(MAX_PREALLOC));
134 for _ in 0..df {
135 let doc = cur.u32()?;
136 let tf = cur.u32()?;
137 postings.push(Posting { doc, tf });
138 }
139 terms.insert(term, TermEntry { df, postings });
140 }
141 Ok((doc_lengths, terms))
142}
143
144fn decode_v2(cur: &mut ByteCursor, n_docs: usize, n_terms: usize) -> Result<Decoded, RuntimeError> {
146 let dls = cur.intpack_column()?;
147 if dls.len() != n_docs {
148 return Err(malformed("bm25 v2: doc-length column mismatch"));
149 }
150 let doc_lengths: Vec<u32> = dls.iter().map(|&x| x as u32).collect();
151
152 let mut dict: Vec<(String, u32)> = Vec::with_capacity(n_terms.min(MAX_PREALLOC));
154 let mut total: usize = 0;
155 for _ in 0..n_terms {
156 let term = cur.term()?;
157 let df = cur.u32()?;
158 total = total.saturating_add(df as usize);
159 dict.push((term, df));
160 }
161 let gaps = cur.intpack_column()?;
162 let tfs = cur.intpack_column()?;
163 if gaps.len() != total || tfs.len() != total {
164 return Err(malformed("bm25 v2: posting column mismatch"));
165 }
166 let mut terms: HashMap<String, TermEntry> = HashMap::with_capacity(n_terms.min(MAX_PREALLOC));
167 let mut k = 0usize;
168 for (term, df) in dict {
169 let mut postings = Vec::with_capacity((df as usize).min(MAX_PREALLOC));
170 let mut prev = 0u32;
171 for _ in 0..df {
172 let doc = prev.wrapping_add(gaps[k] as u32);
173 prev = doc;
174 postings.push(Posting {
175 doc,
176 tf: tfs[k] as u32,
177 });
178 k += 1;
179 }
180 terms.insert(term, TermEntry { df, postings });
181 }
182 Ok((doc_lengths, terms))
183}
184
185struct ByteCursor<'a> {
186 buf: &'a [u8],
187 pos: usize,
188}
189impl<'a> ByteCursor<'a> {
190 fn new(buf: &'a [u8]) -> Self {
191 Self { buf, pos: 0 }
192 }
193 fn u32(&mut self) -> Result<u32, RuntimeError> {
194 Ok(le_u32(self.bytes(4)?)?)
195 }
196 fn f32(&mut self) -> Result<f32, RuntimeError> {
197 Ok(le_f32(self.bytes(4)?)?)
198 }
199 fn bytes(&mut self, n: usize) -> Result<&'a [u8], RuntimeError> {
200 if n > self.buf.len() - self.pos {
201 return Err(malformed(format!("bm25: unexpected EOF (need {})", n)));
202 }
203 let out = &self.buf[self.pos..self.pos + n];
204 self.pos += n;
205 Ok(out)
206 }
207 fn term(&mut self) -> Result<String, RuntimeError> {
208 let len = self.u32()? as usize;
209 let b = self.bytes(len)?;
210 std::str::from_utf8(b)
211 .map(|s| s.to_string())
212 .map_err(|e| malformed(format!("term utf-8: {}", e)))
213 }
214 fn intpack_column(&mut self) -> Result<Vec<u64>, RuntimeError> {
215 let len = self.u32()? as usize;
216 let blob = self.bytes(len)?;
217 unpack_u64s(blob).map_err(RuntimeError::Format)
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 fn v1_header(n_docs: u32, n_terms: u32) -> Vec<u8> {
226 let mut out = 1u32.to_le_bytes().to_vec(); out.extend_from_slice(&1.2f32.to_le_bytes()); out.extend_from_slice(&0.75f32.to_le_bytes()); out.extend_from_slice(&1.0f32.to_le_bytes()); out.extend_from_slice(&n_docs.to_le_bytes());
231 out.extend_from_slice(&n_terms.to_le_bytes());
232 out
233 }
234
235 #[test]
238 fn v1_hostile_n_docs_errors_without_ooming() {
239 let p = v1_header(u32::MAX, 0); assert!(Bm25Index::from_bytes(&p).is_err());
241 }
242
243 #[test]
245 fn v1_hostile_df_errors_without_ooming() {
246 let mut p = v1_header(0, 1);
247 p.extend_from_slice(&1u32.to_le_bytes()); p.extend_from_slice(b"a"); p.extend_from_slice(&u32::MAX.to_le_bytes()); assert!(Bm25Index::from_bytes(&p).is_err());
252 }
253}