Skip to main content

summa_core/structures/postings/
vertical_bp128.rs

1//! SIMD-BP128: Vectorized bitpacking with NEON/SSE intrinsics
2//!
3//! Based on Lemire & Boytsov (2015) "Decoding billions of integers per second through vectorization"
4//! and Quickwit's bitpacking crate architecture.
5//!
6//! Key optimizations:
7//! - **True vertical layout**: Optimal compression (BLOCK_SIZE * bit_width / 8 bytes)
8//! - **Integrated delta decoding**: Fused unpack + prefix sum in single pass
9//! - **128-integer blocks**: 32 groups of 4 integers each
10//! - **NEON intrinsics on ARM**: Uses vld1q_u32, vaddq_u32, etc.
11//! - **Block-level metadata**: Skip info for block-max pruning
12
13use crate::structures::simd;
14use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
15use std::io::{self, Read, Write};
16
17/// Block size: 128 integers (32 groups of 4 for SIMD lanes)
18pub const VERTICAL_BP128_BLOCK_SIZE: usize = 128;
19
20// ============================================================================
21// Public API - True Vertical Bit-Interleaved Layout
22// ============================================================================
23//
24// Vertical Layout Specification:
25// For a block of 128 integers at bit_width bits each:
26// - Total bytes = 128 * bit_width / 8
27// - For each bit position b (0 to bit_width-1):
28//   - Store 16 bytes containing bit b from all 128 integers
29//   - Byte i within those 16 bytes contains bit b from integers i*8 to i*8+7
30//   - Bit j within byte i = bit b of integer i*8+j
31//
32// Example for bit_width=4:
33//   Bytes 0-15:  bit 0 of all 128 integers (16 bytes × 8 bits = 128 bits)
34//   Bytes 16-31: bit 1 of all 128 integers
35//   Bytes 32-47: bit 2 of all 128 integers
36//   Bytes 48-63: bit 3 of all 128 integers
37//   Total: 64 bytes
38//
39// This layout enables SIMD: one 16-byte load gets one bit from all 128 integers.
40
41/// Pack 128 integers using true vertical bit-interleaved layout
42///
43/// Vertical layout stores bit i of all 128 integers together in 16 consecutive bytes.
44/// Total size: exactly 128 * bit_width / 8 bytes (no padding waste)
45///
46/// This layout is optimal for SIMD unpacking: a single 16-byte load retrieves
47/// one bit position from all 128 integers simultaneously.
48pub fn pack_vertical(
49    values: &[u32; VERTICAL_BP128_BLOCK_SIZE],
50    bit_width: u8,
51    output: &mut Vec<u8>,
52) {
53    if bit_width == 0 {
54        return;
55    }
56
57    // Total bytes = 128 * bit_width / 8 = 16 * bit_width
58    let total_bytes = 16 * bit_width as usize;
59    let start = output.len();
60    output.resize(start + total_bytes, 0);
61
62    // For each bit position, pack that bit from all 128 integers into 16 bytes
63    for bit_pos in 0..bit_width as usize {
64        let byte_offset = start + bit_pos * 16;
65
66        // Process 16 bytes (128 integers, 8 per byte)
67        for byte_idx in 0..16 {
68            let base_int = byte_idx * 8;
69            let mut byte_val = 0u8;
70
71            // Pack 8 integers' bits into one byte
72            byte_val |= ((values[base_int] >> bit_pos) & 1) as u8;
73            byte_val |= (((values[base_int + 1] >> bit_pos) & 1) as u8) << 1;
74            byte_val |= (((values[base_int + 2] >> bit_pos) & 1) as u8) << 2;
75            byte_val |= (((values[base_int + 3] >> bit_pos) & 1) as u8) << 3;
76            byte_val |= (((values[base_int + 4] >> bit_pos) & 1) as u8) << 4;
77            byte_val |= (((values[base_int + 5] >> bit_pos) & 1) as u8) << 5;
78            byte_val |= (((values[base_int + 6] >> bit_pos) & 1) as u8) << 6;
79            byte_val |= (((values[base_int + 7] >> bit_pos) & 1) as u8) << 7;
80
81            output[byte_offset + byte_idx] = byte_val;
82        }
83    }
84}
85
86/// Unpack 128 integers from true vertical bit-interleaved layout
87///
88/// Uses SIMD on supported architectures (NEON on aarch64, SSE on x86_64).
89/// Falls back to optimized scalar implementation on other platforms.
90pub fn unpack_vertical(input: &[u8], bit_width: u8, output: &mut [u32; VERTICAL_BP128_BLOCK_SIZE]) {
91    if bit_width == 0 {
92        output.fill(0);
93        return;
94    }
95
96    #[cfg(target_arch = "aarch64")]
97    {
98        unsafe { unpack_vertical_neon(input, bit_width, output) }
99    }
100
101    #[cfg(target_arch = "x86_64")]
102    {
103        if is_x86_feature_detected!("sse2") {
104            unsafe { unpack_vertical_sse(input, bit_width, output) }
105        } else {
106            unpack_vertical_scalar(input, bit_width, output)
107        }
108    }
109
110    #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
111    {
112        unpack_vertical_scalar(input, bit_width, output)
113    }
114}
115
116/// Scalar implementation of vertical unpack (the only path off aarch64/SSE2).
117#[cfg_attr(target_arch = "aarch64", allow(dead_code))]
118#[inline]
119fn unpack_vertical_scalar(
120    input: &[u8],
121    bit_width: u8,
122    output: &mut [u32; VERTICAL_BP128_BLOCK_SIZE],
123) {
124    output.fill(0);
125
126    // For each bit position, scatter that bit to all 128 integers
127    for bit_pos in 0..bit_width as usize {
128        let byte_offset = bit_pos * 16;
129        let bit_mask = 1u32 << bit_pos;
130
131        // Process 16 bytes (128 integers)
132        for byte_idx in 0..16 {
133            let byte_val = input[byte_offset + byte_idx];
134            let base_int = byte_idx * 8;
135
136            // Scatter 8 bits to 8 integers
137            if byte_val & 0x01 != 0 {
138                output[base_int] |= bit_mask;
139            }
140            if byte_val & 0x02 != 0 {
141                output[base_int + 1] |= bit_mask;
142            }
143            if byte_val & 0x04 != 0 {
144                output[base_int + 2] |= bit_mask;
145            }
146            if byte_val & 0x08 != 0 {
147                output[base_int + 3] |= bit_mask;
148            }
149            if byte_val & 0x10 != 0 {
150                output[base_int + 4] |= bit_mask;
151            }
152            if byte_val & 0x20 != 0 {
153                output[base_int + 5] |= bit_mask;
154            }
155            if byte_val & 0x40 != 0 {
156                output[base_int + 6] |= bit_mask;
157            }
158            if byte_val & 0x80 != 0 {
159                output[base_int + 7] |= bit_mask;
160            }
161        }
162    }
163}
164
165/// NEON-optimized vertical unpack for aarch64
166#[cfg(target_arch = "aarch64")]
167#[target_feature(enable = "neon")]
168unsafe fn unpack_vertical_neon(
169    input: &[u8],
170    bit_width: u8,
171    output: &mut [u32; VERTICAL_BP128_BLOCK_SIZE],
172) {
173    use std::arch::aarch64::*;
174
175    unsafe {
176        // Clear output using NEON
177        let zero = vdupq_n_u32(0);
178        for i in (0..VERTICAL_BP128_BLOCK_SIZE).step_by(4) {
179            vst1q_u32(output[i..].as_mut_ptr(), zero);
180        }
181
182        // For each bit position
183        for bit_pos in 0..bit_width as usize {
184            let byte_offset = bit_pos * 16;
185            let bit_mask = 1u32 << bit_pos;
186
187            // Load 16 bytes (one bit-plane for all 128 integers)
188            let bytes = vld1q_u8(input.as_ptr().add(byte_offset));
189
190            // Store to array for processing (NEON lane extraction requires constants)
191            let mut byte_array = [0u8; 16];
192            vst1q_u8(byte_array.as_mut_ptr(), bytes);
193
194            // Process 16 bytes (128 integers, 8 per byte)
195            for (byte_idx, &byte_val) in byte_array.iter().enumerate() {
196                let base_int = byte_idx * 8;
197
198                // Scatter 8 bits to 8 integers using branchless OR
199                output[base_int] |= ((byte_val & 0x01) as u32) * bit_mask;
200                output[base_int + 1] |= (((byte_val >> 1) & 0x01) as u32) * bit_mask;
201                output[base_int + 2] |= (((byte_val >> 2) & 0x01) as u32) * bit_mask;
202                output[base_int + 3] |= (((byte_val >> 3) & 0x01) as u32) * bit_mask;
203                output[base_int + 4] |= (((byte_val >> 4) & 0x01) as u32) * bit_mask;
204                output[base_int + 5] |= (((byte_val >> 5) & 0x01) as u32) * bit_mask;
205                output[base_int + 6] |= (((byte_val >> 6) & 0x01) as u32) * bit_mask;
206                output[base_int + 7] |= (((byte_val >> 7) & 0x01) as u32) * bit_mask;
207            }
208        }
209    }
210}
211
212/// SSE-optimized vertical unpack for x86_64
213#[cfg(target_arch = "x86_64")]
214#[target_feature(enable = "sse2")]
215unsafe fn unpack_vertical_sse(
216    input: &[u8],
217    bit_width: u8,
218    output: &mut [u32; VERTICAL_BP128_BLOCK_SIZE],
219) {
220    use std::arch::x86_64::*;
221
222    unsafe {
223        // Clear output
224        let zero = _mm_setzero_si128();
225        for i in (0..VERTICAL_BP128_BLOCK_SIZE).step_by(4) {
226            _mm_storeu_si128(output[i..].as_mut_ptr() as *mut __m128i, zero);
227        }
228
229        // For each bit position
230        for bit_pos in 0..bit_width as usize {
231            let byte_offset = bit_pos * 16;
232
233            // Load 16 bytes (one bit-plane for all 128 integers)
234            let bytes = _mm_loadu_si128(input.as_ptr().add(byte_offset) as *const __m128i);
235
236            // Extract bytes and scatter bits
237            let mut byte_array = [0u8; 16];
238            _mm_storeu_si128(byte_array.as_mut_ptr() as *mut __m128i, bytes);
239
240            for (byte_idx, &byte_val) in byte_array.iter().enumerate() {
241                let base_int = byte_idx * 8;
242
243                // Scatter 8 bits to 8 integers
244                if byte_val & 0x01 != 0 {
245                    output[base_int] |= 1u32 << bit_pos;
246                }
247                if byte_val & 0x02 != 0 {
248                    output[base_int + 1] |= 1u32 << bit_pos;
249                }
250                if byte_val & 0x04 != 0 {
251                    output[base_int + 2] |= 1u32 << bit_pos;
252                }
253                if byte_val & 0x08 != 0 {
254                    output[base_int + 3] |= 1u32 << bit_pos;
255                }
256                if byte_val & 0x10 != 0 {
257                    output[base_int + 4] |= 1u32 << bit_pos;
258                }
259                if byte_val & 0x20 != 0 {
260                    output[base_int + 5] |= 1u32 << bit_pos;
261                }
262                if byte_val & 0x40 != 0 {
263                    output[base_int + 6] |= 1u32 << bit_pos;
264                }
265                if byte_val & 0x80 != 0 {
266                    output[base_int + 7] |= 1u32 << bit_pos;
267                }
268            }
269        }
270    }
271}
272
273/// Unpack with integrated delta decoding (fused for better performance)
274///
275/// The encoding stores deltas[i] = doc_ids[i+1] - doc_ids[i] - 1
276/// So we have (count-1) deltas for count doc_ids.
277/// first_doc_id is doc_ids[0], and we compute the rest from deltas.
278///
279/// This fused version avoids a separate prefix sum pass by computing
280/// doc_ids inline during unpacking. Uses true vertical bit-interleaved layout.
281pub fn unpack_vertical_d1(
282    input: &[u8],
283    bit_width: u8,
284    first_doc_id: u32,
285    output: &mut [u32; VERTICAL_BP128_BLOCK_SIZE],
286    count: usize,
287) {
288    if count == 0 {
289        return;
290    }
291
292    if bit_width == 0 {
293        // All deltas are 0, so gaps are all 1
294        let mut current = first_doc_id;
295        output[0] = current;
296        for out_val in output.iter_mut().take(count).skip(1) {
297            current = current.wrapping_add(1);
298            *out_val = current;
299        }
300        return;
301    }
302
303    // First unpack all deltas from vertical layout
304    let mut deltas = [0u32; VERTICAL_BP128_BLOCK_SIZE];
305    unpack_vertical(input, bit_width, &mut deltas);
306
307    // Then apply prefix sum with delta decoding
308    output[0] = first_doc_id;
309    let mut current = first_doc_id;
310
311    for i in 1..count {
312        // deltas[i-1] stores (gap - 1), so actual gap = deltas[i-1] + 1
313        current = current.wrapping_add(deltas[i - 1]).wrapping_add(1);
314        output[i] = current;
315    }
316}
317
318/// A single SIMD-BP128 block with metadata
319#[derive(Debug, Clone)]
320pub struct VerticalBP128Block {
321    /// Vertically-packed delta-encoded doc_ids (true vertical bit-interleaved layout)
322    pub doc_data: Vec<u8>,
323    /// Bit width for doc deltas
324    pub doc_bit_width: u8,
325    /// Vertically-packed term frequencies (tf - 1)
326    pub tf_data: Vec<u8>,
327    /// Bit width for term frequencies
328    pub tf_bit_width: u8,
329    /// First doc_id in block (absolute)
330    pub first_doc_id: u32,
331    /// Last doc_id in block (absolute)
332    pub last_doc_id: u32,
333    /// Number of docs in this block
334    pub num_docs: u16,
335    /// Maximum term frequency in block
336    pub max_tf: u32,
337    /// Maximum BM25 score upper bound for block-max pruning
338    pub max_block_score: f32,
339}
340
341impl VerticalBP128Block {
342    /// Serialize block
343    pub fn serialize<W: Write>(&self, writer: &mut W) -> io::Result<()> {
344        writer.write_u32::<LittleEndian>(self.first_doc_id)?;
345        writer.write_u32::<LittleEndian>(self.last_doc_id)?;
346        writer.write_u16::<LittleEndian>(self.num_docs)?;
347        writer.write_u8(self.doc_bit_width)?;
348        writer.write_u8(self.tf_bit_width)?;
349        writer.write_u32::<LittleEndian>(self.max_tf)?;
350        writer.write_f32::<LittleEndian>(self.max_block_score)?;
351
352        writer.write_u16::<LittleEndian>(self.doc_data.len() as u16)?;
353        writer.write_all(&self.doc_data)?;
354
355        writer.write_u16::<LittleEndian>(self.tf_data.len() as u16)?;
356        writer.write_all(&self.tf_data)?;
357
358        Ok(())
359    }
360
361    /// Deserialize block
362    pub fn deserialize<R: Read>(reader: &mut R) -> io::Result<Self> {
363        let first_doc_id = reader.read_u32::<LittleEndian>()?;
364        let last_doc_id = reader.read_u32::<LittleEndian>()?;
365        let num_docs = reader.read_u16::<LittleEndian>()?;
366        let doc_bit_width = reader.read_u8()?;
367        let tf_bit_width = reader.read_u8()?;
368        let max_tf = reader.read_u32::<LittleEndian>()?;
369        let max_block_score = reader.read_f32::<LittleEndian>()?;
370
371        let doc_len = reader.read_u16::<LittleEndian>()? as usize;
372        let mut doc_data = vec![0u8; doc_len];
373        reader.read_exact(&mut doc_data)?;
374
375        let tf_len = reader.read_u16::<LittleEndian>()? as usize;
376        let mut tf_data = vec![0u8; tf_len];
377        reader.read_exact(&mut tf_data)?;
378
379        Ok(Self {
380            doc_data,
381            doc_bit_width,
382            tf_data,
383            tf_bit_width,
384            first_doc_id,
385            last_doc_id,
386            num_docs,
387            max_tf,
388            max_block_score,
389        })
390    }
391
392    /// Decode doc_ids from this block
393    pub fn decode_doc_ids(&self) -> Vec<u32> {
394        let mut output = vec![0u32; self.num_docs as usize];
395        self.decode_doc_ids_into(&mut output);
396        output
397    }
398
399    /// Decode doc_ids into a pre-allocated buffer (avoids allocation)
400    #[inline]
401    pub fn decode_doc_ids_into(&self, output: &mut [u32]) -> usize {
402        let count = self.num_docs as usize;
403        if count == 0 {
404            return 0;
405        }
406
407        // For full blocks, decode directly if output is large enough
408        // For partial blocks, we need the temp buffer due to SIMD alignment
409        if count == VERTICAL_BP128_BLOCK_SIZE && output.len() >= VERTICAL_BP128_BLOCK_SIZE {
410            // SAFETY: output slice is large enough, reinterpret as fixed array
411            let out_array: &mut [u32; VERTICAL_BP128_BLOCK_SIZE] = (&mut output
412                [..VERTICAL_BP128_BLOCK_SIZE])
413                .try_into()
414                .unwrap();
415            unpack_vertical_d1(
416                &self.doc_data,
417                self.doc_bit_width,
418                self.first_doc_id,
419                out_array,
420                count,
421            );
422        } else {
423            // Partial block - need temp buffer for SIMD alignment
424            let mut temp = [0u32; VERTICAL_BP128_BLOCK_SIZE];
425            unpack_vertical_d1(
426                &self.doc_data,
427                self.doc_bit_width,
428                self.first_doc_id,
429                &mut temp,
430                count,
431            );
432            output[..count].copy_from_slice(&temp[..count]);
433        }
434
435        count
436    }
437
438    /// Decode term frequencies from this block
439    pub fn decode_term_freqs(&self) -> Vec<u32> {
440        let mut output = vec![0u32; self.num_docs as usize];
441        self.decode_term_freqs_into(&mut output);
442        output
443    }
444
445    /// Decode term frequencies into a pre-allocated buffer (avoids allocation)
446    #[inline]
447    pub fn decode_term_freqs_into(&self, output: &mut [u32]) -> usize {
448        let count = self.num_docs as usize;
449        if count == 0 {
450            return 0;
451        }
452
453        // For full blocks, decode directly if output is large enough
454        if count == VERTICAL_BP128_BLOCK_SIZE && output.len() >= VERTICAL_BP128_BLOCK_SIZE {
455            let out_array: &mut [u32; VERTICAL_BP128_BLOCK_SIZE] = (&mut output
456                [..VERTICAL_BP128_BLOCK_SIZE])
457                .try_into()
458                .unwrap();
459            unpack_vertical(&self.tf_data, self.tf_bit_width, out_array);
460        } else {
461            // Partial block - need temp buffer for SIMD alignment
462            let mut temp = [0u32; VERTICAL_BP128_BLOCK_SIZE];
463            unpack_vertical(&self.tf_data, self.tf_bit_width, &mut temp);
464            output[..count].copy_from_slice(&temp[..count]);
465        }
466
467        // TF is stored as tf-1, add 1 back
468        simd::add_one(output, count);
469
470        count
471    }
472}
473
474/// SIMD-BP128 posting list with vertical layout and BlockMax support
475#[derive(Debug, Clone)]
476pub struct VerticalBP128PostingList {
477    /// Blocks of postings
478    pub blocks: Vec<VerticalBP128Block>,
479    /// Total document count
480    pub doc_count: u32,
481    /// Maximum score across all blocks
482    pub max_score: f32,
483}
484
485impl VerticalBP128PostingList {
486    /// Create from raw postings
487    pub fn from_postings(doc_ids: &[u32], term_freqs: &[u32], idf: f32) -> Self {
488        assert_eq!(doc_ids.len(), term_freqs.len());
489
490        if doc_ids.is_empty() {
491            return Self {
492                blocks: Vec::new(),
493                doc_count: 0,
494                max_score: 0.0,
495            };
496        }
497
498        let mut blocks = Vec::new();
499        let mut max_score = 0.0f32;
500        let mut i = 0;
501
502        while i < doc_ids.len() {
503            let block_end = (i + VERTICAL_BP128_BLOCK_SIZE).min(doc_ids.len());
504            let block_docs = &doc_ids[i..block_end];
505            let block_tfs = &term_freqs[i..block_end];
506
507            let block = Self::create_block(block_docs, block_tfs, idf);
508            max_score = max_score.max(block.max_block_score);
509            blocks.push(block);
510
511            i = block_end;
512        }
513
514        Self {
515            blocks,
516            doc_count: doc_ids.len() as u32,
517            max_score,
518        }
519    }
520
521    fn create_block(doc_ids: &[u32], term_freqs: &[u32], idf: f32) -> VerticalBP128Block {
522        let num_docs = doc_ids.len();
523        let first_doc_id = doc_ids[0];
524        let last_doc_id = *doc_ids.last().unwrap();
525
526        // Compute deltas (gap - 1)
527        let mut deltas = [0u32; VERTICAL_BP128_BLOCK_SIZE];
528        let mut max_delta = 0u32;
529        for j in 1..num_docs {
530            let delta = doc_ids[j] - doc_ids[j - 1] - 1;
531            deltas[j - 1] = delta;
532            max_delta = max_delta.max(delta);
533        }
534
535        // Compute TFs (tf - 1)
536        let mut tfs = [0u32; VERTICAL_BP128_BLOCK_SIZE];
537        let mut max_tf = 0u32;
538        for (j, &tf) in term_freqs.iter().enumerate() {
539            tfs[j] = tf.saturating_sub(1);
540            max_tf = max_tf.max(tf);
541        }
542
543        let doc_bit_width = simd::bits_needed(max_delta);
544        let tf_bit_width = simd::bits_needed(max_tf.saturating_sub(1));
545
546        let mut doc_data = Vec::new();
547        pack_vertical(&deltas, doc_bit_width, &mut doc_data);
548
549        let mut tf_data = Vec::new();
550        pack_vertical(&tfs, tf_bit_width, &mut tf_data);
551
552        let max_block_score = crate::query::bm25_upper_bound(max_tf as f32, idf);
553
554        VerticalBP128Block {
555            doc_data,
556            doc_bit_width,
557            tf_data,
558            tf_bit_width,
559            first_doc_id,
560            last_doc_id,
561            num_docs: num_docs as u16,
562            max_tf,
563            max_block_score,
564        }
565    }
566
567    /// Serialize
568    pub fn serialize<W: Write>(&self, writer: &mut W) -> io::Result<()> {
569        writer.write_u32::<LittleEndian>(self.doc_count)?;
570        writer.write_f32::<LittleEndian>(self.max_score)?;
571        writer.write_u32::<LittleEndian>(self.blocks.len() as u32)?;
572
573        for block in &self.blocks {
574            block.serialize(writer)?;
575        }
576
577        Ok(())
578    }
579
580    /// Deserialize
581    pub fn deserialize<R: Read>(reader: &mut R) -> io::Result<Self> {
582        let doc_count = reader.read_u32::<LittleEndian>()?;
583        let max_score = reader.read_f32::<LittleEndian>()?;
584        let num_blocks = reader.read_u32::<LittleEndian>()? as usize;
585
586        let mut blocks = Vec::with_capacity(num_blocks);
587        for _ in 0..num_blocks {
588            blocks.push(VerticalBP128Block::deserialize(reader)?);
589        }
590
591        Ok(Self {
592            blocks,
593            doc_count,
594            max_score,
595        })
596    }
597
598    /// Create iterator
599    pub fn iterator(&self) -> VerticalBP128Iterator<'_> {
600        VerticalBP128Iterator::new(self)
601    }
602
603    /// Get approximate size in bytes
604    pub fn size_bytes(&self) -> usize {
605        let mut size = 12; // header
606        for block in &self.blocks {
607            size += 22 + block.doc_data.len() + block.tf_data.len();
608        }
609        size
610    }
611}
612
613/// Iterator over SIMD-BP128 posting list
614pub struct VerticalBP128Iterator<'a> {
615    list: &'a VerticalBP128PostingList,
616    current_block: usize,
617    /// Number of valid elements in current block
618    current_block_len: usize,
619    /// Pre-allocated buffer for decoded doc_ids (avoids allocation per block)
620    block_doc_ids: Vec<u32>,
621    /// Pre-allocated buffer for decoded term freqs
622    block_term_freqs: Vec<u32>,
623    pos_in_block: usize,
624    exhausted: bool,
625}
626
627impl<'a> VerticalBP128Iterator<'a> {
628    pub fn new(list: &'a VerticalBP128PostingList) -> Self {
629        // Pre-allocate buffers to block size to avoid allocations during iteration
630        let mut iter = Self {
631            list,
632            current_block: 0,
633            current_block_len: 0,
634            block_doc_ids: vec![0u32; VERTICAL_BP128_BLOCK_SIZE],
635            block_term_freqs: vec![0u32; VERTICAL_BP128_BLOCK_SIZE],
636            pos_in_block: 0,
637            exhausted: list.blocks.is_empty(),
638        };
639
640        if !iter.exhausted {
641            iter.decode_current_block();
642        }
643
644        iter
645    }
646
647    #[inline]
648    fn decode_current_block(&mut self) {
649        let block = &self.list.blocks[self.current_block];
650        // Decode into pre-allocated buffers (no allocation!)
651        self.current_block_len = block.decode_doc_ids_into(&mut self.block_doc_ids);
652        block.decode_term_freqs_into(&mut self.block_term_freqs);
653        self.pos_in_block = 0;
654    }
655
656    /// Current document ID
657    #[inline]
658    pub fn doc(&self) -> u32 {
659        if self.exhausted {
660            u32::MAX
661        } else {
662            self.block_doc_ids[self.pos_in_block]
663        }
664    }
665
666    /// Current term frequency
667    #[inline]
668    pub fn term_freq(&self) -> u32 {
669        if self.exhausted {
670            0
671        } else {
672            self.block_term_freqs[self.pos_in_block]
673        }
674    }
675
676    /// Advance to next document
677    #[inline]
678    pub fn advance(&mut self) -> u32 {
679        if self.exhausted {
680            return u32::MAX;
681        }
682
683        self.pos_in_block += 1;
684
685        if self.pos_in_block >= self.current_block_len {
686            self.current_block += 1;
687            if self.current_block >= self.list.blocks.len() {
688                self.exhausted = true;
689                return u32::MAX;
690            }
691            self.decode_current_block();
692        }
693
694        self.doc()
695    }
696
697    /// Seek to first doc >= target with block skipping
698    pub fn seek(&mut self, target: u32) -> u32 {
699        if self.exhausted {
700            return u32::MAX;
701        }
702
703        // Binary search for target block
704        let block_idx = self.list.blocks[self.current_block..].binary_search_by(|block| {
705            if block.last_doc_id < target {
706                std::cmp::Ordering::Less
707            } else if block.first_doc_id > target {
708                std::cmp::Ordering::Greater
709            } else {
710                std::cmp::Ordering::Equal
711            }
712        });
713
714        let target_block = match block_idx {
715            Ok(idx) => self.current_block + idx,
716            Err(idx) => {
717                if self.current_block + idx >= self.list.blocks.len() {
718                    self.exhausted = true;
719                    return u32::MAX;
720                }
721                self.current_block + idx
722            }
723        };
724
725        if target_block != self.current_block {
726            self.current_block = target_block;
727            self.decode_current_block();
728        }
729
730        // Binary search within block
731        let pos = self.block_doc_ids[self.pos_in_block..self.current_block_len]
732            .binary_search(&target)
733            .unwrap_or_else(|x| x);
734        self.pos_in_block += pos;
735
736        if self.pos_in_block >= self.current_block_len {
737            self.current_block += 1;
738            if self.current_block >= self.list.blocks.len() {
739                self.exhausted = true;
740                return u32::MAX;
741            }
742            self.decode_current_block();
743        }
744
745        self.doc()
746    }
747
748    /// Get max score for remaining blocks
749    pub fn max_remaining_score(&self) -> f32 {
750        if self.exhausted {
751            return 0.0;
752        }
753        self.list.blocks[self.current_block..]
754            .iter()
755            .map(|b| b.max_block_score)
756            .fold(0.0f32, |a, b| a.max(b))
757    }
758
759    /// Get current block's max score
760    pub fn current_block_max_score(&self) -> f32 {
761        if self.exhausted {
762            0.0
763        } else {
764            self.list.blocks[self.current_block].max_block_score
765        }
766    }
767
768    /// Get current block's max TF
769    pub fn current_block_max_tf(&self) -> u32 {
770        if self.exhausted {
771            0
772        } else {
773            self.list.blocks[self.current_block].max_tf
774        }
775    }
776
777    /// Skip to next block containing doc >= target (for block-max pruning)
778    /// Returns (first_doc_in_block, block_max_score) or None if exhausted
779    pub fn skip_to_block_with_doc(&mut self, target: u32) -> Option<(u32, f32)> {
780        while self.current_block < self.list.blocks.len() {
781            let block = &self.list.blocks[self.current_block];
782            if block.last_doc_id >= target {
783                // Decode this block and position at start
784                self.decode_current_block();
785                return Some((block.first_doc_id, block.max_block_score));
786            }
787            self.current_block += 1;
788        }
789        self.exhausted = true;
790        None
791    }
792
793    /// Check if iterator is exhausted
794    pub fn is_exhausted(&self) -> bool {
795        self.exhausted
796    }
797}
798
799#[cfg(test)]
800mod tests {
801    use super::*;
802
803    #[test]
804    fn test_pack_unpack_vertical() {
805        let mut values = [0u32; VERTICAL_BP128_BLOCK_SIZE];
806        for (i, v) in values.iter_mut().enumerate() {
807            *v = (i * 3) as u32;
808        }
809
810        let max_val = values.iter().max().copied().unwrap();
811        let bit_width = simd::bits_needed(max_val);
812
813        let mut packed = Vec::new();
814        pack_vertical(&values, bit_width, &mut packed);
815
816        let mut unpacked = [0u32; VERTICAL_BP128_BLOCK_SIZE];
817        unpack_vertical(&packed, bit_width, &mut unpacked);
818
819        assert_eq!(values, unpacked);
820    }
821
822    #[test]
823    fn test_pack_unpack_vertical_various_widths() {
824        for bit_width in 1..=20 {
825            let mut values = [0u32; VERTICAL_BP128_BLOCK_SIZE];
826            let max_val = (1u32 << bit_width) - 1;
827            for (i, v) in values.iter_mut().enumerate() {
828                *v = (i as u32) % (max_val + 1);
829            }
830
831            let mut packed = Vec::new();
832            pack_vertical(&values, bit_width, &mut packed);
833
834            let mut unpacked = [0u32; VERTICAL_BP128_BLOCK_SIZE];
835            unpack_vertical(&packed, bit_width, &mut unpacked);
836
837            assert_eq!(values, unpacked, "Failed for bit_width={}", bit_width);
838        }
839    }
840
841    #[test]
842    fn test_simd_bp128_posting_list() {
843        let doc_ids: Vec<u32> = (0..200).map(|i| i * 2).collect();
844        let term_freqs: Vec<u32> = (0..200).map(|i| (i % 10) + 1).collect();
845
846        let list = VerticalBP128PostingList::from_postings(&doc_ids, &term_freqs, 1.0);
847
848        assert_eq!(list.doc_count, 200);
849        assert_eq!(list.blocks.len(), 2); // 128 + 72
850
851        let mut iter = list.iterator();
852        for (i, &expected_doc) in doc_ids.iter().enumerate() {
853            assert_eq!(iter.doc(), expected_doc, "Doc mismatch at {}", i);
854            assert_eq!(iter.term_freq(), term_freqs[i], "TF mismatch at {}", i);
855            if i < doc_ids.len() - 1 {
856                iter.advance();
857            }
858        }
859    }
860
861    #[test]
862    fn test_simd_bp128_seek() {
863        let doc_ids: Vec<u32> = vec![10, 20, 30, 100, 200, 300, 1000, 2000];
864        let term_freqs: Vec<u32> = vec![1, 2, 3, 4, 5, 6, 7, 8];
865
866        let list = VerticalBP128PostingList::from_postings(&doc_ids, &term_freqs, 1.0);
867        let mut iter = list.iterator();
868
869        assert_eq!(iter.seek(25), 30);
870        assert_eq!(iter.seek(100), 100);
871        assert_eq!(iter.seek(500), 1000);
872        assert_eq!(iter.seek(3000), u32::MAX);
873    }
874
875    #[test]
876    fn test_simd_bp128_serialization() {
877        let doc_ids: Vec<u32> = (0..300).map(|i| i * 3).collect();
878        let term_freqs: Vec<u32> = (0..300).map(|i| (i % 5) + 1).collect();
879
880        let list = VerticalBP128PostingList::from_postings(&doc_ids, &term_freqs, 1.5);
881
882        let mut buffer = Vec::new();
883        list.serialize(&mut buffer).unwrap();
884
885        let restored = VerticalBP128PostingList::deserialize(&mut &buffer[..]).unwrap();
886
887        assert_eq!(restored.doc_count, list.doc_count);
888        assert_eq!(restored.blocks.len(), list.blocks.len());
889
890        let mut iter1 = list.iterator();
891        let mut iter2 = restored.iterator();
892
893        while iter1.doc() != u32::MAX {
894            assert_eq!(iter1.doc(), iter2.doc());
895            assert_eq!(iter1.term_freq(), iter2.term_freq());
896            iter1.advance();
897            iter2.advance();
898        }
899    }
900
901    #[test]
902    fn test_vertical_layout_size() {
903        // True vertical layout: BLOCK_SIZE * bit_width / 8 bytes (optimal)
904        let mut values = [0u32; VERTICAL_BP128_BLOCK_SIZE];
905        for (i, v) in values.iter_mut().enumerate() {
906            *v = i as u32;
907        }
908
909        let bit_width = simd::bits_needed(127); // 7 bits
910        assert_eq!(bit_width, 7);
911
912        let mut packed = Vec::new();
913        pack_vertical(&values, bit_width, &mut packed);
914
915        // True vertical layout: 128 * 7 / 8 = 112 bytes (optimal, no padding)
916        let expected_bytes = (VERTICAL_BP128_BLOCK_SIZE * bit_width as usize) / 8;
917        assert_eq!(expected_bytes, 112);
918        assert_eq!(packed.len(), expected_bytes);
919    }
920
921    #[test]
922    fn test_simd_bp128_block_max() {
923        // Create a large posting list that spans multiple blocks
924        let doc_ids: Vec<u32> = (0..500).map(|i| i * 2).collect();
925        // Vary term frequencies so different blocks have different max_tf
926        let term_freqs: Vec<u32> = (0..500)
927            .map(|i| {
928                if i < 128 {
929                    1 // Block 0: max_tf = 1
930                } else if i < 256 {
931                    5 // Block 1: max_tf = 5
932                } else if i < 384 {
933                    10 // Block 2: max_tf = 10
934                } else {
935                    3 // Block 3: max_tf = 3
936                }
937            })
938            .collect();
939
940        let list = VerticalBP128PostingList::from_postings(&doc_ids, &term_freqs, 2.0);
941
942        // Should have 4 blocks (500 docs / 128 per block)
943        assert_eq!(list.blocks.len(), 4);
944        assert_eq!(list.blocks[0].max_tf, 1);
945        assert_eq!(list.blocks[1].max_tf, 5);
946        assert_eq!(list.blocks[2].max_tf, 10);
947        assert_eq!(list.blocks[3].max_tf, 3);
948
949        // Block 2 should have highest score (max_tf = 10)
950        assert!(list.blocks[2].max_block_score > list.blocks[0].max_block_score);
951        assert!(list.blocks[2].max_block_score > list.blocks[1].max_block_score);
952        assert!(list.blocks[2].max_block_score > list.blocks[3].max_block_score);
953
954        // Global max_score should equal block 2's score
955        assert_eq!(list.max_score, list.blocks[2].max_block_score);
956
957        // Test iterator block-max methods
958        let mut iter = list.iterator();
959        assert_eq!(iter.current_block_max_tf(), 1); // Block 0
960
961        // Seek to block 1
962        iter.seek(256); // first doc in block 1
963        assert_eq!(iter.current_block_max_tf(), 5);
964
965        // Seek to block 2
966        iter.seek(512); // first doc in block 2
967        assert_eq!(iter.current_block_max_tf(), 10);
968
969        // Test skip_to_block_with_doc
970        let mut iter2 = list.iterator();
971        let result = iter2.skip_to_block_with_doc(300);
972        assert!(result.is_some());
973        let (first_doc, score) = result.unwrap();
974        assert!(first_doc <= 300);
975        assert!(score > 0.0);
976    }
977}