1use crate::structures::simd;
19use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
20use std::io::{self, Read, Write};
21
22pub const OPT_P4D_BLOCK_SIZE: usize = 128;
24
25const MAX_EXCEPTIONS_RATIO: f32 = 0.10;
28
29pub(crate) fn find_optimal_bit_width(values: &[u32]) -> (u8, usize, usize) {
32 if values.is_empty() {
33 return (0, 0, 0);
34 }
35
36 let n = values.len();
37 let max_exceptions = ((n as f32) * MAX_EXCEPTIONS_RATIO).ceil() as usize;
38
39 let mut bit_counts = [0usize; 33]; for &v in values {
42 let bits = simd::bits_needed(v) as usize;
43 bit_counts[bits] += 1;
44 }
45
46 let mut cumulative = [0usize; 33];
48 cumulative[0] = bit_counts[0];
49 for b in 1..=32 {
50 cumulative[b] = cumulative[b - 1] + bit_counts[b];
51 }
52
53 let mut best_bits = 32u8;
54 let mut best_total = usize::MAX;
55 let mut best_exceptions = 0usize;
56
57 for b in 0..=32u8 {
59 let fitting = if b == 0 {
60 bit_counts[0]
61 } else {
62 cumulative[b as usize]
63 };
64 let exceptions = n - fitting;
65
66 if exceptions > max_exceptions && b < 32 {
68 continue;
69 }
70
71 let main_bits = n * (b as usize);
75 let exception_bits = if b < 32 {
76 exceptions * (7 + (32 - b as usize))
77 } else {
78 0
79 };
80 let total = main_bits + exception_bits;
81
82 if total < best_total {
83 best_total = total;
84 best_bits = b;
85 best_exceptions = exceptions;
86 }
87 }
88
89 (best_bits, best_exceptions, best_total)
90}
91
92pub(crate) fn pack_with_exceptions(values: &[u32], bit_width: u8) -> (Vec<u8>, Vec<(u8, u32)>) {
100 if bit_width == 0 {
101 let exceptions: Vec<(u8, u32)> = values
103 .iter()
104 .enumerate()
105 .filter(|&(_, &v)| v != 0)
106 .map(|(i, &v)| (i as u8, v)) .collect();
108 return (Vec::new(), exceptions);
109 }
110
111 let mut packed = Vec::new();
112 if bit_width >= 32 {
113 super::horizontal_bp128::pack_block_n(values, 32, &mut packed);
115 return (packed, Vec::new());
116 }
117
118 let mask = u32::MAX >> (32 - bit_width);
121 let low: Vec<u32> = values.iter().map(|&value| value & mask).collect();
122 super::horizontal_bp128::pack_block_n(&low, bit_width, &mut packed);
123 let exceptions = values
124 .iter()
125 .enumerate()
126 .filter(|&(_, &value)| value > mask)
127 .map(|(i, &value)| (i as u8, value >> bit_width))
128 .collect();
129 (packed, exceptions)
130}
131
132pub(crate) fn unpack_with_exceptions(
141 packed: &[u8],
142 bit_width: u8,
143 exceptions: &[(u8, u32)],
144 count: usize,
145 output: &mut [u32],
146) {
147 super::horizontal_bp128::unpack_block_n(packed, bit_width, output, count);
148 if bit_width == 32 {
149 return; }
151
152 for &(pos, high_bits) in exceptions {
155 if (pos as usize) < count {
156 let low_bits = output[pos as usize];
157 output[pos as usize] = (high_bits << bit_width) | low_bits;
158 }
159 }
160}
161
162#[inline]
168fn unpack_exceptions_delta_decode(
169 packed: &[u8],
170 bit_width: u8,
171 exceptions: &[(u8, u32)],
172 output: &mut [u32],
173 first_doc_id: u32,
174 count: usize,
175) {
176 if count == 0 {
177 return;
178 }
179
180 output[0] = first_doc_id;
181 if count == 1 {
182 return;
183 }
184
185 let deltas = &mut output[1..count];
186 unpack_with_exceptions(packed, bit_width, exceptions, count - 1, deltas);
187 let mut carry = first_doc_id;
188 for slot in deltas {
189 carry = carry.wrapping_add(*slot).wrapping_add(1);
190 *slot = carry;
191 }
192}
193
194#[derive(Debug, Clone)]
196pub struct OptP4DBlock {
197 pub first_doc_id: u32,
199 pub last_doc_id: u32,
201 pub num_docs: u16,
203 pub doc_bit_width: u8,
205 pub tf_bit_width: u8,
207 pub max_tf: u32,
209 pub max_block_score: f32,
211 pub doc_deltas: Vec<u8>,
213 pub doc_exceptions: Vec<(u8, u32)>,
215 pub term_freqs: Vec<u8>,
217 pub tf_exceptions: Vec<(u8, u32)>,
219}
220
221impl OptP4DBlock {
222 pub fn serialize<W: Write>(&self, writer: &mut W) -> io::Result<()> {
224 writer.write_u32::<LittleEndian>(self.first_doc_id)?;
225 writer.write_u32::<LittleEndian>(self.last_doc_id)?;
226 writer.write_u16::<LittleEndian>(self.num_docs)?;
227 writer.write_u8(self.doc_bit_width)?;
228 writer.write_u8(self.tf_bit_width)?;
229 writer.write_u32::<LittleEndian>(self.max_tf)?;
230 writer.write_f32::<LittleEndian>(self.max_block_score)?;
231
232 writer.write_u16::<LittleEndian>(self.doc_deltas.len() as u16)?;
234 writer.write_all(&self.doc_deltas)?;
235
236 writer.write_u8(self.doc_exceptions.len() as u8)?;
238 for &(pos, val) in &self.doc_exceptions {
239 writer.write_u8(pos)?;
240 writer.write_u32::<LittleEndian>(val)?;
241 }
242
243 writer.write_u16::<LittleEndian>(self.term_freqs.len() as u16)?;
245 writer.write_all(&self.term_freqs)?;
246
247 writer.write_u8(self.tf_exceptions.len() as u8)?;
249 for &(pos, val) in &self.tf_exceptions {
250 writer.write_u8(pos)?;
251 writer.write_u32::<LittleEndian>(val)?;
252 }
253
254 Ok(())
255 }
256
257 pub fn deserialize<R: Read>(reader: &mut R) -> io::Result<Self> {
259 let first_doc_id = reader.read_u32::<LittleEndian>()?;
260 let last_doc_id = reader.read_u32::<LittleEndian>()?;
261 let num_docs = reader.read_u16::<LittleEndian>()?;
262 let doc_bit_width = reader.read_u8()?;
263 let tf_bit_width = reader.read_u8()?;
264 let max_tf = reader.read_u32::<LittleEndian>()?;
265 let max_block_score = reader.read_f32::<LittleEndian>()?;
266
267 let doc_deltas_len = reader.read_u16::<LittleEndian>()? as usize;
269 let mut doc_deltas = vec![0u8; doc_deltas_len];
270 reader.read_exact(&mut doc_deltas)?;
271
272 let num_doc_exceptions = reader.read_u8()? as usize;
274 let mut doc_exceptions = Vec::with_capacity(num_doc_exceptions);
275 for _ in 0..num_doc_exceptions {
276 let pos = reader.read_u8()?;
277 let val = reader.read_u32::<LittleEndian>()?;
278 doc_exceptions.push((pos, val));
279 }
280
281 let term_freqs_len = reader.read_u16::<LittleEndian>()? as usize;
283 let mut term_freqs = vec![0u8; term_freqs_len];
284 reader.read_exact(&mut term_freqs)?;
285
286 let num_tf_exceptions = reader.read_u8()? as usize;
288 let mut tf_exceptions = Vec::with_capacity(num_tf_exceptions);
289 for _ in 0..num_tf_exceptions {
290 let pos = reader.read_u8()?;
291 let val = reader.read_u32::<LittleEndian>()?;
292 tf_exceptions.push((pos, val));
293 }
294
295 Ok(Self {
296 first_doc_id,
297 last_doc_id,
298 num_docs,
299 doc_bit_width,
300 tf_bit_width,
301 max_tf,
302 max_block_score,
303 doc_deltas,
304 doc_exceptions,
305 term_freqs,
306 tf_exceptions,
307 })
308 }
309
310 pub fn decode_doc_ids(&self) -> Vec<u32> {
312 let mut output = vec![0u32; self.num_docs as usize];
313 self.decode_doc_ids_into(&mut output);
314 output
315 }
316
317 #[inline]
319 pub fn decode_doc_ids_into(&self, output: &mut [u32]) -> usize {
320 let count = self.num_docs as usize;
321 if count == 0 {
322 return 0;
323 }
324
325 unpack_exceptions_delta_decode(
327 &self.doc_deltas,
328 self.doc_bit_width,
329 &self.doc_exceptions,
330 output,
331 self.first_doc_id,
332 count,
333 );
334
335 count
336 }
337
338 pub fn decode_term_freqs(&self) -> Vec<u32> {
340 let mut output = vec![0u32; self.num_docs as usize];
341 self.decode_term_freqs_into(&mut output);
342 output
343 }
344
345 #[inline]
347 pub fn decode_term_freqs_into(&self, output: &mut [u32]) -> usize {
348 let count = self.num_docs as usize;
349 if count == 0 {
350 return 0;
351 }
352
353 unpack_with_exceptions(
355 &self.term_freqs,
356 self.tf_bit_width,
357 &self.tf_exceptions,
358 count,
359 output,
360 );
361
362 simd::add_one(output, count);
364
365 count
366 }
367}
368
369#[derive(Debug, Clone)]
371pub struct OptP4DPostingList {
372 pub blocks: Vec<OptP4DBlock>,
374 pub doc_count: u32,
376 pub max_score: f32,
378}
379
380impl OptP4DPostingList {
381 pub fn from_postings(doc_ids: &[u32], term_freqs: &[u32], idf: f32) -> Self {
383 assert_eq!(doc_ids.len(), term_freqs.len());
384
385 if doc_ids.is_empty() {
386 return Self {
387 blocks: Vec::new(),
388 doc_count: 0,
389 max_score: 0.0,
390 };
391 }
392
393 let mut blocks = Vec::new();
394 let mut max_score = 0.0f32;
395 let mut i = 0;
396
397 while i < doc_ids.len() {
398 let block_end = (i + OPT_P4D_BLOCK_SIZE).min(doc_ids.len());
399 let block_docs = &doc_ids[i..block_end];
400 let block_tfs = &term_freqs[i..block_end];
401
402 let block = Self::create_block(block_docs, block_tfs, idf);
403 max_score = max_score.max(block.max_block_score);
404 blocks.push(block);
405
406 i = block_end;
407 }
408
409 Self {
410 blocks,
411 doc_count: doc_ids.len() as u32,
412 max_score,
413 }
414 }
415
416 fn create_block(doc_ids: &[u32], term_freqs: &[u32], idf: f32) -> OptP4DBlock {
417 let num_docs = doc_ids.len();
418 let first_doc_id = doc_ids[0];
419 let last_doc_id = *doc_ids.last().unwrap();
420
421 let mut deltas = [0u32; OPT_P4D_BLOCK_SIZE];
423 for j in 1..num_docs {
424 deltas[j - 1] = doc_ids[j] - doc_ids[j - 1] - 1;
425 }
426
427 let (doc_bit_width, _, _) = find_optimal_bit_width(&deltas[..num_docs.saturating_sub(1)]);
429 let (doc_deltas, doc_exceptions) =
430 pack_with_exceptions(&deltas[..num_docs.saturating_sub(1)], doc_bit_width);
431
432 let mut tfs = [0u32; OPT_P4D_BLOCK_SIZE];
434 let mut max_tf = 0u32;
435
436 for (j, &tf) in term_freqs.iter().enumerate() {
437 tfs[j] = tf - 1; max_tf = max_tf.max(tf);
439 }
440
441 let (tf_bit_width, _, _) = find_optimal_bit_width(&tfs[..num_docs]);
443 let (term_freqs_packed, tf_exceptions) =
444 pack_with_exceptions(&tfs[..num_docs], tf_bit_width);
445
446 let max_block_score = crate::query::bm25_upper_bound(max_tf as f32, idf);
448
449 OptP4DBlock {
450 first_doc_id,
451 last_doc_id,
452 num_docs: num_docs as u16,
453 doc_bit_width,
454 tf_bit_width,
455 max_tf,
456 max_block_score,
457 doc_deltas,
458 doc_exceptions,
459 term_freqs: term_freqs_packed,
460 tf_exceptions,
461 }
462 }
463
464 pub fn serialize<W: Write>(&self, writer: &mut W) -> io::Result<()> {
466 writer.write_u32::<LittleEndian>(self.doc_count)?;
467 writer.write_f32::<LittleEndian>(self.max_score)?;
468 writer.write_u32::<LittleEndian>(self.blocks.len() as u32)?;
469
470 for block in &self.blocks {
471 block.serialize(writer)?;
472 }
473
474 Ok(())
475 }
476
477 pub fn deserialize<R: Read>(reader: &mut R) -> io::Result<Self> {
479 let doc_count = reader.read_u32::<LittleEndian>()?;
480 let max_score = reader.read_f32::<LittleEndian>()?;
481 let num_blocks = reader.read_u32::<LittleEndian>()? as usize;
482
483 let mut blocks = Vec::with_capacity(num_blocks);
484 for _ in 0..num_blocks {
485 blocks.push(OptP4DBlock::deserialize(reader)?);
486 }
487
488 Ok(Self {
489 blocks,
490 doc_count,
491 max_score,
492 })
493 }
494
495 pub fn len(&self) -> u32 {
497 self.doc_count
498 }
499
500 pub fn is_empty(&self) -> bool {
502 self.doc_count == 0
503 }
504
505 pub fn iterator(&self) -> OptP4DIterator<'_> {
507 OptP4DIterator::new(self)
508 }
509}
510
511pub struct OptP4DIterator<'a> {
513 posting_list: &'a OptP4DPostingList,
514 current_block: usize,
515 current_block_len: usize,
517 block_doc_ids: Vec<u32>,
519 block_term_freqs: Vec<u32>,
521 pos_in_block: usize,
522 exhausted: bool,
523}
524
525impl<'a> OptP4DIterator<'a> {
526 pub fn new(posting_list: &'a OptP4DPostingList) -> Self {
527 let mut iter = Self {
529 posting_list,
530 current_block: 0,
531 current_block_len: 0,
532 block_doc_ids: vec![0u32; OPT_P4D_BLOCK_SIZE],
533 block_term_freqs: vec![0u32; OPT_P4D_BLOCK_SIZE],
534 pos_in_block: 0,
535 exhausted: posting_list.blocks.is_empty(),
536 };
537
538 if !iter.exhausted {
539 iter.decode_current_block();
540 }
541
542 iter
543 }
544
545 #[inline]
546 fn decode_current_block(&mut self) {
547 let block = &self.posting_list.blocks[self.current_block];
548 self.current_block_len = block.decode_doc_ids_into(&mut self.block_doc_ids);
550 block.decode_term_freqs_into(&mut self.block_term_freqs);
551 self.pos_in_block = 0;
552 }
553
554 #[inline]
556 pub fn doc(&self) -> u32 {
557 if self.exhausted {
558 u32::MAX
559 } else {
560 self.block_doc_ids[self.pos_in_block]
561 }
562 }
563
564 #[inline]
566 pub fn term_freq(&self) -> u32 {
567 if self.exhausted {
568 0
569 } else {
570 self.block_term_freqs[self.pos_in_block]
571 }
572 }
573
574 #[inline]
576 pub fn advance(&mut self) -> u32 {
577 if self.exhausted {
578 return u32::MAX;
579 }
580
581 self.pos_in_block += 1;
582
583 if self.pos_in_block >= self.current_block_len {
584 self.current_block += 1;
585 if self.current_block >= self.posting_list.blocks.len() {
586 self.exhausted = true;
587 return u32::MAX;
588 }
589 self.decode_current_block();
590 }
591
592 self.doc()
593 }
594
595 pub fn seek(&mut self, target: u32) -> u32 {
597 if self.exhausted {
598 return u32::MAX;
599 }
600
601 while self.current_block < self.posting_list.blocks.len() {
603 let block = &self.posting_list.blocks[self.current_block];
604 if block.last_doc_id >= target {
605 break;
606 }
607 self.current_block += 1;
608 }
609
610 if self.current_block >= self.posting_list.blocks.len() {
611 self.exhausted = true;
612 return u32::MAX;
613 }
614
615 if self.current_block_len == 0 || self.current_block != self.posting_list.blocks.len() - 1 {
617 self.decode_current_block();
618 }
619
620 match self.block_doc_ids[self.pos_in_block..self.current_block_len].binary_search(&target) {
622 Ok(idx) => {
623 self.pos_in_block += idx;
624 }
625 Err(idx) => {
626 self.pos_in_block += idx;
627 if self.pos_in_block >= self.current_block_len {
628 self.current_block += 1;
630 if self.current_block >= self.posting_list.blocks.len() {
631 self.exhausted = true;
632 return u32::MAX;
633 }
634 self.decode_current_block();
635 }
636 }
637 }
638
639 self.doc()
640 }
641}
642
643#[cfg(test)]
644mod tests {
645 use super::*;
646
647 #[test]
648 fn test_bits_needed() {
649 assert_eq!(simd::bits_needed(0), 0);
650 assert_eq!(simd::bits_needed(1), 1);
651 assert_eq!(simd::bits_needed(2), 2);
652 assert_eq!(simd::bits_needed(3), 2);
653 assert_eq!(simd::bits_needed(4), 3);
654 assert_eq!(simd::bits_needed(255), 8);
655 assert_eq!(simd::bits_needed(256), 9);
656 assert_eq!(simd::bits_needed(u32::MAX), 32);
657 }
658
659 #[test]
660 fn test_find_optimal_bit_width() {
661 let values = vec![0u32; 100];
663 let (bits, exceptions, _) = find_optimal_bit_width(&values);
664 assert_eq!(bits, 0);
665 assert_eq!(exceptions, 0);
666
667 let values: Vec<u32> = (0..100).map(|i| i % 16).collect();
669 let (bits, _, _) = find_optimal_bit_width(&values);
670 assert!(bits <= 4);
671
672 let mut values: Vec<u32> = (0..100).map(|i| i % 16).collect();
674 values[50] = 1_000_000; let (bits, exceptions, _) = find_optimal_bit_width(&values);
676 assert!(bits < 20); assert!(exceptions >= 1);
678 }
679
680 #[test]
681 fn test_pack_unpack_with_exceptions() {
682 let values = vec![1, 2, 3, 255, 4, 5, 1000, 6, 7, 8];
683 let (packed, exceptions) = pack_with_exceptions(&values, 4);
684
685 let mut output = vec![0u32; values.len()];
686 unpack_with_exceptions(&packed, 4, &exceptions, values.len(), &mut output);
687
688 assert_eq!(output, values);
689 }
690
691 #[test]
692 fn test_opt_p4d_posting_list_small() {
693 let doc_ids: Vec<u32> = (0..100).map(|i| i * 2).collect();
694 let term_freqs: Vec<u32> = vec![1; 100];
695
696 let list = OptP4DPostingList::from_postings(&doc_ids, &term_freqs, 1.0);
697
698 assert_eq!(list.len(), 100);
699 assert_eq!(list.blocks.len(), 1);
700
701 let mut iter = list.iterator();
703 for (i, &expected) in doc_ids.iter().enumerate() {
704 assert_eq!(iter.doc(), expected, "Mismatch at {}", i);
705 assert_eq!(iter.term_freq(), 1);
706 iter.advance();
707 }
708 assert_eq!(iter.doc(), u32::MAX);
709 }
710
711 #[test]
712 fn test_opt_p4d_posting_list_large() {
713 let doc_ids: Vec<u32> = (0..500).map(|i| i * 3).collect();
714 let term_freqs: Vec<u32> = (0..500).map(|i| (i % 10) + 1).collect();
715
716 let list = OptP4DPostingList::from_postings(&doc_ids, &term_freqs, 1.0);
717
718 assert_eq!(list.len(), 500);
719 assert_eq!(list.blocks.len(), 4); let mut iter = list.iterator();
723 for (i, &expected) in doc_ids.iter().enumerate() {
724 assert_eq!(iter.doc(), expected, "Mismatch at {}", i);
725 assert_eq!(iter.term_freq(), term_freqs[i]);
726 iter.advance();
727 }
728 }
729
730 #[test]
731 fn test_opt_p4d_seek() {
732 let doc_ids: Vec<u32> = vec![10, 20, 30, 100, 200, 300, 1000, 2000];
733 let term_freqs: Vec<u32> = vec![1; 8];
734
735 let list = OptP4DPostingList::from_postings(&doc_ids, &term_freqs, 1.0);
736 let mut iter = list.iterator();
737
738 assert_eq!(iter.seek(25), 30);
739 assert_eq!(iter.seek(100), 100);
740 assert_eq!(iter.seek(500), 1000);
741 assert_eq!(iter.seek(3000), u32::MAX);
742 }
743
744 #[test]
745 fn test_opt_p4d_serialization() {
746 let doc_ids: Vec<u32> = (0..200).map(|i| i * 5).collect();
747 let term_freqs: Vec<u32> = (0..200).map(|i| (i % 5) + 1).collect();
748
749 let list = OptP4DPostingList::from_postings(&doc_ids, &term_freqs, 1.0);
750
751 let mut buffer = Vec::new();
752 list.serialize(&mut buffer).unwrap();
753
754 let restored = OptP4DPostingList::deserialize(&mut &buffer[..]).unwrap();
755
756 assert_eq!(restored.len(), list.len());
757 assert_eq!(restored.blocks.len(), list.blocks.len());
758
759 let mut iter1 = list.iterator();
761 let mut iter2 = restored.iterator();
762
763 while iter1.doc() != u32::MAX {
764 assert_eq!(iter1.doc(), iter2.doc());
765 assert_eq!(iter1.term_freq(), iter2.term_freq());
766 iter1.advance();
767 iter2.advance();
768 }
769 }
770
771 #[test]
772 fn test_opt_p4d_with_outliers() {
773 let mut doc_ids: Vec<u32> = (0..128).map(|i| i * 2).collect();
775 doc_ids[64] = 1_000_000; doc_ids.sort();
779
780 let term_freqs: Vec<u32> = vec![1; 128];
781
782 let list = OptP4DPostingList::from_postings(&doc_ids, &term_freqs, 1.0);
783
784 let mut iter = list.iterator();
786 let mut found_outlier = false;
787 while iter.doc() != u32::MAX {
788 if iter.doc() == 1_000_000 {
789 found_outlier = true;
790 }
791 iter.advance();
792 }
793 assert!(found_outlier, "Outlier value should be preserved");
794 }
795
796 #[test]
797 fn test_opt_p4d_simd_full_blocks() {
798 let doc_ids: Vec<u32> = (0..1024).map(|i| i * 2).collect();
800 let term_freqs: Vec<u32> = (0..1024).map(|i| (i % 20) + 1).collect();
801
802 let list = OptP4DPostingList::from_postings(&doc_ids, &term_freqs, 1.0);
803
804 assert_eq!(list.len(), 1024);
805 assert_eq!(list.blocks.len(), 8); let mut iter = list.iterator();
809 for (i, &expected_doc) in doc_ids.iter().enumerate() {
810 assert_eq!(iter.doc(), expected_doc, "Doc mismatch at {}", i);
811 assert_eq!(iter.term_freq(), term_freqs[i], "TF mismatch at {}", i);
812 iter.advance();
813 }
814 assert_eq!(iter.doc(), u32::MAX);
815 }
816
817 #[test]
818 fn test_opt_p4d_simd_8bit_values() {
819 let doc_ids: Vec<u32> = (0..256).collect();
821 let term_freqs: Vec<u32> = (0..256).map(|i| (i % 100) + 1).collect();
822
823 let list = OptP4DPostingList::from_postings(&doc_ids, &term_freqs, 1.0);
824
825 let mut iter = list.iterator();
827 for (i, &expected_doc) in doc_ids.iter().enumerate() {
828 assert_eq!(iter.doc(), expected_doc, "Doc mismatch at {}", i);
829 assert_eq!(iter.term_freq(), term_freqs[i], "TF mismatch at {}", i);
830 iter.advance();
831 }
832 }
833
834 #[test]
835 fn test_opt_p4d_simd_delta_decode() {
836 let mut doc_ids = Vec::with_capacity(512);
838 let mut current = 0u32;
839 for i in 0..512 {
840 current += (i % 10) + 1; doc_ids.push(current);
842 }
843 let term_freqs: Vec<u32> = vec![1; 512];
844
845 let list = OptP4DPostingList::from_postings(&doc_ids, &term_freqs, 1.0);
846
847 let mut iter = list.iterator();
849 for (i, &expected_doc) in doc_ids.iter().enumerate() {
850 assert_eq!(
851 iter.doc(),
852 expected_doc,
853 "Doc mismatch at {} (expected {}, got {})",
854 i,
855 expected_doc,
856 iter.doc()
857 );
858 iter.advance();
859 }
860 }
861}