1use std::collections::BTreeMap;
36
37use rustc_hash::FxHashMap;
38
39use crate::{
40 block::{Block, BlockType},
41 error::MqdbError,
42};
43
44#[derive(Debug, Default, Clone)]
55pub struct BitmapIndex {
56 map: FxHashMap<BlockType, Vec<u32>>,
57}
58
59impl BitmapIndex {
60 pub fn build(blocks: &[Block]) -> Self {
61 let mut map: FxHashMap<BlockType, Vec<u32>> = FxHashMap::default();
62 for (idx, block) in blocks.iter().enumerate() {
63 map.entry(block.block_type.clone())
64 .or_default()
65 .push(idx as u32);
66 }
67 Self { map }
68 }
69
70 pub fn get(&self, block_type: &BlockType) -> &[u32] {
72 self.map.get(block_type).map(Vec::as_slice).unwrap_or(&[])
73 }
74
75 pub fn get_any(&self, types: &[BlockType]) -> Vec<u32> {
77 let mut result: Vec<u32> = types
78 .iter()
79 .flat_map(|t| self.get(t).iter().copied())
80 .collect();
81 result.sort_unstable();
82 result.dedup();
83 result
84 }
85
86 pub fn contains_type(&self, block_type: &BlockType) -> bool {
88 self.map.contains_key(block_type)
89 }
90}
91
92#[derive(Debug, Default, Clone)]
105pub struct BTreeIndex {
106 by_pre: BTreeMap<u32, u32>,
108 by_post: BTreeMap<u32, u32>,
110}
111
112impl BTreeIndex {
113 pub fn build(blocks: &[Block]) -> Self {
114 let mut by_pre = BTreeMap::new();
115 let mut by_post = BTreeMap::new();
116 for (idx, block) in blocks.iter().enumerate() {
117 by_pre.insert(block.pre, idx as u32);
118 by_post.insert(block.post, idx as u32);
119 }
120 Self { by_pre, by_post }
121 }
122
123 pub fn get_by_pre(&self, pre: u32) -> Option<u32> {
125 self.by_pre.get(&pre).copied()
126 }
127
128 pub fn get_by_post(&self, post: u32) -> Option<u32> {
130 self.by_post.get(&post).copied()
131 }
132
133 pub fn range_by_pre(&self, lo: u32, hi: u32) -> impl Iterator<Item = u32> + '_ {
135 self.by_pre.range(lo..=hi).map(|(_, &idx)| idx)
136 }
137
138 pub fn range_by_post(&self, lo: u32, hi: u32) -> impl Iterator<Item = u32> + '_ {
140 self.by_post.range(lo..=hi).map(|(_, &idx)| idx)
141 }
142}
143
144#[derive(Debug, Default, Clone)]
154pub struct HashIndex {
155 pub by_content: FxHashMap<String, Vec<u32>>,
157 pub by_lang: FxHashMap<String, Vec<u32>>,
159 pub by_depth: FxHashMap<u8, Vec<u32>>,
161}
162
163impl HashIndex {
164 pub fn build(blocks: &[Block]) -> Self {
165 let mut by_content: FxHashMap<String, Vec<u32>> = FxHashMap::default();
166 let mut by_lang: FxHashMap<String, Vec<u32>> = FxHashMap::default();
167 let mut by_depth: FxHashMap<u8, Vec<u32>> = FxHashMap::default();
168
169 for (idx, block) in blocks.iter().enumerate() {
170 let i = idx as u32;
171 by_content
172 .entry(block.content.to_lowercase())
173 .or_default()
174 .push(i);
175
176 if let Some(lang) = block.code_lang() {
177 by_lang.entry(lang.to_string()).or_default().push(i);
178 }
179 if let Some(depth) = block.heading_depth() {
180 by_depth.entry(depth).or_default().push(i);
181 }
182 }
183
184 Self {
185 by_content,
186 by_lang,
187 by_depth,
188 }
189 }
190
191 pub fn by_content(&self, content: &str) -> &[u32] {
193 self.by_content
194 .get(&content.to_lowercase())
195 .map(Vec::as_slice)
196 .unwrap_or(&[])
197 }
198
199 pub fn by_lang(&self, lang: &str) -> &[u32] {
201 self.by_lang.get(lang).map(Vec::as_slice).unwrap_or(&[])
202 }
203
204 pub fn by_depth(&self, depth: u8) -> &[u32] {
206 self.by_depth.get(&depth).map(Vec::as_slice).unwrap_or(&[])
207 }
208}
209
210pub fn tokenize(text: &str) -> Vec<String> {
226 text.to_lowercase()
227 .split(|c: char| !c.is_alphanumeric())
228 .filter(|s| !s.is_empty())
229 .map(str::to_string)
230 .collect()
231}
232
233#[derive(Debug, Default, Clone)]
241pub struct TermIndex {
242 postings: FxHashMap<String, Vec<u32>>,
243}
244
245impl TermIndex {
246 pub fn build(blocks: &[Block]) -> Self {
247 let mut postings: FxHashMap<String, Vec<u32>> = FxHashMap::default();
248 for (idx, block) in blocks.iter().enumerate() {
249 let mut terms = tokenize(&block.content);
253 terms.sort_unstable();
254 terms.dedup();
255 for term in terms {
256 postings.entry(term).or_default().push(idx as u32);
257 }
258 }
259 Self { postings }
260 }
261
262 pub fn intersect(&self, terms: &[String]) -> Vec<u32> {
272 if terms.is_empty() {
273 return Vec::new();
274 }
275 let mut lists: Vec<&[u32]> = Vec::with_capacity(terms.len());
276 for term in terms {
277 match self.postings.get(term) {
278 Some(list) if !list.is_empty() => lists.push(list),
279 _ => return Vec::new(),
280 }
281 }
282 lists.sort_unstable_by_key(|l| l.len());
283
284 let mut acc: Vec<u32> = lists[0].to_vec();
285 for list in &lists[1..] {
286 if acc.is_empty() {
287 break;
288 }
289 acc = merge_intersect(&acc, list);
290 }
291 acc
292 }
293}
294
295fn merge_intersect(a: &[u32], b: &[u32]) -> Vec<u32> {
297 let mut out = Vec::with_capacity(a.len().min(b.len()));
298 let (mut i, mut j) = (0usize, 0usize);
299 while i < a.len() && j < b.len() {
300 match a[i].cmp(&b[j]) {
301 std::cmp::Ordering::Less => i += 1,
302 std::cmp::Ordering::Greater => j += 1,
303 std::cmp::Ordering::Equal => {
304 out.push(a[i]);
305 i += 1;
306 j += 1;
307 }
308 }
309 }
310 out
311}
312
313#[derive(Debug, Default, Clone)]
320pub struct DocumentIndex {
321 pub bitmap: BitmapIndex,
322 pub btree: BTreeIndex,
323 pub hash: HashIndex,
324 pub term: TermIndex,
325}
326
327impl DocumentIndex {
328 pub fn build(blocks: &[Block]) -> Self {
329 Self {
330 bitmap: BitmapIndex::build(blocks),
331 btree: BTreeIndex::build(blocks),
332 hash: HashIndex::build(blocks),
333 term: TermIndex::build(blocks),
334 }
335 }
336
337 pub fn to_bytes(&self) -> Vec<u8> {
339 let mut out = Vec::new();
340
341 let mut bitmap_entries: Vec<(&BlockType, &Vec<u32>)> = self.bitmap.map.iter().collect();
343 bitmap_entries.sort_by_key(|(bt, _)| block_type_ord(bt));
344 out.extend_from_slice(&(bitmap_entries.len() as u32).to_le_bytes());
345 for (bt, indices) in &bitmap_entries {
346 out.push(block_type_ord(bt));
347 out.extend_from_slice(&(indices.len() as u32).to_le_bytes());
348 for &idx in indices.iter() {
349 out.extend_from_slice(&idx.to_le_bytes());
350 }
351 }
352
353 out.extend_from_slice(&(self.btree.by_pre.len() as u32).to_le_bytes());
355 for (&pre, &idx) in &self.btree.by_pre {
356 out.extend_from_slice(&pre.to_le_bytes());
357 out.extend_from_slice(&idx.to_le_bytes());
358 }
359
360 out.extend_from_slice(&(self.btree.by_post.len() as u32).to_le_bytes());
362 for (&post, &idx) in &self.btree.by_post {
363 out.extend_from_slice(&post.to_le_bytes());
364 out.extend_from_slice(&idx.to_le_bytes());
365 }
366
367 let mut content_entries: Vec<(&String, &Vec<u32>)> = self.hash.by_content.iter().collect();
369 content_entries.sort_by_key(|(k, _)| k.as_str());
370 out.extend_from_slice(&(content_entries.len() as u32).to_le_bytes());
371 for (key, indices) in &content_entries {
372 let kb = key.as_bytes();
373 out.extend_from_slice(&(kb.len() as u32).to_le_bytes());
377 out.extend_from_slice(kb);
378 out.extend_from_slice(&(indices.len() as u32).to_le_bytes());
379 for &idx in indices.iter() {
380 out.extend_from_slice(&idx.to_le_bytes());
381 }
382 }
383
384 let mut lang_entries: Vec<(&String, &Vec<u32>)> = self.hash.by_lang.iter().collect();
386 lang_entries.sort_by_key(|(k, _)| k.as_str());
387 out.extend_from_slice(&(lang_entries.len() as u32).to_le_bytes());
388 for (key, indices) in &lang_entries {
389 let kb = key.as_bytes();
390 out.extend_from_slice(&(kb.len() as u32).to_le_bytes());
391 out.extend_from_slice(kb);
392 out.extend_from_slice(&(indices.len() as u32).to_le_bytes());
393 for &idx in indices.iter() {
394 out.extend_from_slice(&idx.to_le_bytes());
395 }
396 }
397
398 let mut depth_entries: Vec<(&u8, &Vec<u32>)> = self.hash.by_depth.iter().collect();
400 depth_entries.sort_by_key(|&(&d, _)| d);
401 out.extend_from_slice(&(depth_entries.len() as u32).to_le_bytes());
402 for &(&depth, indices) in &depth_entries {
403 out.push(depth);
404 out.extend_from_slice(&(indices.len() as u32).to_le_bytes());
405 for &idx in indices.iter() {
406 out.extend_from_slice(&idx.to_le_bytes());
407 }
408 }
409
410 let mut term_entries: Vec<(&String, &Vec<u32>)> = self.term.postings.iter().collect();
414 term_entries.sort_by_key(|(k, _)| k.as_str());
415 out.extend_from_slice(&(term_entries.len() as u32).to_le_bytes());
416 for (term, indices) in &term_entries {
417 let tb = term.as_bytes();
418 out.extend_from_slice(&(tb.len() as u32).to_le_bytes());
419 out.extend_from_slice(tb);
420 out.extend_from_slice(&(indices.len() as u32).to_le_bytes());
421 for &idx in indices.iter() {
422 out.extend_from_slice(&idx.to_le_bytes());
423 }
424 }
425
426 out
427 }
428
429 pub fn from_bytes(data: &[u8]) -> Result<Self, MqdbError> {
431 let mut pos = 0usize;
432
433 macro_rules! read_u8 {
434 () => {{
435 if pos >= data.len() {
436 return Err(MqdbError::Storage("unexpected end of index data".into()));
437 }
438 let v = data[pos];
439 pos += 1;
440 v
441 }};
442 }
443 macro_rules! read_u32 {
444 () => {{
445 let end = pos + 4;
446 if end > data.len() {
447 return Err(MqdbError::Storage("unexpected end of index data".into()));
448 }
449 let v = u32::from_le_bytes(data[pos..end].try_into().unwrap());
450 pos = end;
451 v
452 }};
453 }
454 macro_rules! read_str {
455 ($len:expr) => {{
456 let end = pos + $len;
457 if end > data.len() {
458 return Err(MqdbError::Storage("unexpected end of index data".into()));
459 }
460 let s = String::from_utf8(data[pos..end].to_vec())
461 .map_err(|_| MqdbError::Storage("invalid UTF-8 in index".into()))?;
462 pos = end;
463 s
464 }};
465 }
466
467 let num_bitmap = read_u32!() as usize;
469 let mut bitmap_map: FxHashMap<BlockType, Vec<u32>> = FxHashMap::default();
470 for _ in 0..num_bitmap {
471 let bt = block_type_from_ord(read_u8!())?;
472 let count = read_u32!() as usize;
473 let mut indices = Vec::with_capacity(count);
474 for _ in 0..count {
475 indices.push(read_u32!());
476 }
477 bitmap_map.insert(bt, indices);
478 }
479
480 let num_pre = read_u32!() as usize;
482 let mut by_pre = BTreeMap::new();
483 for _ in 0..num_pre {
484 let pre = read_u32!();
485 let idx = read_u32!();
486 by_pre.insert(pre, idx);
487 }
488
489 let num_post = read_u32!() as usize;
491 let mut by_post = BTreeMap::new();
492 for _ in 0..num_post {
493 let post = read_u32!();
494 let idx = read_u32!();
495 by_post.insert(post, idx);
496 }
497
498 let num_content = read_u32!() as usize;
500 let mut by_content: FxHashMap<String, Vec<u32>> = FxHashMap::default();
501 for _ in 0..num_content {
502 let key_len = read_u32!() as usize;
503 let key = read_str!(key_len);
504 let count = read_u32!() as usize;
505 let mut indices = Vec::with_capacity(count);
506 for _ in 0..count {
507 indices.push(read_u32!());
508 }
509 by_content.insert(key, indices);
510 }
511
512 let num_lang = read_u32!() as usize;
514 let mut by_lang: FxHashMap<String, Vec<u32>> = FxHashMap::default();
515 for _ in 0..num_lang {
516 let key_len = read_u32!() as usize;
517 let key = read_str!(key_len);
518 let count = read_u32!() as usize;
519 let mut indices = Vec::with_capacity(count);
520 for _ in 0..count {
521 indices.push(read_u32!());
522 }
523 by_lang.insert(key, indices);
524 }
525
526 let num_depth = read_u32!() as usize;
528 let mut by_depth: FxHashMap<u8, Vec<u32>> = FxHashMap::default();
529 for _ in 0..num_depth {
530 let depth = read_u8!();
531 let count = read_u32!() as usize;
532 let mut indices = Vec::with_capacity(count);
533 for _ in 0..count {
534 indices.push(read_u32!());
535 }
536 by_depth.insert(depth, indices);
537 }
538
539 let num_terms = read_u32!() as usize;
541 let mut postings: FxHashMap<String, Vec<u32>> = FxHashMap::default();
542 for _ in 0..num_terms {
543 let term_len = read_u32!() as usize;
544 let term = read_str!(term_len);
545 let count = read_u32!() as usize;
546 let mut indices = Vec::with_capacity(count);
547 for _ in 0..count {
548 indices.push(read_u32!());
549 }
550 postings.insert(term, indices);
551 }
552
553 Ok(DocumentIndex {
554 bitmap: BitmapIndex { map: bitmap_map },
555 btree: BTreeIndex { by_pre, by_post },
556 hash: HashIndex {
557 by_content,
558 by_lang,
559 by_depth,
560 },
561 term: TermIndex { postings },
562 })
563 }
564}
565
566fn block_type_ord(bt: &BlockType) -> u8 {
567 match bt {
568 BlockType::Heading => 0,
569 BlockType::Paragraph => 1,
570 BlockType::Code => 2,
571 BlockType::List => 3,
572 BlockType::TableCell => 4,
573 BlockType::TableRow => 5,
574 BlockType::TableAlign => 6,
575 BlockType::Blockquote => 7,
576 BlockType::HorizontalRule => 8,
577 BlockType::Html => 9,
578 BlockType::Yaml => 10,
579 BlockType::Toml => 11,
580 BlockType::Math => 12,
581 BlockType::Definition => 13,
582 BlockType::Footnote => 14,
583 }
584}
585
586fn block_type_from_ord(v: u8) -> Result<BlockType, MqdbError> {
587 match v {
588 0 => Ok(BlockType::Heading),
589 1 => Ok(BlockType::Paragraph),
590 2 => Ok(BlockType::Code),
591 3 => Ok(BlockType::List),
592 4 => Ok(BlockType::TableCell),
593 5 => Ok(BlockType::TableRow),
594 6 => Ok(BlockType::TableAlign),
595 7 => Ok(BlockType::Blockquote),
596 8 => Ok(BlockType::HorizontalRule),
597 9 => Ok(BlockType::Html),
598 10 => Ok(BlockType::Yaml),
599 11 => Ok(BlockType::Toml),
600 12 => Ok(BlockType::Math),
601 13 => Ok(BlockType::Definition),
602 14 => Ok(BlockType::Footnote),
603 _ => Err(MqdbError::Storage(format!("unknown block type ord: {v}"))),
604 }
605}
606
607#[derive(Debug, Clone, PartialEq)]
613pub enum IndexHint {
614 BlockType(Vec<BlockType>),
616 PreExact(u32),
618 PreRange(u32, u32),
620 ContentExact(String),
622 LangExact(String),
624 DepthExact(u8),
626 TermMatch(Vec<String>),
628 FullScan,
630}
631
632impl IndexHint {
633 pub fn resolve(&self, idx: &DocumentIndex) -> Option<Vec<u32>> {
637 match self {
638 IndexHint::BlockType(types) => Some(idx.bitmap.get_any(types)),
639 IndexHint::PreExact(pre) => Some(idx.btree.get_by_pre(*pre).into_iter().collect()),
640 IndexHint::PreRange(lo, hi) => Some(idx.btree.range_by_pre(*lo, *hi).collect()),
641 IndexHint::ContentExact(c) => Some(idx.hash.by_content(c).to_vec()),
642 IndexHint::LangExact(l) => Some(idx.hash.by_lang(l).to_vec()),
643 IndexHint::DepthExact(d) => Some(idx.hash.by_depth(*d).to_vec()),
644 IndexHint::TermMatch(terms) => Some(idx.term.intersect(terms)),
645 IndexHint::FullScan => None,
646 }
647 }
648}
649
650#[cfg(test)]
653mod tests {
654 use super::*;
655 use mq_markdown::Markdown;
656 use rstest::rstest;
657
658 use crate::index::build_blocks;
659
660 fn blocks_from(md: &str) -> Vec<Block> {
661 let doc = md.parse::<Markdown>().unwrap();
662 build_blocks(0, &doc.nodes)
663 }
664
665 #[test]
666 fn test_bitmap_heading_lookup() {
667 let blocks = blocks_from("# H1\n\n## H2\n\nParagraph\n\n```rust\ncode\n```\n");
668 let idx = DocumentIndex::build(&blocks);
669
670 let headings = idx.bitmap.get(&BlockType::Heading);
671 assert_eq!(headings.len(), 2);
672
673 let codes = idx.bitmap.get(&BlockType::Code);
674 assert_eq!(codes.len(), 1);
675
676 let paras = idx.bitmap.get(&BlockType::Paragraph);
677 assert_eq!(paras.len(), 1);
678 }
679
680 #[test]
681 fn test_bitmap_get_any() {
682 let blocks = blocks_from("# H1\n\nParagraph\n\n```rust\ncode\n```\n");
683 let idx = DocumentIndex::build(&blocks);
684
685 let result = idx.bitmap.get_any(&[BlockType::Heading, BlockType::Code]);
686 assert_eq!(result.len(), 2);
687 }
688
689 #[test]
690 fn test_btree_pre_lookup() {
691 let blocks = blocks_from("# H1\n\nParagraph\n");
692 let idx = DocumentIndex::build(&blocks);
693
694 for (i, block) in blocks.iter().enumerate() {
696 let found = idx.btree.get_by_pre(block.pre);
697 assert_eq!(
698 found,
699 Some(i as u32),
700 "pre={} not found in btree",
701 block.pre
702 );
703 }
704 }
705
706 #[test]
707 fn test_btree_pre_range() {
708 let blocks = blocks_from("# A\n\n## B\n\n### C\n\nParagraph\n");
709 let idx = DocumentIndex::build(&blocks);
710
711 let max_pre = blocks.iter().map(|b| b.pre).max().unwrap_or(0);
712 let all: Vec<u32> = idx.btree.range_by_pre(0, max_pre).collect();
713 assert_eq!(
714 all.len(),
715 blocks.len(),
716 "range scan should cover all blocks"
717 );
718 }
719
720 #[test]
721 fn test_hash_content_lookup() {
722 let blocks = blocks_from("## Architecture\n\nDetails\n");
723 let idx = DocumentIndex::build(&blocks);
724
725 let found = idx.hash.by_content("architecture");
726 assert_eq!(found.len(), 1);
727 assert_eq!(blocks[found[0] as usize].content, "Architecture");
728 }
729
730 #[test]
731 fn test_hash_lang_lookup() {
732 let blocks = blocks_from("```rust\nfn main(){}\n```\n\n```python\npass\n```\n");
733 let idx = DocumentIndex::build(&blocks);
734
735 assert_eq!(idx.hash.by_lang("rust").len(), 1);
736 assert_eq!(idx.hash.by_lang("python").len(), 1);
737 assert_eq!(idx.hash.by_lang("go").len(), 0);
738 }
739
740 #[test]
741 fn test_hash_depth_lookup() {
742 let blocks = blocks_from("# H1\n\n## H2\n\n## H2b\n\n### H3\n");
743 let idx = DocumentIndex::build(&blocks);
744
745 assert_eq!(idx.hash.by_depth(1).len(), 1);
746 assert_eq!(idx.hash.by_depth(2).len(), 2);
747 assert_eq!(idx.hash.by_depth(3).len(), 1);
748 }
749
750 #[test]
751 fn test_index_hint_resolve_block_type() {
752 let blocks = blocks_from("# H1\n\nPara\n\n```rust\ncode\n```\n");
753 let idx = DocumentIndex::build(&blocks);
754
755 let hint = IndexHint::BlockType(vec![BlockType::Heading]);
756 let result = hint.resolve(&idx).unwrap();
757 assert_eq!(result.len(), 1);
758 assert_eq!(blocks[result[0] as usize].block_type, BlockType::Heading);
759 }
760
761 #[test]
762 fn test_index_hint_fullscan_returns_none() {
763 let blocks = blocks_from("# H1\n");
764 let idx = DocumentIndex::build(&blocks);
765 assert!(IndexHint::FullScan.resolve(&idx).is_none());
766 }
767
768 #[rstest]
769 #[case(BlockType::Heading, 2)]
770 #[case(BlockType::Paragraph, 1)]
771 #[case(BlockType::Code, 1)]
772 #[case(BlockType::List, 1)]
773 #[case(BlockType::Blockquote, 0)]
774 fn test_bitmap_block_type_count_param(#[case] block_type: BlockType, #[case] expected: usize) {
775 let blocks = blocks_from("# H1\n\n## H2\n\nParagraph\n\n```rust\ncode\n```\n\n- item\n");
776 let idx = DocumentIndex::build(&blocks);
777 assert_eq!(idx.bitmap.get(&block_type).len(), expected);
778 }
779
780 #[rstest]
781 #[case(1, 1)]
782 #[case(2, 2)]
783 #[case(3, 1)]
784 #[case(4, 0)]
785 fn test_hash_depth_count_param(#[case] depth: u8, #[case] expected: usize) {
786 let blocks = blocks_from("# H1\n\n## H2a\n\n## H2b\n\n### H3\n");
787 let idx = DocumentIndex::build(&blocks);
788 assert_eq!(idx.hash.by_depth(depth).len(), expected);
789 }
790
791 #[rstest]
792 #[case("rust", 1)]
793 #[case("python", 1)]
794 #[case("go", 0)]
795 fn test_hash_lang_count_param(#[case] lang: &str, #[case] expected: usize) {
796 let blocks = blocks_from("```rust\nfn main(){}\n```\n\n```python\npass\n```\n");
797 let idx = DocumentIndex::build(&blocks);
798 assert_eq!(idx.hash.by_lang(lang).len(), expected);
799 }
800
801 #[rstest]
802 #[case(vec![BlockType::Heading], 2)]
803 #[case(vec![BlockType::Paragraph], 1)]
804 #[case(vec![BlockType::Code], 1)]
805 #[case(vec![BlockType::Heading, BlockType::Code], 3)]
806 fn test_index_hint_block_type_count_param(
807 #[case] types: Vec<BlockType>,
808 #[case] expected: usize,
809 ) {
810 let blocks = blocks_from("# H1\n\n## H2\n\nParagraph\n\n```rust\ncode\n```\n");
811 let idx = DocumentIndex::build(&blocks);
812 let result = IndexHint::BlockType(types).resolve(&idx).unwrap();
813 assert_eq!(result.len(), expected);
814 }
815
816 #[rstest]
817 #[case("fn main() {}", vec!["fn", "main"])]
818 #[case("v1.2.3", vec!["v1", "2", "3"])]
819 #[case("", vec![])]
820 #[case("CamelCase HTML_tag", vec!["camelcase", "html", "tag"])]
821 fn test_tokenize_param(#[case] input: &str, #[case] expected: Vec<&str>) {
822 let expected: Vec<String> = expected.into_iter().map(str::to_string).collect();
823 assert_eq!(tokenize(input), expected);
824 }
825
826 #[test]
827 fn test_term_index_build_and_postings() {
828 let blocks = blocks_from("# Hello World\n\nSome prose about Rust\n");
829 let idx = DocumentIndex::build(&blocks);
830 let hits = idx.term.intersect(&["rust".to_string()]);
831 assert_eq!(hits.len(), 1);
832 assert!(blocks[hits[0] as usize].content.contains("Rust"));
833 }
834
835 #[test]
836 fn test_term_index_intersect_and_semantics() {
837 let blocks = blocks_from("# H1\n\nfoo bar baz\n\nfoo only\n");
838 let idx = DocumentIndex::build(&blocks);
839
840 let both = idx.term.intersect(&["foo".to_string(), "bar".to_string()]);
841 assert_eq!(both.len(), 1);
842
843 let missing = idx
844 .term
845 .intersect(&["foo".to_string(), "nonexistent".to_string()]);
846 assert!(missing.is_empty());
847
848 assert!(idx.term.intersect(&[]).is_empty());
849 }
850
851 #[test]
852 fn test_document_index_to_bytes_from_bytes_roundtrip_includes_term_index() {
853 let blocks =
854 blocks_from("# Title\n\nSome prose here.\n\n```rust\nfn main() { let x = 1; }\n```\n");
855 let idx = DocumentIndex::build(&blocks);
856 let restored = DocumentIndex::from_bytes(&idx.to_bytes()).unwrap();
857
858 let mut original: Vec<(String, Vec<u32>)> = idx
859 .term
860 .postings
861 .iter()
862 .map(|(k, v)| (k.clone(), v.clone()))
863 .collect();
864 let mut round_tripped: Vec<(String, Vec<u32>)> = restored
865 .term
866 .postings
867 .iter()
868 .map(|(k, v)| (k.clone(), v.clone()))
869 .collect();
870 original.sort();
871 round_tripped.sort();
872
873 assert!(!original.is_empty());
874 assert_eq!(original, round_tripped);
875 }
876}