1use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
9use std::io::{self, Cursor, Read, Write};
10
11use super::config::WeightQuantization;
12use crate::DocId;
13use crate::directories::OwnedBytes;
14use crate::structures::postings::TERMINATED;
15use crate::structures::simd;
16
17pub const BLOCK_SIZE: usize = 128;
18pub const MAX_BLOCK_SIZE: usize = 256;
19
20#[derive(Debug, Clone, Copy)]
21pub struct BlockHeader {
22 pub count: u16,
23 pub doc_id_bits: u8,
24 pub ordinal_bits: u8,
25 pub weight_quant: WeightQuantization,
26 pub first_doc_id: DocId,
27 pub max_weight: f32,
28}
29
30impl BlockHeader {
31 pub const SIZE: usize = 16;
32
33 pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
34 w.write_u16::<LittleEndian>(self.count)?;
35 w.write_u8(self.doc_id_bits)?;
36 w.write_u8(self.ordinal_bits)?;
37 w.write_u8(self.weight_quant as u8)?;
38 w.write_u8(0)?;
39 w.write_u16::<LittleEndian>(0)?;
40 w.write_u32::<LittleEndian>(self.first_doc_id)?;
41 w.write_f32::<LittleEndian>(self.max_weight)?;
42 Ok(())
43 }
44
45 pub fn read<R: Read>(r: &mut R) -> io::Result<Self> {
46 let count = r.read_u16::<LittleEndian>()?;
47 let doc_id_bits = r.read_u8()?;
48 let ordinal_bits = r.read_u8()?;
49 let weight_quant_byte = r.read_u8()?;
50 let _ = r.read_u8()?;
51 let _ = r.read_u16::<LittleEndian>()?;
52 let first_doc_id = r.read_u32::<LittleEndian>()?;
53 let max_weight = r.read_f32::<LittleEndian>()?;
54
55 let weight_quant = WeightQuantization::from_u8(weight_quant_byte)
56 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Invalid weight quant"))?;
57
58 Ok(Self {
59 count,
60 doc_id_bits,
61 ordinal_bits,
62 weight_quant,
63 first_doc_id,
64 max_weight,
65 })
66 }
67}
68
69#[derive(Debug, Clone)]
70pub struct SparseBlock {
71 pub header: BlockHeader,
72 pub doc_ids_data: OwnedBytes,
74 pub ordinals_data: OwnedBytes,
76 pub weights_data: OwnedBytes,
78 last_doc_id: DocId,
80}
81
82impl SparseBlock {
83 pub fn from_postings(
84 postings: &[(DocId, u16, f32)],
85 weight_quant: WeightQuantization,
86 ) -> io::Result<Self> {
87 assert!(!postings.is_empty() && postings.len() <= MAX_BLOCK_SIZE);
88
89 let count = postings.len();
90 let first_doc_id = postings[0].0;
91
92 let mut deltas = [0u32; MAX_BLOCK_SIZE];
96 let mut ordinals = [0u32; MAX_BLOCK_SIZE];
97 let mut weights = [0.0f32; MAX_BLOCK_SIZE];
98 let mut prev = first_doc_id;
99 let mut max_ordinal = 0u16;
100 let mut max_weight = 0.0f32;
101 for (index, &(doc_id, ordinal, weight)) in postings.iter().enumerate() {
102 deltas[index] = doc_id.saturating_sub(prev);
103 ordinals[index] = u32::from(ordinal);
104 weights[index] = weight;
105 max_ordinal = max_ordinal.max(ordinal);
106 max_weight = max_weight.max(weight.abs());
107 prev = doc_id;
108 }
109 deltas[0] = 0;
110
111 let doc_id_bits = simd::round_bit_width(find_optimal_bit_width(&deltas[1..count]));
112 let ordinal_bits = if max_ordinal == 0 {
113 0
114 } else {
115 simd::round_bit_width(bits_needed_u16(max_ordinal))
116 };
117
118 let doc_ids_data = OwnedBytes::new({
119 let rounded = simd::RoundedBitWidth::from_u8(doc_id_bits);
120 let num_deltas = count - 1;
121 let byte_count = num_deltas * rounded.bytes_per_value();
122 let mut data = vec![0u8; byte_count];
123 simd::pack_rounded(&deltas[1..count], rounded, &mut data);
124 data
125 });
126 let ordinals_data = OwnedBytes::new(if ordinal_bits > 0 {
127 let rounded = simd::RoundedBitWidth::from_u8(ordinal_bits);
128 let byte_count = count * rounded.bytes_per_value();
129 let mut data = vec![0u8; byte_count];
130 simd::pack_rounded(&ordinals[..count], rounded, &mut data);
131 data
132 } else {
133 Vec::new()
134 });
135 let weights_data = OwnedBytes::new(encode_weights(&weights[..count], weight_quant)?);
136
137 let last_doc_id = postings.last().unwrap().0;
138
139 Ok(Self {
140 header: BlockHeader {
141 count: count as u16,
142 doc_id_bits,
143 ordinal_bits,
144 weight_quant,
145 first_doc_id,
146 max_weight,
147 },
148 doc_ids_data,
149 ordinals_data,
150 weights_data,
151 last_doc_id,
152 })
153 }
154
155 #[inline]
157 pub fn last_doc_id(&self) -> DocId {
158 self.last_doc_id
159 }
160
161 pub fn decode_doc_ids(&self) -> Vec<DocId> {
162 let mut out = Vec::with_capacity(self.header.count as usize);
163 self.decode_doc_ids_into(&mut out);
164 out
165 }
166
167 pub fn decode_doc_ids_into(&self, out: &mut Vec<DocId>) {
171 let count = self.header.count as usize;
172 out.clear();
173 out.resize(count, 0);
174 out[0] = self.header.first_doc_id;
175
176 if count > 1 {
177 let bits = self.header.doc_id_bits;
178 if bits == 0 {
179 out[1..].fill(self.header.first_doc_id);
181 } else {
182 simd::unpack_rounded(
184 &self.doc_ids_data,
185 simd::RoundedBitWidth::from_u8(bits),
186 &mut out[1..],
187 count - 1,
188 );
189 for i in 1..count {
191 out[i] += out[i - 1];
192 }
193 }
194 }
195 }
196
197 pub fn decode_ordinals(&self) -> Vec<u16> {
198 let mut out = Vec::with_capacity(self.header.count as usize);
199 self.decode_ordinals_into(&mut out);
200 out
201 }
202
203 pub fn decode_ordinals_into(&self, out: &mut Vec<u16>) {
207 let count = self.header.count as usize;
208 out.clear();
209 if self.header.ordinal_bits == 0 {
210 out.resize(count, 0u16);
211 } else {
212 let mut temp = [0u32; MAX_BLOCK_SIZE];
214 simd::unpack_rounded(
215 &self.ordinals_data,
216 simd::RoundedBitWidth::from_u8(self.header.ordinal_bits),
217 &mut temp[..count],
218 count,
219 );
220 out.reserve(count);
221 for &v in &temp[..count] {
222 out.push(v as u16);
223 }
224 }
225 }
226
227 pub fn decode_weights(&self) -> Vec<f32> {
228 let mut out = Vec::with_capacity(self.header.count as usize);
229 self.decode_weights_into(&mut out);
230 out
231 }
232
233 pub fn decode_weights_into(&self, out: &mut Vec<f32>) {
235 out.clear();
236 decode_weights_into(
237 &self.weights_data,
238 self.header.weight_quant,
239 self.header.count as usize,
240 out,
241 );
242 }
243
244 pub fn decode_scored_weights_into(&self, query_weight: f32, out: &mut Vec<f32>) {
252 out.clear();
253 let count = self.header.count as usize;
254 match self.header.weight_quant {
255 WeightQuantization::UInt8 if self.weights_data.len() >= 8 => {
256 let scale = f32::from_le_bytes([
258 self.weights_data[0],
259 self.weights_data[1],
260 self.weights_data[2],
261 self.weights_data[3],
262 ]);
263 let min_val = f32::from_le_bytes([
264 self.weights_data[4],
265 self.weights_data[5],
266 self.weights_data[6],
267 self.weights_data[7],
268 ]);
269 let eff_scale = query_weight * scale;
271 let eff_bias = query_weight * min_val;
272 out.resize(count, 0.0);
273 simd::dequantize_uint8(&self.weights_data[8..], out, eff_scale, eff_bias, count);
274 }
275 _ => {
276 decode_weights_into(&self.weights_data, self.header.weight_quant, count, out);
278 for w in out.iter_mut() {
279 *w *= query_weight;
280 }
281 }
282 }
283 }
284
285 #[inline]
300 pub fn accumulate_scored_weights(
301 &self,
302 query_weight: f32,
303 doc_ids: &[u32],
304 flat_scores: &mut [f32],
305 base_doc: u32,
306 dirty: &mut Vec<u32>,
307 ) -> usize {
308 let count = self.header.count as usize;
309 match self.header.weight_quant {
310 WeightQuantization::UInt8 if self.weights_data.len() >= 8 => {
311 let scale = f32::from_le_bytes([
313 self.weights_data[0],
314 self.weights_data[1],
315 self.weights_data[2],
316 self.weights_data[3],
317 ]);
318 let min_val = f32::from_le_bytes([
319 self.weights_data[4],
320 self.weights_data[5],
321 self.weights_data[6],
322 self.weights_data[7],
323 ]);
324 let eff_scale = query_weight * scale;
325 let eff_bias = query_weight * min_val;
326 let quant_data = &self.weights_data[8..];
327
328 for i in 0..count.min(quant_data.len()).min(doc_ids.len()) {
329 let w = quant_data[i] as f32 * eff_scale + eff_bias;
330 let off = (doc_ids[i] - base_doc) as usize;
331 if off >= flat_scores.len() {
332 continue;
333 }
334 if flat_scores[off] == 0.0 {
335 dirty.push(doc_ids[i]);
336 }
337 flat_scores[off] += w;
338 }
339 count
340 }
341 _ => {
342 let mut weights_buf = Vec::with_capacity(count);
344 decode_weights_into(
345 &self.weights_data,
346 self.header.weight_quant,
347 count,
348 &mut weights_buf,
349 );
350 for i in 0..count.min(weights_buf.len()).min(doc_ids.len()) {
351 let w = weights_buf[i] * query_weight;
352 let off = (doc_ids[i] - base_doc) as usize;
353 if off >= flat_scores.len() {
354 continue;
355 }
356 if flat_scores[off] == 0.0 {
357 dirty.push(doc_ids[i]);
358 }
359 flat_scores[off] += w;
360 }
361 count
362 }
363 }
364 }
365
366 pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
367 self.header.write(w)?;
368 if self.doc_ids_data.len() > u16::MAX as usize
369 || self.ordinals_data.len() > u16::MAX as usize
370 || self.weights_data.len() > u16::MAX as usize
371 {
372 return Err(io::Error::new(
373 io::ErrorKind::InvalidData,
374 format!(
375 "sparse sub-block too large for u16 length: doc_ids={}B ords={}B wts={}B",
376 self.doc_ids_data.len(),
377 self.ordinals_data.len(),
378 self.weights_data.len()
379 ),
380 ));
381 }
382 w.write_u16::<LittleEndian>(self.doc_ids_data.len() as u16)?;
383 w.write_u16::<LittleEndian>(self.ordinals_data.len() as u16)?;
384 w.write_u16::<LittleEndian>(self.weights_data.len() as u16)?;
385 w.write_u16::<LittleEndian>(0)?;
386 w.write_all(&self.doc_ids_data)?;
387 w.write_all(&self.ordinals_data)?;
388 w.write_all(&self.weights_data)?;
389 Ok(())
390 }
391
392 pub fn read<R: Read>(r: &mut R) -> io::Result<Self> {
393 let header = BlockHeader::read(r)?;
394 let doc_ids_len = r.read_u16::<LittleEndian>()? as usize;
395 let ordinals_len = r.read_u16::<LittleEndian>()? as usize;
396 let weights_len = r.read_u16::<LittleEndian>()? as usize;
397 let _ = r.read_u16::<LittleEndian>()?;
398
399 let mut doc_ids_vec = vec![0u8; doc_ids_len];
400 r.read_exact(&mut doc_ids_vec)?;
401 let mut ordinals_vec = vec![0u8; ordinals_len];
402 r.read_exact(&mut ordinals_vec)?;
403 let mut weights_vec = vec![0u8; weights_len];
404 r.read_exact(&mut weights_vec)?;
405
406 let last_doc_id = compute_last_doc(&header, &doc_ids_vec);
408
409 Ok(Self {
410 header,
411 doc_ids_data: OwnedBytes::new(doc_ids_vec),
412 ordinals_data: OwnedBytes::new(ordinals_vec),
413 weights_data: OwnedBytes::new(weights_vec),
414 last_doc_id,
415 })
416 }
417
418 pub fn from_owned_bytes(data: crate::directories::OwnedBytes) -> crate::Result<Self> {
424 let b = data.as_slice();
425 if b.len() < BlockHeader::SIZE + 8 {
426 return Err(crate::Error::Corruption(
427 "sparse block too small".to_string(),
428 ));
429 }
430 let mut cursor = Cursor::new(&b[..BlockHeader::SIZE]);
431 let header =
432 BlockHeader::read(&mut cursor).map_err(|e| crate::Error::Corruption(e.to_string()))?;
433
434 if header.count == 0 {
435 let hex: String = b
436 .iter()
437 .take(32)
438 .map(|x| format!("{x:02x}"))
439 .collect::<Vec<_>>()
440 .join(" ");
441 return Err(crate::Error::Corruption(format!(
442 "sparse block has count=0 (data_len={}, first_32_bytes=[{}])",
443 b.len(),
444 hex
445 )));
446 }
447
448 let p = BlockHeader::SIZE;
449 let doc_ids_len = u16::from_le_bytes([b[p], b[p + 1]]) as usize;
450 let ordinals_len = u16::from_le_bytes([b[p + 2], b[p + 3]]) as usize;
451 let weights_len = u16::from_le_bytes([b[p + 4], b[p + 5]]) as usize;
452 let data_start = p + 8;
455 let ord_start = data_start + doc_ids_len;
456 let wt_start = ord_start + ordinals_len;
457 let expected_end = wt_start + weights_len;
458
459 if expected_end > b.len() {
460 let hex: String = b
461 .iter()
462 .take(32)
463 .map(|x| format!("{x:02x}"))
464 .collect::<Vec<_>>()
465 .join(" ");
466 return Err(crate::Error::Corruption(format!(
467 "sparse block sub-block overflow: count={} doc_ids={}B ords={}B wts={}B need={}B have={}B (first_32=[{}])",
468 header.count,
469 doc_ids_len,
470 ordinals_len,
471 weights_len,
472 expected_end,
473 b.len(),
474 hex
475 )));
476 }
477
478 let doc_ids_slice = data.slice(data_start..ord_start);
479 let last_doc_id = compute_last_doc(&header, &doc_ids_slice);
481
482 Ok(Self {
483 header,
484 doc_ids_data: doc_ids_slice,
485 ordinals_data: data.slice(ord_start..wt_start),
486 weights_data: data.slice(wt_start..wt_start + weights_len),
487 last_doc_id,
488 })
489 }
490
491 pub fn with_doc_offset(&self, doc_offset: u32) -> Self {
497 Self {
498 header: BlockHeader {
499 first_doc_id: self.header.first_doc_id + doc_offset,
500 ..self.header
501 },
502 doc_ids_data: self.doc_ids_data.clone(),
503 ordinals_data: self.ordinals_data.clone(),
504 weights_data: self.weights_data.clone(),
505 last_doc_id: self.last_doc_id + doc_offset,
506 }
507 }
508}
509
510#[derive(Debug, Clone)]
515pub struct BlockSparsePostingList {
516 pub doc_count: u32,
517 pub blocks: Vec<SparseBlock>,
518}
519
520impl BlockSparsePostingList {
521 pub fn from_postings_with_block_size(
523 postings: &[(DocId, u16, f32)],
524 weight_quant: WeightQuantization,
525 block_size: usize,
526 ) -> io::Result<Self> {
527 if postings.is_empty() {
528 return Ok(Self {
529 doc_count: 0,
530 blocks: Vec::new(),
531 });
532 }
533
534 let block_size = block_size.clamp(16, MAX_BLOCK_SIZE);
535 let mut blocks = Vec::with_capacity(postings.len().div_ceil(block_size));
536 for chunk in postings.chunks(block_size) {
537 blocks.push(SparseBlock::from_postings(chunk, weight_quant)?);
538 }
539
540 let mut unique_docs = 1u32;
545 for i in 1..postings.len() {
546 if postings[i].0 != postings[i - 1].0 {
547 unique_docs += 1;
548 }
549 }
550
551 Ok(Self {
552 doc_count: unique_docs,
553 blocks,
554 })
555 }
556
557 pub fn from_postings(
559 postings: &[(DocId, u16, f32)],
560 weight_quant: WeightQuantization,
561 ) -> io::Result<Self> {
562 Self::from_postings_with_block_size(postings, weight_quant, BLOCK_SIZE)
563 }
564
565 pub fn from_postings_with_partition(
571 postings: &[(DocId, u16, f32)],
572 weight_quant: WeightQuantization,
573 partition: &[usize],
574 ) -> io::Result<Self> {
575 if postings.is_empty() {
576 return Ok(Self {
577 doc_count: 0,
578 blocks: Vec::new(),
579 });
580 }
581
582 let mut blocks = Vec::with_capacity(partition.len());
583 let mut offset = 0;
584 for &block_size in partition {
585 let end = (offset + block_size).min(postings.len());
586 blocks.push(SparseBlock::from_postings(
587 &postings[offset..end],
588 weight_quant,
589 )?);
590 offset = end;
591 }
592
593 let mut unique_docs = 1u32;
594 for i in 1..postings.len() {
595 if postings[i].0 != postings[i - 1].0 {
596 unique_docs += 1;
597 }
598 }
599
600 Ok(Self {
601 doc_count: unique_docs,
602 blocks,
603 })
604 }
605
606 pub fn doc_count(&self) -> u32 {
607 self.doc_count
608 }
609
610 pub fn num_blocks(&self) -> usize {
611 self.blocks.len()
612 }
613
614 pub fn global_max_weight(&self) -> f32 {
615 self.blocks
616 .iter()
617 .map(|b| b.header.max_weight)
618 .fold(0.0f32, f32::max)
619 }
620
621 pub fn block_max_weight(&self, block_idx: usize) -> Option<f32> {
622 self.blocks.get(block_idx).map(|b| b.header.max_weight)
623 }
624
625 pub fn size_bytes(&self) -> usize {
627 use std::mem::size_of;
628
629 let header_size = size_of::<u32>() * 2; let blocks_size: usize = self
631 .blocks
632 .iter()
633 .map(|b| {
634 size_of::<BlockHeader>()
635 + b.doc_ids_data.len()
636 + b.ordinals_data.len()
637 + b.weights_data.len()
638 })
639 .sum();
640 header_size + blocks_size
641 }
642
643 pub fn iterator(&self) -> BlockSparsePostingIterator<'_> {
644 BlockSparsePostingIterator::new(self)
645 }
646
647 pub fn serialize(&self) -> io::Result<(Vec<u8>, Vec<super::SparseSkipEntry>)> {
653 let serialized_bytes = self.blocks.iter().try_fold(0usize, |total, block| {
654 total
655 .checked_add(
656 BlockHeader::SIZE
657 + 8
658 + block.doc_ids_data.len()
659 + block.ordinals_data.len()
660 + block.weights_data.len(),
661 )
662 .ok_or_else(|| {
663 io::Error::new(io::ErrorKind::InvalidInput, "sparse posting size overflow")
664 })
665 })?;
666 let mut block_data = Vec::with_capacity(serialized_bytes);
667 let mut skip_entries = Vec::with_capacity(self.blocks.len());
668
669 for block in &self.blocks {
670 let offset = block_data.len();
671 block.write(&mut block_data)?;
672 let length = u32::try_from(block_data.len() - offset).map_err(|_| {
673 io::Error::new(
674 io::ErrorKind::InvalidData,
675 "serialized sparse block is too large",
676 )
677 })?;
678
679 let first_doc = block.header.first_doc_id;
680 let last_doc = block.last_doc_id;
681
682 skip_entries.push(super::SparseSkipEntry::new(
683 first_doc,
684 last_doc,
685 offset as u64,
686 length,
687 block.header.max_weight,
688 ));
689 }
690
691 Ok((block_data, skip_entries))
692 }
693
694 #[cfg(test)]
699 pub fn from_parts(
700 doc_count: u32,
701 block_data: &[u8],
702 skip_entries: &[super::SparseSkipEntry],
703 ) -> io::Result<Self> {
704 let mut blocks = Vec::with_capacity(skip_entries.len());
705 for entry in skip_entries {
706 let start = entry.offset as usize;
707 let end = start + entry.length as usize;
708 blocks.push(SparseBlock::read(&mut std::io::Cursor::new(
709 &block_data[start..end],
710 ))?);
711 }
712 Ok(Self { doc_count, blocks })
713 }
714
715 pub fn decode_all(&self) -> Vec<(DocId, u16, f32)> {
716 let total_postings: usize = self.blocks.iter().map(|b| b.header.count as usize).sum();
717 let mut result = Vec::with_capacity(total_postings);
718 for block in &self.blocks {
719 let doc_ids = block.decode_doc_ids();
720 let ordinals = block.decode_ordinals();
721 let weights = block.decode_weights();
722 for i in 0..block.header.count as usize {
723 result.push((doc_ids[i], ordinals[i], weights[i]));
724 }
725 }
726 result
727 }
728
729 pub fn merge_with_offsets(lists: &[(&BlockSparsePostingList, u32)]) -> Self {
740 if lists.is_empty() {
741 return Self {
742 doc_count: 0,
743 blocks: Vec::new(),
744 };
745 }
746
747 let total_blocks: usize = lists.iter().map(|(pl, _)| pl.blocks.len()).sum();
749 let total_docs: u32 = lists.iter().map(|(pl, _)| pl.doc_count).sum();
750
751 let mut merged_blocks = Vec::with_capacity(total_blocks);
752
753 for (posting_list, doc_offset) in lists {
755 for block in &posting_list.blocks {
756 merged_blocks.push(block.with_doc_offset(*doc_offset));
757 }
758 }
759
760 Self {
761 doc_count: total_docs,
762 blocks: merged_blocks,
763 }
764 }
765
766 fn find_block(&self, target: DocId) -> Option<usize> {
767 if self.blocks.is_empty() {
768 return None;
769 }
770 let idx = self
773 .blocks
774 .partition_point(|b| b.header.first_doc_id <= target);
775 if idx == 0 {
776 Some(0)
778 } else {
779 Some(idx - 1)
780 }
781 }
782}
783
784pub struct BlockSparsePostingIterator<'a> {
789 posting_list: &'a BlockSparsePostingList,
790 block_idx: usize,
791 in_block_idx: usize,
792 current_doc_ids: Vec<DocId>,
793 current_ordinals: Vec<u16>,
794 current_weights: Vec<f32>,
795 ordinals_decoded: bool,
797 exhausted: bool,
798}
799
800impl<'a> BlockSparsePostingIterator<'a> {
801 fn new(posting_list: &'a BlockSparsePostingList) -> Self {
802 let mut iter = Self {
803 posting_list,
804 block_idx: 0,
805 in_block_idx: 0,
806 current_doc_ids: Vec::with_capacity(128),
807 current_ordinals: Vec::with_capacity(128),
808 current_weights: Vec::with_capacity(128),
809 ordinals_decoded: false,
810 exhausted: posting_list.blocks.is_empty(),
811 };
812 if !iter.exhausted {
813 iter.load_block(0);
814 }
815 iter
816 }
817
818 fn load_block(&mut self, block_idx: usize) {
819 if let Some(block) = self.posting_list.blocks.get(block_idx) {
820 block.decode_doc_ids_into(&mut self.current_doc_ids);
821 block.decode_weights_into(&mut self.current_weights);
822 self.ordinals_decoded = false;
824 self.block_idx = block_idx;
825 self.in_block_idx = 0;
826 }
827 }
828
829 #[inline]
831 fn ensure_ordinals_decoded(&mut self) {
832 if !self.ordinals_decoded {
833 if let Some(block) = self.posting_list.blocks.get(self.block_idx) {
834 block.decode_ordinals_into(&mut self.current_ordinals);
835 }
836 self.ordinals_decoded = true;
837 }
838 }
839
840 #[inline]
841 pub fn doc(&self) -> DocId {
842 if self.exhausted {
843 TERMINATED
844 } else {
845 self.current_doc_ids[self.in_block_idx]
847 }
848 }
849
850 #[inline]
851 pub fn weight(&self) -> f32 {
852 if self.exhausted {
853 return 0.0;
854 }
855 self.current_weights[self.in_block_idx]
857 }
858
859 #[inline]
860 pub fn ordinal(&mut self) -> u16 {
861 if self.exhausted {
862 return 0;
863 }
864 self.ensure_ordinals_decoded();
865 self.current_ordinals[self.in_block_idx]
866 }
867
868 pub fn advance(&mut self) -> DocId {
869 if self.exhausted {
870 return TERMINATED;
871 }
872 self.in_block_idx += 1;
873 if self.in_block_idx >= self.current_doc_ids.len() {
874 self.block_idx += 1;
875 if self.block_idx >= self.posting_list.blocks.len() {
876 self.exhausted = true;
877 } else {
878 self.load_block(self.block_idx);
879 }
880 }
881 self.doc()
882 }
883
884 pub fn seek(&mut self, target: DocId) -> DocId {
885 if self.exhausted {
886 return TERMINATED;
887 }
888 if self.doc() >= target {
889 return self.doc();
890 }
891
892 if let Some(&last_doc) = self.current_doc_ids.last()
894 && last_doc >= target
895 {
896 let remaining = &self.current_doc_ids[self.in_block_idx..];
897 let pos = crate::structures::simd::find_first_ge_u32(remaining, target);
898 self.in_block_idx += pos;
899 if self.in_block_idx >= self.current_doc_ids.len() {
900 self.block_idx += 1;
901 if self.block_idx >= self.posting_list.blocks.len() {
902 self.exhausted = true;
903 } else {
904 self.load_block(self.block_idx);
905 }
906 }
907 return self.doc();
908 }
909
910 if let Some(block_idx) = self.posting_list.find_block(target) {
912 self.load_block(block_idx);
913 let pos = crate::structures::simd::find_first_ge_u32(&self.current_doc_ids, target);
914 self.in_block_idx = pos;
915 if self.in_block_idx >= self.current_doc_ids.len() {
916 self.block_idx += 1;
917 if self.block_idx >= self.posting_list.blocks.len() {
918 self.exhausted = true;
919 } else {
920 self.load_block(self.block_idx);
921 }
922 }
923 } else {
924 self.exhausted = true;
925 }
926 self.doc()
927 }
928
929 pub fn skip_to_next_block(&mut self) -> DocId {
932 if self.exhausted {
933 return TERMINATED;
934 }
935 let next = self.block_idx + 1;
936 if next >= self.posting_list.blocks.len() {
937 self.exhausted = true;
938 return TERMINATED;
939 }
940 self.load_block(next);
941 self.doc()
942 }
943
944 pub fn is_exhausted(&self) -> bool {
945 self.exhausted
946 }
947
948 pub fn current_block_max_weight(&self) -> f32 {
949 self.posting_list
950 .blocks
951 .get(self.block_idx)
952 .map(|b| b.header.max_weight)
953 .unwrap_or(0.0)
954 }
955
956 pub fn current_block_max_contribution(&self, query_weight: f32) -> f32 {
957 query_weight * self.current_block_max_weight()
958 }
959}
960
961fn compute_last_doc(header: &BlockHeader, doc_ids_data: &[u8]) -> DocId {
968 let count = header.count as usize;
969 if count <= 1 {
970 return header.first_doc_id;
971 }
972 let bits = header.doc_id_bits;
973 if bits == 0 {
974 return header.first_doc_id; }
976 let rounded = simd::RoundedBitWidth::from_u8(bits);
977 let num_deltas = count - 1;
978 let mut deltas = [0u32; MAX_BLOCK_SIZE];
979 simd::unpack_rounded(doc_ids_data, rounded, &mut deltas[..num_deltas], num_deltas);
980 let sum: u32 = deltas[..num_deltas].iter().sum();
981 header.first_doc_id + sum
982}
983
984fn find_optimal_bit_width(values: &[u32]) -> u8 {
985 if values.is_empty() {
986 return 0;
987 }
988 let max_val = values.iter().copied().max().unwrap_or(0);
989 simd::bits_needed(max_val)
990}
991
992fn bits_needed_u16(val: u16) -> u8 {
993 if val == 0 {
994 0
995 } else {
996 16 - val.leading_zeros() as u8
997 }
998}
999
1000fn encode_weights(weights: &[f32], quant: WeightQuantization) -> io::Result<Vec<u8>> {
1005 let encoded_len = match quant {
1006 WeightQuantization::Float32 => weights.len().saturating_mul(4),
1007 WeightQuantization::Float16 => weights.len().saturating_mul(2),
1008 WeightQuantization::UInt8 => 8usize.saturating_add(weights.len()),
1009 WeightQuantization::UInt4 => 8usize.saturating_add(weights.len().div_ceil(2)),
1010 };
1011 let mut data = Vec::with_capacity(encoded_len);
1012 match quant {
1013 WeightQuantization::Float32 => {
1014 for &w in weights {
1015 data.write_f32::<LittleEndian>(w)?;
1016 }
1017 }
1018 WeightQuantization::Float16 => {
1019 use half::f16;
1020 for &w in weights {
1021 data.write_u16::<LittleEndian>(f16::from_f32(w).to_bits())?;
1022 }
1023 }
1024 WeightQuantization::UInt8 => {
1025 let min = weights.iter().copied().fold(f32::INFINITY, f32::min);
1026 let max = weights.iter().copied().fold(f32::NEG_INFINITY, f32::max);
1027 let range = max - min;
1028 let scale = if range < f32::EPSILON {
1029 1.0
1030 } else {
1031 range / 255.0
1032 };
1033 data.write_f32::<LittleEndian>(scale)?;
1034 data.write_f32::<LittleEndian>(min)?;
1035 for &w in weights {
1036 data.write_u8(((w - min) / scale).round() as u8)?;
1037 }
1038 }
1039 WeightQuantization::UInt4 => {
1040 let min = weights.iter().copied().fold(f32::INFINITY, f32::min);
1041 let max = weights.iter().copied().fold(f32::NEG_INFINITY, f32::max);
1042 let range = max - min;
1043 let scale = if range < f32::EPSILON {
1044 1.0
1045 } else {
1046 range / 15.0
1047 };
1048 data.write_f32::<LittleEndian>(scale)?;
1049 data.write_f32::<LittleEndian>(min)?;
1050 let mut i = 0;
1051 while i < weights.len() {
1052 let q1 = ((weights[i] - min) / scale).round() as u8 & 0x0F;
1053 let q2 = if i + 1 < weights.len() {
1054 ((weights[i + 1] - min) / scale).round() as u8 & 0x0F
1055 } else {
1056 0
1057 };
1058 data.write_u8((q2 << 4) | q1)?;
1059 i += 2;
1060 }
1061 }
1062 }
1063 Ok(data)
1064}
1065
1066fn decode_weights_into(data: &[u8], quant: WeightQuantization, count: usize, out: &mut Vec<f32>) {
1067 match quant {
1068 WeightQuantization::Float32 => {
1069 out.reserve(count);
1070 for chunk in data[..count * 4].as_chunks::<4>().0 {
1071 out.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
1072 }
1073 }
1074 WeightQuantization::Float16 => {
1075 use half::f16;
1078 use half::slice::HalfFloatSliceExt;
1079 let byte_count = count * 2;
1080 let src = &data[..byte_count];
1081 let mut f16_buf = [f16::ZERO; MAX_BLOCK_SIZE];
1082 for (value, chunk) in f16_buf[..count].iter_mut().zip(src.as_chunks::<2>().0) {
1083 *value = f16::from_bits(u16::from_le_bytes([chunk[0], chunk[1]]));
1084 }
1085 let start = out.len();
1086 out.resize(start + count, 0.0);
1087 f16_buf[..count].convert_to_f32_slice(&mut out[start..start + count]);
1088 }
1089 WeightQuantization::UInt8 => {
1090 let mut cursor = Cursor::new(data);
1091 let scale = cursor.read_f32::<LittleEndian>().unwrap_or(1.0);
1092 let min_val = cursor.read_f32::<LittleEndian>().unwrap_or(0.0);
1093 let offset = cursor.position() as usize;
1094 out.resize(count, 0.0);
1095 simd::dequantize_uint8(&data[offset..], out, scale, min_val, count);
1096 }
1097 WeightQuantization::UInt4 => {
1098 let mut cursor = Cursor::new(data);
1099 let scale = cursor.read_f32::<LittleEndian>().unwrap_or(1.0);
1100 let min = cursor.read_f32::<LittleEndian>().unwrap_or(0.0);
1101 let mut i = 0;
1102 while i < count {
1103 let byte = cursor.read_u8().unwrap_or(0);
1104 out.push((byte & 0x0F) as f32 * scale + min);
1105 i += 1;
1106 if i < count {
1107 out.push((byte >> 4) as f32 * scale + min);
1108 i += 1;
1109 }
1110 }
1111 }
1112 }
1113}
1114
1115#[cfg(test)]
1116mod tests {
1117 use super::*;
1118
1119 #[test]
1120 fn test_block_roundtrip() {
1121 let postings = vec![
1122 (10u32, 0u16, 1.5f32),
1123 (15, 0, 2.0),
1124 (20, 1, 0.5),
1125 (100, 0, 3.0),
1126 ];
1127 let block = SparseBlock::from_postings(&postings, WeightQuantization::Float32).unwrap();
1128
1129 assert_eq!(block.decode_doc_ids(), vec![10, 15, 20, 100]);
1130 assert_eq!(block.decode_ordinals(), vec![0, 0, 1, 0]);
1131 let weights = block.decode_weights();
1132 assert!((weights[0] - 1.5).abs() < 0.01);
1133 }
1134
1135 #[test]
1136 fn test_max_size_block_ordinal_decode() {
1137 let postings: Vec<(DocId, u16, f32)> = (0..MAX_BLOCK_SIZE)
1138 .map(|i| (i as DocId, i as u16, i as f32 + 1.0))
1139 .collect();
1140 let list = BlockSparsePostingList::from_postings_with_block_size(
1141 &postings,
1142 WeightQuantization::Float32,
1143 MAX_BLOCK_SIZE,
1144 )
1145 .unwrap();
1146
1147 assert_eq!(list.num_blocks(), 1);
1148 assert_eq!(list.blocks[0].decode_ordinals().len(), MAX_BLOCK_SIZE);
1149 assert_eq!(list.blocks[0].decode_ordinals()[255], 255);
1150 }
1151
1152 #[test]
1153 fn test_configurable_block_size_is_honored() {
1154 let postings: Vec<(DocId, u16, f32)> = (0..300).map(|i| (i, 0, i as f32 + 1.0)).collect();
1155
1156 let small = BlockSparsePostingList::from_postings_with_block_size(
1157 &postings,
1158 WeightQuantization::Float32,
1159 64,
1160 )
1161 .unwrap();
1162 let large = BlockSparsePostingList::from_postings_with_block_size(
1163 &postings,
1164 WeightQuantization::Float32,
1165 256,
1166 )
1167 .unwrap();
1168
1169 assert_eq!(small.num_blocks(), 5);
1170 assert_eq!(small.blocks[0].header.count, 64);
1171 assert_eq!(large.num_blocks(), 2);
1172 assert_eq!(large.blocks[0].header.count, 256);
1173 }
1174
1175 #[test]
1176 fn test_posting_list() {
1177 let postings: Vec<(DocId, u16, f32)> =
1178 (0..300).map(|i| (i * 2, 0, i as f32 * 0.1)).collect();
1179 let list =
1180 BlockSparsePostingList::from_postings(&postings, WeightQuantization::Float32).unwrap();
1181
1182 assert_eq!(list.doc_count(), 300);
1183 assert_eq!(list.num_blocks(), 3);
1184
1185 let mut iter = list.iterator();
1186 assert_eq!(iter.doc(), 0);
1187 iter.advance();
1188 assert_eq!(iter.doc(), 2);
1189 }
1190
1191 #[test]
1192 fn test_serialization() {
1193 let postings = vec![(1u32, 0u16, 0.5f32), (10, 1, 1.5), (100, 0, 2.5)];
1194 let list =
1195 BlockSparsePostingList::from_postings(&postings, WeightQuantization::UInt8).unwrap();
1196
1197 let (block_data, skip_entries) = list.serialize().unwrap();
1198 let list2 =
1199 BlockSparsePostingList::from_parts(list.doc_count(), &block_data, &skip_entries)
1200 .unwrap();
1201
1202 assert_eq!(list.doc_count(), list2.doc_count());
1203 }
1204
1205 #[test]
1206 fn streaming_serialization_is_byte_identical_to_per_block_buffers() {
1207 let postings: Vec<(DocId, u16, f32)> = (0..1_000)
1208 .map(|index| {
1209 (
1210 index / 2,
1211 (index % 2) as u16,
1212 (index * 17 % 101) as f32 / 101.0,
1213 )
1214 })
1215 .collect();
1216 let list = BlockSparsePostingList::from_postings_with_block_size(
1217 &postings,
1218 WeightQuantization::Float16,
1219 64,
1220 )
1221 .unwrap();
1222
1223 let mut reference_data = Vec::new();
1224 let mut reference_skip = Vec::new();
1225 for block in &list.blocks {
1226 let mut buffered = Vec::new();
1227 block.write(&mut buffered).unwrap();
1228 reference_skip.push(super::super::SparseSkipEntry::new(
1229 block.header.first_doc_id,
1230 block.last_doc_id,
1231 reference_data.len() as u64,
1232 buffered.len() as u32,
1233 block.header.max_weight,
1234 ));
1235 reference_data.extend_from_slice(&buffered);
1236 }
1237
1238 let (data, skip) = list.serialize().unwrap();
1239 assert_eq!(data, reference_data);
1240 assert_eq!(skip.len(), reference_skip.len());
1241 for (actual, expected) in skip.iter().zip(&reference_skip) {
1242 assert_eq!(actual.first_doc, expected.first_doc);
1243 assert_eq!(actual.last_doc, expected.last_doc);
1244 assert_eq!(actual.offset, expected.offset);
1245 assert_eq!(actual.length, expected.length);
1246 assert_eq!(actual.max_weight.to_bits(), expected.max_weight.to_bits());
1247 }
1248 }
1249
1250 #[test]
1251 fn test_seek() {
1252 let postings: Vec<(DocId, u16, f32)> = (0..500).map(|i| (i * 3, 0, i as f32)).collect();
1253 let list =
1254 BlockSparsePostingList::from_postings(&postings, WeightQuantization::Float32).unwrap();
1255
1256 let mut iter = list.iterator();
1257 assert_eq!(iter.seek(300), 300);
1258 assert_eq!(iter.seek(301), 303);
1259 assert_eq!(iter.seek(2000), TERMINATED);
1260 }
1261
1262 #[test]
1263 fn test_merge_with_offsets() {
1264 let postings1: Vec<(DocId, u16, f32)> = vec![(0, 0, 1.0), (5, 0, 2.0), (10, 1, 3.0)];
1266 let list1 =
1267 BlockSparsePostingList::from_postings(&postings1, WeightQuantization::Float32).unwrap();
1268
1269 let postings2: Vec<(DocId, u16, f32)> = vec![(0, 0, 4.0), (3, 1, 5.0), (7, 0, 6.0)];
1271 let list2 =
1272 BlockSparsePostingList::from_postings(&postings2, WeightQuantization::Float32).unwrap();
1273
1274 let merged = BlockSparsePostingList::merge_with_offsets(&[(&list1, 0), (&list2, 100)]);
1276
1277 assert_eq!(merged.doc_count(), 6);
1278
1279 let decoded = merged.decode_all();
1281 assert_eq!(decoded.len(), 6);
1282
1283 assert_eq!(decoded[0].0, 0);
1285 assert_eq!(decoded[1].0, 5);
1286 assert_eq!(decoded[2].0, 10);
1287
1288 assert_eq!(decoded[3].0, 100); assert_eq!(decoded[4].0, 103); assert_eq!(decoded[5].0, 107); assert!((decoded[0].2 - 1.0).abs() < 0.01);
1295 assert!((decoded[3].2 - 4.0).abs() < 0.01);
1296
1297 assert_eq!(decoded[2].1, 1); assert_eq!(decoded[4].1, 1); }
1301
1302 #[test]
1303 fn test_merge_with_offsets_multi_block() {
1304 let postings1: Vec<(DocId, u16, f32)> = (0..200).map(|i| (i * 2, 0, i as f32)).collect();
1306 let list1 =
1307 BlockSparsePostingList::from_postings(&postings1, WeightQuantization::Float32).unwrap();
1308 assert!(list1.num_blocks() > 1, "Should have multiple blocks");
1309
1310 let postings2: Vec<(DocId, u16, f32)> = (0..150).map(|i| (i * 3, 1, i as f32)).collect();
1311 let list2 =
1312 BlockSparsePostingList::from_postings(&postings2, WeightQuantization::Float32).unwrap();
1313
1314 let merged = BlockSparsePostingList::merge_with_offsets(&[(&list1, 0), (&list2, 1000)]);
1316
1317 assert_eq!(merged.doc_count(), 350);
1318 assert_eq!(merged.num_blocks(), list1.num_blocks() + list2.num_blocks());
1319
1320 let mut iter = merged.iterator();
1322
1323 assert_eq!(iter.doc(), 0);
1325
1326 let doc = iter.seek(1000);
1328 assert_eq!(doc, 1000); iter.advance();
1332 assert_eq!(iter.doc(), 1003); }
1334
1335 #[test]
1336 fn test_merge_with_offsets_serialize_roundtrip() {
1337 let postings1: Vec<(DocId, u16, f32)> = vec![(0, 0, 1.0), (5, 0, 2.0), (10, 1, 3.0)];
1339 let list1 =
1340 BlockSparsePostingList::from_postings(&postings1, WeightQuantization::Float32).unwrap();
1341
1342 let postings2: Vec<(DocId, u16, f32)> = vec![(0, 0, 4.0), (3, 1, 5.0), (7, 0, 6.0)];
1343 let list2 =
1344 BlockSparsePostingList::from_postings(&postings2, WeightQuantization::Float32).unwrap();
1345
1346 let merged = BlockSparsePostingList::merge_with_offsets(&[(&list1, 0), (&list2, 100)]);
1348
1349 let (block_data, skip_entries) = merged.serialize().unwrap();
1351 let loaded =
1352 BlockSparsePostingList::from_parts(merged.doc_count(), &block_data, &skip_entries)
1353 .unwrap();
1354
1355 let decoded = loaded.decode_all();
1357 assert_eq!(decoded.len(), 6);
1358
1359 assert_eq!(decoded[0].0, 0);
1361 assert_eq!(decoded[1].0, 5);
1362 assert_eq!(decoded[2].0, 10);
1363
1364 assert_eq!(decoded[3].0, 100, "First doc of seg2 should be 0+100=100");
1366 assert_eq!(decoded[4].0, 103, "Second doc of seg2 should be 3+100=103");
1367 assert_eq!(decoded[5].0, 107, "Third doc of seg2 should be 7+100=107");
1368
1369 let mut iter = loaded.iterator();
1371 assert_eq!(iter.doc(), 0);
1372 iter.advance();
1373 assert_eq!(iter.doc(), 5);
1374 iter.advance();
1375 assert_eq!(iter.doc(), 10);
1376 iter.advance();
1377 assert_eq!(iter.doc(), 100);
1378 iter.advance();
1379 assert_eq!(iter.doc(), 103);
1380 iter.advance();
1381 assert_eq!(iter.doc(), 107);
1382 }
1383
1384 #[test]
1385 fn test_merge_seek_after_roundtrip() {
1386 let postings1: Vec<(DocId, u16, f32)> = (0..200).map(|i| (i * 2, 0, 1.0)).collect();
1388 let list1 =
1389 BlockSparsePostingList::from_postings(&postings1, WeightQuantization::Float32).unwrap();
1390
1391 let postings2: Vec<(DocId, u16, f32)> = (0..150).map(|i| (i * 3, 0, 2.0)).collect();
1392 let list2 =
1393 BlockSparsePostingList::from_postings(&postings2, WeightQuantization::Float32).unwrap();
1394
1395 let merged = BlockSparsePostingList::merge_with_offsets(&[(&list1, 0), (&list2, 1000)]);
1397
1398 let (block_data, skip_entries) = merged.serialize().unwrap();
1400 let loaded =
1401 BlockSparsePostingList::from_parts(merged.doc_count(), &block_data, &skip_entries)
1402 .unwrap();
1403
1404 let mut iter = loaded.iterator();
1406
1407 let doc = iter.seek(100);
1409 assert_eq!(doc, 100, "Seek to 100 in segment 1");
1410
1411 let doc = iter.seek(1000);
1413 assert_eq!(doc, 1000, "Seek to 1000 (first doc of segment 2)");
1414
1415 let doc = iter.seek(1050);
1417 assert!(
1418 doc >= 1050,
1419 "Seek to 1050 should find doc >= 1050, got {}",
1420 doc
1421 );
1422
1423 let doc = iter.seek(500);
1425 assert!(
1426 doc >= 1050,
1427 "Seek backwards should not go back, got {}",
1428 doc
1429 );
1430
1431 let mut iter2 = loaded.iterator();
1433
1434 let mut count = 0;
1436 let mut prev_doc = 0;
1437 while iter2.doc() != super::TERMINATED {
1438 let current = iter2.doc();
1439 if count > 0 {
1440 assert!(
1441 current > prev_doc,
1442 "Docs should be monotonically increasing: {} vs {}",
1443 prev_doc,
1444 current
1445 );
1446 }
1447 prev_doc = current;
1448 iter2.advance();
1449 count += 1;
1450 }
1451 assert_eq!(count, 350, "Should have 350 total docs");
1452 }
1453
1454 #[test]
1455 fn test_doc_count_multi_value() {
1456 let postings: Vec<(DocId, u16, f32)> = vec![
1459 (0, 0, 1.0),
1460 (0, 1, 1.5),
1461 (0, 2, 2.0),
1462 (5, 0, 3.0),
1463 (5, 1, 3.5),
1464 (10, 0, 4.0),
1465 ];
1466 let list =
1467 BlockSparsePostingList::from_postings(&postings, WeightQuantization::Float32).unwrap();
1468
1469 assert_eq!(list.doc_count(), 3);
1471
1472 let decoded = list.decode_all();
1474 assert_eq!(decoded.len(), 6);
1475 }
1476
1477 #[test]
1481 fn test_zero_copy_merge_patches_first_doc_id() {
1482 use crate::structures::SparseSkipEntry;
1483
1484 let postings1: Vec<(DocId, u16, f32)> = (0..200).map(|i| (i * 2, 0, i as f32)).collect();
1486 let list1 =
1487 BlockSparsePostingList::from_postings(&postings1, WeightQuantization::Float32).unwrap();
1488 assert!(list1.num_blocks() > 1);
1489
1490 let postings2: Vec<(DocId, u16, f32)> = (0..150).map(|i| (i * 3, 1, i as f32)).collect();
1491 let list2 =
1492 BlockSparsePostingList::from_postings(&postings2, WeightQuantization::Float32).unwrap();
1493
1494 let (raw1, skip1) = list1.serialize().unwrap();
1496 let (raw2, skip2) = list2.serialize().unwrap();
1497
1498 let doc_offset: u32 = 1000; let total_docs = list1.doc_count() + list2.doc_count();
1501
1502 let mut merged_skip = Vec::new();
1504 let mut cumulative_offset = 0u64;
1505 for entry in &skip1 {
1506 merged_skip.push(SparseSkipEntry::new(
1507 entry.first_doc,
1508 entry.last_doc,
1509 cumulative_offset + entry.offset,
1510 entry.length,
1511 entry.max_weight,
1512 ));
1513 }
1514 if let Some(last) = skip1.last() {
1515 cumulative_offset += last.offset + last.length as u64;
1516 }
1517 for entry in &skip2 {
1518 merged_skip.push(SparseSkipEntry::new(
1519 entry.first_doc + doc_offset,
1520 entry.last_doc + doc_offset,
1521 cumulative_offset + entry.offset,
1522 entry.length,
1523 entry.max_weight,
1524 ));
1525 }
1526
1527 let mut merged_block_data = Vec::new();
1529 merged_block_data.extend_from_slice(&raw1);
1530
1531 const FIRST_DOC_ID_OFFSET: usize = 8;
1532 let mut buf2 = raw2.to_vec();
1533 for entry in &skip2 {
1534 let off = entry.offset as usize + FIRST_DOC_ID_OFFSET;
1535 if off + 4 <= buf2.len() {
1536 let old = u32::from_le_bytes(buf2[off..off + 4].try_into().unwrap());
1537 let patched = (old + doc_offset).to_le_bytes();
1538 buf2[off..off + 4].copy_from_slice(&patched);
1539 }
1540 }
1541 merged_block_data.extend_from_slice(&buf2);
1542
1543 let loaded =
1545 BlockSparsePostingList::from_parts(total_docs, &merged_block_data, &merged_skip)
1546 .unwrap();
1547 assert_eq!(loaded.doc_count(), 350);
1548
1549 let mut iter = loaded.iterator();
1550
1551 assert_eq!(iter.doc(), 0);
1553 let doc = iter.seek(100);
1554 assert_eq!(doc, 100);
1555 let doc = iter.seek(398);
1556 assert_eq!(doc, 398);
1557
1558 let doc = iter.seek(1000);
1560 assert_eq!(doc, 1000, "First doc of segment 2 should be 1000");
1561 iter.advance();
1562 assert_eq!(iter.doc(), 1003, "Second doc of segment 2 should be 1003");
1563 let doc = iter.seek(1447);
1564 assert_eq!(doc, 1447, "Last doc of segment 2 should be 1447");
1565
1566 iter.advance();
1568 assert_eq!(iter.doc(), super::TERMINATED);
1569
1570 let reference =
1572 BlockSparsePostingList::merge_with_offsets(&[(&list1, 0), (&list2, doc_offset)]);
1573 let mut ref_iter = reference.iterator();
1574 let mut zc_iter = loaded.iterator();
1575 while ref_iter.doc() != super::TERMINATED {
1576 assert_eq!(
1577 ref_iter.doc(),
1578 zc_iter.doc(),
1579 "Zero-copy and reference merge should produce identical doc_ids"
1580 );
1581 assert!(
1582 (ref_iter.weight() - zc_iter.weight()).abs() < 0.01,
1583 "Weights should match: {} vs {}",
1584 ref_iter.weight(),
1585 zc_iter.weight()
1586 );
1587 ref_iter.advance();
1588 zc_iter.advance();
1589 }
1590 assert_eq!(zc_iter.doc(), super::TERMINATED);
1591 }
1592
1593 #[test]
1594 fn test_doc_count_single_value() {
1595 let postings: Vec<(DocId, u16, f32)> =
1597 vec![(0, 0, 1.0), (5, 0, 2.0), (10, 0, 3.0), (15, 0, 4.0)];
1598 let list =
1599 BlockSparsePostingList::from_postings(&postings, WeightQuantization::Float32).unwrap();
1600
1601 assert_eq!(list.doc_count(), 4);
1603 }
1604
1605 #[test]
1606 fn test_doc_count_multi_value_serialization_roundtrip() {
1607 let postings: Vec<(DocId, u16, f32)> =
1609 vec![(0, 0, 1.0), (0, 1, 1.5), (5, 0, 2.0), (5, 1, 2.5)];
1610 let list =
1611 BlockSparsePostingList::from_postings(&postings, WeightQuantization::Float32).unwrap();
1612 assert_eq!(list.doc_count(), 2);
1613
1614 let (block_data, skip_entries) = list.serialize().unwrap();
1615 let loaded =
1616 BlockSparsePostingList::from_parts(list.doc_count(), &block_data, &skip_entries)
1617 .unwrap();
1618 assert_eq!(loaded.doc_count(), 2);
1619 }
1620
1621 #[test]
1622 fn test_merge_preserves_weights_and_ordinals() {
1623 let postings1: Vec<(DocId, u16, f32)> = vec![(0, 0, 1.5), (5, 1, 2.5), (10, 2, 3.5)];
1625 let list1 =
1626 BlockSparsePostingList::from_postings(&postings1, WeightQuantization::Float32).unwrap();
1627
1628 let postings2: Vec<(DocId, u16, f32)> = vec![(0, 0, 4.5), (3, 1, 5.5), (7, 3, 6.5)];
1629 let list2 =
1630 BlockSparsePostingList::from_postings(&postings2, WeightQuantization::Float32).unwrap();
1631
1632 let merged = BlockSparsePostingList::merge_with_offsets(&[(&list1, 0), (&list2, 100)]);
1634
1635 let (block_data, skip_entries) = merged.serialize().unwrap();
1637 let loaded =
1638 BlockSparsePostingList::from_parts(merged.doc_count(), &block_data, &skip_entries)
1639 .unwrap();
1640
1641 let mut iter = loaded.iterator();
1643
1644 assert_eq!(iter.doc(), 0);
1646 assert!(
1647 (iter.weight() - 1.5).abs() < 0.01,
1648 "Weight should be 1.5, got {}",
1649 iter.weight()
1650 );
1651 assert_eq!(iter.ordinal(), 0);
1652
1653 iter.advance();
1654 assert_eq!(iter.doc(), 5);
1655 assert!(
1656 (iter.weight() - 2.5).abs() < 0.01,
1657 "Weight should be 2.5, got {}",
1658 iter.weight()
1659 );
1660 assert_eq!(iter.ordinal(), 1);
1661
1662 iter.advance();
1663 assert_eq!(iter.doc(), 10);
1664 assert!(
1665 (iter.weight() - 3.5).abs() < 0.01,
1666 "Weight should be 3.5, got {}",
1667 iter.weight()
1668 );
1669 assert_eq!(iter.ordinal(), 2);
1670
1671 iter.advance();
1673 assert_eq!(iter.doc(), 100);
1674 assert!(
1675 (iter.weight() - 4.5).abs() < 0.01,
1676 "Weight should be 4.5, got {}",
1677 iter.weight()
1678 );
1679 assert_eq!(iter.ordinal(), 0);
1680
1681 iter.advance();
1682 assert_eq!(iter.doc(), 103);
1683 assert!(
1684 (iter.weight() - 5.5).abs() < 0.01,
1685 "Weight should be 5.5, got {}",
1686 iter.weight()
1687 );
1688 assert_eq!(iter.ordinal(), 1);
1689
1690 iter.advance();
1691 assert_eq!(iter.doc(), 107);
1692 assert!(
1693 (iter.weight() - 6.5).abs() < 0.01,
1694 "Weight should be 6.5, got {}",
1695 iter.weight()
1696 );
1697 assert_eq!(iter.ordinal(), 3);
1698
1699 iter.advance();
1701 assert_eq!(iter.doc(), super::TERMINATED);
1702 }
1703
1704 #[test]
1705 fn test_merge_global_max_weight() {
1706 let postings1: Vec<(DocId, u16, f32)> = vec![
1708 (0, 0, 3.0),
1709 (1, 0, 7.0), (2, 0, 2.0),
1711 ];
1712 let list1 =
1713 BlockSparsePostingList::from_postings(&postings1, WeightQuantization::Float32).unwrap();
1714
1715 let postings2: Vec<(DocId, u16, f32)> = vec![
1716 (0, 0, 5.0),
1717 (1, 0, 4.0),
1718 (2, 0, 6.0), ];
1720 let list2 =
1721 BlockSparsePostingList::from_postings(&postings2, WeightQuantization::Float32).unwrap();
1722
1723 assert!((list1.global_max_weight() - 7.0).abs() < 0.01);
1725 assert!((list2.global_max_weight() - 6.0).abs() < 0.01);
1726
1727 let merged = BlockSparsePostingList::merge_with_offsets(&[(&list1, 0), (&list2, 100)]);
1729
1730 assert!(
1732 (merged.global_max_weight() - 7.0).abs() < 0.01,
1733 "Global max should be 7.0, got {}",
1734 merged.global_max_weight()
1735 );
1736
1737 let (block_data, skip_entries) = merged.serialize().unwrap();
1739 let loaded =
1740 BlockSparsePostingList::from_parts(merged.doc_count(), &block_data, &skip_entries)
1741 .unwrap();
1742
1743 assert!(
1744 (loaded.global_max_weight() - 7.0).abs() < 0.01,
1745 "After roundtrip, global max should still be 7.0, got {}",
1746 loaded.global_max_weight()
1747 );
1748 }
1749
1750 #[test]
1751 fn test_scoring_simulation_after_merge() {
1752 let postings1: Vec<(DocId, u16, f32)> = vec![
1754 (0, 0, 0.5), (5, 0, 0.8), ];
1757 let list1 =
1758 BlockSparsePostingList::from_postings(&postings1, WeightQuantization::Float32).unwrap();
1759
1760 let postings2: Vec<(DocId, u16, f32)> = vec![
1761 (0, 0, 0.6), (3, 0, 0.9), ];
1764 let list2 =
1765 BlockSparsePostingList::from_postings(&postings2, WeightQuantization::Float32).unwrap();
1766
1767 let merged = BlockSparsePostingList::merge_with_offsets(&[(&list1, 0), (&list2, 100)]);
1769
1770 let (block_data, skip_entries) = merged.serialize().unwrap();
1772 let loaded =
1773 BlockSparsePostingList::from_parts(merged.doc_count(), &block_data, &skip_entries)
1774 .unwrap();
1775
1776 let query_weight = 2.0f32;
1778 let mut iter = loaded.iterator();
1779
1780 assert_eq!(iter.doc(), 0);
1783 let score = query_weight * iter.weight();
1784 assert!(
1785 (score - 1.0).abs() < 0.01,
1786 "Doc 0 score should be 1.0, got {}",
1787 score
1788 );
1789
1790 iter.advance();
1791 assert_eq!(iter.doc(), 5);
1793 let score = query_weight * iter.weight();
1794 assert!(
1795 (score - 1.6).abs() < 0.01,
1796 "Doc 5 score should be 1.6, got {}",
1797 score
1798 );
1799
1800 iter.advance();
1801 assert_eq!(iter.doc(), 100);
1803 let score = query_weight * iter.weight();
1804 assert!(
1805 (score - 1.2).abs() < 0.01,
1806 "Doc 100 score should be 1.2, got {}",
1807 score
1808 );
1809
1810 iter.advance();
1811 assert_eq!(iter.doc(), 103);
1813 let score = query_weight * iter.weight();
1814 assert!(
1815 (score - 1.8).abs() < 0.01,
1816 "Doc 103 score should be 1.8, got {}",
1817 score
1818 );
1819 }
1820}