1use byteorder::{LittleEndian, WriteBytesExt};
9use std::io::{self, Read, Write};
10
11use super::posting_common::{read_vint, write_vint};
12use crate::DocId;
13use crate::directories::OwnedBytes;
14use crate::structures::simd;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct Posting {
19 pub doc_id: DocId,
20 pub term_freq: u32,
21}
22
23#[derive(Debug, Clone, Default)]
25pub struct PostingList {
26 postings: Vec<Posting>,
27}
28
29impl PostingList {
30 pub fn new() -> Self {
31 Self::default()
32 }
33
34 pub fn with_capacity(capacity: usize) -> Self {
35 Self {
36 postings: Vec::with_capacity(capacity),
37 }
38 }
39
40 pub fn push(&mut self, doc_id: DocId, term_freq: u32) {
42 debug_assert!(
43 self.postings.is_empty() || self.postings.last().unwrap().doc_id < doc_id,
44 "Postings must be added in sorted order"
45 );
46 self.postings.push(Posting { doc_id, term_freq });
47 }
48
49 pub fn add(&mut self, doc_id: DocId, term_freq: u32) {
51 if let Some(last) = self.postings.last_mut()
52 && last.doc_id == doc_id
53 {
54 last.term_freq += term_freq;
55 return;
56 }
57 self.postings.push(Posting { doc_id, term_freq });
58 }
59
60 pub fn doc_count(&self) -> u32 {
62 self.postings.len() as u32
63 }
64
65 pub fn len(&self) -> usize {
66 self.postings.len()
67 }
68
69 pub fn is_empty(&self) -> bool {
70 self.postings.is_empty()
71 }
72
73 pub fn iter(&self) -> impl Iterator<Item = &Posting> {
74 self.postings.iter()
75 }
76
77 pub fn serialize<W: Write>(&self, writer: &mut W) -> io::Result<()> {
79 write_vint(writer, self.postings.len() as u64)?;
81
82 let mut prev_doc_id = 0u32;
83 for posting in &self.postings {
84 let delta = posting.doc_id - prev_doc_id;
86 write_vint(writer, delta as u64)?;
87 write_vint(writer, posting.term_freq as u64)?;
88 prev_doc_id = posting.doc_id;
89 }
90
91 Ok(())
92 }
93
94 pub fn deserialize<R: Read>(reader: &mut R) -> io::Result<Self> {
96 let count = read_vint(reader)? as usize;
97 let mut postings = Vec::with_capacity(count);
98
99 let mut prev_doc_id = 0u32;
100 for _ in 0..count {
101 let delta = read_vint(reader)? as u32;
102 let term_freq = read_vint(reader)? as u32;
103 let doc_id = prev_doc_id + delta;
104 postings.push(Posting { doc_id, term_freq });
105 prev_doc_id = doc_id;
106 }
107
108 Ok(Self { postings })
109 }
110}
111
112pub struct PostingListIterator<'a> {
114 postings: &'a [Posting],
115 position: usize,
116}
117
118impl<'a> PostingListIterator<'a> {
119 pub fn new(posting_list: &'a PostingList) -> Self {
120 Self {
121 postings: &posting_list.postings,
122 position: 0,
123 }
124 }
125
126 pub fn doc(&self) -> DocId {
128 if self.position < self.postings.len() {
129 self.postings[self.position].doc_id
130 } else {
131 TERMINATED
132 }
133 }
134
135 pub fn term_freq(&self) -> u32 {
137 if self.position < self.postings.len() {
138 self.postings[self.position].term_freq
139 } else {
140 0
141 }
142 }
143
144 pub fn advance(&mut self) -> DocId {
146 self.position += 1;
147 self.doc()
148 }
149
150 pub fn seek(&mut self, target: DocId) -> DocId {
152 let remaining = &self.postings[self.position..];
153 let offset = remaining.partition_point(|p| p.doc_id < target);
154 self.position += offset;
155 self.doc()
156 }
157
158 pub fn size_hint(&self) -> usize {
160 self.postings.len().saturating_sub(self.position)
161 }
162}
163
164pub const TERMINATED: DocId = DocId::MAX;
166
167pub const BLOCK_SIZE: usize = 128;
176
177const L1_INTERVAL: usize = 8;
179
180const L0_SIZE: usize = 16;
183
184const L1_SIZE: usize = 4;
186
187const FOOTER_SIZE: usize = 24;
189
190#[inline]
194fn read_l0(bytes: &[u8], idx: usize) -> (u32, u32, u32, f32) {
195 let b = &bytes[idx * L0_SIZE..][..L0_SIZE];
196 let first_doc = u32::from_le_bytes([b[0], b[1], b[2], b[3]]);
197 let last_doc = u32::from_le_bytes([b[4], b[5], b[6], b[7]]);
198 let offset = u32::from_le_bytes([b[8], b[9], b[10], b[11]]);
199 let max_weight = f32::from_le_bytes([b[12], b[13], b[14], b[15]]);
200 (first_doc, last_doc, offset, max_weight)
201}
202
203#[inline]
205fn write_l0(buf: &mut Vec<u8>, first_doc: u32, last_doc: u32, offset: u32, max_weight: f32) {
206 buf.extend_from_slice(&first_doc.to_le_bytes());
207 buf.extend_from_slice(&last_doc.to_le_bytes());
208 buf.extend_from_slice(&offset.to_le_bytes());
209 buf.extend_from_slice(&max_weight.to_le_bytes());
210}
211
212#[inline]
217fn block_data_size(stream: &[u8], pos: usize) -> usize {
218 let count = u16::from_le_bytes(stream[pos..pos + 2].try_into().unwrap()) as usize;
219 let doc_rounded = simd::RoundedBitWidth::from_u8(stream[pos + 6]);
220 let tf_rounded = simd::RoundedBitWidth::from_u8(stream[pos + 7]);
221 let delta_bytes = if count > 1 {
222 (count - 1) * doc_rounded.bytes_per_value()
223 } else {
224 0
225 };
226 8 + delta_bytes + count * tf_rounded.bytes_per_value()
227}
228
229#[derive(Debug, Clone)]
230pub struct BlockPostingList {
231 stream: OwnedBytes,
233 l0_bytes: OwnedBytes,
236 l0_count: usize,
238 l1_docs: Vec<u32>,
241 doc_count: u32,
243 max_tf: u32,
245}
246
247impl BlockPostingList {
248 #[inline]
250 fn read_l0_entry(&self, idx: usize) -> (u32, u32, u32, f32) {
251 read_l0(&self.l0_bytes, idx)
252 }
253
254 pub fn from_posting_list(list: &PostingList) -> io::Result<Self> {
263 let mut stream: Vec<u8> = Vec::new();
264 let mut l0_buf: Vec<u8> = Vec::new();
265 let mut l1_docs: Vec<u32> = Vec::new();
266 let mut l0_count = 0usize;
267 let mut max_tf = 0u32;
268
269 let postings = &list.postings;
270 let mut i = 0;
271
272 let mut deltas = Vec::with_capacity(BLOCK_SIZE);
274 let mut tf_buf = Vec::with_capacity(BLOCK_SIZE);
275
276 while i < postings.len() {
277 if stream.len() > u32::MAX as usize {
278 return Err(io::Error::new(
279 io::ErrorKind::InvalidData,
280 "posting list stream exceeds u32::MAX bytes",
281 ));
282 }
283 let block_start = stream.len() as u32;
284 let block_end = (i + BLOCK_SIZE).min(postings.len());
285 let block = &postings[i..block_end];
286 let count = block.len();
287
288 let block_max_tf = block.iter().map(|p| p.term_freq).max().unwrap_or(0);
290 max_tf = max_tf.max(block_max_tf);
291
292 let base_doc_id = block.first().unwrap().doc_id;
293 let last_doc_id = block.last().unwrap().doc_id;
294
295 deltas.clear();
297 let mut prev = base_doc_id;
298 for posting in block.iter().skip(1) {
299 deltas.push(posting.doc_id - prev);
300 prev = posting.doc_id;
301 }
302 let max_delta = deltas.iter().copied().max().unwrap_or(0);
303 let doc_id_bits = simd::round_bit_width(simd::bits_needed(max_delta));
304
305 tf_buf.clear();
307 tf_buf.extend(block.iter().map(|p| p.term_freq));
308 let tf_bits = simd::round_bit_width(simd::bits_needed(block_max_tf));
309
310 stream.write_u16::<LittleEndian>(count as u16)?;
312 stream.write_u32::<LittleEndian>(base_doc_id)?;
313 stream.push(doc_id_bits);
314 stream.push(tf_bits);
315
316 if count > 1 {
318 let rounded = simd::RoundedBitWidth::from_u8(doc_id_bits);
319 let byte_count = (count - 1) * rounded.bytes_per_value();
320 let start = stream.len();
321 stream.resize(start + byte_count, 0);
322 simd::pack_rounded(&deltas, rounded, &mut stream[start..]);
323 }
324
325 {
327 let rounded = simd::RoundedBitWidth::from_u8(tf_bits);
328 let byte_count = count * rounded.bytes_per_value();
329 let start = stream.len();
330 stream.resize(start + byte_count, 0);
331 simd::pack_rounded(&tf_buf, rounded, &mut stream[start..]);
332 }
333
334 write_l0(
336 &mut l0_buf,
337 base_doc_id,
338 last_doc_id,
339 block_start,
340 block_max_tf as f32,
341 );
342 l0_count += 1;
343
344 if l0_count.is_multiple_of(L1_INTERVAL) {
346 l1_docs.push(last_doc_id);
347 }
348
349 i = block_end;
350 }
351
352 if !l0_count.is_multiple_of(L1_INTERVAL) && l0_count > 0 {
354 let (_, last_doc, _, _) = read_l0(&l0_buf, l0_count - 1);
355 l1_docs.push(last_doc);
356 }
357
358 Ok(Self {
359 stream: OwnedBytes::new(stream),
360 l0_bytes: OwnedBytes::new(l0_buf),
361 l0_count,
362 l1_docs,
363 doc_count: postings.len() as u32,
364 max_tf,
365 })
366 }
367
368 pub fn serialize<W: Write>(&self, writer: &mut W) -> io::Result<()> {
378 writer.write_all(&self.stream)?;
379 writer.write_all(&self.l0_bytes)?;
380 for &doc in &self.l1_docs {
381 writer.write_u32::<LittleEndian>(doc)?;
382 }
383
384 writer.write_u64::<LittleEndian>(self.stream.len() as u64)?;
386 writer.write_u32::<LittleEndian>(self.l0_count as u32)?;
387 writer.write_u32::<LittleEndian>(self.l1_docs.len() as u32)?;
388 writer.write_u32::<LittleEndian>(self.doc_count)?;
389 writer.write_u32::<LittleEndian>(self.max_tf)?;
390
391 Ok(())
392 }
393
394 pub fn deserialize(raw: &[u8]) -> io::Result<Self> {
396 if raw.len() < FOOTER_SIZE {
397 return Err(io::Error::new(
398 io::ErrorKind::InvalidData,
399 "posting data too short",
400 ));
401 }
402
403 let f = raw.len() - FOOTER_SIZE;
404 let stream_len = u64::from_le_bytes(raw[f..f + 8].try_into().unwrap()) as usize;
405 let l0_count = u32::from_le_bytes(raw[f + 8..f + 12].try_into().unwrap()) as usize;
406 let l1_count = u32::from_le_bytes(raw[f + 12..f + 16].try_into().unwrap()) as usize;
407 let doc_count = u32::from_le_bytes(raw[f + 16..f + 20].try_into().unwrap());
408 let max_tf = u32::from_le_bytes(raw[f + 20..f + 24].try_into().unwrap());
409
410 let l0_start = stream_len;
411 let l0_end = l0_start + l0_count * L0_SIZE;
412 let l1_start = l0_end;
413
414 let l1_docs = Self::extract_l1_docs(&raw[l1_start..], l1_count);
415
416 Ok(Self {
417 stream: OwnedBytes::new(raw[..stream_len].to_vec()),
418 l0_bytes: OwnedBytes::new(raw[l0_start..l0_end].to_vec()),
419 l0_count,
420 l1_docs,
421 doc_count,
422 max_tf,
423 })
424 }
425
426 pub fn deserialize_zero_copy(raw: OwnedBytes) -> io::Result<Self> {
430 if raw.len() < FOOTER_SIZE {
431 return Err(io::Error::new(
432 io::ErrorKind::InvalidData,
433 "posting data too short",
434 ));
435 }
436
437 let f = raw.len() - FOOTER_SIZE;
438 let stream_len = u64::from_le_bytes(raw[f..f + 8].try_into().unwrap()) as usize;
439 let l0_count = u32::from_le_bytes(raw[f + 8..f + 12].try_into().unwrap()) as usize;
440 let l1_count = u32::from_le_bytes(raw[f + 12..f + 16].try_into().unwrap()) as usize;
441 let doc_count = u32::from_le_bytes(raw[f + 16..f + 20].try_into().unwrap());
442 let max_tf = u32::from_le_bytes(raw[f + 20..f + 24].try_into().unwrap());
443
444 let l0_start = stream_len;
445 let l0_end = l0_start + l0_count * L0_SIZE;
446 let l1_start = l0_end;
447
448 let l1_docs = Self::extract_l1_docs(&raw[l1_start..], l1_count);
449
450 Ok(Self {
451 stream: raw.slice(0..stream_len),
452 l0_bytes: raw.slice(l0_start..l0_end),
453 l0_count,
454 l1_docs,
455 doc_count,
456 max_tf,
457 })
458 }
459
460 fn extract_l1_docs(bytes: &[u8], count: usize) -> Vec<u32> {
462 let mut docs = Vec::with_capacity(count);
463 for i in 0..count {
464 let p = i * L1_SIZE;
465 docs.push(u32::from_le_bytes(bytes[p..p + 4].try_into().unwrap()));
466 }
467 docs
468 }
469
470 pub fn doc_count(&self) -> u32 {
471 self.doc_count
472 }
473
474 pub fn max_tf(&self) -> u32 {
476 self.max_tf
477 }
478
479 pub fn num_blocks(&self) -> usize {
481 self.l0_count
482 }
483
484 pub fn block_max_tf(&self, block_idx: usize) -> Option<u32> {
486 if block_idx >= self.l0_count {
487 return None;
488 }
489 let (_, _, _, max_weight) = self.read_l0_entry(block_idx);
490 Some(max_weight as u32)
491 }
492
493 pub fn concatenate_blocks(sources: &[(BlockPostingList, u32)]) -> io::Result<Self> {
496 let mut stream: Vec<u8> = Vec::new();
497 let mut l0_buf: Vec<u8> = Vec::new();
498 let mut l1_docs: Vec<u32> = Vec::new();
499 let mut l0_count = 0usize;
500 let mut total_docs = 0u32;
501 let mut max_tf = 0u32;
502
503 for (source, doc_offset) in sources {
504 max_tf = max_tf.max(source.max_tf);
505 for block_idx in 0..source.num_blocks() {
506 let (first_doc, last_doc, offset, max_weight) = source.read_l0_entry(block_idx);
507 let blk_size = block_data_size(&source.stream, offset as usize);
508 let block_bytes = &source.stream[offset as usize..offset as usize + blk_size];
509
510 let count = u16::from_le_bytes(block_bytes[0..2].try_into().unwrap());
511 if stream.len() > u32::MAX as usize {
512 return Err(io::Error::new(
513 io::ErrorKind::InvalidData,
514 "posting list stream exceeds u32::MAX bytes during concatenation",
515 ));
516 }
517 let new_offset = stream.len() as u32;
518
519 stream.write_u16::<LittleEndian>(count)?;
521 stream.write_u32::<LittleEndian>(first_doc + doc_offset)?;
522 stream.extend_from_slice(&block_bytes[6..]);
523
524 let new_last = last_doc + doc_offset;
525 write_l0(
526 &mut l0_buf,
527 first_doc + doc_offset,
528 new_last,
529 new_offset,
530 max_weight,
531 );
532 l0_count += 1;
533 total_docs += count as u32;
534
535 if l0_count.is_multiple_of(L1_INTERVAL) {
536 l1_docs.push(new_last);
537 }
538 }
539 }
540
541 if !l0_count.is_multiple_of(L1_INTERVAL) && l0_count > 0 {
543 let (_, last_doc, _, _) = read_l0(&l0_buf, l0_count - 1);
544 l1_docs.push(last_doc);
545 }
546
547 Ok(Self {
548 stream: OwnedBytes::new(stream),
549 l0_bytes: OwnedBytes::new(l0_buf),
550 l0_count,
551 l1_docs,
552 doc_count: total_docs,
553 max_tf,
554 })
555 }
556
557 pub fn concatenate_streaming<W: Write>(
572 sources: &[(&[u8], u32)], writer: &mut W,
574 ) -> crate::Result<(u32, usize)> {
575 struct SourceMeta {
576 stream_len: usize,
577 l0_count: usize,
578 }
579
580 let mut metas: Vec<SourceMeta> = Vec::with_capacity(sources.len());
581 let mut total_docs = 0u32;
582 let mut merged_max_tf = 0u32;
583
584 for (source_index, (raw, _)) in sources.iter().enumerate() {
585 if raw.len() < FOOTER_SIZE {
586 return Err(crate::Error::Corruption(format!(
587 "posting list source {} is shorter than its footer: {} bytes < {}",
588 source_index,
589 raw.len(),
590 FOOTER_SIZE,
591 )));
592 }
593 let f = raw.len() - FOOTER_SIZE;
594 let stream_len = u64::from_le_bytes(raw[f..f + 8].try_into().unwrap()) as usize;
595 let l0_count = u32::from_le_bytes(raw[f + 8..f + 12].try_into().unwrap()) as usize;
596 let doc_count = u32::from_le_bytes(raw[f + 16..f + 20].try_into().unwrap());
598 let max_tf = u32::from_le_bytes(raw[f + 20..f + 24].try_into().unwrap());
599 total_docs += doc_count;
600 merged_max_tf = merged_max_tf.max(max_tf);
601 metas.push(SourceMeta {
602 stream_len,
603 l0_count,
604 });
605 }
606
607 let mut out_l0: Vec<u8> = Vec::new();
610 let mut out_l1_docs: Vec<u32> = Vec::new();
611 let mut out_l0_count = 0usize;
612 let mut stream_written = 0u64;
613 let mut patch_buf = [0u8; 8];
614
615 for (src_idx, meta) in metas.iter().enumerate() {
616 let (raw, doc_offset) = &sources[src_idx];
617 let l0_base = meta.stream_len; let src_stream = &raw[..meta.stream_len];
619
620 for i in 0..meta.l0_count {
621 let (first_doc, last_doc, offset, max_weight) = read_l0(&raw[l0_base..], i);
623
624 let blk_size = block_data_size(src_stream, offset as usize);
626 let block = &src_stream[offset as usize..offset as usize + blk_size];
627
628 let new_last = last_doc + doc_offset;
630 if stream_written > u32::MAX as u64 {
631 return Err(io::Error::new(
632 io::ErrorKind::InvalidData,
633 "posting list stream exceeds u32::MAX bytes during streaming merge",
634 )
635 .into());
636 }
637 write_l0(
638 &mut out_l0,
639 first_doc + doc_offset,
640 new_last,
641 stream_written as u32,
642 max_weight,
643 );
644 out_l0_count += 1;
645
646 if out_l0_count.is_multiple_of(L1_INTERVAL) {
648 out_l1_docs.push(new_last);
649 }
650
651 patch_buf.copy_from_slice(&block[0..8]);
653 let blk_first = u32::from_le_bytes(patch_buf[2..6].try_into().unwrap());
654 patch_buf[2..6].copy_from_slice(&(blk_first + doc_offset).to_le_bytes());
655 writer.write_all(&patch_buf)?;
656 writer.write_all(&block[8..])?;
657
658 stream_written += blk_size as u64;
659 }
660 }
661
662 if !out_l0_count.is_multiple_of(L1_INTERVAL) && out_l0_count > 0 {
664 let (_, last_doc, _, _) = read_l0(&out_l0, out_l0_count - 1);
665 out_l1_docs.push(last_doc);
666 }
667
668 writer.write_all(&out_l0)?;
670 for &doc in &out_l1_docs {
671 writer.write_u32::<LittleEndian>(doc)?;
672 }
673
674 writer.write_u64::<LittleEndian>(stream_written)?;
675 writer.write_u32::<LittleEndian>(out_l0_count as u32)?;
676 writer.write_u32::<LittleEndian>(out_l1_docs.len() as u32)?;
677 writer.write_u32::<LittleEndian>(total_docs)?;
678 writer.write_u32::<LittleEndian>(merged_max_tf)?;
679
680 let l1_bytes_len = out_l1_docs.len() * L1_SIZE;
681 let total_bytes = stream_written as usize + out_l0.len() + l1_bytes_len + FOOTER_SIZE;
682 Ok((total_docs, total_bytes))
683 }
684
685 pub fn decode_block_into(
692 &self,
693 block_idx: usize,
694 doc_ids: &mut Vec<u32>,
695 tfs: &mut Vec<u32>,
696 ) -> bool {
697 if let Some((offset, tf_start, count)) = self.decode_block_doc_ids_only(block_idx, doc_ids)
698 {
699 self.decode_block_tfs_deferred(offset, tf_start, count, tfs);
700 true
701 } else {
702 false
703 }
704 }
705
706 pub fn decode_block_doc_ids_only(
711 &self,
712 block_idx: usize,
713 doc_ids: &mut Vec<u32>,
714 ) -> Option<(usize, usize, usize)> {
715 if block_idx >= self.l0_count {
716 return None;
717 }
718
719 let (_, _, offset, _) = self.read_l0_entry(block_idx);
720 let pos = offset as usize;
721 let blk_size = block_data_size(&self.stream, pos);
722 let block_data = &self.stream[pos..pos + blk_size];
723
724 let count = u16::from_le_bytes(block_data[0..2].try_into().unwrap()) as usize;
726 let first_doc = u32::from_le_bytes(block_data[2..6].try_into().unwrap());
727 let doc_id_bits = block_data[6];
728
729 doc_ids.clear();
730 doc_ids.resize(count, 0);
731 doc_ids[0] = first_doc;
732
733 let doc_rounded = simd::RoundedBitWidth::from_u8(doc_id_bits);
734 let deltas_bytes = if count > 1 {
735 (count - 1) * doc_rounded.bytes_per_value()
736 } else {
737 0
738 };
739
740 if count > 1 {
741 simd::unpack_rounded(
742 &block_data[8..8 + deltas_bytes],
743 doc_rounded,
744 &mut doc_ids[1..],
745 count - 1,
746 );
747 for i in 1..count {
748 doc_ids[i] += doc_ids[i - 1];
749 }
750 }
751
752 let tfs_start = 8 + deltas_bytes;
753 Some((pos, tfs_start, count))
754 }
755
756 pub fn decode_block_tfs_deferred(
760 &self,
761 block_offset: usize,
762 tf_start: usize,
763 count: usize,
764 tfs: &mut Vec<u32>,
765 ) {
766 let blk_size = block_data_size(&self.stream, block_offset);
767 let block_data = &self.stream[block_offset..block_offset + blk_size];
768 let tf_bits = block_data[7];
769 let tf_rounded = simd::RoundedBitWidth::from_u8(tf_bits);
770
771 tfs.clear();
772 tfs.resize(count, 0);
773 simd::unpack_rounded(
774 &block_data[tf_start..tf_start + count * tf_rounded.bytes_per_value()],
775 tf_rounded,
776 tfs,
777 count,
778 );
779 }
780
781 #[inline]
783 pub fn block_first_doc(&self, block_idx: usize) -> Option<DocId> {
784 if block_idx >= self.l0_count {
785 return None;
786 }
787 let (first_doc, _, _, _) = self.read_l0_entry(block_idx);
788 Some(first_doc)
789 }
790
791 #[inline]
793 pub fn block_last_doc(&self, block_idx: usize) -> Option<DocId> {
794 if block_idx >= self.l0_count {
795 return None;
796 }
797 let (_, last_doc, _, _) = self.read_l0_entry(block_idx);
798 Some(last_doc)
799 }
800
801 pub fn seek_block(&self, target: DocId, from_block: usize) -> Option<usize> {
809 if from_block >= self.l0_count {
810 return None;
811 }
812
813 let from_l1 = from_block / L1_INTERVAL;
814
815 let l1_idx = if !self.l1_docs.is_empty() {
817 let idx = from_l1 + simd::find_first_ge_u32(&self.l1_docs[from_l1..], target);
818 if idx >= self.l1_docs.len() {
819 return None;
820 }
821 idx
822 } else {
823 return None;
824 };
825
826 let start = (l1_idx * L1_INTERVAL).max(from_block);
828 let end = ((l1_idx + 1) * L1_INTERVAL).min(self.l0_count);
829 let count = end - start;
830
831 let mut last_docs = [u32::MAX; L1_INTERVAL];
832 for (j, idx) in (start..end).enumerate() {
833 let (_, ld, _, _) = read_l0(&self.l0_bytes, idx);
834 last_docs[j] = ld;
835 }
836 let within = simd::find_first_ge_u32(&last_docs[..count], target);
837 let block_idx = start + within;
838
839 if block_idx < self.l0_count {
840 Some(block_idx)
841 } else {
842 None
843 }
844 }
845
846 pub fn iterator(&self) -> BlockPostingIterator<'_> {
848 BlockPostingIterator::new(self)
849 }
850
851 pub fn into_iterator(self) -> BlockPostingIterator<'static> {
853 BlockPostingIterator::owned(self)
854 }
855}
856
857pub struct BlockPostingIterator<'a> {
864 block_list: std::borrow::Cow<'a, BlockPostingList>,
865 current_block: usize,
866 block_doc_ids: Vec<u32>,
867 block_tfs: Vec<u32>,
868 position_in_block: usize,
869 exhausted: bool,
870}
871
872impl<'a> BlockPostingIterator<'a> {
873 fn new(block_list: &'a BlockPostingList) -> Self {
874 let exhausted = block_list.l0_count == 0;
875 let mut iter = Self {
876 block_list: std::borrow::Cow::Borrowed(block_list),
877 current_block: 0,
878 block_doc_ids: Vec::with_capacity(BLOCK_SIZE),
879 block_tfs: Vec::with_capacity(BLOCK_SIZE),
880 position_in_block: 0,
881 exhausted,
882 };
883 if !iter.exhausted {
884 iter.load_block(0);
885 }
886 iter
887 }
888
889 fn owned(block_list: BlockPostingList) -> BlockPostingIterator<'static> {
890 let exhausted = block_list.l0_count == 0;
891 let mut iter = BlockPostingIterator {
892 block_list: std::borrow::Cow::Owned(block_list),
893 current_block: 0,
894 block_doc_ids: Vec::with_capacity(BLOCK_SIZE),
895 block_tfs: Vec::with_capacity(BLOCK_SIZE),
896 position_in_block: 0,
897 exhausted,
898 };
899 if !iter.exhausted {
900 iter.load_block(0);
901 }
902 iter
903 }
904
905 fn load_block(&mut self, block_idx: usize) {
906 if block_idx >= self.block_list.l0_count {
907 self.exhausted = true;
908 return;
909 }
910
911 self.current_block = block_idx;
912 self.position_in_block = 0;
913
914 self.block_list
915 .decode_block_into(block_idx, &mut self.block_doc_ids, &mut self.block_tfs);
916 }
917
918 pub fn doc(&self) -> DocId {
919 if self.exhausted {
920 TERMINATED
921 } else if self.position_in_block < self.block_doc_ids.len() {
922 self.block_doc_ids[self.position_in_block]
923 } else {
924 TERMINATED
925 }
926 }
927
928 pub fn term_freq(&self) -> u32 {
929 if self.exhausted || self.position_in_block >= self.block_tfs.len() {
930 0
931 } else {
932 self.block_tfs[self.position_in_block]
933 }
934 }
935
936 pub fn advance(&mut self) -> DocId {
937 if self.exhausted {
938 return TERMINATED;
939 }
940
941 self.position_in_block += 1;
942 if self.position_in_block >= self.block_doc_ids.len() {
943 self.load_block(self.current_block + 1);
944 }
945 self.doc()
946 }
947
948 pub fn seek(&mut self, target: DocId) -> DocId {
949 if self.exhausted {
950 return TERMINATED;
951 }
952
953 let block_idx = match self.block_list.seek_block(target, self.current_block) {
955 Some(idx) => idx,
956 None => {
957 self.exhausted = true;
958 return TERMINATED;
959 }
960 };
961
962 if block_idx != self.current_block {
963 self.load_block(block_idx);
964 }
965
966 let remaining = &self.block_doc_ids[self.position_in_block..];
968 let pos = crate::structures::simd::find_first_ge_u32(remaining, target);
969 self.position_in_block += pos;
970
971 if self.position_in_block >= self.block_doc_ids.len() {
972 self.load_block(self.current_block + 1);
973 }
974 self.doc()
975 }
976
977 pub fn skip_to_next_block(&mut self) -> DocId {
981 if self.exhausted {
982 return TERMINATED;
983 }
984 self.load_block(self.current_block + 1);
985 self.doc()
986 }
987
988 #[inline]
990 pub fn current_block_idx(&self) -> usize {
991 self.current_block
992 }
993
994 #[inline]
996 pub fn num_blocks(&self) -> usize {
997 self.block_list.l0_count
998 }
999
1000 #[inline]
1002 pub fn current_block_max_tf(&self) -> u32 {
1003 if self.exhausted || self.current_block >= self.block_list.l0_count {
1004 0
1005 } else {
1006 let (_, _, _, max_weight) = self.block_list.read_l0_entry(self.current_block);
1007 max_weight as u32
1008 }
1009 }
1010}
1011
1012#[cfg(test)]
1013mod tests {
1014 use super::*;
1015
1016 #[test]
1017 fn test_posting_list_basic() {
1018 let mut list = PostingList::new();
1019 list.push(1, 2);
1020 list.push(5, 1);
1021 list.push(10, 3);
1022
1023 assert_eq!(list.len(), 3);
1024
1025 let mut iter = PostingListIterator::new(&list);
1026 assert_eq!(iter.doc(), 1);
1027 assert_eq!(iter.term_freq(), 2);
1028
1029 assert_eq!(iter.advance(), 5);
1030 assert_eq!(iter.term_freq(), 1);
1031
1032 assert_eq!(iter.advance(), 10);
1033 assert_eq!(iter.term_freq(), 3);
1034
1035 assert_eq!(iter.advance(), TERMINATED);
1036 }
1037
1038 #[test]
1039 fn test_posting_list_serialization() {
1040 let mut list = PostingList::new();
1041 for i in 0..100 {
1042 list.push(i * 3, (i % 5) + 1);
1043 }
1044
1045 let mut buffer = Vec::new();
1046 list.serialize(&mut buffer).unwrap();
1047
1048 let deserialized = PostingList::deserialize(&mut &buffer[..]).unwrap();
1049 assert_eq!(deserialized.len(), list.len());
1050
1051 for (a, b) in list.iter().zip(deserialized.iter()) {
1052 assert_eq!(a, b);
1053 }
1054 }
1055
1056 #[test]
1057 fn test_posting_list_seek() {
1058 let mut list = PostingList::new();
1059 for i in 0..100 {
1060 list.push(i * 2, 1);
1061 }
1062
1063 let mut iter = PostingListIterator::new(&list);
1064
1065 assert_eq!(iter.seek(50), 50);
1066 assert_eq!(iter.seek(51), 52);
1067 assert_eq!(iter.seek(200), TERMINATED);
1068 }
1069
1070 #[test]
1071 fn test_block_posting_list() {
1072 let mut list = PostingList::new();
1073 for i in 0..500 {
1074 list.push(i * 2, (i % 10) + 1);
1075 }
1076
1077 let block_list = BlockPostingList::from_posting_list(&list).unwrap();
1078 assert_eq!(block_list.doc_count(), 500);
1079
1080 let mut iter = block_list.iterator();
1081 assert_eq!(iter.doc(), 0);
1082 assert_eq!(iter.term_freq(), 1);
1083
1084 assert_eq!(iter.seek(500), 500);
1086 assert_eq!(iter.seek(998), 998);
1087 assert_eq!(iter.seek(1000), TERMINATED);
1088 }
1089
1090 #[test]
1091 fn test_block_posting_list_serialization() {
1092 let mut list = PostingList::new();
1093 for i in 0..300 {
1094 list.push(i * 3, i + 1);
1095 }
1096
1097 let block_list = BlockPostingList::from_posting_list(&list).unwrap();
1098
1099 let mut buffer = Vec::new();
1100 block_list.serialize(&mut buffer).unwrap();
1101
1102 let deserialized = BlockPostingList::deserialize(&buffer[..]).unwrap();
1103 assert_eq!(deserialized.doc_count(), block_list.doc_count());
1104
1105 let mut iter1 = block_list.iterator();
1107 let mut iter2 = deserialized.iterator();
1108
1109 while iter1.doc() != TERMINATED {
1110 assert_eq!(iter1.doc(), iter2.doc());
1111 assert_eq!(iter1.term_freq(), iter2.term_freq());
1112 iter1.advance();
1113 iter2.advance();
1114 }
1115 assert_eq!(iter2.doc(), TERMINATED);
1116 }
1117
1118 fn collect_postings(bpl: &BlockPostingList) -> Vec<(u32, u32)> {
1120 let mut result = Vec::new();
1121 let mut it = bpl.iterator();
1122 while it.doc() != TERMINATED {
1123 result.push((it.doc(), it.term_freq()));
1124 it.advance();
1125 }
1126 result
1127 }
1128
1129 fn build_bpl(postings: &[(u32, u32)]) -> BlockPostingList {
1131 let mut pl = PostingList::new();
1132 for &(doc_id, tf) in postings {
1133 pl.push(doc_id, tf);
1134 }
1135 BlockPostingList::from_posting_list(&pl).unwrap()
1136 }
1137
1138 fn serialize_bpl(bpl: &BlockPostingList) -> Vec<u8> {
1140 let mut buf = Vec::new();
1141 bpl.serialize(&mut buf).unwrap();
1142 buf
1143 }
1144
1145 #[test]
1146 fn test_concatenate_blocks_two_segments() {
1147 let a: Vec<(u32, u32)> = (0..100).map(|i| (i * 2, i + 1)).collect();
1149 let bpl_a = build_bpl(&a);
1150
1151 let b: Vec<(u32, u32)> = (0..100).map(|i| (i * 3, i + 2)).collect();
1153 let bpl_b = build_bpl(&b);
1154
1155 let merged =
1157 BlockPostingList::concatenate_blocks(&[(bpl_a.clone(), 0), (bpl_b.clone(), 200)])
1158 .unwrap();
1159
1160 assert_eq!(merged.doc_count(), 200);
1161
1162 let postings = collect_postings(&merged);
1163 assert_eq!(postings.len(), 200);
1164
1165 for (i, p) in postings.iter().enumerate().take(100) {
1167 assert_eq!(*p, (i as u32 * 2, i as u32 + 1));
1168 }
1169 for i in 0..100 {
1171 assert_eq!(postings[100 + i], (i as u32 * 3 + 200, i as u32 + 2));
1172 }
1173 }
1174
1175 #[test]
1176 fn test_concatenate_streaming_matches_blocks() {
1177 let seg_a: Vec<(u32, u32)> = (0..250).map(|i| (i * 2, (i % 7) + 1)).collect();
1179 let seg_b: Vec<(u32, u32)> = (0..180).map(|i| (i * 5, (i % 3) + 1)).collect();
1180 let seg_c: Vec<(u32, u32)> = (0..90).map(|i| (i * 10, (i % 11) + 1)).collect();
1181
1182 let bpl_a = build_bpl(&seg_a);
1183 let bpl_b = build_bpl(&seg_b);
1184 let bpl_c = build_bpl(&seg_c);
1185
1186 let offset_b = 1000u32;
1187 let offset_c = 2000u32;
1188
1189 let ref_merged = BlockPostingList::concatenate_blocks(&[
1191 (bpl_a.clone(), 0),
1192 (bpl_b.clone(), offset_b),
1193 (bpl_c.clone(), offset_c),
1194 ])
1195 .unwrap();
1196 let mut ref_buf = Vec::new();
1197 ref_merged.serialize(&mut ref_buf).unwrap();
1198
1199 let bytes_a = serialize_bpl(&bpl_a);
1201 let bytes_b = serialize_bpl(&bpl_b);
1202 let bytes_c = serialize_bpl(&bpl_c);
1203
1204 let sources: Vec<(&[u8], u32)> =
1205 vec![(&bytes_a, 0), (&bytes_b, offset_b), (&bytes_c, offset_c)];
1206 let mut stream_buf = Vec::new();
1207 let (doc_count, bytes_written) =
1208 BlockPostingList::concatenate_streaming(&sources, &mut stream_buf).unwrap();
1209
1210 assert_eq!(doc_count, 520); assert_eq!(bytes_written, stream_buf.len());
1212
1213 let ref_postings = collect_postings(&BlockPostingList::deserialize(&ref_buf).unwrap());
1215 let stream_postings =
1216 collect_postings(&BlockPostingList::deserialize(&stream_buf).unwrap());
1217
1218 assert_eq!(ref_postings.len(), stream_postings.len());
1219 for (i, (r, s)) in ref_postings.iter().zip(stream_postings.iter()).enumerate() {
1220 assert_eq!(r, s, "mismatch at posting {}", i);
1221 }
1222 }
1223
1224 #[test]
1225 fn test_concatenate_streaming_short_source_returns_corruption() {
1226 let seg_a: Vec<(u32, u32)> = (0..250).map(|i| (i * 2, (i % 7) + 1)).collect();
1231 let seg_c: Vec<(u32, u32)> = (0..90).map(|i| (i * 10, (i % 11) + 1)).collect();
1232 let bytes_a = serialize_bpl(&build_bpl(&seg_a));
1233 let bytes_c = serialize_bpl(&build_bpl(&seg_c));
1234 let short = vec![0u8; FOOTER_SIZE - 1]; let sources: Vec<(&[u8], u32)> = vec![(&bytes_a, 0), (&short, 1000), (&bytes_c, 2000)];
1237 let mut out = Vec::new();
1238 let result = BlockPostingList::concatenate_streaming(&sources, &mut out);
1239 assert!(
1240 matches!(result, Err(crate::Error::Corruption(_))),
1241 "short/corrupt source must be a Corruption error, not silently skipped: {:?}",
1242 result.map(|r| r.0)
1243 );
1244 }
1245
1246 #[test]
1247 fn test_multi_round_merge() {
1248 let segments: Vec<Vec<(u32, u32)>> = (0..4)
1255 .map(|seg| (0..200).map(|i| (i * 3, (i + seg * 7) % 10 + 1)).collect())
1256 .collect();
1257
1258 let bpls: Vec<BlockPostingList> = segments.iter().map(|s| build_bpl(s)).collect();
1259 let serialized: Vec<Vec<u8>> = bpls.iter().map(serialize_bpl).collect();
1260
1261 let mut merged_01 = Vec::new();
1263 let sources_01: Vec<(&[u8], u32)> = vec![(&serialized[0], 0), (&serialized[1], 600)];
1264 let (dc_01, _) =
1265 BlockPostingList::concatenate_streaming(&sources_01, &mut merged_01).unwrap();
1266 assert_eq!(dc_01, 400);
1267
1268 let mut merged_23 = Vec::new();
1269 let sources_23: Vec<(&[u8], u32)> = vec![(&serialized[2], 0), (&serialized[3], 600)];
1270 let (dc_23, _) =
1271 BlockPostingList::concatenate_streaming(&sources_23, &mut merged_23).unwrap();
1272 assert_eq!(dc_23, 400);
1273
1274 let mut final_merged = Vec::new();
1276 let sources_final: Vec<(&[u8], u32)> = vec![(&merged_01, 0), (&merged_23, 1200)];
1277 let (dc_final, _) =
1278 BlockPostingList::concatenate_streaming(&sources_final, &mut final_merged).unwrap();
1279 assert_eq!(dc_final, 800);
1280
1281 let final_bpl = BlockPostingList::deserialize(&final_merged).unwrap();
1283 let postings = collect_postings(&final_bpl);
1284 assert_eq!(postings.len(), 800);
1285
1286 assert_eq!(postings[0].0, 0); assert_eq!(postings[199].0, 597); assert_eq!(postings[200].0, 600); assert_eq!(postings[399].0, 1197); assert_eq!(postings[400].0, 1200); assert_eq!(postings[799].0, 2397); for seg in 0u32..4 {
1299 for i in 0u32..200 {
1300 let idx = (seg * 200 + i) as usize;
1301 assert_eq!(
1302 postings[idx].1,
1303 (i + seg * 7) % 10 + 1,
1304 "seg{} tf[{}]",
1305 seg,
1306 i
1307 );
1308 }
1309 }
1310
1311 let mut it = final_bpl.iterator();
1313 assert_eq!(it.seek(600), 600);
1314 assert_eq!(it.seek(1200), 1200);
1315 assert_eq!(it.seek(2397), 2397);
1316 assert_eq!(it.seek(2398), TERMINATED);
1317 }
1318
1319 #[test]
1320 fn test_large_scale_merge() {
1321 let num_segments = 5;
1324 let docs_per_segment = 2000;
1325 let docs_gap = 3; let segments: Vec<Vec<(u32, u32)>> = (0..num_segments)
1328 .map(|seg| {
1329 (0..docs_per_segment)
1330 .map(|i| (i as u32 * docs_gap, (i as u32 + seg as u32) % 20 + 1))
1331 .collect()
1332 })
1333 .collect();
1334
1335 let bpls: Vec<BlockPostingList> = segments.iter().map(|s| build_bpl(s)).collect();
1336
1337 for bpl in &bpls {
1339 assert!(
1340 bpl.num_blocks() >= 15,
1341 "expected >=15 blocks, got {}",
1342 bpl.num_blocks()
1343 );
1344 }
1345
1346 let serialized: Vec<Vec<u8>> = bpls.iter().map(serialize_bpl).collect();
1347
1348 let max_doc_per_seg = (docs_per_segment as u32 - 1) * docs_gap;
1350 let offsets: Vec<u32> = (0..num_segments)
1351 .map(|i| i as u32 * (max_doc_per_seg + 1))
1352 .collect();
1353
1354 let sources: Vec<(&[u8], u32)> = serialized
1355 .iter()
1356 .zip(offsets.iter())
1357 .map(|(b, o)| (b.as_slice(), *o))
1358 .collect();
1359
1360 let mut merged = Vec::new();
1361 let (doc_count, _) =
1362 BlockPostingList::concatenate_streaming(&sources, &mut merged).unwrap();
1363 assert_eq!(doc_count, (num_segments * docs_per_segment) as u32);
1364
1365 let merged_bpl = BlockPostingList::deserialize(&merged).unwrap();
1367 let postings = collect_postings(&merged_bpl);
1368 assert_eq!(postings.len(), num_segments * docs_per_segment);
1369
1370 for i in 1..postings.len() {
1372 assert!(
1373 postings[i].0 > postings[i - 1].0 || (i % docs_per_segment == 0), "doc_id not increasing at {}: {} vs {}",
1375 i,
1376 postings[i - 1].0,
1377 postings[i].0,
1378 );
1379 }
1380
1381 let mut it = merged_bpl.iterator();
1383 for (seg, &expected_first) in offsets.iter().enumerate() {
1384 assert_eq!(
1385 it.seek(expected_first),
1386 expected_first,
1387 "seek to segment {} start",
1388 seg
1389 );
1390 }
1391 }
1392
1393 #[test]
1394 fn test_merge_edge_cases() {
1395 let bpl_a = build_bpl(&[(0, 5)]);
1397 let bpl_b = build_bpl(&[(0, 3)]);
1398
1399 let merged =
1400 BlockPostingList::concatenate_blocks(&[(bpl_a.clone(), 0), (bpl_b.clone(), 1)])
1401 .unwrap();
1402 assert_eq!(merged.doc_count(), 2);
1403 let p = collect_postings(&merged);
1404 assert_eq!(p, vec![(0, 5), (1, 3)]);
1405
1406 let exact_block: Vec<(u32, u32)> = (0..BLOCK_SIZE as u32).map(|i| (i, i % 5 + 1)).collect();
1408 let bpl_exact = build_bpl(&exact_block);
1409 assert_eq!(bpl_exact.num_blocks(), 1);
1410
1411 let bytes = serialize_bpl(&bpl_exact);
1412 let mut out = Vec::new();
1413 let sources: Vec<(&[u8], u32)> = vec![(&bytes, 0), (&bytes, BLOCK_SIZE as u32)];
1414 let (dc, _) = BlockPostingList::concatenate_streaming(&sources, &mut out).unwrap();
1415 assert_eq!(dc, BLOCK_SIZE as u32 * 2);
1416
1417 let merged = BlockPostingList::deserialize(&out).unwrap();
1418 let postings = collect_postings(&merged);
1419 assert_eq!(postings.len(), BLOCK_SIZE * 2);
1420 assert_eq!(postings[BLOCK_SIZE].0, BLOCK_SIZE as u32);
1422
1423 let over_block: Vec<(u32, u32)> = (0..BLOCK_SIZE as u32 + 1).map(|i| (i * 2, 1)).collect();
1425 let bpl_over = build_bpl(&over_block);
1426 assert_eq!(bpl_over.num_blocks(), 2);
1427 }
1428
1429 #[test]
1430 fn test_streaming_roundtrip_single_source() {
1431 let docs: Vec<(u32, u32)> = (0..500).map(|i| (i * 7, i % 15 + 1)).collect();
1433 let bpl = build_bpl(&docs);
1434 let direct = serialize_bpl(&bpl);
1435
1436 let sources: Vec<(&[u8], u32)> = vec![(&direct, 0)];
1437 let mut streamed = Vec::new();
1438 BlockPostingList::concatenate_streaming(&sources, &mut streamed).unwrap();
1439
1440 let p1 = collect_postings(&BlockPostingList::deserialize(&direct).unwrap());
1442 let p2 = collect_postings(&BlockPostingList::deserialize(&streamed).unwrap());
1443 assert_eq!(p1, p2);
1444 }
1445
1446 #[test]
1447 fn test_max_tf_preserved_through_merge() {
1448 let mut a = Vec::new();
1450 for i in 0..200 {
1451 a.push((i * 2, if i == 100 { 50 } else { 1 }));
1452 }
1453 let bpl_a = build_bpl(&a);
1454 assert_eq!(bpl_a.max_tf(), 50);
1455
1456 let mut b = Vec::new();
1458 for i in 0..200 {
1459 b.push((i * 2, if i == 50 { 30 } else { 2 }));
1460 }
1461 let bpl_b = build_bpl(&b);
1462 assert_eq!(bpl_b.max_tf(), 30);
1463
1464 let bytes_a = serialize_bpl(&bpl_a);
1466 let bytes_b = serialize_bpl(&bpl_b);
1467 let sources: Vec<(&[u8], u32)> = vec![(&bytes_a, 0), (&bytes_b, 1000)];
1468 let mut out = Vec::new();
1469 BlockPostingList::concatenate_streaming(&sources, &mut out).unwrap();
1470
1471 let merged = BlockPostingList::deserialize(&out).unwrap();
1472 assert_eq!(merged.max_tf(), 50);
1473 assert_eq!(merged.doc_count(), 400);
1474 }
1475
1476 #[test]
1479 fn test_l0_l1_counts() {
1480 let bpl = build_bpl(&(0..50u32).map(|i| (i, 1)).collect::<Vec<_>>());
1482 assert_eq!(bpl.num_blocks(), 1);
1483 assert_eq!(bpl.l1_docs.len(), 1);
1484
1485 let n = BLOCK_SIZE * L1_INTERVAL;
1487 let bpl = build_bpl(&(0..n as u32).map(|i| (i * 2, 1)).collect::<Vec<_>>());
1488 assert_eq!(bpl.num_blocks(), L1_INTERVAL);
1489 assert_eq!(bpl.l1_docs.len(), 1);
1490
1491 let n = BLOCK_SIZE * L1_INTERVAL + 1;
1493 let bpl = build_bpl(&(0..n as u32).map(|i| (i * 2, 1)).collect::<Vec<_>>());
1494 assert_eq!(bpl.num_blocks(), L1_INTERVAL + 1);
1495 assert_eq!(bpl.l1_docs.len(), 2);
1496
1497 let n = BLOCK_SIZE * L1_INTERVAL * 3;
1499 let bpl = build_bpl(&(0..n as u32).map(|i| (i, 1)).collect::<Vec<_>>());
1500 assert_eq!(bpl.num_blocks(), L1_INTERVAL * 3);
1501 assert_eq!(bpl.l1_docs.len(), 3);
1502 }
1503
1504 #[test]
1505 fn test_l1_last_doc_values() {
1506 let n = BLOCK_SIZE * 20;
1508 let docs: Vec<(u32, u32)> = (0..n as u32).map(|i| (i * 3, 1)).collect();
1509 let bpl = build_bpl(&docs);
1510 assert_eq!(bpl.num_blocks(), 20);
1511 assert_eq!(bpl.l1_docs.len(), 3); let expected_l1_0 = bpl.block_last_doc(7).unwrap();
1515 assert_eq!(bpl.l1_docs[0], expected_l1_0);
1516
1517 let expected_l1_1 = bpl.block_last_doc(15).unwrap();
1519 assert_eq!(bpl.l1_docs[1], expected_l1_1);
1520
1521 let expected_l1_2 = bpl.block_last_doc(19).unwrap();
1523 assert_eq!(bpl.l1_docs[2], expected_l1_2);
1524 }
1525
1526 #[test]
1527 fn test_seek_block_basic() {
1528 let n = BLOCK_SIZE * 20;
1530 let docs: Vec<(u32, u32)> = (0..n as u32).map(|i| (i * 10, 1)).collect();
1531 let bpl = build_bpl(&docs);
1532
1533 assert_eq!(bpl.seek_block(0, 0), Some(0));
1535
1536 for blk in 0..20 {
1538 let first = bpl.block_first_doc(blk).unwrap();
1539 assert_eq!(
1540 bpl.seek_block(first, 0),
1541 Some(blk),
1542 "seek to block {} first_doc",
1543 blk
1544 );
1545 }
1546
1547 for blk in 0..20 {
1549 let last = bpl.block_last_doc(blk).unwrap();
1550 assert_eq!(
1551 bpl.seek_block(last, 0),
1552 Some(blk),
1553 "seek to block {} last_doc",
1554 blk
1555 );
1556 }
1557
1558 let max_doc = bpl.block_last_doc(19).unwrap();
1560 assert_eq!(bpl.seek_block(max_doc + 1, 0), None);
1561
1562 let mid_doc = bpl.block_first_doc(10).unwrap();
1564 assert_eq!(bpl.seek_block(mid_doc, 10), Some(10));
1565 assert_eq!(
1566 bpl.seek_block(mid_doc, 11),
1567 Some(11).or(bpl.seek_block(mid_doc, 11))
1568 );
1569 }
1570
1571 #[test]
1572 fn test_seek_block_across_l1_boundaries() {
1573 let n = BLOCK_SIZE * 24;
1575 let docs: Vec<(u32, u32)> = (0..n as u32).map(|i| (i * 5, 1)).collect();
1576 let bpl = build_bpl(&docs);
1577 assert_eq!(bpl.l1_docs.len(), 3);
1578
1579 for group in 0..3 {
1581 let blk = group * L1_INTERVAL;
1582 let target = bpl.block_first_doc(blk).unwrap();
1583 assert_eq!(
1584 bpl.seek_block(target, 0),
1585 Some(blk),
1586 "seek to group {} block {}",
1587 group,
1588 blk
1589 );
1590 }
1591
1592 let target = bpl.block_first_doc(20).unwrap() + 1;
1594 assert_eq!(bpl.seek_block(target, 0), Some(20));
1595 }
1596
1597 #[test]
1598 fn test_block_data_size_helper() {
1599 let docs: Vec<(u32, u32)> = (0..500u32).map(|i| (i * 7, (i % 20) + 1)).collect();
1601 let bpl = build_bpl(&docs);
1602
1603 for blk in 0..bpl.num_blocks() {
1604 let (_, _, offset, _) = bpl.read_l0_entry(blk);
1605 let computed_size = block_data_size(&bpl.stream, offset as usize);
1606
1607 if blk + 1 < bpl.num_blocks() {
1610 let (_, _, next_offset, _) = bpl.read_l0_entry(blk + 1);
1611 assert_eq!(
1612 computed_size,
1613 (next_offset - offset) as usize,
1614 "block_data_size mismatch at block {}",
1615 blk
1616 );
1617 } else {
1618 assert_eq!(
1620 offset as usize + computed_size,
1621 bpl.stream.len(),
1622 "last block size mismatch"
1623 );
1624 }
1625 }
1626 }
1627
1628 #[test]
1629 fn test_l0_entry_roundtrip() {
1630 let docs: Vec<(u32, u32)> = (0..1000u32).map(|i| (i * 3, (i % 10) + 1)).collect();
1632 let bpl = build_bpl(&docs);
1633
1634 let bytes = serialize_bpl(&bpl);
1635 let bpl2 = BlockPostingList::deserialize(&bytes).unwrap();
1636
1637 assert_eq!(bpl.num_blocks(), bpl2.num_blocks());
1638 for blk in 0..bpl.num_blocks() {
1639 assert_eq!(
1640 bpl.read_l0_entry(blk),
1641 bpl2.read_l0_entry(blk),
1642 "L0 entry mismatch at block {}",
1643 blk
1644 );
1645 }
1646
1647 assert_eq!(bpl.l1_docs, bpl2.l1_docs);
1649 }
1650
1651 #[test]
1652 fn test_zero_copy_deserialize_matches() {
1653 let docs: Vec<(u32, u32)> = (0..2000u32).map(|i| (i * 2, (i % 5) + 1)).collect();
1654 let bpl = build_bpl(&docs);
1655 let bytes = serialize_bpl(&bpl);
1656
1657 let copied = BlockPostingList::deserialize(&bytes).unwrap();
1658 let zero_copy =
1659 BlockPostingList::deserialize_zero_copy(OwnedBytes::new(bytes.clone())).unwrap();
1660
1661 assert_eq!(copied.l0_count, zero_copy.l0_count);
1663 assert_eq!(copied.l1_docs, zero_copy.l1_docs);
1664 assert_eq!(copied.doc_count, zero_copy.doc_count);
1665 assert_eq!(copied.max_tf, zero_copy.max_tf);
1666
1667 let p1 = collect_postings(&copied);
1669 let p2 = collect_postings(&zero_copy);
1670 assert_eq!(p1, p2);
1671 }
1672
1673 #[test]
1674 fn test_l1_preserved_through_streaming_merge() {
1675 let seg_a = build_bpl(&(0..1000u32).map(|i| (i * 2, 1)).collect::<Vec<_>>());
1677 let seg_b = build_bpl(&(0..800u32).map(|i| (i * 3, 2)).collect::<Vec<_>>());
1678 let seg_c = build_bpl(&(0..500u32).map(|i| (i * 5, 3)).collect::<Vec<_>>());
1679
1680 let bytes_a = serialize_bpl(&seg_a);
1681 let bytes_b = serialize_bpl(&seg_b);
1682 let bytes_c = serialize_bpl(&seg_c);
1683
1684 let sources: Vec<(&[u8], u32)> = vec![(&bytes_a, 0), (&bytes_b, 10000), (&bytes_c, 20000)];
1685 let mut out = Vec::new();
1686 BlockPostingList::concatenate_streaming(&sources, &mut out).unwrap();
1687
1688 let merged = BlockPostingList::deserialize(&out).unwrap();
1689 let expected_l1_count = merged.num_blocks().div_ceil(L1_INTERVAL);
1690 assert_eq!(merged.l1_docs.len(), expected_l1_count);
1691
1692 for (i, &l1_doc) in merged.l1_docs.iter().enumerate() {
1694 let last_block_in_group = ((i + 1) * L1_INTERVAL - 1).min(merged.num_blocks() - 1);
1695 let expected = merged.block_last_doc(last_block_in_group).unwrap();
1696 assert_eq!(l1_doc, expected, "L1[{}] mismatch", i);
1697 }
1698
1699 for blk in 0..merged.num_blocks() {
1701 let first = merged.block_first_doc(blk).unwrap();
1702 assert_eq!(merged.seek_block(first, 0), Some(blk));
1703 }
1704 }
1705
1706 #[test]
1707 fn test_seek_block_single_block() {
1708 let bpl = build_bpl(&[(0, 1), (10, 2), (20, 3)]);
1710 assert_eq!(bpl.num_blocks(), 1);
1711 assert_eq!(bpl.l1_docs.len(), 1);
1712
1713 assert_eq!(bpl.seek_block(0, 0), Some(0));
1714 assert_eq!(bpl.seek_block(10, 0), Some(0));
1715 assert_eq!(bpl.seek_block(20, 0), Some(0));
1716 assert_eq!(bpl.seek_block(21, 0), None);
1717 }
1718
1719 #[test]
1720 fn test_footer_size() {
1721 let docs: Vec<(u32, u32)> = (0..500u32).map(|i| (i * 2, 1)).collect();
1723 let bpl = build_bpl(&docs);
1724 let bytes = serialize_bpl(&bpl);
1725
1726 let expected =
1727 bpl.stream.len() + bpl.l0_count * L0_SIZE + bpl.l1_docs.len() * L1_SIZE + FOOTER_SIZE;
1728 assert_eq!(bytes.len(), expected);
1729 }
1730
1731 #[test]
1732 fn test_seek_block_from_block_skips_earlier() {
1733 let n = BLOCK_SIZE * 16;
1735 let docs: Vec<(u32, u32)> = (0..n as u32).map(|i| (i * 3, 1)).collect();
1736 let bpl = build_bpl(&docs);
1737
1738 let target_in_5 = bpl.block_first_doc(5).unwrap() + 1;
1740 let result = bpl.seek_block(target_in_5, 8);
1743 assert!(result.is_some());
1744 assert!(result.unwrap() >= 8);
1745 }
1746}