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
190const FOOTER_V2_SIZE: usize = FOOTER_SIZE + 20;
196
197const FOOTER_MAGIC: u32 = 0x324C_5042;
199
200const FLAG_POS_CURSORS: u32 = 1;
202
203const FLAG_LEN_BOUNDS: u32 = 2;
207
208const FLAG_L1_BOUNDS: u32 = 4;
212
213fn group_bounds_from_l0(l0: &[u8], l0_count: usize) -> Vec<u32> {
216 let mut groups = Vec::with_capacity(l0_count.div_ceil(L1_INTERVAL));
217 let mut idx = 0;
218 while idx < l0_count {
219 let end = (idx + L1_INTERVAL).min(l0_count);
220 let mut max_tf = 0u32;
221 let mut min_len = u32::MAX;
222 for block in idx..end {
223 let (_, _, _, word) = read_l0(l0, block);
224 let (tf, len) = unpack_bounds(word, true);
225 max_tf = max_tf.max(tf);
226 min_len = min_len.min(len.unwrap_or(1));
227 }
228 groups.push(pack_bounds(max_tf, min_len));
229 idx = end;
230 }
231 groups
232}
233
234#[inline]
236fn pack_bounds(max_tf: u32, min_len: u32) -> u32 {
237 max_tf.min(u16::MAX as u32) | (min_len.min(u16::MAX as u32) << 16)
238}
239
240#[inline]
243fn unpack_bounds(word: u32, packed: bool) -> (u32, Option<u32>) {
244 if packed {
245 (word & 0xFFFF, Some(word >> 16))
246 } else {
247 (f32::from_bits(word) as u32, None)
248 }
249}
250
251const CURSOR_SIZE: usize = 8;
254
255struct Footer {
257 stream_len: usize,
258 l0_count: usize,
259 l1_count: usize,
260 doc_count: u32,
261 max_tf: u32,
262 total_positions: u64,
263 has_cursors: bool,
264 len_bounds: bool,
265 l1_bounds: bool,
266 min_len: u32,
267}
268
269impl Footer {
270 fn parse(raw: &[u8]) -> io::Result<Self> {
271 if raw.len() < FOOTER_SIZE {
272 return Err(io::Error::new(
273 io::ErrorKind::InvalidData,
274 "posting data too short",
275 ));
276 }
277 let extended = raw.len() >= FOOTER_V2_SIZE
278 && u32::from_le_bytes(raw[raw.len() - 4..].try_into().unwrap()) == FOOTER_MAGIC;
279 let f = raw.len()
280 - if extended {
281 FOOTER_V2_SIZE
282 } else {
283 FOOTER_SIZE
284 };
285 let stream_len = u64::from_le_bytes(raw[f..f + 8].try_into().unwrap()) as usize;
286 let l0_count = u32::from_le_bytes(raw[f + 8..f + 12].try_into().unwrap()) as usize;
287 let l1_count = u32::from_le_bytes(raw[f + 12..f + 16].try_into().unwrap()) as usize;
288 let doc_count = u32::from_le_bytes(raw[f + 16..f + 20].try_into().unwrap());
289 let max_tf = u32::from_le_bytes(raw[f + 20..f + 24].try_into().unwrap());
290 let (total_positions, flags, min_len) = if extended {
291 let total = u64::from_le_bytes(raw[f + 24..f + 32].try_into().unwrap());
292 let flags = u32::from_le_bytes(raw[f + 32..f + 36].try_into().unwrap());
293 let min_len = u32::from_le_bytes(raw[f + 36..f + 40].try_into().unwrap());
294 (total, flags, min_len)
295 } else {
296 (0, 0, 0)
297 };
298 let footer = Self {
299 stream_len,
300 l0_count,
301 l1_count,
302 doc_count,
303 max_tf,
304 total_positions,
305 has_cursors: flags & FLAG_POS_CURSORS != 0,
306 len_bounds: flags & FLAG_LEN_BOUNDS != 0,
307 l1_bounds: flags & FLAG_L1_BOUNDS != 0,
308 min_len,
309 };
310 if footer.cursors_end() > f {
311 return Err(io::Error::new(
312 io::ErrorKind::InvalidData,
313 "posting list sections exceed the footer offset",
314 ));
315 }
316 Ok(footer)
317 }
318
319 fn l0_start(&self) -> usize {
320 self.stream_len
321 }
322 fn l0_end(&self) -> usize {
323 self.l0_start() + self.l0_count * L0_SIZE
324 }
325 fn l1_end(&self) -> usize {
326 self.l0_end() + self.l1_count * L1_SIZE
327 }
328 fn l1_bounds_end(&self) -> usize {
329 self.l1_end() + if self.l1_bounds { self.l1_count * 4 } else { 0 }
330 }
331 fn cursors_end(&self) -> usize {
332 self.l1_bounds_end()
333 + if self.has_cursors {
334 self.l0_count * CURSOR_SIZE
335 } else {
336 0
337 }
338 }
339}
340
341#[inline]
348fn read_l0(bytes: &[u8], idx: usize) -> (u32, u32, u32, u32) {
349 let b = &bytes[idx * L0_SIZE..][..L0_SIZE];
350 let first_doc = u32::from_le_bytes([b[0], b[1], b[2], b[3]]);
351 let last_doc = u32::from_le_bytes([b[4], b[5], b[6], b[7]]);
352 let offset = u32::from_le_bytes([b[8], b[9], b[10], b[11]]);
353 let bounds = u32::from_le_bytes([b[12], b[13], b[14], b[15]]);
354 (first_doc, last_doc, offset, bounds)
355}
356
357#[inline]
359fn write_l0(buf: &mut Vec<u8>, first_doc: u32, last_doc: u32, offset: u32, bounds: u32) {
360 buf.extend_from_slice(&first_doc.to_le_bytes());
361 buf.extend_from_slice(&last_doc.to_le_bytes());
362 buf.extend_from_slice(&offset.to_le_bytes());
363 buf.extend_from_slice(&bounds.to_le_bytes());
364}
365
366#[inline]
371fn block_data_size(stream: &[u8], pos: usize) -> usize {
372 let count = u16::from_le_bytes(stream[pos..pos + 2].try_into().unwrap()) as usize;
373 let doc_rounded = simd::RoundedBitWidth::from_u8(stream[pos + 6]);
374 let tf_rounded = simd::RoundedBitWidth::from_u8(stream[pos + 7]);
375 let delta_bytes = if count > 1 {
376 (count - 1) * doc_rounded.bytes_per_value()
377 } else {
378 0
379 };
380 8 + delta_bytes + count * tf_rounded.bytes_per_value()
381}
382
383#[derive(Debug, Clone)]
384pub struct BlockPostingList {
385 stream: OwnedBytes,
387 l0_bytes: OwnedBytes,
390 l0_count: usize,
392 l1_docs: Vec<u32>,
395 l1_bounds: Vec<u32>,
398 doc_count: u32,
400 max_tf: u32,
402 pos_cursors: Option<OwnedBytes>,
406 total_positions: u64,
409 len_bounds: bool,
411 min_len: u32,
413}
414
415impl BlockPostingList {
416 #[inline]
418 fn read_l0_entry(&self, idx: usize) -> (u32, u32, u32, u32) {
419 read_l0(&self.l0_bytes, idx)
420 }
421
422 pub fn from_posting_list(list: &PostingList) -> io::Result<Self> {
431 Self::build(list, false, None)
432 }
433
434 pub fn from_posting_list_with_positions(list: &PostingList) -> io::Result<Self> {
439 Self::build(list, true, None)
440 }
441
442 pub fn from_posting_list_with(
447 list: &PostingList,
448 with_positions: bool,
449 length_of: Option<&dyn Fn(DocId) -> u32>,
450 ) -> io::Result<Self> {
451 Self::build(list, with_positions, length_of)
452 }
453
454 fn build(
455 list: &PostingList,
456 with_positions: bool,
457 length_of: Option<&dyn Fn(DocId) -> u32>,
458 ) -> io::Result<Self> {
459 let mut stream: Vec<u8> = Vec::new();
460 let mut l0_buf: Vec<u8> = Vec::new();
461 let mut l1_docs: Vec<u32> = Vec::new();
462 let mut cursors: Vec<u8> = Vec::new();
463 let mut positions_so_far = 0u64;
464 let mut l0_count = 0usize;
465 let mut max_tf = 0u32;
466 let mut list_min_len = u32::MAX;
467
468 let postings = &list.postings;
469 let mut i = 0;
470
471 let mut deltas = Vec::with_capacity(BLOCK_SIZE);
473 let mut tf_buf = Vec::with_capacity(BLOCK_SIZE);
474
475 while i < postings.len() {
476 if stream.len() > u32::MAX as usize {
477 return Err(io::Error::new(
478 io::ErrorKind::InvalidData,
479 "posting list stream exceeds u32::MAX bytes",
480 ));
481 }
482 let block_start = stream.len() as u32;
483 let block_end = (i + BLOCK_SIZE).min(postings.len());
484 let block = &postings[i..block_end];
485 let count = block.len();
486
487 let block_max_tf = block.iter().map(|p| p.term_freq).max().unwrap_or(0);
489 max_tf = max_tf.max(block_max_tf);
490
491 let base_doc_id = block.first().unwrap().doc_id;
492 let last_doc_id = block.last().unwrap().doc_id;
493
494 deltas.clear();
496 let mut prev = base_doc_id;
497 for posting in block.iter().skip(1) {
498 deltas.push(posting.doc_id - prev);
499 prev = posting.doc_id;
500 }
501 let max_delta = deltas.iter().copied().max().unwrap_or(0);
502 let doc_id_bits = simd::round_bit_width(simd::bits_needed(max_delta));
503
504 tf_buf.clear();
506 tf_buf.extend(block.iter().map(|p| p.term_freq));
507 let tf_bits = simd::round_bit_width(simd::bits_needed(block_max_tf));
508
509 stream.write_u16::<LittleEndian>(count as u16)?;
511 stream.write_u32::<LittleEndian>(base_doc_id)?;
512 stream.push(doc_id_bits);
513 stream.push(tf_bits);
514
515 if count > 1 {
517 let rounded = simd::RoundedBitWidth::from_u8(doc_id_bits);
518 let byte_count = (count - 1) * rounded.bytes_per_value();
519 let start = stream.len();
520 stream.resize(start + byte_count, 0);
521 simd::pack_rounded(&deltas, rounded, &mut stream[start..]);
522 }
523
524 {
526 let rounded = simd::RoundedBitWidth::from_u8(tf_bits);
527 let byte_count = count * rounded.bytes_per_value();
528 let start = stream.len();
529 stream.resize(start + byte_count, 0);
530 simd::pack_rounded(&tf_buf, rounded, &mut stream[start..]);
531 }
532
533 let block_min_len = length_of.map_or(1, |length_of| {
535 block
536 .iter()
537 .map(|p| length_of(p.doc_id).max(1))
538 .min()
539 .unwrap_or(1)
540 });
541 list_min_len = list_min_len.min(block_min_len);
542 write_l0(
543 &mut l0_buf,
544 base_doc_id,
545 last_doc_id,
546 block_start,
547 pack_bounds(block_max_tf, block_min_len),
548 );
549 l0_count += 1;
550 if with_positions {
551 cursors.extend_from_slice(&positions_so_far.to_le_bytes());
552 positions_so_far += block.iter().map(|p| p.term_freq as u64).sum::<u64>();
553 }
554
555 if l0_count.is_multiple_of(L1_INTERVAL) {
557 l1_docs.push(last_doc_id);
558 }
559
560 i = block_end;
561 }
562
563 if !l0_count.is_multiple_of(L1_INTERVAL) && l0_count > 0 {
565 let (_, last_doc, _, _) = read_l0(&l0_buf, l0_count - 1);
566 l1_docs.push(last_doc);
567 }
568 let l1_bounds = group_bounds_from_l0(&l0_buf, l0_count);
569
570 Ok(Self {
571 stream: OwnedBytes::new(stream),
572 l0_bytes: OwnedBytes::new(l0_buf),
573 l0_count,
574 l1_docs,
575 l1_bounds,
576 doc_count: postings.len() as u32,
577 max_tf,
578 pos_cursors: with_positions.then(|| OwnedBytes::new(cursors)),
579 total_positions: positions_so_far,
580 len_bounds: true,
581 min_len: if list_min_len == u32::MAX {
582 1
583 } else {
584 list_min_len
585 },
586 })
587 }
588
589 pub fn serialize<W: Write>(&self, writer: &mut W) -> io::Result<()> {
602 writer.write_all(&self.stream)?;
603 writer.write_all(&self.l0_bytes)?;
604 for &doc in &self.l1_docs {
605 writer.write_u32::<LittleEndian>(doc)?;
606 }
607 for &bounds in &self.l1_bounds {
608 writer.write_u32::<LittleEndian>(bounds)?;
609 }
610 if let Some(cursors) = &self.pos_cursors {
611 writer.write_all(cursors)?;
612 }
613 Self::write_footer(
614 writer,
615 self.stream.len() as u64,
616 self.l0_count,
617 self.l1_docs.len(),
618 self.doc_count,
619 self.max_tf,
620 self.total_positions,
621 self.pos_cursors.is_some(),
622 self.len_bounds.then_some(self.min_len),
623 !self.l1_bounds.is_empty(),
624 )
625 }
626
627 #[allow(clippy::too_many_arguments)]
628 fn write_footer<W: Write>(
629 writer: &mut W,
630 stream_len: u64,
631 l0_count: usize,
632 l1_count: usize,
633 doc_count: u32,
634 max_tf: u32,
635 total_positions: u64,
636 has_cursors: bool,
637 min_len: Option<u32>,
638 l1_bounds: bool,
639 ) -> io::Result<()> {
640 writer.write_u64::<LittleEndian>(stream_len)?;
641 writer.write_u32::<LittleEndian>(l0_count as u32)?;
642 writer.write_u32::<LittleEndian>(l1_count as u32)?;
643 writer.write_u32::<LittleEndian>(doc_count)?;
644 writer.write_u32::<LittleEndian>(max_tf)?;
645 writer.write_u64::<LittleEndian>(total_positions)?;
646 let mut flags = 0u32;
647 if has_cursors {
648 flags |= FLAG_POS_CURSORS;
649 }
650 if min_len.is_some() {
651 flags |= FLAG_LEN_BOUNDS;
652 }
653 if l1_bounds {
654 flags |= FLAG_L1_BOUNDS;
655 }
656 writer.write_u32::<LittleEndian>(flags)?;
657 writer.write_u32::<LittleEndian>(min_len.unwrap_or(0))?;
658 writer.write_u32::<LittleEndian>(FOOTER_MAGIC)?;
659 Ok(())
660 }
661
662 pub fn deserialize(raw: &[u8]) -> io::Result<Self> {
664 Self::deserialize_zero_copy(OwnedBytes::new(raw.to_vec()))
665 }
666
667 pub fn deserialize_zero_copy(raw: OwnedBytes) -> io::Result<Self> {
671 let footer = Footer::parse(raw.as_slice())?;
672 let l1_docs = Self::extract_l1_docs(&raw[footer.l0_end()..], footer.l1_count);
673 let l1_bounds = if footer.l1_bounds {
674 Self::extract_l1_docs(&raw[footer.l1_end()..], footer.l1_count)
675 } else {
676 Vec::new()
677 };
678 let pos_cursors = footer
679 .has_cursors
680 .then(|| raw.slice(footer.l1_bounds_end()..footer.cursors_end()));
681
682 Ok(Self {
683 stream: raw.slice(0..footer.stream_len),
684 l0_bytes: raw.slice(footer.l0_start()..footer.l0_end()),
685 l0_count: footer.l0_count,
686 l1_docs,
687 l1_bounds,
688 doc_count: footer.doc_count,
689 max_tf: footer.max_tf,
690 pos_cursors,
691 total_positions: footer.total_positions,
692 len_bounds: footer.len_bounds,
693 min_len: footer.min_len,
694 })
695 }
696
697 pub fn min_len(&self) -> Option<u32> {
700 self.len_bounds.then_some(self.min_len)
701 }
702
703 #[inline]
705 pub fn block_bounds(&self, block_idx: usize) -> Option<(u32, Option<u32>)> {
706 if block_idx >= self.l0_count {
707 return None;
708 }
709 let (_, _, _, word) = self.read_l0_entry(block_idx);
710 Some(unpack_bounds(word, self.len_bounds))
711 }
712
713 #[inline]
716 pub fn group_bounds(&self, block_idx: usize) -> Option<(u32, u32)> {
717 if block_idx >= self.l0_count {
718 return None;
719 }
720 let word = *self.l1_bounds.get(block_idx / L1_INTERVAL)?;
721 let (max_tf, min_len) = unpack_bounds(word, true);
722 Some((max_tf, min_len.unwrap_or(1)))
723 }
724
725 #[inline]
727 pub fn group_last_doc(&self, block_idx: usize) -> Option<DocId> {
728 self.l1_docs.get(block_idx / L1_INTERVAL).copied()
729 }
730
731 #[inline]
733 pub fn is_group_start(&self, block_idx: usize) -> bool {
734 block_idx.is_multiple_of(L1_INTERVAL)
735 }
736
737 #[inline]
740 pub fn next_group_block(&self, block_idx: usize) -> usize {
741 ((block_idx / L1_INTERVAL + 1) * L1_INTERVAL).min(self.l0_count)
742 }
743
744 pub fn has_cursors_bytes(raw: &[u8]) -> bool {
746 Footer::parse(raw).is_ok_and(|footer| footer.has_cursors)
747 }
748
749 pub fn has_position_cursors(&self) -> bool {
751 self.pos_cursors.is_some()
752 }
753
754 pub fn total_positions(&self) -> u64 {
756 self.total_positions
757 }
758
759 #[inline]
761 pub fn pos_cursor(&self, block_idx: usize) -> Option<u64> {
762 let cursors = self.pos_cursors.as_ref()?;
763 let p = block_idx * CURSOR_SIZE;
764 cursors
765 .get(p..p + CURSOR_SIZE)
766 .map(|b| u64::from_le_bytes(b.try_into().unwrap()))
767 }
768
769 fn extract_l1_docs(bytes: &[u8], count: usize) -> Vec<u32> {
771 let mut docs = Vec::with_capacity(count);
772 for i in 0..count {
773 let p = i * L1_SIZE;
774 docs.push(u32::from_le_bytes(bytes[p..p + 4].try_into().unwrap()));
775 }
776 docs
777 }
778
779 pub fn doc_count(&self) -> u32 {
780 self.doc_count
781 }
782
783 pub fn max_tf(&self) -> u32 {
785 self.max_tf
786 }
787
788 pub fn num_blocks(&self) -> usize {
790 self.l0_count
791 }
792
793 pub fn block_max_tf(&self, block_idx: usize) -> Option<u32> {
795 self.block_bounds(block_idx).map(|(max_tf, _)| max_tf)
796 }
797
798 pub fn concatenate_blocks(sources: &[(BlockPostingList, u32)]) -> io::Result<Self> {
801 let mut stream: Vec<u8> = Vec::new();
802 let mut l0_buf: Vec<u8> = Vec::new();
803 let mut l1_docs: Vec<u32> = Vec::new();
804 let mut l0_count = 0usize;
805 let mut total_docs = 0u32;
806 let mut max_tf = 0u32;
807 let all_cursors = sources.iter().all(|(s, _)| s.has_position_cursors());
808 if !all_cursors && sources.iter().any(|(s, _)| s.has_position_cursors()) {
809 return Err(io::Error::new(
810 io::ErrorKind::InvalidData,
811 "cannot concatenate posting lists with and without position cursors",
812 ));
813 }
814 let mut cursors: Vec<u8> = Vec::new();
815 let mut positions_before = 0u64;
816 let mut min_len = u32::MAX;
817
818 for (source, doc_offset) in sources {
819 max_tf = max_tf.max(source.max_tf);
820 min_len = min_len.min(source.min_len().unwrap_or(1));
821 for block_idx in 0..source.num_blocks() {
822 if all_cursors {
823 let cursor = source.pos_cursor(block_idx).unwrap_or(0) + positions_before;
824 cursors.extend_from_slice(&cursor.to_le_bytes());
825 }
826 let (first_doc, last_doc, offset, word) = source.read_l0_entry(block_idx);
827 let (block_max_tf, block_min_len) = unpack_bounds(word, source.len_bounds);
828 let bounds = pack_bounds(block_max_tf, block_min_len.unwrap_or(1));
829 let blk_size = block_data_size(&source.stream, offset as usize);
830 let block_bytes = &source.stream[offset as usize..offset as usize + blk_size];
831
832 let count = u16::from_le_bytes(block_bytes[0..2].try_into().unwrap());
833 if stream.len() > u32::MAX as usize {
834 return Err(io::Error::new(
835 io::ErrorKind::InvalidData,
836 "posting list stream exceeds u32::MAX bytes during concatenation",
837 ));
838 }
839 let new_offset = stream.len() as u32;
840
841 stream.write_u16::<LittleEndian>(count)?;
843 stream.write_u32::<LittleEndian>(first_doc + doc_offset)?;
844 stream.extend_from_slice(&block_bytes[6..]);
845
846 let new_last = last_doc + doc_offset;
847 write_l0(
848 &mut l0_buf,
849 first_doc + doc_offset,
850 new_last,
851 new_offset,
852 bounds,
853 );
854 l0_count += 1;
855 total_docs += count as u32;
856
857 if l0_count.is_multiple_of(L1_INTERVAL) {
858 l1_docs.push(new_last);
859 }
860 }
861 positions_before += source.total_positions;
862 }
863
864 if !l0_count.is_multiple_of(L1_INTERVAL) && l0_count > 0 {
866 let (_, last_doc, _, _) = read_l0(&l0_buf, l0_count - 1);
867 l1_docs.push(last_doc);
868 }
869 let l1_bounds = group_bounds_from_l0(&l0_buf, l0_count);
870
871 Ok(Self {
872 stream: OwnedBytes::new(stream),
873 l0_bytes: OwnedBytes::new(l0_buf),
874 l0_count,
875 l1_docs,
876 l1_bounds,
877 doc_count: total_docs,
878 max_tf,
879 pos_cursors: all_cursors.then(|| OwnedBytes::new(cursors)),
880 total_positions: if all_cursors { positions_before } else { 0 },
881 len_bounds: true,
882 min_len: if min_len == u32::MAX { 1 } else { min_len },
883 })
884 }
885
886 pub fn concatenate_streaming<W: Write>(
901 sources: &[(&[u8], u32)], writer: &mut W,
903 ) -> crate::Result<(u32, usize)> {
904 let mut metas: Vec<Footer> = Vec::with_capacity(sources.len());
905 let mut total_docs = 0u32;
906 let mut merged_max_tf = 0u32;
907 let mut merged_min_len = u32::MAX;
908
909 for (source_index, (raw, _)) in sources.iter().enumerate() {
910 let footer = Footer::parse(raw).map_err(|e| {
911 crate::Error::Corruption(format!(
912 "posting list source {source_index} has an invalid footer: {e}"
913 ))
914 })?;
915 total_docs += footer.doc_count;
916 merged_max_tf = merged_max_tf.max(footer.max_tf);
917 merged_min_len = merged_min_len.min(if footer.len_bounds { footer.min_len } else { 1 });
918 metas.push(footer);
919 }
920 let all_cursors = metas.iter().all(|m| m.has_cursors);
921 if !all_cursors && metas.iter().any(|m| m.has_cursors) {
922 return Err(crate::Error::Corruption(
923 "cannot concatenate posting lists with and without position cursors".into(),
924 ));
925 }
926
927 let mut out_l0: Vec<u8> = Vec::new();
930 let mut out_l1_docs: Vec<u32> = Vec::new();
931 let mut out_cursors: Vec<u8> = Vec::new();
932 let mut positions_before = 0u64;
933 let mut out_l0_count = 0usize;
934 let mut stream_written = 0u64;
935 let mut patch_buf = [0u8; 8];
936
937 for (src_idx, meta) in metas.iter().enumerate() {
938 let (raw, doc_offset) = &sources[src_idx];
939 let l0_base = meta.l0_start(); let src_stream = &raw[..meta.stream_len];
941 let cursors_base = meta.l1_bounds_end();
942
943 for i in 0..meta.l0_count {
944 let (first_doc, last_doc, offset, word) = read_l0(&raw[l0_base..], i);
946 let (block_max_tf, block_min_len) = unpack_bounds(word, meta.len_bounds);
947 let bounds = pack_bounds(block_max_tf, block_min_len.unwrap_or(1));
948 if all_cursors {
949 let p = cursors_base + i * CURSOR_SIZE;
950 let cursor = u64::from_le_bytes(raw[p..p + CURSOR_SIZE].try_into().unwrap());
951 out_cursors.extend_from_slice(&(cursor + positions_before).to_le_bytes());
952 }
953
954 let blk_size = block_data_size(src_stream, offset as usize);
956 let block = &src_stream[offset as usize..offset as usize + blk_size];
957
958 let new_last = last_doc + doc_offset;
960 if stream_written > u32::MAX as u64 {
961 return Err(io::Error::new(
962 io::ErrorKind::InvalidData,
963 "posting list stream exceeds u32::MAX bytes during streaming merge",
964 )
965 .into());
966 }
967 write_l0(
968 &mut out_l0,
969 first_doc + doc_offset,
970 new_last,
971 stream_written as u32,
972 bounds,
973 );
974 out_l0_count += 1;
975
976 if out_l0_count.is_multiple_of(L1_INTERVAL) {
978 out_l1_docs.push(new_last);
979 }
980
981 patch_buf.copy_from_slice(&block[0..8]);
983 let blk_first = u32::from_le_bytes(patch_buf[2..6].try_into().unwrap());
984 patch_buf[2..6].copy_from_slice(&(blk_first + doc_offset).to_le_bytes());
985 writer.write_all(&patch_buf)?;
986 writer.write_all(&block[8..])?;
987
988 stream_written += blk_size as u64;
989 }
990 positions_before += meta.total_positions;
991 }
992
993 if !out_l0_count.is_multiple_of(L1_INTERVAL) && out_l0_count > 0 {
995 let (_, last_doc, _, _) = read_l0(&out_l0, out_l0_count - 1);
996 out_l1_docs.push(last_doc);
997 }
998
999 let out_l1_bounds = group_bounds_from_l0(&out_l0, out_l0_count);
1001 writer.write_all(&out_l0)?;
1002 for &doc in &out_l1_docs {
1003 writer.write_u32::<LittleEndian>(doc)?;
1004 }
1005 for &bounds in &out_l1_bounds {
1006 writer.write_u32::<LittleEndian>(bounds)?;
1007 }
1008 writer.write_all(&out_cursors)?;
1009 Self::write_footer(
1010 writer,
1011 stream_written,
1012 out_l0_count,
1013 out_l1_docs.len(),
1014 total_docs,
1015 merged_max_tf,
1016 if all_cursors { positions_before } else { 0 },
1017 all_cursors,
1018 Some(if merged_min_len == u32::MAX {
1019 1
1020 } else {
1021 merged_min_len
1022 }),
1023 true,
1024 )?;
1025
1026 let l1_bytes_len = out_l1_docs.len() * L1_SIZE + out_l1_bounds.len() * 4;
1027 let total_bytes = stream_written as usize
1028 + out_l0.len()
1029 + l1_bytes_len
1030 + out_cursors.len()
1031 + FOOTER_V2_SIZE;
1032 Ok((total_docs, total_bytes))
1033 }
1034
1035 pub fn decode_block_into(
1042 &self,
1043 block_idx: usize,
1044 doc_ids: &mut Vec<u32>,
1045 tfs: &mut Vec<u32>,
1046 ) -> bool {
1047 if let Some((offset, tf_start, count)) = self.decode_block_doc_ids_only(block_idx, doc_ids)
1048 {
1049 self.decode_block_tfs_deferred(offset, tf_start, count, tfs);
1050 true
1051 } else {
1052 false
1053 }
1054 }
1055
1056 pub fn decode_block_doc_ids_only(
1061 &self,
1062 block_idx: usize,
1063 doc_ids: &mut Vec<u32>,
1064 ) -> Option<(usize, usize, usize)> {
1065 if block_idx >= self.l0_count {
1066 return None;
1067 }
1068
1069 let (_, _, offset, _) = self.read_l0_entry(block_idx);
1070 let pos = offset as usize;
1071 let blk_size = block_data_size(&self.stream, pos);
1072 let block_data = &self.stream[pos..pos + blk_size];
1073
1074 let count = u16::from_le_bytes(block_data[0..2].try_into().unwrap()) as usize;
1076 let first_doc = u32::from_le_bytes(block_data[2..6].try_into().unwrap());
1077 let doc_id_bits = block_data[6];
1078
1079 doc_ids.clear();
1080 doc_ids.resize(count, 0);
1081 doc_ids[0] = first_doc;
1082
1083 let doc_rounded = simd::RoundedBitWidth::from_u8(doc_id_bits);
1084 let deltas_bytes = if count > 1 {
1085 (count - 1) * doc_rounded.bytes_per_value()
1086 } else {
1087 0
1088 };
1089
1090 if count > 1 {
1091 simd::unpack_rounded(
1092 &block_data[8..8 + deltas_bytes],
1093 doc_rounded,
1094 &mut doc_ids[1..],
1095 count - 1,
1096 );
1097 for i in 1..count {
1098 doc_ids[i] += doc_ids[i - 1];
1099 }
1100 }
1101
1102 let tfs_start = 8 + deltas_bytes;
1103 Some((pos, tfs_start, count))
1104 }
1105
1106 pub fn decode_block_tfs_deferred(
1110 &self,
1111 block_offset: usize,
1112 tf_start: usize,
1113 count: usize,
1114 tfs: &mut Vec<u32>,
1115 ) {
1116 let blk_size = block_data_size(&self.stream, block_offset);
1117 let block_data = &self.stream[block_offset..block_offset + blk_size];
1118 let tf_bits = block_data[7];
1119 let tf_rounded = simd::RoundedBitWidth::from_u8(tf_bits);
1120
1121 tfs.clear();
1122 tfs.resize(count, 0);
1123 simd::unpack_rounded(
1124 &block_data[tf_start..tf_start + count * tf_rounded.bytes_per_value()],
1125 tf_rounded,
1126 tfs,
1127 count,
1128 );
1129 }
1130
1131 #[inline]
1133 pub fn block_first_doc(&self, block_idx: usize) -> Option<DocId> {
1134 if block_idx >= self.l0_count {
1135 return None;
1136 }
1137 let (first_doc, _, _, _) = self.read_l0_entry(block_idx);
1138 Some(first_doc)
1139 }
1140
1141 #[inline]
1143 pub fn block_last_doc(&self, block_idx: usize) -> Option<DocId> {
1144 if block_idx >= self.l0_count {
1145 return None;
1146 }
1147 let (_, last_doc, _, _) = self.read_l0_entry(block_idx);
1148 Some(last_doc)
1149 }
1150
1151 pub fn seek_block(&self, target: DocId, from_block: usize) -> Option<usize> {
1159 if from_block >= self.l0_count {
1160 return None;
1161 }
1162
1163 let from_l1 = from_block / L1_INTERVAL;
1164
1165 let l1_idx = if !self.l1_docs.is_empty() {
1167 let idx = from_l1 + simd::find_first_ge_u32(&self.l1_docs[from_l1..], target);
1168 if idx >= self.l1_docs.len() {
1169 return None;
1170 }
1171 idx
1172 } else {
1173 return None;
1174 };
1175
1176 let start = (l1_idx * L1_INTERVAL).max(from_block);
1178 let end = ((l1_idx + 1) * L1_INTERVAL).min(self.l0_count);
1179 let count = end - start;
1180
1181 let mut last_docs = [u32::MAX; L1_INTERVAL];
1182 for (j, idx) in (start..end).enumerate() {
1183 let (_, ld, _, _) = read_l0(&self.l0_bytes, idx);
1184 last_docs[j] = ld;
1185 }
1186 let within = simd::find_first_ge_u32(&last_docs[..count], target);
1187 let block_idx = start + within;
1188
1189 if block_idx < self.l0_count {
1190 Some(block_idx)
1191 } else {
1192 None
1193 }
1194 }
1195
1196 pub fn iterator(&self) -> BlockPostingIterator<'_> {
1198 BlockPostingIterator::new(self)
1199 }
1200
1201 pub fn into_iterator(self) -> BlockPostingIterator<'static> {
1203 BlockPostingIterator::owned(self)
1204 }
1205}
1206
1207pub struct BlockPostingIterator<'a> {
1214 block_list: std::borrow::Cow<'a, BlockPostingList>,
1215 current_block: usize,
1216 block_doc_ids: Vec<u32>,
1217 block_tfs: Vec<u32>,
1218 position_in_block: usize,
1219 tf_prefix: u64,
1223 exhausted: bool,
1224}
1225
1226impl<'a> BlockPostingIterator<'a> {
1227 fn new(block_list: &'a BlockPostingList) -> Self {
1228 let exhausted = block_list.l0_count == 0;
1229 let mut iter = Self {
1230 block_list: std::borrow::Cow::Borrowed(block_list),
1231 current_block: 0,
1232 block_doc_ids: Vec::with_capacity(BLOCK_SIZE),
1233 block_tfs: Vec::with_capacity(BLOCK_SIZE),
1234 position_in_block: 0,
1235 tf_prefix: 0,
1236 exhausted,
1237 };
1238 if !iter.exhausted {
1239 iter.load_block(0);
1240 }
1241 iter
1242 }
1243
1244 fn owned(block_list: BlockPostingList) -> BlockPostingIterator<'static> {
1245 let exhausted = block_list.l0_count == 0;
1246 let mut iter = BlockPostingIterator {
1247 block_list: std::borrow::Cow::Owned(block_list),
1248 current_block: 0,
1249 block_doc_ids: Vec::with_capacity(BLOCK_SIZE),
1250 block_tfs: Vec::with_capacity(BLOCK_SIZE),
1251 position_in_block: 0,
1252 tf_prefix: 0,
1253 exhausted,
1254 };
1255 if !iter.exhausted {
1256 iter.load_block(0);
1257 }
1258 iter
1259 }
1260
1261 fn load_block(&mut self, block_idx: usize) {
1262 if block_idx >= self.block_list.l0_count {
1263 self.exhausted = true;
1264 return;
1265 }
1266
1267 self.current_block = block_idx;
1268 self.position_in_block = 0;
1269 self.tf_prefix = 0;
1270
1271 self.block_list
1272 .decode_block_into(block_idx, &mut self.block_doc_ids, &mut self.block_tfs);
1273 }
1274
1275 #[inline]
1280 pub fn position_cursor(&self) -> u64 {
1281 self.block_list.pos_cursor(self.current_block).unwrap_or(0) + self.tf_prefix
1282 }
1283
1284 pub fn doc(&self) -> DocId {
1285 if self.exhausted {
1286 TERMINATED
1287 } else if self.position_in_block < self.block_doc_ids.len() {
1288 self.block_doc_ids[self.position_in_block]
1289 } else {
1290 TERMINATED
1291 }
1292 }
1293
1294 pub fn term_freq(&self) -> u32 {
1295 if self.exhausted || self.position_in_block >= self.block_tfs.len() {
1296 0
1297 } else {
1298 self.block_tfs[self.position_in_block]
1299 }
1300 }
1301
1302 pub fn advance(&mut self) -> DocId {
1303 if self.exhausted {
1304 return TERMINATED;
1305 }
1306
1307 if let Some(&tf) = self.block_tfs.get(self.position_in_block) {
1308 self.tf_prefix += tf as u64;
1309 }
1310 self.position_in_block += 1;
1311 if self.position_in_block >= self.block_doc_ids.len() {
1312 self.load_block(self.current_block + 1);
1313 }
1314 self.doc()
1315 }
1316
1317 pub fn seek(&mut self, target: DocId) -> DocId {
1318 if self.exhausted {
1319 return TERMINATED;
1320 }
1321
1322 let block_idx = match self.block_list.seek_block(target, self.current_block) {
1324 Some(idx) => idx,
1325 None => {
1326 self.exhausted = true;
1327 return TERMINATED;
1328 }
1329 };
1330
1331 if block_idx != self.current_block {
1332 self.load_block(block_idx);
1333 }
1334
1335 let remaining = &self.block_doc_ids[self.position_in_block..];
1337 let pos = crate::structures::simd::find_first_ge_u32(remaining, target);
1338 self.tf_prefix += self.block_tfs[self.position_in_block..self.position_in_block + pos]
1339 .iter()
1340 .map(|&tf| tf as u64)
1341 .sum::<u64>();
1342 self.position_in_block += pos;
1343
1344 if self.position_in_block >= self.block_doc_ids.len() {
1345 self.load_block(self.current_block + 1);
1346 }
1347 self.doc()
1348 }
1349
1350 pub fn skip_to_next_block(&mut self) -> DocId {
1354 if self.exhausted {
1355 return TERMINATED;
1356 }
1357 self.load_block(self.current_block + 1);
1358 self.doc()
1359 }
1360
1361 #[inline]
1363 pub fn current_block_idx(&self) -> usize {
1364 self.current_block
1365 }
1366
1367 #[inline]
1369 pub fn num_blocks(&self) -> usize {
1370 self.block_list.l0_count
1371 }
1372
1373 #[inline]
1375 pub fn current_block_max_tf(&self) -> u32 {
1376 if self.exhausted || self.current_block >= self.block_list.l0_count {
1377 0
1378 } else {
1379 self.block_list
1380 .block_max_tf(self.current_block)
1381 .unwrap_or(0)
1382 }
1383 }
1384}
1385
1386#[cfg(test)]
1387mod tests {
1388 use super::*;
1389
1390 #[test]
1391 fn test_posting_list_basic() {
1392 let mut list = PostingList::new();
1393 list.push(1, 2);
1394 list.push(5, 1);
1395 list.push(10, 3);
1396
1397 assert_eq!(list.len(), 3);
1398
1399 let mut iter = PostingListIterator::new(&list);
1400 assert_eq!(iter.doc(), 1);
1401 assert_eq!(iter.term_freq(), 2);
1402
1403 assert_eq!(iter.advance(), 5);
1404 assert_eq!(iter.term_freq(), 1);
1405
1406 assert_eq!(iter.advance(), 10);
1407 assert_eq!(iter.term_freq(), 3);
1408
1409 assert_eq!(iter.advance(), TERMINATED);
1410 }
1411
1412 #[test]
1413 fn test_posting_list_serialization() {
1414 let mut list = PostingList::new();
1415 for i in 0..100 {
1416 list.push(i * 3, (i % 5) + 1);
1417 }
1418
1419 let mut buffer = Vec::new();
1420 list.serialize(&mut buffer).unwrap();
1421
1422 let deserialized = PostingList::deserialize(&mut &buffer[..]).unwrap();
1423 assert_eq!(deserialized.len(), list.len());
1424
1425 for (a, b) in list.iter().zip(deserialized.iter()) {
1426 assert_eq!(a, b);
1427 }
1428 }
1429
1430 #[test]
1431 fn test_posting_list_seek() {
1432 let mut list = PostingList::new();
1433 for i in 0..100 {
1434 list.push(i * 2, 1);
1435 }
1436
1437 let mut iter = PostingListIterator::new(&list);
1438
1439 assert_eq!(iter.seek(50), 50);
1440 assert_eq!(iter.seek(51), 52);
1441 assert_eq!(iter.seek(200), TERMINATED);
1442 }
1443
1444 #[test]
1445 fn test_block_posting_list() {
1446 let mut list = PostingList::new();
1447 for i in 0..500 {
1448 list.push(i * 2, (i % 10) + 1);
1449 }
1450
1451 let block_list = BlockPostingList::from_posting_list(&list).unwrap();
1452 assert_eq!(block_list.doc_count(), 500);
1453
1454 let mut iter = block_list.iterator();
1455 assert_eq!(iter.doc(), 0);
1456 assert_eq!(iter.term_freq(), 1);
1457
1458 assert_eq!(iter.seek(500), 500);
1460 assert_eq!(iter.seek(998), 998);
1461 assert_eq!(iter.seek(1000), TERMINATED);
1462 }
1463
1464 #[test]
1465 fn test_block_posting_list_serialization() {
1466 let mut list = PostingList::new();
1467 for i in 0..300 {
1468 list.push(i * 3, i + 1);
1469 }
1470
1471 let block_list = BlockPostingList::from_posting_list(&list).unwrap();
1472
1473 let mut buffer = Vec::new();
1474 block_list.serialize(&mut buffer).unwrap();
1475
1476 let deserialized = BlockPostingList::deserialize(&buffer[..]).unwrap();
1477 assert_eq!(deserialized.doc_count(), block_list.doc_count());
1478
1479 let mut iter1 = block_list.iterator();
1481 let mut iter2 = deserialized.iterator();
1482
1483 while iter1.doc() != TERMINATED {
1484 assert_eq!(iter1.doc(), iter2.doc());
1485 assert_eq!(iter1.term_freq(), iter2.term_freq());
1486 iter1.advance();
1487 iter2.advance();
1488 }
1489 assert_eq!(iter2.doc(), TERMINATED);
1490 }
1491
1492 fn collect_postings(bpl: &BlockPostingList) -> Vec<(u32, u32)> {
1494 let mut result = Vec::new();
1495 let mut it = bpl.iterator();
1496 while it.doc() != TERMINATED {
1497 result.push((it.doc(), it.term_freq()));
1498 it.advance();
1499 }
1500 result
1501 }
1502
1503 fn build_bpl(postings: &[(u32, u32)]) -> BlockPostingList {
1505 let mut pl = PostingList::new();
1506 for &(doc_id, tf) in postings {
1507 pl.push(doc_id, tf);
1508 }
1509 BlockPostingList::from_posting_list(&pl).unwrap()
1510 }
1511
1512 fn serialize_bpl(bpl: &BlockPostingList) -> Vec<u8> {
1514 let mut buf = Vec::new();
1515 bpl.serialize(&mut buf).unwrap();
1516 buf
1517 }
1518
1519 #[test]
1520 fn test_concatenate_blocks_two_segments() {
1521 let a: Vec<(u32, u32)> = (0..100).map(|i| (i * 2, i + 1)).collect();
1523 let bpl_a = build_bpl(&a);
1524
1525 let b: Vec<(u32, u32)> = (0..100).map(|i| (i * 3, i + 2)).collect();
1527 let bpl_b = build_bpl(&b);
1528
1529 let merged =
1531 BlockPostingList::concatenate_blocks(&[(bpl_a.clone(), 0), (bpl_b.clone(), 200)])
1532 .unwrap();
1533
1534 assert_eq!(merged.doc_count(), 200);
1535
1536 let postings = collect_postings(&merged);
1537 assert_eq!(postings.len(), 200);
1538
1539 for (i, p) in postings.iter().enumerate().take(100) {
1541 assert_eq!(*p, (i as u32 * 2, i as u32 + 1));
1542 }
1543 for i in 0..100 {
1545 assert_eq!(postings[100 + i], (i as u32 * 3 + 200, i as u32 + 2));
1546 }
1547 }
1548
1549 #[test]
1550 fn test_concatenate_streaming_matches_blocks() {
1551 let seg_a: Vec<(u32, u32)> = (0..250).map(|i| (i * 2, (i % 7) + 1)).collect();
1553 let seg_b: Vec<(u32, u32)> = (0..180).map(|i| (i * 5, (i % 3) + 1)).collect();
1554 let seg_c: Vec<(u32, u32)> = (0..90).map(|i| (i * 10, (i % 11) + 1)).collect();
1555
1556 let bpl_a = build_bpl(&seg_a);
1557 let bpl_b = build_bpl(&seg_b);
1558 let bpl_c = build_bpl(&seg_c);
1559
1560 let offset_b = 1000u32;
1561 let offset_c = 2000u32;
1562
1563 let ref_merged = BlockPostingList::concatenate_blocks(&[
1565 (bpl_a.clone(), 0),
1566 (bpl_b.clone(), offset_b),
1567 (bpl_c.clone(), offset_c),
1568 ])
1569 .unwrap();
1570 let mut ref_buf = Vec::new();
1571 ref_merged.serialize(&mut ref_buf).unwrap();
1572
1573 let bytes_a = serialize_bpl(&bpl_a);
1575 let bytes_b = serialize_bpl(&bpl_b);
1576 let bytes_c = serialize_bpl(&bpl_c);
1577
1578 let sources: Vec<(&[u8], u32)> =
1579 vec![(&bytes_a, 0), (&bytes_b, offset_b), (&bytes_c, offset_c)];
1580 let mut stream_buf = Vec::new();
1581 let (doc_count, bytes_written) =
1582 BlockPostingList::concatenate_streaming(&sources, &mut stream_buf).unwrap();
1583
1584 assert_eq!(doc_count, 520); assert_eq!(bytes_written, stream_buf.len());
1586
1587 let ref_postings = collect_postings(&BlockPostingList::deserialize(&ref_buf).unwrap());
1589 let stream_postings =
1590 collect_postings(&BlockPostingList::deserialize(&stream_buf).unwrap());
1591
1592 assert_eq!(ref_postings.len(), stream_postings.len());
1593 for (i, (r, s)) in ref_postings.iter().zip(stream_postings.iter()).enumerate() {
1594 assert_eq!(r, s, "mismatch at posting {}", i);
1595 }
1596 }
1597
1598 #[test]
1599 fn test_concatenate_streaming_short_source_returns_corruption() {
1600 let seg_a: Vec<(u32, u32)> = (0..250).map(|i| (i * 2, (i % 7) + 1)).collect();
1605 let seg_c: Vec<(u32, u32)> = (0..90).map(|i| (i * 10, (i % 11) + 1)).collect();
1606 let bytes_a = serialize_bpl(&build_bpl(&seg_a));
1607 let bytes_c = serialize_bpl(&build_bpl(&seg_c));
1608 let short = vec![0u8; FOOTER_SIZE - 1]; let sources: Vec<(&[u8], u32)> = vec![(&bytes_a, 0), (&short, 1000), (&bytes_c, 2000)];
1611 let mut out = Vec::new();
1612 let result = BlockPostingList::concatenate_streaming(&sources, &mut out);
1613 assert!(
1614 matches!(result, Err(crate::Error::Corruption(_))),
1615 "short/corrupt source must be a Corruption error, not silently skipped: {:?}",
1616 result.map(|r| r.0)
1617 );
1618 }
1619
1620 #[test]
1621 fn test_multi_round_merge() {
1622 let segments: Vec<Vec<(u32, u32)>> = (0..4)
1629 .map(|seg| (0..200).map(|i| (i * 3, (i + seg * 7) % 10 + 1)).collect())
1630 .collect();
1631
1632 let bpls: Vec<BlockPostingList> = segments.iter().map(|s| build_bpl(s)).collect();
1633 let serialized: Vec<Vec<u8>> = bpls.iter().map(serialize_bpl).collect();
1634
1635 let mut merged_01 = Vec::new();
1637 let sources_01: Vec<(&[u8], u32)> = vec![(&serialized[0], 0), (&serialized[1], 600)];
1638 let (dc_01, _) =
1639 BlockPostingList::concatenate_streaming(&sources_01, &mut merged_01).unwrap();
1640 assert_eq!(dc_01, 400);
1641
1642 let mut merged_23 = Vec::new();
1643 let sources_23: Vec<(&[u8], u32)> = vec![(&serialized[2], 0), (&serialized[3], 600)];
1644 let (dc_23, _) =
1645 BlockPostingList::concatenate_streaming(&sources_23, &mut merged_23).unwrap();
1646 assert_eq!(dc_23, 400);
1647
1648 let mut final_merged = Vec::new();
1650 let sources_final: Vec<(&[u8], u32)> = vec![(&merged_01, 0), (&merged_23, 1200)];
1651 let (dc_final, _) =
1652 BlockPostingList::concatenate_streaming(&sources_final, &mut final_merged).unwrap();
1653 assert_eq!(dc_final, 800);
1654
1655 let final_bpl = BlockPostingList::deserialize(&final_merged).unwrap();
1657 let postings = collect_postings(&final_bpl);
1658 assert_eq!(postings.len(), 800);
1659
1660 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 {
1673 for i in 0u32..200 {
1674 let idx = (seg * 200 + i) as usize;
1675 assert_eq!(
1676 postings[idx].1,
1677 (i + seg * 7) % 10 + 1,
1678 "seg{} tf[{}]",
1679 seg,
1680 i
1681 );
1682 }
1683 }
1684
1685 let mut it = final_bpl.iterator();
1687 assert_eq!(it.seek(600), 600);
1688 assert_eq!(it.seek(1200), 1200);
1689 assert_eq!(it.seek(2397), 2397);
1690 assert_eq!(it.seek(2398), TERMINATED);
1691 }
1692
1693 #[test]
1694 fn test_large_scale_merge() {
1695 let num_segments = 5;
1698 let docs_per_segment = 2000;
1699 let docs_gap = 3; let segments: Vec<Vec<(u32, u32)>> = (0..num_segments)
1702 .map(|seg| {
1703 (0..docs_per_segment)
1704 .map(|i| (i as u32 * docs_gap, (i as u32 + seg as u32) % 20 + 1))
1705 .collect()
1706 })
1707 .collect();
1708
1709 let bpls: Vec<BlockPostingList> = segments.iter().map(|s| build_bpl(s)).collect();
1710
1711 for bpl in &bpls {
1713 assert!(
1714 bpl.num_blocks() >= 15,
1715 "expected >=15 blocks, got {}",
1716 bpl.num_blocks()
1717 );
1718 }
1719
1720 let serialized: Vec<Vec<u8>> = bpls.iter().map(serialize_bpl).collect();
1721
1722 let max_doc_per_seg = (docs_per_segment as u32 - 1) * docs_gap;
1724 let offsets: Vec<u32> = (0..num_segments)
1725 .map(|i| i as u32 * (max_doc_per_seg + 1))
1726 .collect();
1727
1728 let sources: Vec<(&[u8], u32)> = serialized
1729 .iter()
1730 .zip(offsets.iter())
1731 .map(|(b, o)| (b.as_slice(), *o))
1732 .collect();
1733
1734 let mut merged = Vec::new();
1735 let (doc_count, _) =
1736 BlockPostingList::concatenate_streaming(&sources, &mut merged).unwrap();
1737 assert_eq!(doc_count, (num_segments * docs_per_segment) as u32);
1738
1739 let merged_bpl = BlockPostingList::deserialize(&merged).unwrap();
1741 let postings = collect_postings(&merged_bpl);
1742 assert_eq!(postings.len(), num_segments * docs_per_segment);
1743
1744 for i in 1..postings.len() {
1746 assert!(
1747 postings[i].0 > postings[i - 1].0 || (i % docs_per_segment == 0), "doc_id not increasing at {}: {} vs {}",
1749 i,
1750 postings[i - 1].0,
1751 postings[i].0,
1752 );
1753 }
1754
1755 let mut it = merged_bpl.iterator();
1757 for (seg, &expected_first) in offsets.iter().enumerate() {
1758 assert_eq!(
1759 it.seek(expected_first),
1760 expected_first,
1761 "seek to segment {} start",
1762 seg
1763 );
1764 }
1765 }
1766
1767 #[test]
1768 fn test_merge_edge_cases() {
1769 let bpl_a = build_bpl(&[(0, 5)]);
1771 let bpl_b = build_bpl(&[(0, 3)]);
1772
1773 let merged =
1774 BlockPostingList::concatenate_blocks(&[(bpl_a.clone(), 0), (bpl_b.clone(), 1)])
1775 .unwrap();
1776 assert_eq!(merged.doc_count(), 2);
1777 let p = collect_postings(&merged);
1778 assert_eq!(p, vec![(0, 5), (1, 3)]);
1779
1780 let exact_block: Vec<(u32, u32)> = (0..BLOCK_SIZE as u32).map(|i| (i, i % 5 + 1)).collect();
1782 let bpl_exact = build_bpl(&exact_block);
1783 assert_eq!(bpl_exact.num_blocks(), 1);
1784
1785 let bytes = serialize_bpl(&bpl_exact);
1786 let mut out = Vec::new();
1787 let sources: Vec<(&[u8], u32)> = vec![(&bytes, 0), (&bytes, BLOCK_SIZE as u32)];
1788 let (dc, _) = BlockPostingList::concatenate_streaming(&sources, &mut out).unwrap();
1789 assert_eq!(dc, BLOCK_SIZE as u32 * 2);
1790
1791 let merged = BlockPostingList::deserialize(&out).unwrap();
1792 let postings = collect_postings(&merged);
1793 assert_eq!(postings.len(), BLOCK_SIZE * 2);
1794 assert_eq!(postings[BLOCK_SIZE].0, BLOCK_SIZE as u32);
1796
1797 let over_block: Vec<(u32, u32)> = (0..BLOCK_SIZE as u32 + 1).map(|i| (i * 2, 1)).collect();
1799 let bpl_over = build_bpl(&over_block);
1800 assert_eq!(bpl_over.num_blocks(), 2);
1801 }
1802
1803 #[test]
1804 fn test_streaming_roundtrip_single_source() {
1805 let docs: Vec<(u32, u32)> = (0..500).map(|i| (i * 7, i % 15 + 1)).collect();
1807 let bpl = build_bpl(&docs);
1808 let direct = serialize_bpl(&bpl);
1809
1810 let sources: Vec<(&[u8], u32)> = vec![(&direct, 0)];
1811 let mut streamed = Vec::new();
1812 BlockPostingList::concatenate_streaming(&sources, &mut streamed).unwrap();
1813
1814 let p1 = collect_postings(&BlockPostingList::deserialize(&direct).unwrap());
1816 let p2 = collect_postings(&BlockPostingList::deserialize(&streamed).unwrap());
1817 assert_eq!(p1, p2);
1818 }
1819
1820 #[test]
1821 fn test_max_tf_preserved_through_merge() {
1822 let mut a = Vec::new();
1824 for i in 0..200 {
1825 a.push((i * 2, if i == 100 { 50 } else { 1 }));
1826 }
1827 let bpl_a = build_bpl(&a);
1828 assert_eq!(bpl_a.max_tf(), 50);
1829
1830 let mut b = Vec::new();
1832 for i in 0..200 {
1833 b.push((i * 2, if i == 50 { 30 } else { 2 }));
1834 }
1835 let bpl_b = build_bpl(&b);
1836 assert_eq!(bpl_b.max_tf(), 30);
1837
1838 let bytes_a = serialize_bpl(&bpl_a);
1840 let bytes_b = serialize_bpl(&bpl_b);
1841 let sources: Vec<(&[u8], u32)> = vec![(&bytes_a, 0), (&bytes_b, 1000)];
1842 let mut out = Vec::new();
1843 BlockPostingList::concatenate_streaming(&sources, &mut out).unwrap();
1844
1845 let merged = BlockPostingList::deserialize(&out).unwrap();
1846 assert_eq!(merged.max_tf(), 50);
1847 assert_eq!(merged.doc_count(), 400);
1848 }
1849
1850 #[test]
1853 fn test_l0_l1_counts() {
1854 let bpl = build_bpl(&(0..50u32).map(|i| (i, 1)).collect::<Vec<_>>());
1856 assert_eq!(bpl.num_blocks(), 1);
1857 assert_eq!(bpl.l1_docs.len(), 1);
1858
1859 let n = BLOCK_SIZE * L1_INTERVAL;
1861 let bpl = build_bpl(&(0..n as u32).map(|i| (i * 2, 1)).collect::<Vec<_>>());
1862 assert_eq!(bpl.num_blocks(), L1_INTERVAL);
1863 assert_eq!(bpl.l1_docs.len(), 1);
1864
1865 let n = BLOCK_SIZE * L1_INTERVAL + 1;
1867 let bpl = build_bpl(&(0..n as u32).map(|i| (i * 2, 1)).collect::<Vec<_>>());
1868 assert_eq!(bpl.num_blocks(), L1_INTERVAL + 1);
1869 assert_eq!(bpl.l1_docs.len(), 2);
1870
1871 let n = BLOCK_SIZE * L1_INTERVAL * 3;
1873 let bpl = build_bpl(&(0..n as u32).map(|i| (i, 1)).collect::<Vec<_>>());
1874 assert_eq!(bpl.num_blocks(), L1_INTERVAL * 3);
1875 assert_eq!(bpl.l1_docs.len(), 3);
1876 }
1877
1878 #[test]
1879 fn test_l1_last_doc_values() {
1880 let n = BLOCK_SIZE * 20;
1882 let docs: Vec<(u32, u32)> = (0..n as u32).map(|i| (i * 3, 1)).collect();
1883 let bpl = build_bpl(&docs);
1884 assert_eq!(bpl.num_blocks(), 20);
1885 assert_eq!(bpl.l1_docs.len(), 3); let expected_l1_0 = bpl.block_last_doc(7).unwrap();
1889 assert_eq!(bpl.l1_docs[0], expected_l1_0);
1890
1891 let expected_l1_1 = bpl.block_last_doc(15).unwrap();
1893 assert_eq!(bpl.l1_docs[1], expected_l1_1);
1894
1895 let expected_l1_2 = bpl.block_last_doc(19).unwrap();
1897 assert_eq!(bpl.l1_docs[2], expected_l1_2);
1898 }
1899
1900 #[test]
1901 fn test_seek_block_basic() {
1902 let n = BLOCK_SIZE * 20;
1904 let docs: Vec<(u32, u32)> = (0..n as u32).map(|i| (i * 10, 1)).collect();
1905 let bpl = build_bpl(&docs);
1906
1907 assert_eq!(bpl.seek_block(0, 0), Some(0));
1909
1910 for blk in 0..20 {
1912 let first = bpl.block_first_doc(blk).unwrap();
1913 assert_eq!(
1914 bpl.seek_block(first, 0),
1915 Some(blk),
1916 "seek to block {} first_doc",
1917 blk
1918 );
1919 }
1920
1921 for blk in 0..20 {
1923 let last = bpl.block_last_doc(blk).unwrap();
1924 assert_eq!(
1925 bpl.seek_block(last, 0),
1926 Some(blk),
1927 "seek to block {} last_doc",
1928 blk
1929 );
1930 }
1931
1932 let max_doc = bpl.block_last_doc(19).unwrap();
1934 assert_eq!(bpl.seek_block(max_doc + 1, 0), None);
1935
1936 let mid_doc = bpl.block_first_doc(10).unwrap();
1938 assert_eq!(bpl.seek_block(mid_doc, 10), Some(10));
1939 assert_eq!(
1940 bpl.seek_block(mid_doc, 11),
1941 Some(11).or(bpl.seek_block(mid_doc, 11))
1942 );
1943 }
1944
1945 #[test]
1946 fn test_seek_block_across_l1_boundaries() {
1947 let n = BLOCK_SIZE * 24;
1949 let docs: Vec<(u32, u32)> = (0..n as u32).map(|i| (i * 5, 1)).collect();
1950 let bpl = build_bpl(&docs);
1951 assert_eq!(bpl.l1_docs.len(), 3);
1952
1953 for group in 0..3 {
1955 let blk = group * L1_INTERVAL;
1956 let target = bpl.block_first_doc(blk).unwrap();
1957 assert_eq!(
1958 bpl.seek_block(target, 0),
1959 Some(blk),
1960 "seek to group {} block {}",
1961 group,
1962 blk
1963 );
1964 }
1965
1966 let target = bpl.block_first_doc(20).unwrap() + 1;
1968 assert_eq!(bpl.seek_block(target, 0), Some(20));
1969 }
1970
1971 #[test]
1972 fn test_block_data_size_helper() {
1973 let docs: Vec<(u32, u32)> = (0..500u32).map(|i| (i * 7, (i % 20) + 1)).collect();
1975 let bpl = build_bpl(&docs);
1976
1977 for blk in 0..bpl.num_blocks() {
1978 let (_, _, offset, _) = bpl.read_l0_entry(blk);
1979 let computed_size = block_data_size(&bpl.stream, offset as usize);
1980
1981 if blk + 1 < bpl.num_blocks() {
1984 let (_, _, next_offset, _) = bpl.read_l0_entry(blk + 1);
1985 assert_eq!(
1986 computed_size,
1987 (next_offset - offset) as usize,
1988 "block_data_size mismatch at block {}",
1989 blk
1990 );
1991 } else {
1992 assert_eq!(
1994 offset as usize + computed_size,
1995 bpl.stream.len(),
1996 "last block size mismatch"
1997 );
1998 }
1999 }
2000 }
2001
2002 #[test]
2003 fn test_l0_entry_roundtrip() {
2004 let docs: Vec<(u32, u32)> = (0..1000u32).map(|i| (i * 3, (i % 10) + 1)).collect();
2006 let bpl = build_bpl(&docs);
2007
2008 let bytes = serialize_bpl(&bpl);
2009 let bpl2 = BlockPostingList::deserialize(&bytes).unwrap();
2010
2011 assert_eq!(bpl.num_blocks(), bpl2.num_blocks());
2012 for blk in 0..bpl.num_blocks() {
2013 assert_eq!(
2014 bpl.read_l0_entry(blk),
2015 bpl2.read_l0_entry(blk),
2016 "L0 entry mismatch at block {}",
2017 blk
2018 );
2019 }
2020
2021 assert_eq!(bpl.l1_docs, bpl2.l1_docs);
2023 }
2024
2025 #[test]
2026 fn test_zero_copy_deserialize_matches() {
2027 let docs: Vec<(u32, u32)> = (0..2000u32).map(|i| (i * 2, (i % 5) + 1)).collect();
2028 let bpl = build_bpl(&docs);
2029 let bytes = serialize_bpl(&bpl);
2030
2031 let copied = BlockPostingList::deserialize(&bytes).unwrap();
2032 let zero_copy =
2033 BlockPostingList::deserialize_zero_copy(OwnedBytes::new(bytes.clone())).unwrap();
2034
2035 assert_eq!(copied.l0_count, zero_copy.l0_count);
2037 assert_eq!(copied.l1_docs, zero_copy.l1_docs);
2038 assert_eq!(copied.doc_count, zero_copy.doc_count);
2039 assert_eq!(copied.max_tf, zero_copy.max_tf);
2040
2041 let p1 = collect_postings(&copied);
2043 let p2 = collect_postings(&zero_copy);
2044 assert_eq!(p1, p2);
2045 }
2046
2047 #[test]
2048 fn test_l1_preserved_through_streaming_merge() {
2049 let seg_a = build_bpl(&(0..1000u32).map(|i| (i * 2, 1)).collect::<Vec<_>>());
2051 let seg_b = build_bpl(&(0..800u32).map(|i| (i * 3, 2)).collect::<Vec<_>>());
2052 let seg_c = build_bpl(&(0..500u32).map(|i| (i * 5, 3)).collect::<Vec<_>>());
2053
2054 let bytes_a = serialize_bpl(&seg_a);
2055 let bytes_b = serialize_bpl(&seg_b);
2056 let bytes_c = serialize_bpl(&seg_c);
2057
2058 let sources: Vec<(&[u8], u32)> = vec![(&bytes_a, 0), (&bytes_b, 10000), (&bytes_c, 20000)];
2059 let mut out = Vec::new();
2060 BlockPostingList::concatenate_streaming(&sources, &mut out).unwrap();
2061
2062 let merged = BlockPostingList::deserialize(&out).unwrap();
2063 let expected_l1_count = merged.num_blocks().div_ceil(L1_INTERVAL);
2064 assert_eq!(merged.l1_docs.len(), expected_l1_count);
2065
2066 for (i, &l1_doc) in merged.l1_docs.iter().enumerate() {
2068 let last_block_in_group = ((i + 1) * L1_INTERVAL - 1).min(merged.num_blocks() - 1);
2069 let expected = merged.block_last_doc(last_block_in_group).unwrap();
2070 assert_eq!(l1_doc, expected, "L1[{}] mismatch", i);
2071 }
2072
2073 for blk in 0..merged.num_blocks() {
2075 let first = merged.block_first_doc(blk).unwrap();
2076 assert_eq!(merged.seek_block(first, 0), Some(blk));
2077 }
2078 }
2079
2080 #[test]
2081 fn test_seek_block_single_block() {
2082 let bpl = build_bpl(&[(0, 1), (10, 2), (20, 3)]);
2084 assert_eq!(bpl.num_blocks(), 1);
2085 assert_eq!(bpl.l1_docs.len(), 1);
2086
2087 assert_eq!(bpl.seek_block(0, 0), Some(0));
2088 assert_eq!(bpl.seek_block(10, 0), Some(0));
2089 assert_eq!(bpl.seek_block(20, 0), Some(0));
2090 assert_eq!(bpl.seek_block(21, 0), None);
2091 }
2092
2093 #[test]
2094 fn test_footer_size() {
2095 let docs: Vec<(u32, u32)> = (0..500u32).map(|i| (i * 2, 1)).collect();
2097 let bpl = build_bpl(&docs);
2098 let bytes = serialize_bpl(&bpl);
2099
2100 let expected = bpl.stream.len()
2101 + bpl.l0_count * L0_SIZE
2102 + bpl.l1_docs.len() * (L1_SIZE + 4)
2103 + FOOTER_V2_SIZE;
2104 assert_eq!(bytes.len(), expected);
2105 }
2106
2107 fn build_bpl_with_positions(postings: &[(u32, u32)]) -> BlockPostingList {
2108 let mut list = PostingList::new();
2109 for &(doc, tf) in postings {
2110 list.push(doc, tf);
2111 }
2112 BlockPostingList::from_posting_list_with_positions(&list).unwrap()
2113 }
2114
2115 fn expected_cursors(postings: &[(u32, u32)]) -> Vec<u64> {
2117 let mut acc = 0u64;
2118 postings
2119 .iter()
2120 .map(|&(_, tf)| {
2121 let c = acc;
2122 acc += tf as u64;
2123 c
2124 })
2125 .collect()
2126 }
2127
2128 fn iterator_cursors(bpl: &BlockPostingList) -> Vec<u64> {
2129 let mut it = bpl.iterator();
2130 let mut out = Vec::new();
2131 while it.doc() != TERMINATED {
2132 out.push(it.position_cursor());
2133 it.advance();
2134 }
2135 out
2136 }
2137
2138 #[test]
2139 fn position_cursors_survive_serialization_and_seeks() {
2140 let docs: Vec<(u32, u32)> = (0..700u32).map(|i| (i * 3, i % 5 + 1)).collect();
2141 let bpl = build_bpl_with_positions(&docs);
2142 assert!(bpl.has_position_cursors());
2143 assert_eq!(
2144 bpl.total_positions(),
2145 docs.iter().map(|&(_, tf)| tf as u64).sum::<u64>()
2146 );
2147 assert_eq!(bpl.pos_cursor(0), Some(0));
2148 assert_eq!(
2149 bpl.pos_cursor(1),
2150 Some(docs[..128].iter().map(|&(_, tf)| tf as u64).sum::<u64>())
2151 );
2152 assert_eq!(iterator_cursors(&bpl), expected_cursors(&docs));
2153
2154 let bytes = serialize_bpl(&bpl);
2155 assert_eq!(
2156 bytes.len(),
2157 bpl.stream.len()
2158 + bpl.l0_count * (L0_SIZE + CURSOR_SIZE)
2159 + bpl.l1_docs.len() * (L1_SIZE + 4)
2160 + FOOTER_V2_SIZE
2161 );
2162 assert!(BlockPostingList::has_cursors_bytes(&bytes));
2163 let decoded =
2164 BlockPostingList::deserialize_zero_copy(OwnedBytes::new(bytes.clone())).unwrap();
2165 assert_eq!(iterator_cursors(&decoded), expected_cursors(&docs));
2166 assert_eq!(decoded.total_positions(), bpl.total_positions());
2167
2168 let mut it = decoded.iterator();
2170 let expected = expected_cursors(&docs);
2171 for (i, &(doc, _)) in docs.iter().enumerate().step_by(37) {
2172 assert_eq!(it.seek(doc), doc);
2173 assert_eq!(it.position_cursor(), expected[i], "cursor at doc {doc}");
2174 }
2175 let mut it = decoded.iterator();
2176 assert_eq!(it.seek(docs[600].0 + 1), docs[601].0);
2177 assert_eq!(it.position_cursor(), expected[601]);
2178
2179 let plain = build_bpl(&docs);
2182 assert!(!plain.has_position_cursors());
2183 assert_eq!(plain.pos_cursor(0), None);
2184 assert_eq!(plain.total_positions(), 0);
2185 }
2186
2187 #[test]
2188 fn length_bounds_are_packed_per_block_and_survive_merges() {
2189 let docs: Vec<(u32, u32)> = (0..300u32).map(|i| (i, i % 3 + 1)).collect();
2190 let length_of = |doc: u32| 10 + (doc % 50) * 7;
2191 let mut list = PostingList::new();
2192 for &(doc, tf) in &docs {
2193 list.push(doc, tf);
2194 }
2195 let bpl = BlockPostingList::from_posting_list_with(&list, true, Some(&length_of)).unwrap();
2196 assert_eq!(bpl.min_len(), Some(10));
2197 assert_eq!(bpl.block_bounds(0), Some((3, Some(10))));
2198 assert_eq!(bpl.block_bounds(2), Some((3, Some(52))));
2200 assert_eq!(bpl.block_max_tf(2), Some(3));
2201
2202 let bytes = serialize_bpl(&bpl);
2203 let decoded = BlockPostingList::deserialize(&bytes).unwrap();
2204 assert_eq!(decoded.min_len(), Some(10));
2205 assert_eq!(decoded.block_bounds(2), Some((3, Some(52))));
2206 assert_eq!(decoded.group_bounds(0), Some((3, 10)));
2209 assert_eq!(decoded.group_bounds(2), Some((3, 10)));
2210 assert_eq!(decoded.group_bounds(3), None);
2211 assert_eq!(decoded.group_last_doc(1), Some(299));
2212 assert_eq!(decoded.next_group_block(1), 3);
2213
2214 let plain = build_bpl(&docs);
2216 assert_eq!(plain.min_len(), Some(1));
2217 assert_eq!(plain.block_bounds(0), Some((3, Some(1))));
2218
2219 let mut out = Vec::new();
2221 BlockPostingList::concatenate_streaming(&[(&bytes, 0), (&bytes, 1000)], &mut out).unwrap();
2222 let merged = BlockPostingList::deserialize(&out).unwrap();
2223 assert_eq!(merged.min_len(), Some(10));
2224 assert_eq!(merged.block_bounds(2), Some((3, Some(52))));
2225 assert_eq!(merged.block_bounds(3), Some((3, Some(10))));
2226 assert_eq!(merged.block_max_tf(5), Some(3));
2227 assert_eq!(merged.group_bounds(5), Some((3, 10)));
2230 assert_eq!(merged.group_last_doc(5), Some(1299));
2231 assert_eq!(merged.next_group_block(5), 6);
2232 }
2233
2234 #[test]
2235 fn legacy_footer_without_magic_still_deserializes() {
2236 let docs: Vec<(u32, u32)> = (0..300u32).map(|i| (i * 2, 1 + i % 3)).collect();
2237 let bpl = build_bpl(&docs);
2238 let bytes = serialize_bpl(&bpl);
2239 let legacy = bytes[..bytes.len() - (FOOTER_V2_SIZE - FOOTER_SIZE)].to_vec();
2241 assert!(!BlockPostingList::has_cursors_bytes(&legacy));
2242 let decoded = BlockPostingList::deserialize(&legacy).unwrap();
2244 assert_eq!(collect_postings(&decoded), docs);
2245 assert_eq!(decoded.max_tf(), 3);
2246 assert!(!decoded.has_position_cursors());
2247 assert_eq!(decoded.min_len(), None);
2248 assert_eq!(decoded.group_bounds(0), None);
2249 let mut out = Vec::new();
2251 let (count, written) =
2252 BlockPostingList::concatenate_streaming(&[(&legacy, 0), (&legacy, 1000)], &mut out)
2253 .unwrap();
2254 assert_eq!(count, 600);
2255 assert_eq!(written, out.len());
2256 let merged = BlockPostingList::deserialize(&out).unwrap();
2257 assert_eq!(merged.doc_count(), 600);
2258 assert!(!merged.has_position_cursors());
2259 }
2260
2261 #[test]
2262 fn streaming_merge_rebases_position_cursors() {
2263 let a: Vec<(u32, u32)> = (0..200u32).map(|i| (i, i % 4 + 1)).collect();
2264 let b: Vec<(u32, u32)> = (0..150u32).map(|i| (i * 2, 2)).collect();
2265 let bytes_a = serialize_bpl(&build_bpl_with_positions(&a));
2266 let bytes_b = serialize_bpl(&build_bpl_with_positions(&b));
2267 let mut out = Vec::new();
2268 let (count, written) =
2269 BlockPostingList::concatenate_streaming(&[(&bytes_a, 0), (&bytes_b, 1000)], &mut out)
2270 .unwrap();
2271 assert_eq!(count, 350);
2272 assert_eq!(written, out.len());
2273 let merged = BlockPostingList::deserialize(&out).unwrap();
2274 assert!(merged.has_position_cursors());
2275 let all: Vec<(u32, u32)> = a
2276 .iter()
2277 .copied()
2278 .chain(b.iter().map(|&(d, tf)| (d + 1000, tf)))
2279 .collect();
2280 assert_eq!(collect_postings(&merged), all);
2281 assert_eq!(iterator_cursors(&merged), expected_cursors(&all));
2282 assert_eq!(
2283 merged.total_positions(),
2284 all.iter().map(|&(_, tf)| tf as u64).sum::<u64>()
2285 );
2286 let reference = BlockPostingList::concatenate_blocks(&[
2288 (build_bpl_with_positions(&a), 0),
2289 (build_bpl_with_positions(&b), 1000),
2290 ])
2291 .unwrap();
2292 assert_eq!(iterator_cursors(&reference), expected_cursors(&all));
2293 let plain = serialize_bpl(&build_bpl(&b));
2295 assert!(
2296 BlockPostingList::concatenate_streaming(
2297 &[(&bytes_a, 0), (&plain, 1000)],
2298 &mut Vec::new()
2299 )
2300 .is_err()
2301 );
2302 }
2303
2304 #[test]
2305 fn test_seek_block_from_block_skips_earlier() {
2306 let n = BLOCK_SIZE * 16;
2308 let docs: Vec<(u32, u32)> = (0..n as u32).map(|i| (i * 3, 1)).collect();
2309 let bpl = build_bpl(&docs);
2310
2311 let target_in_5 = bpl.block_first_doc(5).unwrap() + 1;
2313 let result = bpl.seek_block(target_in_5, 8);
2316 assert!(result.is_some());
2317 assert!(result.unwrap() >= 8);
2318 }
2319}