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]
297 pub fn accumulate_scored_weights(
298 &self,
299 query_weight: f32,
300 doc_ids: &[u32],
301 flat_scores: &mut [f32],
302 base_doc: u32,
303 dirty: &mut Vec<u32>,
304 ) -> usize {
305 let count = self.header.count as usize;
306 match self.header.weight_quant {
307 WeightQuantization::UInt8 if self.weights_data.len() >= 8 => {
308 let scale = f32::from_le_bytes([
310 self.weights_data[0],
311 self.weights_data[1],
312 self.weights_data[2],
313 self.weights_data[3],
314 ]);
315 let min_val = f32::from_le_bytes([
316 self.weights_data[4],
317 self.weights_data[5],
318 self.weights_data[6],
319 self.weights_data[7],
320 ]);
321 let eff_scale = query_weight * scale;
322 let eff_bias = query_weight * min_val;
323 let quant_data = &self.weights_data[8..];
324
325 for i in 0..count.min(quant_data.len()).min(doc_ids.len()) {
326 let w = quant_data[i] as f32 * eff_scale + eff_bias;
327 let off = (doc_ids[i] - base_doc) as usize;
328 if off >= flat_scores.len() {
329 continue;
330 }
331 if flat_scores[off] == 0.0 {
332 dirty.push(doc_ids[i]);
333 }
334 flat_scores[off] += w;
335 }
336 count
337 }
338 _ => {
339 let mut weights_buf = Vec::with_capacity(count);
341 decode_weights_into(
342 &self.weights_data,
343 self.header.weight_quant,
344 count,
345 &mut weights_buf,
346 );
347 for i in 0..count.min(weights_buf.len()).min(doc_ids.len()) {
348 let w = weights_buf[i] * query_weight;
349 let off = (doc_ids[i] - base_doc) as usize;
350 if off >= flat_scores.len() {
351 continue;
352 }
353 if flat_scores[off] == 0.0 {
354 dirty.push(doc_ids[i]);
355 }
356 flat_scores[off] += w;
357 }
358 count
359 }
360 }
361 }
362
363 pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
364 self.header.write(w)?;
365 if self.doc_ids_data.len() > u16::MAX as usize
366 || self.ordinals_data.len() > u16::MAX as usize
367 || self.weights_data.len() > u16::MAX as usize
368 {
369 return Err(io::Error::new(
370 io::ErrorKind::InvalidData,
371 format!(
372 "sparse sub-block too large for u16 length: doc_ids={}B ords={}B wts={}B",
373 self.doc_ids_data.len(),
374 self.ordinals_data.len(),
375 self.weights_data.len()
376 ),
377 ));
378 }
379 w.write_u16::<LittleEndian>(self.doc_ids_data.len() as u16)?;
380 w.write_u16::<LittleEndian>(self.ordinals_data.len() as u16)?;
381 w.write_u16::<LittleEndian>(self.weights_data.len() as u16)?;
382 w.write_u16::<LittleEndian>(0)?;
383 w.write_all(&self.doc_ids_data)?;
384 w.write_all(&self.ordinals_data)?;
385 w.write_all(&self.weights_data)?;
386 Ok(())
387 }
388
389 pub fn read<R: Read>(r: &mut R) -> io::Result<Self> {
390 let header = BlockHeader::read(r)?;
391 let doc_ids_len = r.read_u16::<LittleEndian>()? as usize;
392 let ordinals_len = r.read_u16::<LittleEndian>()? as usize;
393 let weights_len = r.read_u16::<LittleEndian>()? as usize;
394 let _ = r.read_u16::<LittleEndian>()?;
395
396 let mut doc_ids_vec = vec![0u8; doc_ids_len];
397 r.read_exact(&mut doc_ids_vec)?;
398 let mut ordinals_vec = vec![0u8; ordinals_len];
399 r.read_exact(&mut ordinals_vec)?;
400 let mut weights_vec = vec![0u8; weights_len];
401 r.read_exact(&mut weights_vec)?;
402
403 let last_doc_id = compute_last_doc(&header, &doc_ids_vec);
405
406 Ok(Self {
407 header,
408 doc_ids_data: OwnedBytes::new(doc_ids_vec),
409 ordinals_data: OwnedBytes::new(ordinals_vec),
410 weights_data: OwnedBytes::new(weights_vec),
411 last_doc_id,
412 })
413 }
414
415 pub fn from_owned_bytes(data: crate::directories::OwnedBytes) -> crate::Result<Self> {
421 let b = data.as_slice();
422 if b.len() < BlockHeader::SIZE + 8 {
423 return Err(crate::Error::Corruption(
424 "sparse block too small".to_string(),
425 ));
426 }
427 let mut cursor = Cursor::new(&b[..BlockHeader::SIZE]);
428 let header =
429 BlockHeader::read(&mut cursor).map_err(|e| crate::Error::Corruption(e.to_string()))?;
430
431 if header.count == 0 {
432 let hex: String = b
433 .iter()
434 .take(32)
435 .map(|x| format!("{x:02x}"))
436 .collect::<Vec<_>>()
437 .join(" ");
438 return Err(crate::Error::Corruption(format!(
439 "sparse block has count=0 (data_len={}, first_32_bytes=[{}])",
440 b.len(),
441 hex
442 )));
443 }
444
445 let p = BlockHeader::SIZE;
446 let doc_ids_len = u16::from_le_bytes([b[p], b[p + 1]]) as usize;
447 let ordinals_len = u16::from_le_bytes([b[p + 2], b[p + 3]]) as usize;
448 let weights_len = u16::from_le_bytes([b[p + 4], b[p + 5]]) as usize;
449 let data_start = p + 8;
452 let ord_start = data_start + doc_ids_len;
453 let wt_start = ord_start + ordinals_len;
454 let expected_end = wt_start + weights_len;
455
456 if expected_end > b.len() {
457 let hex: String = b
458 .iter()
459 .take(32)
460 .map(|x| format!("{x:02x}"))
461 .collect::<Vec<_>>()
462 .join(" ");
463 return Err(crate::Error::Corruption(format!(
464 "sparse block sub-block overflow: count={} doc_ids={}B ords={}B wts={}B need={}B have={}B (first_32=[{}])",
465 header.count,
466 doc_ids_len,
467 ordinals_len,
468 weights_len,
469 expected_end,
470 b.len(),
471 hex
472 )));
473 }
474
475 let doc_ids_slice = data.slice(data_start..ord_start);
476 let last_doc_id = compute_last_doc(&header, &doc_ids_slice);
478
479 Ok(Self {
480 header,
481 doc_ids_data: doc_ids_slice,
482 ordinals_data: data.slice(ord_start..wt_start),
483 weights_data: data.slice(wt_start..wt_start + weights_len),
484 last_doc_id,
485 })
486 }
487
488 pub fn with_doc_offset(&self, doc_offset: u32) -> Self {
494 Self {
495 header: BlockHeader {
496 first_doc_id: self.header.first_doc_id + doc_offset,
497 ..self.header
498 },
499 doc_ids_data: self.doc_ids_data.clone(),
500 ordinals_data: self.ordinals_data.clone(),
501 weights_data: self.weights_data.clone(),
502 last_doc_id: self.last_doc_id + doc_offset,
503 }
504 }
505}
506
507#[derive(Debug, Clone)]
512pub struct BlockSparsePostingList {
513 pub doc_count: u32,
514 pub blocks: Vec<SparseBlock>,
515}
516
517impl BlockSparsePostingList {
518 pub fn from_postings_with_block_size(
520 postings: &[(DocId, u16, f32)],
521 weight_quant: WeightQuantization,
522 block_size: usize,
523 ) -> io::Result<Self> {
524 if postings.is_empty() {
525 return Ok(Self {
526 doc_count: 0,
527 blocks: Vec::new(),
528 });
529 }
530
531 let block_size = block_size.clamp(16, MAX_BLOCK_SIZE);
532 let mut blocks = Vec::with_capacity(postings.len().div_ceil(block_size));
533 for chunk in postings.chunks(block_size) {
534 blocks.push(SparseBlock::from_postings(chunk, weight_quant)?);
535 }
536
537 let mut unique_docs = 1u32;
542 for i in 1..postings.len() {
543 if postings[i].0 != postings[i - 1].0 {
544 unique_docs += 1;
545 }
546 }
547
548 Ok(Self {
549 doc_count: unique_docs,
550 blocks,
551 })
552 }
553
554 pub fn from_postings(
556 postings: &[(DocId, u16, f32)],
557 weight_quant: WeightQuantization,
558 ) -> io::Result<Self> {
559 Self::from_postings_with_block_size(postings, weight_quant, BLOCK_SIZE)
560 }
561
562 pub fn from_postings_with_partition(
568 postings: &[(DocId, u16, f32)],
569 weight_quant: WeightQuantization,
570 partition: &[usize],
571 ) -> io::Result<Self> {
572 if postings.is_empty() {
573 return Ok(Self {
574 doc_count: 0,
575 blocks: Vec::new(),
576 });
577 }
578
579 let mut blocks = Vec::with_capacity(partition.len());
580 let mut offset = 0;
581 for &block_size in partition {
582 let end = (offset + block_size).min(postings.len());
583 blocks.push(SparseBlock::from_postings(
584 &postings[offset..end],
585 weight_quant,
586 )?);
587 offset = end;
588 }
589
590 let mut unique_docs = 1u32;
591 for i in 1..postings.len() {
592 if postings[i].0 != postings[i - 1].0 {
593 unique_docs += 1;
594 }
595 }
596
597 Ok(Self {
598 doc_count: unique_docs,
599 blocks,
600 })
601 }
602
603 pub fn doc_count(&self) -> u32 {
604 self.doc_count
605 }
606
607 pub fn num_blocks(&self) -> usize {
608 self.blocks.len()
609 }
610
611 pub fn global_max_weight(&self) -> f32 {
612 self.blocks
613 .iter()
614 .map(|b| b.header.max_weight)
615 .fold(0.0f32, f32::max)
616 }
617
618 pub fn block_max_weight(&self, block_idx: usize) -> Option<f32> {
619 self.blocks.get(block_idx).map(|b| b.header.max_weight)
620 }
621
622 pub fn size_bytes(&self) -> usize {
624 use std::mem::size_of;
625
626 let header_size = size_of::<u32>() * 2; let blocks_size: usize = self
628 .blocks
629 .iter()
630 .map(|b| {
631 size_of::<BlockHeader>()
632 + b.doc_ids_data.len()
633 + b.ordinals_data.len()
634 + b.weights_data.len()
635 })
636 .sum();
637 header_size + blocks_size
638 }
639
640 pub fn iterator(&self) -> BlockSparsePostingIterator<'_> {
641 BlockSparsePostingIterator::new(self)
642 }
643
644 pub fn serialize(&self) -> io::Result<(Vec<u8>, Vec<super::SparseSkipEntry>)> {
650 let serialized_bytes = self.blocks.iter().try_fold(0usize, |total, block| {
651 total
652 .checked_add(
653 BlockHeader::SIZE
654 + 8
655 + block.doc_ids_data.len()
656 + block.ordinals_data.len()
657 + block.weights_data.len(),
658 )
659 .ok_or_else(|| {
660 io::Error::new(io::ErrorKind::InvalidInput, "sparse posting size overflow")
661 })
662 })?;
663 let mut block_data = Vec::with_capacity(serialized_bytes);
664 let mut skip_entries = Vec::with_capacity(self.blocks.len());
665
666 for block in &self.blocks {
667 let offset = block_data.len();
668 block.write(&mut block_data)?;
669 let length = u32::try_from(block_data.len() - offset).map_err(|_| {
670 io::Error::new(
671 io::ErrorKind::InvalidData,
672 "serialized sparse block is too large",
673 )
674 })?;
675
676 let first_doc = block.header.first_doc_id;
677 let last_doc = block.last_doc_id;
678
679 skip_entries.push(super::SparseSkipEntry::new(
680 first_doc,
681 last_doc,
682 offset as u64,
683 length,
684 block.header.max_weight,
685 ));
686 }
687
688 Ok((block_data, skip_entries))
689 }
690
691 #[cfg(test)]
696 pub fn from_parts(
697 doc_count: u32,
698 block_data: &[u8],
699 skip_entries: &[super::SparseSkipEntry],
700 ) -> io::Result<Self> {
701 let mut blocks = Vec::with_capacity(skip_entries.len());
702 for entry in skip_entries {
703 let start = entry.offset as usize;
704 let end = start + entry.length as usize;
705 blocks.push(SparseBlock::read(&mut std::io::Cursor::new(
706 &block_data[start..end],
707 ))?);
708 }
709 Ok(Self { doc_count, blocks })
710 }
711
712 pub fn decode_all(&self) -> Vec<(DocId, u16, f32)> {
713 let total_postings: usize = self.blocks.iter().map(|b| b.header.count as usize).sum();
714 let mut result = Vec::with_capacity(total_postings);
715 for block in &self.blocks {
716 let doc_ids = block.decode_doc_ids();
717 let ordinals = block.decode_ordinals();
718 let weights = block.decode_weights();
719 for i in 0..block.header.count as usize {
720 result.push((doc_ids[i], ordinals[i], weights[i]));
721 }
722 }
723 result
724 }
725
726 pub fn merge_with_offsets(lists: &[(&BlockSparsePostingList, u32)]) -> Self {
737 if lists.is_empty() {
738 return Self {
739 doc_count: 0,
740 blocks: Vec::new(),
741 };
742 }
743
744 let total_blocks: usize = lists.iter().map(|(pl, _)| pl.blocks.len()).sum();
746 let total_docs: u32 = lists.iter().map(|(pl, _)| pl.doc_count).sum();
747
748 let mut merged_blocks = Vec::with_capacity(total_blocks);
749
750 for (posting_list, doc_offset) in lists {
752 for block in &posting_list.blocks {
753 merged_blocks.push(block.with_doc_offset(*doc_offset));
754 }
755 }
756
757 Self {
758 doc_count: total_docs,
759 blocks: merged_blocks,
760 }
761 }
762
763 fn find_block(&self, target: DocId) -> Option<usize> {
764 if self.blocks.is_empty() {
765 return None;
766 }
767 let idx = self
770 .blocks
771 .partition_point(|b| b.header.first_doc_id <= target);
772 if idx == 0 {
773 Some(0)
775 } else {
776 Some(idx - 1)
777 }
778 }
779}
780
781pub struct BlockSparsePostingIterator<'a> {
786 posting_list: &'a BlockSparsePostingList,
787 block_idx: usize,
788 in_block_idx: usize,
789 current_doc_ids: Vec<DocId>,
790 current_ordinals: Vec<u16>,
791 current_weights: Vec<f32>,
792 ordinals_decoded: bool,
794 exhausted: bool,
795}
796
797impl<'a> BlockSparsePostingIterator<'a> {
798 fn new(posting_list: &'a BlockSparsePostingList) -> Self {
799 let mut iter = Self {
800 posting_list,
801 block_idx: 0,
802 in_block_idx: 0,
803 current_doc_ids: Vec::with_capacity(128),
804 current_ordinals: Vec::with_capacity(128),
805 current_weights: Vec::with_capacity(128),
806 ordinals_decoded: false,
807 exhausted: posting_list.blocks.is_empty(),
808 };
809 if !iter.exhausted {
810 iter.load_block(0);
811 }
812 iter
813 }
814
815 fn load_block(&mut self, block_idx: usize) {
816 if let Some(block) = self.posting_list.blocks.get(block_idx) {
817 block.decode_doc_ids_into(&mut self.current_doc_ids);
818 block.decode_weights_into(&mut self.current_weights);
819 self.ordinals_decoded = false;
821 self.block_idx = block_idx;
822 self.in_block_idx = 0;
823 }
824 }
825
826 #[inline]
828 fn ensure_ordinals_decoded(&mut self) {
829 if !self.ordinals_decoded {
830 if let Some(block) = self.posting_list.blocks.get(self.block_idx) {
831 block.decode_ordinals_into(&mut self.current_ordinals);
832 }
833 self.ordinals_decoded = true;
834 }
835 }
836
837 #[inline]
838 pub fn doc(&self) -> DocId {
839 if self.exhausted {
840 TERMINATED
841 } else {
842 self.current_doc_ids[self.in_block_idx]
844 }
845 }
846
847 #[inline]
848 pub fn weight(&self) -> f32 {
849 if self.exhausted {
850 return 0.0;
851 }
852 self.current_weights[self.in_block_idx]
854 }
855
856 #[inline]
857 pub fn ordinal(&mut self) -> u16 {
858 if self.exhausted {
859 return 0;
860 }
861 self.ensure_ordinals_decoded();
862 self.current_ordinals[self.in_block_idx]
863 }
864
865 pub fn advance(&mut self) -> DocId {
866 if self.exhausted {
867 return TERMINATED;
868 }
869 self.in_block_idx += 1;
870 if self.in_block_idx >= self.current_doc_ids.len() {
871 self.block_idx += 1;
872 if self.block_idx >= self.posting_list.blocks.len() {
873 self.exhausted = true;
874 } else {
875 self.load_block(self.block_idx);
876 }
877 }
878 self.doc()
879 }
880
881 pub fn seek(&mut self, target: DocId) -> DocId {
882 if self.exhausted {
883 return TERMINATED;
884 }
885 if self.doc() >= target {
886 return self.doc();
887 }
888
889 if let Some(&last_doc) = self.current_doc_ids.last()
891 && last_doc >= target
892 {
893 let remaining = &self.current_doc_ids[self.in_block_idx..];
894 let pos = crate::structures::simd::find_first_ge_u32(remaining, target);
895 self.in_block_idx += pos;
896 if self.in_block_idx >= self.current_doc_ids.len() {
897 self.block_idx += 1;
898 if self.block_idx >= self.posting_list.blocks.len() {
899 self.exhausted = true;
900 } else {
901 self.load_block(self.block_idx);
902 }
903 }
904 return self.doc();
905 }
906
907 if let Some(block_idx) = self.posting_list.find_block(target) {
909 self.load_block(block_idx);
910 let pos = crate::structures::simd::find_first_ge_u32(&self.current_doc_ids, target);
911 self.in_block_idx = pos;
912 if self.in_block_idx >= self.current_doc_ids.len() {
913 self.block_idx += 1;
914 if self.block_idx >= self.posting_list.blocks.len() {
915 self.exhausted = true;
916 } else {
917 self.load_block(self.block_idx);
918 }
919 }
920 } else {
921 self.exhausted = true;
922 }
923 self.doc()
924 }
925
926 pub fn skip_to_next_block(&mut self) -> DocId {
929 if self.exhausted {
930 return TERMINATED;
931 }
932 let next = self.block_idx + 1;
933 if next >= self.posting_list.blocks.len() {
934 self.exhausted = true;
935 return TERMINATED;
936 }
937 self.load_block(next);
938 self.doc()
939 }
940
941 pub fn is_exhausted(&self) -> bool {
942 self.exhausted
943 }
944
945 pub fn current_block_max_weight(&self) -> f32 {
946 self.posting_list
947 .blocks
948 .get(self.block_idx)
949 .map(|b| b.header.max_weight)
950 .unwrap_or(0.0)
951 }
952
953 pub fn current_block_max_contribution(&self, query_weight: f32) -> f32 {
954 query_weight * self.current_block_max_weight()
955 }
956}
957
958fn compute_last_doc(header: &BlockHeader, doc_ids_data: &[u8]) -> DocId {
965 let count = header.count as usize;
966 if count <= 1 {
967 return header.first_doc_id;
968 }
969 let bits = header.doc_id_bits;
970 if bits == 0 {
971 return header.first_doc_id; }
973 let rounded = simd::RoundedBitWidth::from_u8(bits);
974 let num_deltas = count - 1;
975 let mut deltas = [0u32; MAX_BLOCK_SIZE];
976 simd::unpack_rounded(doc_ids_data, rounded, &mut deltas[..num_deltas], num_deltas);
977 let sum: u32 = deltas[..num_deltas].iter().sum();
978 header.first_doc_id + sum
979}
980
981fn find_optimal_bit_width(values: &[u32]) -> u8 {
982 if values.is_empty() {
983 return 0;
984 }
985 let max_val = values.iter().copied().max().unwrap_or(0);
986 simd::bits_needed(max_val)
987}
988
989fn bits_needed_u16(val: u16) -> u8 {
990 if val == 0 {
991 0
992 } else {
993 16 - val.leading_zeros() as u8
994 }
995}
996
997fn encode_weights(weights: &[f32], quant: WeightQuantization) -> io::Result<Vec<u8>> {
1002 let encoded_len = match quant {
1003 WeightQuantization::Float32 => weights.len().saturating_mul(4),
1004 WeightQuantization::Float16 => weights.len().saturating_mul(2),
1005 WeightQuantization::UInt8 => 8usize.saturating_add(weights.len()),
1006 WeightQuantization::UInt4 => 8usize.saturating_add(weights.len().div_ceil(2)),
1007 };
1008 let mut data = Vec::with_capacity(encoded_len);
1009 match quant {
1010 WeightQuantization::Float32 => {
1011 for &w in weights {
1012 data.write_f32::<LittleEndian>(w)?;
1013 }
1014 }
1015 WeightQuantization::Float16 => {
1016 use half::f16;
1017 for &w in weights {
1018 data.write_u16::<LittleEndian>(f16::from_f32(w).to_bits())?;
1019 }
1020 }
1021 WeightQuantization::UInt8 => {
1022 let min = weights.iter().copied().fold(f32::INFINITY, f32::min);
1023 let max = weights.iter().copied().fold(f32::NEG_INFINITY, f32::max);
1024 let range = max - min;
1025 let scale = if range < f32::EPSILON {
1026 1.0
1027 } else {
1028 range / 255.0
1029 };
1030 data.write_f32::<LittleEndian>(scale)?;
1031 data.write_f32::<LittleEndian>(min)?;
1032 for &w in weights {
1033 data.write_u8(((w - min) / scale).round() as u8)?;
1034 }
1035 }
1036 WeightQuantization::UInt4 => {
1037 let min = weights.iter().copied().fold(f32::INFINITY, f32::min);
1038 let max = weights.iter().copied().fold(f32::NEG_INFINITY, f32::max);
1039 let range = max - min;
1040 let scale = if range < f32::EPSILON {
1041 1.0
1042 } else {
1043 range / 15.0
1044 };
1045 data.write_f32::<LittleEndian>(scale)?;
1046 data.write_f32::<LittleEndian>(min)?;
1047 let mut i = 0;
1048 while i < weights.len() {
1049 let q1 = ((weights[i] - min) / scale).round() as u8 & 0x0F;
1050 let q2 = if i + 1 < weights.len() {
1051 ((weights[i + 1] - min) / scale).round() as u8 & 0x0F
1052 } else {
1053 0
1054 };
1055 data.write_u8((q2 << 4) | q1)?;
1056 i += 2;
1057 }
1058 }
1059 }
1060 Ok(data)
1061}
1062
1063fn decode_weights_into(data: &[u8], quant: WeightQuantization, count: usize, out: &mut Vec<f32>) {
1064 match quant {
1065 WeightQuantization::Float32 => {
1066 out.reserve(count);
1067 for chunk in data[..count * 4].as_chunks::<4>().0 {
1068 out.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
1069 }
1070 }
1071 WeightQuantization::Float16 => {
1072 use half::f16;
1075 use half::slice::HalfFloatSliceExt;
1076 let byte_count = count * 2;
1077 let src = &data[..byte_count];
1078 let mut f16_buf = [f16::ZERO; MAX_BLOCK_SIZE];
1079 for (value, chunk) in f16_buf[..count].iter_mut().zip(src.as_chunks::<2>().0) {
1080 *value = f16::from_bits(u16::from_le_bytes([chunk[0], chunk[1]]));
1081 }
1082 let start = out.len();
1083 out.resize(start + count, 0.0);
1084 f16_buf[..count].convert_to_f32_slice(&mut out[start..start + count]);
1085 }
1086 WeightQuantization::UInt8 => {
1087 let mut cursor = Cursor::new(data);
1088 let scale = cursor.read_f32::<LittleEndian>().unwrap_or(1.0);
1089 let min_val = cursor.read_f32::<LittleEndian>().unwrap_or(0.0);
1090 let offset = cursor.position() as usize;
1091 out.resize(count, 0.0);
1092 simd::dequantize_uint8(&data[offset..], out, scale, min_val, count);
1093 }
1094 WeightQuantization::UInt4 => {
1095 let mut cursor = Cursor::new(data);
1096 let scale = cursor.read_f32::<LittleEndian>().unwrap_or(1.0);
1097 let min = cursor.read_f32::<LittleEndian>().unwrap_or(0.0);
1098 let mut i = 0;
1099 while i < count {
1100 let byte = cursor.read_u8().unwrap_or(0);
1101 out.push((byte & 0x0F) as f32 * scale + min);
1102 i += 1;
1103 if i < count {
1104 out.push((byte >> 4) as f32 * scale + min);
1105 i += 1;
1106 }
1107 }
1108 }
1109 }
1110}
1111
1112#[cfg(test)]
1113mod tests {
1114 use super::*;
1115
1116 #[test]
1117 fn test_block_roundtrip() {
1118 let postings = vec![
1119 (10u32, 0u16, 1.5f32),
1120 (15, 0, 2.0),
1121 (20, 1, 0.5),
1122 (100, 0, 3.0),
1123 ];
1124 let block = SparseBlock::from_postings(&postings, WeightQuantization::Float32).unwrap();
1125
1126 assert_eq!(block.decode_doc_ids(), vec![10, 15, 20, 100]);
1127 assert_eq!(block.decode_ordinals(), vec![0, 0, 1, 0]);
1128 let weights = block.decode_weights();
1129 assert!((weights[0] - 1.5).abs() < 0.01);
1130 }
1131
1132 #[test]
1133 fn test_max_size_block_ordinal_decode() {
1134 let postings: Vec<(DocId, u16, f32)> = (0..MAX_BLOCK_SIZE)
1135 .map(|i| (i as DocId, i as u16, i as f32 + 1.0))
1136 .collect();
1137 let list = BlockSparsePostingList::from_postings_with_block_size(
1138 &postings,
1139 WeightQuantization::Float32,
1140 MAX_BLOCK_SIZE,
1141 )
1142 .unwrap();
1143
1144 assert_eq!(list.num_blocks(), 1);
1145 assert_eq!(list.blocks[0].decode_ordinals().len(), MAX_BLOCK_SIZE);
1146 assert_eq!(list.blocks[0].decode_ordinals()[255], 255);
1147 }
1148
1149 #[test]
1150 fn test_configurable_block_size_is_honored() {
1151 let postings: Vec<(DocId, u16, f32)> = (0..300).map(|i| (i, 0, i as f32 + 1.0)).collect();
1152
1153 let small = BlockSparsePostingList::from_postings_with_block_size(
1154 &postings,
1155 WeightQuantization::Float32,
1156 64,
1157 )
1158 .unwrap();
1159 let large = BlockSparsePostingList::from_postings_with_block_size(
1160 &postings,
1161 WeightQuantization::Float32,
1162 256,
1163 )
1164 .unwrap();
1165
1166 assert_eq!(small.num_blocks(), 5);
1167 assert_eq!(small.blocks[0].header.count, 64);
1168 assert_eq!(large.num_blocks(), 2);
1169 assert_eq!(large.blocks[0].header.count, 256);
1170 }
1171
1172 #[test]
1173 fn test_posting_list() {
1174 let postings: Vec<(DocId, u16, f32)> =
1175 (0..300).map(|i| (i * 2, 0, i as f32 * 0.1)).collect();
1176 let list =
1177 BlockSparsePostingList::from_postings(&postings, WeightQuantization::Float32).unwrap();
1178
1179 assert_eq!(list.doc_count(), 300);
1180 assert_eq!(list.num_blocks(), 3);
1181
1182 let mut iter = list.iterator();
1183 assert_eq!(iter.doc(), 0);
1184 iter.advance();
1185 assert_eq!(iter.doc(), 2);
1186 }
1187
1188 #[test]
1189 fn test_serialization() {
1190 let postings = vec![(1u32, 0u16, 0.5f32), (10, 1, 1.5), (100, 0, 2.5)];
1191 let list =
1192 BlockSparsePostingList::from_postings(&postings, WeightQuantization::UInt8).unwrap();
1193
1194 let (block_data, skip_entries) = list.serialize().unwrap();
1195 let list2 =
1196 BlockSparsePostingList::from_parts(list.doc_count(), &block_data, &skip_entries)
1197 .unwrap();
1198
1199 assert_eq!(list.doc_count(), list2.doc_count());
1200 }
1201
1202 #[test]
1203 fn streaming_serialization_is_byte_identical_to_per_block_buffers() {
1204 let postings: Vec<(DocId, u16, f32)> = (0..1_000)
1205 .map(|index| {
1206 (
1207 index / 2,
1208 (index % 2) as u16,
1209 (index * 17 % 101) as f32 / 101.0,
1210 )
1211 })
1212 .collect();
1213 let list = BlockSparsePostingList::from_postings_with_block_size(
1214 &postings,
1215 WeightQuantization::Float16,
1216 64,
1217 )
1218 .unwrap();
1219
1220 let mut reference_data = Vec::new();
1221 let mut reference_skip = Vec::new();
1222 for block in &list.blocks {
1223 let mut buffered = Vec::new();
1224 block.write(&mut buffered).unwrap();
1225 reference_skip.push(super::super::SparseSkipEntry::new(
1226 block.header.first_doc_id,
1227 block.last_doc_id,
1228 reference_data.len() as u64,
1229 buffered.len() as u32,
1230 block.header.max_weight,
1231 ));
1232 reference_data.extend_from_slice(&buffered);
1233 }
1234
1235 let (data, skip) = list.serialize().unwrap();
1236 assert_eq!(data, reference_data);
1237 assert_eq!(skip.len(), reference_skip.len());
1238 for (actual, expected) in skip.iter().zip(&reference_skip) {
1239 assert_eq!(actual.first_doc, expected.first_doc);
1240 assert_eq!(actual.last_doc, expected.last_doc);
1241 assert_eq!(actual.offset, expected.offset);
1242 assert_eq!(actual.length, expected.length);
1243 assert_eq!(actual.max_weight.to_bits(), expected.max_weight.to_bits());
1244 }
1245 }
1246
1247 #[test]
1248 fn test_seek() {
1249 let postings: Vec<(DocId, u16, f32)> = (0..500).map(|i| (i * 3, 0, i as f32)).collect();
1250 let list =
1251 BlockSparsePostingList::from_postings(&postings, WeightQuantization::Float32).unwrap();
1252
1253 let mut iter = list.iterator();
1254 assert_eq!(iter.seek(300), 300);
1255 assert_eq!(iter.seek(301), 303);
1256 assert_eq!(iter.seek(2000), TERMINATED);
1257 }
1258
1259 #[test]
1260 fn test_merge_with_offsets() {
1261 let postings1: Vec<(DocId, u16, f32)> = vec![(0, 0, 1.0), (5, 0, 2.0), (10, 1, 3.0)];
1263 let list1 =
1264 BlockSparsePostingList::from_postings(&postings1, WeightQuantization::Float32).unwrap();
1265
1266 let postings2: Vec<(DocId, u16, f32)> = vec![(0, 0, 4.0), (3, 1, 5.0), (7, 0, 6.0)];
1268 let list2 =
1269 BlockSparsePostingList::from_postings(&postings2, WeightQuantization::Float32).unwrap();
1270
1271 let merged = BlockSparsePostingList::merge_with_offsets(&[(&list1, 0), (&list2, 100)]);
1273
1274 assert_eq!(merged.doc_count(), 6);
1275
1276 let decoded = merged.decode_all();
1278 assert_eq!(decoded.len(), 6);
1279
1280 assert_eq!(decoded[0].0, 0);
1282 assert_eq!(decoded[1].0, 5);
1283 assert_eq!(decoded[2].0, 10);
1284
1285 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);
1292 assert!((decoded[3].2 - 4.0).abs() < 0.01);
1293
1294 assert_eq!(decoded[2].1, 1); assert_eq!(decoded[4].1, 1); }
1298
1299 #[test]
1300 fn test_merge_with_offsets_multi_block() {
1301 let postings1: Vec<(DocId, u16, f32)> = (0..200).map(|i| (i * 2, 0, i as f32)).collect();
1303 let list1 =
1304 BlockSparsePostingList::from_postings(&postings1, WeightQuantization::Float32).unwrap();
1305 assert!(list1.num_blocks() > 1, "Should have multiple blocks");
1306
1307 let postings2: Vec<(DocId, u16, f32)> = (0..150).map(|i| (i * 3, 1, i as f32)).collect();
1308 let list2 =
1309 BlockSparsePostingList::from_postings(&postings2, WeightQuantization::Float32).unwrap();
1310
1311 let merged = BlockSparsePostingList::merge_with_offsets(&[(&list1, 0), (&list2, 1000)]);
1313
1314 assert_eq!(merged.doc_count(), 350);
1315 assert_eq!(merged.num_blocks(), list1.num_blocks() + list2.num_blocks());
1316
1317 let mut iter = merged.iterator();
1319
1320 assert_eq!(iter.doc(), 0);
1322
1323 let doc = iter.seek(1000);
1325 assert_eq!(doc, 1000); iter.advance();
1329 assert_eq!(iter.doc(), 1003); }
1331
1332 #[test]
1333 fn test_merge_with_offsets_serialize_roundtrip() {
1334 let postings1: Vec<(DocId, u16, f32)> = vec![(0, 0, 1.0), (5, 0, 2.0), (10, 1, 3.0)];
1336 let list1 =
1337 BlockSparsePostingList::from_postings(&postings1, WeightQuantization::Float32).unwrap();
1338
1339 let postings2: Vec<(DocId, u16, f32)> = vec![(0, 0, 4.0), (3, 1, 5.0), (7, 0, 6.0)];
1340 let list2 =
1341 BlockSparsePostingList::from_postings(&postings2, WeightQuantization::Float32).unwrap();
1342
1343 let merged = BlockSparsePostingList::merge_with_offsets(&[(&list1, 0), (&list2, 100)]);
1345
1346 let (block_data, skip_entries) = merged.serialize().unwrap();
1348 let loaded =
1349 BlockSparsePostingList::from_parts(merged.doc_count(), &block_data, &skip_entries)
1350 .unwrap();
1351
1352 let decoded = loaded.decode_all();
1354 assert_eq!(decoded.len(), 6);
1355
1356 assert_eq!(decoded[0].0, 0);
1358 assert_eq!(decoded[1].0, 5);
1359 assert_eq!(decoded[2].0, 10);
1360
1361 assert_eq!(decoded[3].0, 100, "First doc of seg2 should be 0+100=100");
1363 assert_eq!(decoded[4].0, 103, "Second doc of seg2 should be 3+100=103");
1364 assert_eq!(decoded[5].0, 107, "Third doc of seg2 should be 7+100=107");
1365
1366 let mut iter = loaded.iterator();
1368 assert_eq!(iter.doc(), 0);
1369 iter.advance();
1370 assert_eq!(iter.doc(), 5);
1371 iter.advance();
1372 assert_eq!(iter.doc(), 10);
1373 iter.advance();
1374 assert_eq!(iter.doc(), 100);
1375 iter.advance();
1376 assert_eq!(iter.doc(), 103);
1377 iter.advance();
1378 assert_eq!(iter.doc(), 107);
1379 }
1380
1381 #[test]
1382 fn test_merge_seek_after_roundtrip() {
1383 let postings1: Vec<(DocId, u16, f32)> = (0..200).map(|i| (i * 2, 0, 1.0)).collect();
1385 let list1 =
1386 BlockSparsePostingList::from_postings(&postings1, WeightQuantization::Float32).unwrap();
1387
1388 let postings2: Vec<(DocId, u16, f32)> = (0..150).map(|i| (i * 3, 0, 2.0)).collect();
1389 let list2 =
1390 BlockSparsePostingList::from_postings(&postings2, WeightQuantization::Float32).unwrap();
1391
1392 let merged = BlockSparsePostingList::merge_with_offsets(&[(&list1, 0), (&list2, 1000)]);
1394
1395 let (block_data, skip_entries) = merged.serialize().unwrap();
1397 let loaded =
1398 BlockSparsePostingList::from_parts(merged.doc_count(), &block_data, &skip_entries)
1399 .unwrap();
1400
1401 let mut iter = loaded.iterator();
1403
1404 let doc = iter.seek(100);
1406 assert_eq!(doc, 100, "Seek to 100 in segment 1");
1407
1408 let doc = iter.seek(1000);
1410 assert_eq!(doc, 1000, "Seek to 1000 (first doc of segment 2)");
1411
1412 let doc = iter.seek(1050);
1414 assert!(
1415 doc >= 1050,
1416 "Seek to 1050 should find doc >= 1050, got {}",
1417 doc
1418 );
1419
1420 let doc = iter.seek(500);
1422 assert!(
1423 doc >= 1050,
1424 "Seek backwards should not go back, got {}",
1425 doc
1426 );
1427
1428 let mut iter2 = loaded.iterator();
1430
1431 let mut count = 0;
1433 let mut prev_doc = 0;
1434 while iter2.doc() != super::TERMINATED {
1435 let current = iter2.doc();
1436 if count > 0 {
1437 assert!(
1438 current > prev_doc,
1439 "Docs should be monotonically increasing: {} vs {}",
1440 prev_doc,
1441 current
1442 );
1443 }
1444 prev_doc = current;
1445 iter2.advance();
1446 count += 1;
1447 }
1448 assert_eq!(count, 350, "Should have 350 total docs");
1449 }
1450
1451 #[test]
1452 fn test_doc_count_multi_value() {
1453 let postings: Vec<(DocId, u16, f32)> = vec![
1456 (0, 0, 1.0),
1457 (0, 1, 1.5),
1458 (0, 2, 2.0),
1459 (5, 0, 3.0),
1460 (5, 1, 3.5),
1461 (10, 0, 4.0),
1462 ];
1463 let list =
1464 BlockSparsePostingList::from_postings(&postings, WeightQuantization::Float32).unwrap();
1465
1466 assert_eq!(list.doc_count(), 3);
1468
1469 let decoded = list.decode_all();
1471 assert_eq!(decoded.len(), 6);
1472 }
1473
1474 #[test]
1478 fn test_zero_copy_merge_patches_first_doc_id() {
1479 use crate::structures::SparseSkipEntry;
1480
1481 let postings1: Vec<(DocId, u16, f32)> = (0..200).map(|i| (i * 2, 0, i as f32)).collect();
1483 let list1 =
1484 BlockSparsePostingList::from_postings(&postings1, WeightQuantization::Float32).unwrap();
1485 assert!(list1.num_blocks() > 1);
1486
1487 let postings2: Vec<(DocId, u16, f32)> = (0..150).map(|i| (i * 3, 1, i as f32)).collect();
1488 let list2 =
1489 BlockSparsePostingList::from_postings(&postings2, WeightQuantization::Float32).unwrap();
1490
1491 let (raw1, skip1) = list1.serialize().unwrap();
1493 let (raw2, skip2) = list2.serialize().unwrap();
1494
1495 let doc_offset: u32 = 1000; let total_docs = list1.doc_count() + list2.doc_count();
1498
1499 let mut merged_skip = Vec::new();
1501 let mut cumulative_offset = 0u64;
1502 for entry in &skip1 {
1503 merged_skip.push(SparseSkipEntry::new(
1504 entry.first_doc,
1505 entry.last_doc,
1506 cumulative_offset + entry.offset,
1507 entry.length,
1508 entry.max_weight,
1509 ));
1510 }
1511 if let Some(last) = skip1.last() {
1512 cumulative_offset += last.offset + last.length as u64;
1513 }
1514 for entry in &skip2 {
1515 merged_skip.push(SparseSkipEntry::new(
1516 entry.first_doc + doc_offset,
1517 entry.last_doc + doc_offset,
1518 cumulative_offset + entry.offset,
1519 entry.length,
1520 entry.max_weight,
1521 ));
1522 }
1523
1524 let mut merged_block_data = Vec::new();
1526 merged_block_data.extend_from_slice(&raw1);
1527
1528 const FIRST_DOC_ID_OFFSET: usize = 8;
1529 let mut buf2 = raw2.to_vec();
1530 for entry in &skip2 {
1531 let off = entry.offset as usize + FIRST_DOC_ID_OFFSET;
1532 if off + 4 <= buf2.len() {
1533 let old = u32::from_le_bytes(buf2[off..off + 4].try_into().unwrap());
1534 let patched = (old + doc_offset).to_le_bytes();
1535 buf2[off..off + 4].copy_from_slice(&patched);
1536 }
1537 }
1538 merged_block_data.extend_from_slice(&buf2);
1539
1540 let loaded =
1542 BlockSparsePostingList::from_parts(total_docs, &merged_block_data, &merged_skip)
1543 .unwrap();
1544 assert_eq!(loaded.doc_count(), 350);
1545
1546 let mut iter = loaded.iterator();
1547
1548 assert_eq!(iter.doc(), 0);
1550 let doc = iter.seek(100);
1551 assert_eq!(doc, 100);
1552 let doc = iter.seek(398);
1553 assert_eq!(doc, 398);
1554
1555 let doc = iter.seek(1000);
1557 assert_eq!(doc, 1000, "First doc of segment 2 should be 1000");
1558 iter.advance();
1559 assert_eq!(iter.doc(), 1003, "Second doc of segment 2 should be 1003");
1560 let doc = iter.seek(1447);
1561 assert_eq!(doc, 1447, "Last doc of segment 2 should be 1447");
1562
1563 iter.advance();
1565 assert_eq!(iter.doc(), super::TERMINATED);
1566
1567 let reference =
1569 BlockSparsePostingList::merge_with_offsets(&[(&list1, 0), (&list2, doc_offset)]);
1570 let mut ref_iter = reference.iterator();
1571 let mut zc_iter = loaded.iterator();
1572 while ref_iter.doc() != super::TERMINATED {
1573 assert_eq!(
1574 ref_iter.doc(),
1575 zc_iter.doc(),
1576 "Zero-copy and reference merge should produce identical doc_ids"
1577 );
1578 assert!(
1579 (ref_iter.weight() - zc_iter.weight()).abs() < 0.01,
1580 "Weights should match: {} vs {}",
1581 ref_iter.weight(),
1582 zc_iter.weight()
1583 );
1584 ref_iter.advance();
1585 zc_iter.advance();
1586 }
1587 assert_eq!(zc_iter.doc(), super::TERMINATED);
1588 }
1589
1590 #[test]
1591 fn test_doc_count_single_value() {
1592 let postings: Vec<(DocId, u16, f32)> =
1594 vec![(0, 0, 1.0), (5, 0, 2.0), (10, 0, 3.0), (15, 0, 4.0)];
1595 let list =
1596 BlockSparsePostingList::from_postings(&postings, WeightQuantization::Float32).unwrap();
1597
1598 assert_eq!(list.doc_count(), 4);
1600 }
1601
1602 #[test]
1603 fn test_doc_count_multi_value_serialization_roundtrip() {
1604 let postings: Vec<(DocId, u16, f32)> =
1606 vec![(0, 0, 1.0), (0, 1, 1.5), (5, 0, 2.0), (5, 1, 2.5)];
1607 let list =
1608 BlockSparsePostingList::from_postings(&postings, WeightQuantization::Float32).unwrap();
1609 assert_eq!(list.doc_count(), 2);
1610
1611 let (block_data, skip_entries) = list.serialize().unwrap();
1612 let loaded =
1613 BlockSparsePostingList::from_parts(list.doc_count(), &block_data, &skip_entries)
1614 .unwrap();
1615 assert_eq!(loaded.doc_count(), 2);
1616 }
1617
1618 #[test]
1619 fn test_merge_preserves_weights_and_ordinals() {
1620 let postings1: Vec<(DocId, u16, f32)> = vec![(0, 0, 1.5), (5, 1, 2.5), (10, 2, 3.5)];
1622 let list1 =
1623 BlockSparsePostingList::from_postings(&postings1, WeightQuantization::Float32).unwrap();
1624
1625 let postings2: Vec<(DocId, u16, f32)> = vec![(0, 0, 4.5), (3, 1, 5.5), (7, 3, 6.5)];
1626 let list2 =
1627 BlockSparsePostingList::from_postings(&postings2, WeightQuantization::Float32).unwrap();
1628
1629 let merged = BlockSparsePostingList::merge_with_offsets(&[(&list1, 0), (&list2, 100)]);
1631
1632 let (block_data, skip_entries) = merged.serialize().unwrap();
1634 let loaded =
1635 BlockSparsePostingList::from_parts(merged.doc_count(), &block_data, &skip_entries)
1636 .unwrap();
1637
1638 let mut iter = loaded.iterator();
1640
1641 assert_eq!(iter.doc(), 0);
1643 assert!(
1644 (iter.weight() - 1.5).abs() < 0.01,
1645 "Weight should be 1.5, got {}",
1646 iter.weight()
1647 );
1648 assert_eq!(iter.ordinal(), 0);
1649
1650 iter.advance();
1651 assert_eq!(iter.doc(), 5);
1652 assert!(
1653 (iter.weight() - 2.5).abs() < 0.01,
1654 "Weight should be 2.5, got {}",
1655 iter.weight()
1656 );
1657 assert_eq!(iter.ordinal(), 1);
1658
1659 iter.advance();
1660 assert_eq!(iter.doc(), 10);
1661 assert!(
1662 (iter.weight() - 3.5).abs() < 0.01,
1663 "Weight should be 3.5, got {}",
1664 iter.weight()
1665 );
1666 assert_eq!(iter.ordinal(), 2);
1667
1668 iter.advance();
1670 assert_eq!(iter.doc(), 100);
1671 assert!(
1672 (iter.weight() - 4.5).abs() < 0.01,
1673 "Weight should be 4.5, got {}",
1674 iter.weight()
1675 );
1676 assert_eq!(iter.ordinal(), 0);
1677
1678 iter.advance();
1679 assert_eq!(iter.doc(), 103);
1680 assert!(
1681 (iter.weight() - 5.5).abs() < 0.01,
1682 "Weight should be 5.5, got {}",
1683 iter.weight()
1684 );
1685 assert_eq!(iter.ordinal(), 1);
1686
1687 iter.advance();
1688 assert_eq!(iter.doc(), 107);
1689 assert!(
1690 (iter.weight() - 6.5).abs() < 0.01,
1691 "Weight should be 6.5, got {}",
1692 iter.weight()
1693 );
1694 assert_eq!(iter.ordinal(), 3);
1695
1696 iter.advance();
1698 assert_eq!(iter.doc(), super::TERMINATED);
1699 }
1700
1701 #[test]
1702 fn test_merge_global_max_weight() {
1703 let postings1: Vec<(DocId, u16, f32)> = vec![
1705 (0, 0, 3.0),
1706 (1, 0, 7.0), (2, 0, 2.0),
1708 ];
1709 let list1 =
1710 BlockSparsePostingList::from_postings(&postings1, WeightQuantization::Float32).unwrap();
1711
1712 let postings2: Vec<(DocId, u16, f32)> = vec![
1713 (0, 0, 5.0),
1714 (1, 0, 4.0),
1715 (2, 0, 6.0), ];
1717 let list2 =
1718 BlockSparsePostingList::from_postings(&postings2, WeightQuantization::Float32).unwrap();
1719
1720 assert!((list1.global_max_weight() - 7.0).abs() < 0.01);
1722 assert!((list2.global_max_weight() - 6.0).abs() < 0.01);
1723
1724 let merged = BlockSparsePostingList::merge_with_offsets(&[(&list1, 0), (&list2, 100)]);
1726
1727 assert!(
1729 (merged.global_max_weight() - 7.0).abs() < 0.01,
1730 "Global max should be 7.0, got {}",
1731 merged.global_max_weight()
1732 );
1733
1734 let (block_data, skip_entries) = merged.serialize().unwrap();
1736 let loaded =
1737 BlockSparsePostingList::from_parts(merged.doc_count(), &block_data, &skip_entries)
1738 .unwrap();
1739
1740 assert!(
1741 (loaded.global_max_weight() - 7.0).abs() < 0.01,
1742 "After roundtrip, global max should still be 7.0, got {}",
1743 loaded.global_max_weight()
1744 );
1745 }
1746
1747 #[test]
1748 fn test_scoring_simulation_after_merge() {
1749 let postings1: Vec<(DocId, u16, f32)> = vec![
1751 (0, 0, 0.5), (5, 0, 0.8), ];
1754 let list1 =
1755 BlockSparsePostingList::from_postings(&postings1, WeightQuantization::Float32).unwrap();
1756
1757 let postings2: Vec<(DocId, u16, f32)> = vec![
1758 (0, 0, 0.6), (3, 0, 0.9), ];
1761 let list2 =
1762 BlockSparsePostingList::from_postings(&postings2, WeightQuantization::Float32).unwrap();
1763
1764 let merged = BlockSparsePostingList::merge_with_offsets(&[(&list1, 0), (&list2, 100)]);
1766
1767 let (block_data, skip_entries) = merged.serialize().unwrap();
1769 let loaded =
1770 BlockSparsePostingList::from_parts(merged.doc_count(), &block_data, &skip_entries)
1771 .unwrap();
1772
1773 let query_weight = 2.0f32;
1775 let mut iter = loaded.iterator();
1776
1777 assert_eq!(iter.doc(), 0);
1780 let score = query_weight * iter.weight();
1781 assert!(
1782 (score - 1.0).abs() < 0.01,
1783 "Doc 0 score should be 1.0, got {}",
1784 score
1785 );
1786
1787 iter.advance();
1788 assert_eq!(iter.doc(), 5);
1790 let score = query_weight * iter.weight();
1791 assert!(
1792 (score - 1.6).abs() < 0.01,
1793 "Doc 5 score should be 1.6, got {}",
1794 score
1795 );
1796
1797 iter.advance();
1798 assert_eq!(iter.doc(), 100);
1800 let score = query_weight * iter.weight();
1801 assert!(
1802 (score - 1.2).abs() < 0.01,
1803 "Doc 100 score should be 1.2, got {}",
1804 score
1805 );
1806
1807 iter.advance();
1808 assert_eq!(iter.doc(), 103);
1810 let score = query_weight * iter.weight();
1811 assert!(
1812 (score - 1.8).abs() < 0.01,
1813 "Doc 103 score should be 1.8, got {}",
1814 score
1815 );
1816 }
1817}