1use std::collections::BTreeMap;
34
35pub const TERMS_PER_BLOCK: usize = 16;
40
41#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
43pub struct TermInfo {
44 pub document_frequency: u32,
46 pub postings_offset: u64,
48 pub postings_length: u32,
50}
51
52#[derive(Clone, Debug, Eq, PartialEq)]
54pub enum DictError {
55 NotSorted {
57 term: String,
59 },
60 Duplicate {
62 term: String,
64 },
65 EmptyTerm,
67}
68
69impl std::fmt::Display for DictError {
70 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 match self {
72 Self::NotSorted { term } => write!(formatter, "term {term:?} is out of order"),
73 Self::Duplicate { term } => write!(formatter, "term {term:?} appears twice"),
74 Self::EmptyTerm => formatter.write_str("an empty term is not a valid query"),
75 }
76 }
77}
78
79impl std::error::Error for DictError {}
80
81#[derive(Clone, Debug, Eq, PartialEq)]
83struct BlockIndexEntry {
84 head: Vec<u8>,
86 first_term: usize,
88}
89
90#[derive(Clone, Debug, Default)]
92pub struct TermDictionary {
93 terms: Vec<Vec<u8>>,
95 infos: Vec<TermInfo>,
97 index: Vec<BlockIndexEntry>,
99 encoded: Vec<u8>,
101}
102
103impl TermDictionary {
104 #[must_use]
110 pub fn from_map(entries: &BTreeMap<Vec<u8>, TermInfo>) -> Self {
111 let mut dictionary = Self::default();
112 for (term, info) in entries {
113 let _ = dictionary.push(term, *info);
115 }
116 dictionary
117 }
118
119 pub fn push(&mut self, term: &[u8], info: TermInfo) -> Result<(), DictError> {
125 if let Some(last) = self.terms.last() {
126 match term.cmp(last.as_slice()) {
127 std::cmp::Ordering::Less => {
128 return Err(DictError::NotSorted {
129 term: String::from_utf8_lossy(term).into_owned(),
130 });
131 }
132 std::cmp::Ordering::Equal => {
133 return Err(DictError::Duplicate {
134 term: String::from_utf8_lossy(term).into_owned(),
135 });
136 }
137 std::cmp::Ordering::Greater => {}
138 }
139 }
140
141 let position = self.terms.len();
142 if position.is_multiple_of(TERMS_PER_BLOCK) {
143 self.index.push(BlockIndexEntry {
144 head: term.to_vec(),
145 first_term: position,
146 });
147 self.encoded.push(0);
149 push_varint(&mut self.encoded, term.len());
150 self.encoded.extend_from_slice(term);
151 } else {
152 let shared = self
153 .terms
154 .last()
155 .map(|previous| shared_prefix_len(previous, term))
156 .unwrap_or(0)
157 .min(255);
158 let suffix = term.get(shared..).unwrap_or(&[]);
159 self.encoded.push(u8::try_from(shared).unwrap_or(u8::MAX));
160 push_varint(&mut self.encoded, suffix.len());
161 self.encoded.extend_from_slice(suffix);
162 }
163
164 self.terms.push(term.to_vec());
165 self.infos.push(info);
166 Ok(())
167 }
168
169 #[must_use]
171 pub fn len(&self) -> usize {
172 self.terms.len()
173 }
174
175 #[must_use]
177 pub fn is_empty(&self) -> bool {
178 self.terms.is_empty()
179 }
180
181 #[must_use]
183 pub fn encoded_bytes(&self) -> &[u8] {
184 &self.encoded
185 }
186
187 #[must_use]
189 pub fn term_at(&self, index: usize) -> Option<&[u8]> {
190 self.terms.get(index).map(Vec::as_slice)
191 }
192
193 #[must_use]
195 pub fn info_at(&self, index: usize) -> Option<TermInfo> {
196 self.infos.get(index).copied()
197 }
198
199 #[must_use]
201 pub fn get(&self, term: &[u8]) -> Option<TermInfo> {
202 let index = self.seek_exact(term)?;
203 self.infos.get(index).copied()
204 }
205
206 #[must_use]
211 pub fn seek_exact(&self, term: &[u8]) -> Option<usize> {
212 let block = self.block_for(term)?;
213 let start = self.index.get(block)?.first_term;
214 let end = (start + TERMS_PER_BLOCK).min(self.terms.len());
215 for index in start..end {
216 match self.terms.get(index)?.as_slice().cmp(term) {
217 std::cmp::Ordering::Less => {}
218 std::cmp::Ordering::Equal => return Some(index),
219 std::cmp::Ordering::Greater => return None,
220 }
221 }
222 None
223 }
224
225 #[must_use]
231 pub fn seek_ceiling(&self, term: &[u8]) -> Option<usize> {
232 let mut low = 0_usize;
234 let mut high = self.terms.len();
235 while low < high {
236 let middle = low + (high - low) / 2;
237 let candidate = self.terms.get(middle)?;
238 if candidate.as_slice() < term {
239 low = middle + 1;
240 } else {
241 high = middle;
242 }
243 }
244 (low < self.terms.len()).then_some(low)
245 }
246
247 pub fn prefix_range(&self, prefix: &[u8]) -> Result<std::ops::Range<usize>, DictError> {
255 if prefix.is_empty() {
256 return Err(DictError::EmptyTerm);
257 }
258 let Some(start) = self.seek_ceiling(prefix) else {
259 return Ok(self.terms.len()..self.terms.len());
260 };
261 let mut end = start;
262 while let Some(term) = self.terms.get(end) {
263 if term.starts_with(prefix) {
264 end += 1;
265 } else {
266 break;
267 }
268 }
269 Ok(start..end)
270 }
271
272 fn block_for(&self, term: &[u8]) -> Option<usize> {
274 if self.index.is_empty() {
275 return None;
276 }
277 let mut low = 0_usize;
279 let mut high = self.index.len();
280 while low < high {
281 let middle = low + (high - low) / 2;
282 let head = &self.index.get(middle)?.head;
283 if head.as_slice() <= term {
284 low = middle + 1;
285 } else {
286 high = middle;
287 }
288 }
289 low.checked_sub(1)
290 }
291
292 #[must_use]
298 pub fn decode_terms(&self) -> Option<Vec<Vec<u8>>> {
299 let mut out: Vec<Vec<u8>> = Vec::with_capacity(self.terms.len());
300 let mut cursor = 0_usize;
301 let mut previous: Vec<u8> = Vec::new();
302 let mut position = 0_usize;
303 while cursor < self.encoded.len() {
304 let shared = usize::from(*self.encoded.get(cursor)?);
305 cursor += 1;
306 let (length, consumed) = read_varint(self.encoded.get(cursor..)?)?;
307 cursor += consumed;
308 let suffix = self.encoded.get(cursor..cursor + length)?;
309 cursor += length;
310 let mut term = if position.is_multiple_of(TERMS_PER_BLOCK) {
311 Vec::new()
312 } else {
313 previous.get(..shared)?.to_vec()
314 };
315 term.extend_from_slice(suffix);
316 previous = term.clone();
317 out.push(term);
318 position += 1;
319 }
320 Some(out)
321 }
322}
323
324fn shared_prefix_len(left: &[u8], right: &[u8]) -> usize {
325 left.iter()
326 .zip(right.iter())
327 .take_while(|(a, b)| a == b)
328 .count()
329}
330
331fn push_varint(output: &mut Vec<u8>, mut value: usize) {
332 loop {
333 let byte = u8::try_from(value & 0x7F).unwrap_or(0);
334 value >>= 7;
335 if value == 0 {
336 output.push(byte);
337 return;
338 }
339 output.push(byte | 0x80);
340 }
341}
342
343fn read_varint(input: &[u8]) -> Option<(usize, usize)> {
344 let mut value = 0_usize;
345 let mut shift = 0_u32;
346 for (index, byte) in input.iter().enumerate() {
347 if shift >= usize::BITS {
348 return None;
349 }
350 value |= usize::from(byte & 0x7F) << shift;
351 if byte & 0x80 == 0 {
352 return Some((value, index + 1));
353 }
354 shift += 7;
355 }
356 None
357}
358
359#[cfg(test)]
360#[allow(
361 clippy::expect_used,
362 clippy::indexing_slicing,
363 clippy::panic,
364 clippy::unwrap_used
365)]
366mod tests {
367 use super::*;
368
369 fn build(terms: &[&str]) -> TermDictionary {
370 let mut dictionary = TermDictionary::default();
371 for (index, term) in terms.iter().enumerate() {
372 dictionary
373 .push(
374 term.as_bytes(),
375 TermInfo {
376 document_frequency: u32::try_from(index + 1).expect("small"),
377 postings_offset: index as u64 * 16,
378 postings_length: 16,
379 },
380 )
381 .expect("sorted fixture");
382 }
383 dictionary
384 }
385
386 fn sorted_sample() -> Vec<String> {
387 let mut terms: Vec<String> = Vec::new();
388 for first in b'a'..=b'f' {
389 for second in b'a'..=b'z' {
390 terms.push(format!("{}{}", char::from(first), char::from(second)));
391 }
392 }
393 terms.sort();
394 terms
395 }
396
397 #[test]
398 fn exact_lookup_finds_every_term_and_only_those() {
399 let sample = sorted_sample();
400 let refs: Vec<&str> = sample.iter().map(String::as_str).collect();
401 let dictionary = build(&refs);
402 assert_eq!(dictionary.len(), sample.len());
403 for (index, term) in sample.iter().enumerate() {
404 let info = dictionary.get(term.as_bytes()).expect("term present");
405 assert_eq!(
406 info.document_frequency,
407 u32::try_from(index + 1).expect("small")
408 );
409 }
410 for absent in ["", "zzz", "aa0", "g", "ab_"] {
411 assert_eq!(dictionary.get(absent.as_bytes()), None, "{absent:?}");
412 }
413 }
414
415 #[test]
416 fn front_coding_round_trips_through_its_own_bytes() {
417 let sample = sorted_sample();
418 let refs: Vec<&str> = sample.iter().map(String::as_str).collect();
419 let dictionary = build(&refs);
420 let decoded = dictionary.decode_terms().expect("decodes");
421 let expected: Vec<Vec<u8>> = sample.iter().map(|t| t.as_bytes().to_vec()).collect();
422 assert_eq!(decoded, expected);
423 }
424
425 #[test]
426 fn front_coding_actually_shares_prefixes() {
427 let dictionary = build(&["engine", "engineer", "engineering", "engines"]);
428 assert!(
431 dictionary.encoded_bytes().len() < 30,
432 "front coding did not compress: {} bytes",
433 dictionary.encoded_bytes().len()
434 );
435 assert_eq!(dictionary.decode_terms().expect("decodes").len(), 4);
436 }
437
438 #[test]
439 fn prefix_range_equals_brute_force() {
440 let sample = sorted_sample();
441 let refs: Vec<&str> = sample.iter().map(String::as_str).collect();
442 let dictionary = build(&refs);
443 for prefix in ["a", "b", "ab", "zz", "f"] {
444 let range = dictionary.prefix_range(prefix.as_bytes()).expect("prefix");
445 let actual: Vec<&str> = range
446 .clone()
447 .filter_map(|index| dictionary.term_at(index))
448 .filter_map(|term| std::str::from_utf8(term).ok())
449 .collect();
450 let expected: Vec<&str> = sample
451 .iter()
452 .map(String::as_str)
453 .filter(|term| term.starts_with(prefix))
454 .collect();
455 assert_eq!(actual, expected, "prefix {prefix:?}");
456 }
457 }
458
459 #[test]
460 fn an_empty_prefix_is_a_typed_error_not_a_full_scan() {
461 let dictionary = build(&["alpha", "beta"]);
462 assert_eq!(dictionary.prefix_range(b""), Err(DictError::EmptyTerm));
463 }
464
465 #[test]
466 fn seek_ceiling_lands_on_the_first_term_at_or_after_the_key() {
467 let dictionary = build(&["ant", "bee", "cat", "dog"]);
468 assert_eq!(dictionary.seek_ceiling(b"ant"), Some(0));
469 assert_eq!(dictionary.seek_ceiling(b"apple"), Some(1));
470 assert_eq!(dictionary.seek_ceiling(b"bee"), Some(1));
471 assert_eq!(dictionary.seek_ceiling(b"cz"), Some(3));
472 assert_eq!(dictionary.seek_ceiling(b"zebra"), None);
473 assert_eq!(dictionary.seek_ceiling(b""), Some(0));
474 }
475
476 #[test]
477 fn out_of_order_and_duplicate_terms_are_refused() {
478 let mut dictionary = TermDictionary::default();
479 dictionary
480 .push(b"beta", TermInfo::default())
481 .expect("first");
482 assert_eq!(
483 dictionary.push(b"alpha", TermInfo::default()),
484 Err(DictError::NotSorted {
485 term: String::from("alpha")
486 })
487 );
488 assert_eq!(
489 dictionary.push(b"beta", TermInfo::default()),
490 Err(DictError::Duplicate {
491 term: String::from("beta")
492 })
493 );
494 }
495
496 #[test]
497 fn an_empty_dictionary_answers_every_query_negatively() {
498 let dictionary = TermDictionary::default();
499 assert!(dictionary.is_empty());
500 assert_eq!(dictionary.get(b"anything"), None);
501 assert_eq!(dictionary.seek_exact(b"anything"), None);
502 assert_eq!(dictionary.seek_ceiling(b"anything"), None);
503 assert_eq!(dictionary.prefix_range(b"a").expect("prefix"), 0..0);
504 }
505
506 #[test]
507 fn building_from_a_sorted_map_matches_incremental_pushes() {
508 let mut map = BTreeMap::new();
509 for (index, term) in ["ant", "bee", "cat"].into_iter().enumerate() {
510 map.insert(
511 term.as_bytes().to_vec(),
512 TermInfo {
513 document_frequency: u32::try_from(index + 1).expect("small"),
514 postings_offset: index as u64 * 16,
515 postings_length: 16,
516 },
517 );
518 }
519 let from_map = TermDictionary::from_map(&map);
520 let incremental = build(&["ant", "bee", "cat"]);
521 assert_eq!(from_map.encoded_bytes(), incremental.encoded_bytes());
522 assert_eq!(from_map.len(), 3);
523 }
524
525 #[test]
526 fn varints_round_trip_at_the_boundaries() {
527 for value in [0_usize, 1, 127, 128, 300, 16_383, 16_384, 1_000_000] {
528 let mut buffer = Vec::new();
529 push_varint(&mut buffer, value);
530 let (decoded, consumed) = read_varint(&buffer).expect("decodes");
531 assert_eq!(decoded, value);
532 assert_eq!(consumed, buffer.len());
533 }
534 assert_eq!(read_varint(&[0x80]), None, "an unterminated varint");
535 }
536
537 #[test]
538 fn lookups_work_across_many_blocks() {
539 let sample = sorted_sample();
541 assert!(sample.len() > TERMS_PER_BLOCK * 4);
542 let refs: Vec<&str> = sample.iter().map(String::as_str).collect();
543 let dictionary = build(&refs);
544 assert_eq!(
545 dictionary.seek_exact(sample.last().expect("non-empty").as_bytes()),
546 Some(sample.len() - 1)
547 );
548 assert_eq!(dictionary.seek_exact(sample[0].as_bytes()), Some(0));
549 }
550}