Skip to main content

summa_core/structures/postings/
opt_p4d.rs

1//! OptP4D (Optimized Patched Frame-of-Reference Delta) posting list compression
2//!
3//! OptP4D is an improvement over PForDelta that finds the optimal bit width for each block
4//! by trying all possible bit widths and selecting the one that minimizes total storage.
5//!
6//! Key features:
7//! - Block-based compression (128 integers per block for SIMD alignment)
8//! - Delta encoding for doc IDs
9//! - Optimal bit-width selection per block
10//! - Patched coding: exceptions (values that don't fit) stored separately
11//! - Fast SIMD-friendly decoding with NEON (ARM) and SSE (x86) support
12//!
13//! Format per block:
14//! - Header: bit_width (5 bits) + num_exceptions (7 bits) + first_doc_id (32 bits)
15//! - Main array: 128 values packed at `bit_width` bits each
16//! - Exceptions: [position (7 bits), high_bits (32 - bit_width bits)] for each exception
17
18use crate::structures::simd;
19use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
20use std::io::{self, Read, Write};
21
22/// Block size for OptP4D (128 integers for SIMD alignment)
23pub const OPT_P4D_BLOCK_SIZE: usize = 128;
24
25/// Maximum number of exceptions before we increase bit width
26/// (keeping exceptions under ~10% of block for good compression)
27const MAX_EXCEPTIONS_RATIO: f32 = 0.10;
28
29/// Find the optimal bit width for a block of values
30/// Returns (bit_width, exception_count, total_bits)
31pub(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    // Count how many values need each bit width
40    let mut bit_counts = [0usize; 33]; // bit_counts[b] = count of values needing exactly b bits
41    for &v in values {
42        let bits = simd::bits_needed(v) as usize;
43        bit_counts[bits] += 1;
44    }
45
46    // Compute cumulative counts: values that fit in b bits or less
47    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    // Try each bit width and compute total storage
58    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        // Skip if too many exceptions
67        if exceptions > max_exceptions && b < 32 {
68            continue;
69        }
70
71        // Calculate total bits:
72        // - Main array: n * b bits
73        // - Exceptions: exceptions * (7 bits position + (32 - b) bits high value)
74        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
92/// Pack values into a bitpacked array with the given bit width (NewPFD/OptPFD style)
93///
94/// Following the paper "Decoding billions of integers per second through vectorization":
95/// - Store the first b bits (low bits) of ALL values in the main array
96/// - For exceptions (values >= 2^b), store only the HIGH (32-b) bits separately with positions
97///
98/// Returns the packed bytes and a list of exceptions (position, high_bits)
99pub(crate) fn pack_with_exceptions(values: &[u32], bit_width: u8) -> (Vec<u8>, Vec<(u8, u32)>) {
100    if bit_width == 0 {
101        // All values must be 0, exceptions store full value
102        let exceptions: Vec<(u8, u32)> = values
103            .iter()
104            .enumerate()
105            .filter(|&(_, &v)| v != 0)
106            .map(|(i, &v)| (i as u8, v)) // For b=0, high bits = full value
107            .collect();
108        return (Vec::new(), exceptions);
109    }
110
111    let mut packed = Vec::new();
112    if bit_width >= 32 {
113        // No exceptions possible, just pack all 32 bits
114        super::horizontal_bp128::pack_block_n(values, 32, &mut packed);
115        return (packed, Vec::new());
116    }
117
118    // Low b bits of every value go through the shared little-endian packer;
119    // the high bits of values that do not fit become exceptions.
120    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
132/// Unpack values from a bitpacked array and apply exceptions (NewPFD/OptPFD style)
133///
134/// Following the paper "Decoding billions of integers per second through vectorization":
135/// - Low b bits are stored in the main array for ALL values
136/// - Exceptions store only the HIGH (32-b) bits
137/// - Reconstruct: value = (high_bits << b) | low_bits
138///
139/// Uses SIMD acceleration for common bit widths (8, 16, 32)
140pub(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; // No exceptions for 32-bit values.
150    }
151
152    // Apply exceptions: combine high bits with low bits already in output
153    // value = (high_bits << bit_width) | low_bits
154    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/// Unpack + exceptions + delta decode for doc_ids.
163///
164/// The `count - 1` gap-minus-one deltas are decoded in place through the
165/// bounded shared unpacker (never reading past `packed`), then turned into
166/// absolute ids by one prefix-sum pass.
167#[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/// A single OptP4D block
195#[derive(Debug, Clone)]
196pub struct OptP4DBlock {
197    /// First doc_id in this block (absolute)
198    pub first_doc_id: u32,
199    /// Last doc_id in this block (absolute)
200    pub last_doc_id: u32,
201    /// Number of documents in this block
202    pub num_docs: u16,
203    /// Bit width for delta encoding
204    pub doc_bit_width: u8,
205    /// Bit width for term frequencies
206    pub tf_bit_width: u8,
207    /// Maximum term frequency in this block
208    pub max_tf: u32,
209    /// Maximum block score for MaxScore pruning
210    pub max_block_score: f32,
211    /// Packed doc deltas
212    pub doc_deltas: Vec<u8>,
213    /// Doc delta exceptions: (position, full_delta)
214    pub doc_exceptions: Vec<(u8, u32)>,
215    /// Packed term frequencies
216    pub term_freqs: Vec<u8>,
217    /// TF exceptions: (position, full_tf)
218    pub tf_exceptions: Vec<(u8, u32)>,
219}
220
221impl OptP4DBlock {
222    /// Serialize the block
223    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        // Write doc deltas
233        writer.write_u16::<LittleEndian>(self.doc_deltas.len() as u16)?;
234        writer.write_all(&self.doc_deltas)?;
235
236        // Write doc exceptions
237        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        // Write term freqs
244        writer.write_u16::<LittleEndian>(self.term_freqs.len() as u16)?;
245        writer.write_all(&self.term_freqs)?;
246
247        // Write tf exceptions
248        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    /// Deserialize a block
258    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        // Read doc deltas
268        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        // Read doc exceptions
273        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        // Read term freqs
282        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        // Read tf exceptions
287        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    /// Decode doc_ids from this block using SIMD-accelerated delta decoding
311    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    /// Decode doc_ids into a pre-allocated buffer (avoids allocation)
318    #[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        // Fused unpack + exceptions + delta decode - no intermediate buffer
326        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    /// Decode term frequencies from this block using SIMD acceleration
339    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    /// Decode term frequencies into a pre-allocated buffer (avoids allocation)
346    #[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 TFs with exceptions (SIMD-accelerated for 8/16/32-bit)
354        unpack_with_exceptions(
355            &self.term_freqs,
356            self.tf_bit_width,
357            &self.tf_exceptions,
358            count,
359            output,
360        );
361
362        // TF is stored as tf-1, so add 1 back using SIMD
363        simd::add_one(output, count);
364
365        count
366    }
367}
368
369/// OptP4D posting list
370#[derive(Debug, Clone)]
371pub struct OptP4DPostingList {
372    /// Blocks of postings
373    pub blocks: Vec<OptP4DBlock>,
374    /// Total document count
375    pub doc_count: u32,
376    /// Maximum score across all blocks
377    pub max_score: f32,
378}
379
380impl OptP4DPostingList {
381    /// Create from raw doc_ids and term frequencies
382    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        // Compute deltas using stack array (delta - 1 to save one bit)
422        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        // Find optimal bit width for deltas
428        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        // Compute max TF and prepare TF array using stack array (store tf-1)
433        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; // Store tf-1
438            max_tf = max_tf.max(tf);
439        }
440
441        // Find optimal bit width for TFs
442        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        // BM25F upper bound score
447        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    /// Serialize the posting list
465    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    /// Deserialize a posting list
478    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    /// Get document count
496    pub fn len(&self) -> u32 {
497        self.doc_count
498    }
499
500    /// Check if empty
501    pub fn is_empty(&self) -> bool {
502        self.doc_count == 0
503    }
504
505    /// Create an iterator
506    pub fn iterator(&self) -> OptP4DIterator<'_> {
507        OptP4DIterator::new(self)
508    }
509}
510
511/// Iterator over OptP4D posting list
512pub struct OptP4DIterator<'a> {
513    posting_list: &'a OptP4DPostingList,
514    current_block: usize,
515    /// Number of valid elements in current block
516    current_block_len: usize,
517    /// Pre-allocated buffer for decoded doc_ids (avoids allocation per block)
518    block_doc_ids: Vec<u32>,
519    /// Pre-allocated buffer for decoded term freqs
520    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        // Pre-allocate buffers to block size to avoid allocations during iteration
528        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        // Decode into pre-allocated buffers (no allocation!)
549        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    /// Current document ID
555    #[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    /// Current term frequency
565    #[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    /// Advance to next document
575    #[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    /// Seek to first doc >= target
596    pub fn seek(&mut self, target: u32) -> u32 {
597        if self.exhausted {
598            return u32::MAX;
599        }
600
601        // Skip blocks where last_doc_id < target
602        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        // Decode block if needed
616        if self.current_block_len == 0 || self.current_block != self.posting_list.blocks.len() - 1 {
617            self.decode_current_block();
618        }
619
620        // Binary search within block
621        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                    // Move to next block
629                    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        // All zeros
662        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        // All small values
668        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        // Mix with outliers
673        let mut values: Vec<u32> = (0..100).map(|i| i % 16).collect();
674        values[50] = 1_000_000; // outlier
675        let (bits, exceptions, _) = find_optimal_bit_width(&values);
676        assert!(bits < 20); // Should use small bit width with exception
677        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        // Verify iteration
702        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); // 500 / 128 = 3.9 -> 4 blocks
720
721        // Verify iteration
722        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        // Verify iteration matches
760        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        // Create data with some outliers to test exception handling
774        let mut doc_ids: Vec<u32> = (0..128).map(|i| i * 2).collect();
775        doc_ids[64] = 1_000_000; // Large outlier
776
777        // Fix: ensure doc_ids are sorted
778        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        // Verify the outlier is handled correctly
785        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        // Test with multiple full 128-integer blocks to exercise SIMD paths
799        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); // 1024 / 128 = 8 full blocks
806
807        // Verify all values are decoded correctly
808        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        // Test with values that fit in 8 bits to exercise SIMD 8-bit unpack
820        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        // Verify all values
826        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        // Test SIMD delta decoding with various gap sizes
837        let mut doc_ids = Vec::with_capacity(512);
838        let mut current = 0u32;
839        for i in 0..512 {
840            current += (i % 10) + 1; // Variable gaps
841            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        // Verify delta decoding is correct
848        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}