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