1use crate::directories::{FileHandle, OwnedBytes};
23use crate::segment::bmp_adaptive::{AdaptiveBlock, AdaptivePostings};
24use crate::segment::bmp_grid::CompressedGrid;
25
26pub const BMP_SUPERBLOCK_SIZE: u32 = 8;
33
34pub const BMP_COARSE_SUPERBLOCKS: u32 = 256;
40
41#[inline(always)]
53unsafe fn read_u32_unchecked(base: *const u8, idx: usize) -> u32 {
54 unsafe {
55 let p = base.add(idx * 4);
56 u32::from_le((p as *const u32).read_unaligned())
57 }
58}
59
60#[inline(always)]
66unsafe fn read_u64_unchecked(base: *const u8, idx: usize) -> u64 {
67 unsafe {
68 let p = base.add(idx * 8);
69 u64::from_le((p as *const u64).read_unaligned())
70 }
71}
72
73#[derive(Debug, Clone)]
75pub struct BmpDimStats {
76 pub nonzero_dims: u32,
77 pub declared_dims: u32,
78 pub total_postings: u64,
79 pub p50_postings_per_dim: u64,
80 pub p99_postings_per_dim: u64,
81 pub max_postings_per_dim: u64,
82 pub top_1pct_share: f64,
84 pub saturated_impacts: u64,
86 pub top_dims: Vec<(u32, u64)>,
87}
88
89#[derive(Clone)]
104pub struct BmpIndex {
105 pub bmp_block_size: u32,
107 pub num_blocks: u32,
109 pub num_virtual_docs: u32,
111 pub max_weight_scale: f32,
113 pub total_vectors: u32,
115 segment_num_docs: u32,
118
119 dims: u32,
122 total_terms: u64,
123 total_postings: u64,
124 grid_bits: u8,
126 num_real_docs: u32,
128 single_valued: bool,
131 logically_ordered: bool,
132 forward: Option<crate::segment::bmp_forward::BmpForward>,
133
134 block_data_starts_bytes: OwnedBytes,
137 block_data_bytes: OwnedBytes,
139 block_grid: CompressedGrid,
142 superblock_grid: CompressedGrid,
144 pub num_superblocks: u32,
146 coarse_grid: CompressedGrid,
148 pub num_coarse_groups: u32,
150 doc_map_ids_bytes: OwnedBytes,
152 doc_map_ordinals_bytes: OwnedBytes,
154
155 #[cfg_attr(not(feature = "native"), allow(dead_code))]
160 source: FileHandle,
161 #[cfg_attr(not(feature = "native"), allow(dead_code))]
162 blob_offset: u64,
163 #[cfg_attr(not(feature = "native"), allow(dead_code))]
164 blob_len: u64,
165 #[cfg_attr(not(feature = "native"), allow(dead_code))]
169 doc_map_offset: u64,
170}
171
172impl BmpIndex {
178 pub fn parse(
187 handle: FileHandle,
188 blob_offset: u64,
189 blob_len: u64,
190 total_docs: u32,
191 total_vectors: u32,
192 ) -> crate::Result<Self> {
193 use crate::segment::format::{BMP_BLOB_FOOTER_SIZE, BMP_BLOB_MAGIC};
194
195 if blob_len < BMP_BLOB_FOOTER_SIZE as u64 {
196 return Err(crate::Error::Corruption(
197 "BMP blob too small for versioned footer".into(),
198 ));
199 }
200
201 let blob_end = blob_offset
203 .checked_add(blob_len)
204 .ok_or_else(|| crate::Error::Corruption("BMP blob range overflows u64".into()))?;
205 let footer_start = blob_end - BMP_BLOB_FOOTER_SIZE as u64;
206 let footer_bytes = handle
207 .read_bytes_range_sync(footer_start..blob_end)
208 .map_err(crate::Error::Io)?;
209 let fb = footer_bytes.as_slice();
210
211 let total_terms = u64::from_le_bytes(fb[0..8].try_into().unwrap());
212 let total_postings = u64::from_le_bytes(fb[8..16].try_into().unwrap());
213 let grid_offset = u64::from_le_bytes(fb[16..24].try_into().unwrap());
214 let sb_grid_offset = u64::from_le_bytes(fb[24..32].try_into().unwrap());
215 let coarse_grid_offset = u64::from_le_bytes(fb[32..40].try_into().unwrap());
216 let num_blocks = u32::from_le_bytes(fb[40..44].try_into().unwrap());
217 let dims = u32::from_le_bytes(fb[44..48].try_into().unwrap());
218 let bmp_block_size = u32::from_le_bytes(fb[48..52].try_into().unwrap());
219 let num_virtual_docs = u32::from_le_bytes(fb[52..56].try_into().unwrap());
220 let max_weight_scale = f32::from_le_bytes(fb[56..60].try_into().unwrap());
221 let doc_map_offset = u64::from_le_bytes(fb[60..68].try_into().unwrap());
222 let num_real_docs = u32::from_le_bytes(fb[68..72].try_into().unwrap());
223 let grid_bits_raw = u32::from_le_bytes(fb[72..76].try_into().unwrap());
224 let magic = u32::from_le_bytes(fb[76..80].try_into().unwrap());
225
226 if magic != BMP_BLOB_MAGIC {
227 return Err(crate::Error::Corruption(format!(
228 "Unsupported BMP blob magic: {:#x} (expected BMPB {:#x}); migrate or rebuild \
229 the index with a compatible Summa release.",
230 magic, BMP_BLOB_MAGIC
231 )));
232 }
233 let grid_bits: u8 = match grid_bits_raw {
234 4 => 4,
235 2 => 2,
236 other => {
237 return Err(crate::Error::Corruption(format!(
238 "Unsupported BMP grid_bits {} (expected 2 or 4) — data too new to read?",
239 other
240 )));
241 }
242 };
243
244 if num_blocks == 0 {
246 if blob_len
247 != (BMP_BLOB_FOOTER_SIZE + crate::segment::bmp_forward::TRAILER_BYTES) as u64
248 {
249 return Err(crate::Error::Corruption(
250 "empty BMP index must contain only the storage trailer and footer".into(),
251 ));
252 }
253 if num_virtual_docs != 0
254 || num_real_docs != 0
255 || total_terms != 0
256 || total_postings != 0
257 || grid_offset != 0
258 || sb_grid_offset != 0
259 || coarse_grid_offset != 0
260 || doc_map_offset != 0
261 {
262 return Err(crate::Error::Corruption(format!(
263 "empty BMP index has non-zero document counts (virtual={}, real={})",
264 num_virtual_docs, num_real_docs
265 )));
266 }
267 if !(1..=256).contains(&bmp_block_size)
268 || !max_weight_scale.is_finite()
269 || max_weight_scale <= 0.0
270 {
271 return Err(crate::Error::Corruption(
272 "invalid empty BMP block size or scale".into(),
273 ));
274 }
275 let forward = crate::segment::bmp_forward::BmpForward::parse_optional(
276 handle
277 .read_bytes_range_sync(blob_offset..footer_start)
278 .map_err(crate::Error::Io)?,
279 0,
280 total_docs,
281 dims,
282 )?;
283 return Ok(Self {
284 bmp_block_size,
285 num_blocks,
286 num_virtual_docs,
287 max_weight_scale,
288 total_vectors,
289 segment_num_docs: total_docs,
290 dims,
291 total_terms: 0,
292 total_postings: 0,
293 grid_bits,
294 num_real_docs,
295 single_valued: true,
296 logically_ordered: true,
297 forward,
298 block_data_starts_bytes: OwnedBytes::empty(),
299 block_data_bytes: OwnedBytes::empty(),
300 block_grid: CompressedGrid::empty(),
301 superblock_grid: CompressedGrid::empty(),
302 num_superblocks: 0,
303 coarse_grid: CompressedGrid::empty(),
304 num_coarse_groups: 0,
305 doc_map_ids_bytes: OwnedBytes::empty(),
306 doc_map_ordinals_bytes: OwnedBytes::empty(),
307 source: handle,
308 blob_offset,
309 blob_len,
310 doc_map_offset,
311 });
312 }
313
314 if !(1..=256).contains(&bmp_block_size) {
315 return Err(crate::Error::Corruption(format!(
316 "invalid BMP block size {} (expected 1..=256)",
317 bmp_block_size
318 )));
319 }
320 let expected_virtual_docs = u64::from(num_blocks) * u64::from(bmp_block_size);
321 if expected_virtual_docs != u64::from(num_virtual_docs) {
322 return Err(crate::Error::Corruption(format!(
323 "BMP block/document mismatch: {} blocks × {} != {} virtual docs",
324 num_blocks, bmp_block_size, num_virtual_docs
325 )));
326 }
327 if num_real_docs > num_virtual_docs {
328 return Err(crate::Error::Corruption(format!(
329 "BMP real document count {} exceeds virtual count {}",
330 num_real_docs, num_virtual_docs
331 )));
332 }
333 if !max_weight_scale.is_finite() || max_weight_scale <= 0.0 {
334 return Err(crate::Error::Corruption(format!(
335 "invalid BMP max-weight scale {}",
336 max_weight_scale
337 )));
338 }
339
340 let data_len = blob_len - BMP_BLOB_FOOTER_SIZE as u64;
342 let data_len_usize = usize::try_from(data_len).map_err(|_| {
343 crate::Error::Corruption("BMP blob is too large for this platform".into())
344 })?;
345 let blob = handle
346 .read_bytes_range_sync(blob_offset..footer_start)
347 .map_err(crate::Error::Io)?;
348
349 let num_blocks_usize = num_blocks as usize;
352 let section_a_size = num_blocks_usize
353 .checked_add(1)
354 .and_then(|count| count.checked_mul(8))
355 .ok_or_else(|| {
356 crate::Error::Corruption("BMP block-offset table size overflows usize".into())
357 })?;
358 let grid_start = usize::try_from(grid_offset).map_err(|_| {
359 crate::Error::Corruption("BMP grid offset is too large for this platform".into())
360 })?;
361 let bds_start = grid_start.checked_sub(section_a_size).ok_or_else(|| {
362 crate::Error::Corruption(format!(
363 "BMP grid offset {} precedes {}-byte block-offset table",
364 grid_offset, section_a_size
365 ))
366 })?;
367 if grid_start > data_len_usize {
368 return Err(crate::Error::Corruption(format!(
369 "BMP grid offset {} exceeds data length {}",
370 grid_start, data_len_usize
371 )));
372 }
373
374 let block_data_bytes = blob.slice(0..bds_start);
376 let block_data_starts_bytes = blob.slice(bds_start..grid_start);
378
379 let num_superblocks = num_blocks.div_ceil(BMP_SUPERBLOCK_SIZE);
384 let num_coarse_groups = num_superblocks.div_ceil(BMP_COARSE_SUPERBLOCKS);
385 let sb_grid_start = usize::try_from(sb_grid_offset).map_err(|_| {
386 crate::Error::Corruption("BMP superblock-grid offset is too large".into())
387 })?;
388 if sb_grid_start < grid_start || sb_grid_start > data_len_usize {
389 return Err(crate::Error::Corruption(format!(
390 "BMP section order mismatch: block grid starts at {}, superblock grid at {}, data ends at {}",
391 grid_start, sb_grid_start, data_len_usize
392 )));
393 }
394 let coarse_grid_start = usize::try_from(coarse_grid_offset)
395 .map_err(|_| crate::Error::Corruption("BMP coarse-grid offset is too large".into()))?;
396 if coarse_grid_start < sb_grid_start || coarse_grid_start > data_len_usize {
397 return Err(crate::Error::Corruption(format!(
398 "BMP section order mismatch: superblock grid starts at {}, coarse grid at {}, data ends at {}",
399 sb_grid_start, coarse_grid_start, data_len_usize
400 )));
401 }
402
403 let dm_start = usize::try_from(doc_map_offset)
404 .map_err(|_| crate::Error::Corruption("BMP document-map offset is too large".into()))?;
405 if dm_start < coarse_grid_start || dm_start > data_len_usize {
406 return Err(crate::Error::Corruption(format!(
407 "BMP section order mismatch: coarse grid starts at {}, document map at {}, data ends at {}",
408 coarse_grid_start, dm_start, data_len_usize
409 )));
410 }
411 let dm_ids_len = (num_virtual_docs as usize).checked_mul(4).ok_or_else(|| {
412 crate::Error::Corruption("BMP document-id map size overflows usize".into())
413 })?;
414 let dm_ords_len = (num_virtual_docs as usize).checked_mul(2).ok_or_else(|| {
415 crate::Error::Corruption("BMP ordinal map size overflows usize".into())
416 })?;
417 let dm_ids_end = dm_start.checked_add(dm_ids_len).ok_or_else(|| {
418 crate::Error::Corruption("BMP document-id map end overflows usize".into())
419 })?;
420 let dm_ords_end = dm_ids_end.checked_add(dm_ords_len).ok_or_else(|| {
421 crate::Error::Corruption("BMP ordinal map end overflows usize".into())
422 })?;
423 if dm_ords_end > data_len_usize {
424 return Err(crate::Error::Corruption(format!(
425 "BMP data length mismatch: sections end at {}, blob data ends at {}",
426 dm_ords_end, data_len_usize
427 )));
428 }
429
430 let forward = crate::segment::bmp_forward::BmpForward::parse_optional(
431 blob.slice(dm_ords_end..data_len_usize),
432 num_real_docs,
433 total_docs,
434 dims,
435 )?;
436
437 let block_grid = CompressedGrid::parse(
439 blob.slice(grid_start..sb_grid_start),
440 dims as usize,
441 num_blocks as usize,
442 grid_bits,
443 "BMP block grid",
444 )?;
445 let superblock_grid = CompressedGrid::parse(
446 blob.slice(sb_grid_start..coarse_grid_start),
447 dims as usize,
448 num_superblocks as usize,
449 4,
450 "BMP superblock grid",
451 )?;
452 let coarse_grid = CompressedGrid::parse(
453 blob.slice(coarse_grid_start..dm_start),
454 dims as usize,
455 num_coarse_groups as usize,
456 4,
457 "BMP coarse grid",
458 )?;
459 let doc_map_ids_bytes = blob.slice(dm_start..dm_ids_end);
460 let doc_map_ordinals_bytes = blob.slice(dm_ids_end..dm_ords_end);
461 let logically_ordered = crate::segment::logical_address::logically_ordered(
462 doc_map_ids_bytes
463 .as_slice()
464 .chunks_exact(4)
465 .zip(doc_map_ordinals_bytes.as_slice().chunks_exact(2))
466 .map(|(doc, ordinal)| {
467 let doc = u32::from_le_bytes(doc.try_into().unwrap());
468 (doc != u32::MAX).then(|| crate::segment::logical_address::LogicalUnit {
469 doc,
470 ordinal: u16::from_le_bytes(ordinal.try_into().unwrap()),
471 })
472 }),
473 );
474 let single_valued = doc_map_ordinals_bytes
475 .as_slice()
476 .chunks_exact(2)
477 .all(|ordinal| ordinal == [0, 0]);
478
479 let starts = block_data_starts_bytes.as_slice();
482 let mut previous = 0u64;
483 for index in 0..=num_blocks_usize {
484 let offset = index * 8;
485 let current = u64::from_le_bytes(starts[offset..offset + 8].try_into().unwrap());
486 if (index == 0 && current != 0) || current < previous || current > bds_start as u64 {
487 return Err(crate::Error::Corruption(format!(
488 "invalid BMP block offset at {}: {} (previous={}, data_limit={})",
489 index, current, previous, bds_start
490 )));
491 }
492 if current > previous && current - previous < 8 {
493 return Err(crate::Error::Corruption(format!(
494 "BMP block {} is too small for a header ({} bytes)",
495 index - 1,
496 current - previous
497 )));
498 }
499 previous = current;
500 }
501
502 #[cfg(feature = "native")]
514 {
515 block_data_bytes.madvise(libc::MADV_RANDOM);
516 doc_map_ids_bytes.madvise(libc::MADV_RANDOM);
517 doc_map_ordinals_bytes.madvise(libc::MADV_RANDOM);
518 block_grid.madvise_rows(libc::MADV_RANDOM);
519 superblock_grid.madvise_rows(libc::MADV_RANDOM);
520 coarse_grid.madvise_rows(libc::MADV_SEQUENTIAL);
521 }
522
523 log::debug!(
524 "BMPB index loaded: num_blocks={}, num_superblocks={}, coarse_groups={}, dims={}, bmp_block_size={}, \
525 num_virtual_docs={}, num_real_docs={}, max_weight_scale={:.4}, postings={}, \
526 block_grid={}, superblock_grid={}, coarse_grid={}, single_valued={}, block_data={}, doc_map={}, forward={}",
527 num_blocks,
528 num_superblocks,
529 num_coarse_groups,
530 dims,
531 bmp_block_size,
532 num_virtual_docs,
533 num_real_docs,
534 max_weight_scale,
535 total_postings,
536 crate::format_bytes(block_grid.encoded_bytes() as u64),
537 crate::format_bytes(superblock_grid.encoded_bytes() as u64),
538 crate::format_bytes(coarse_grid.encoded_bytes() as u64),
539 single_valued,
540 crate::format_bytes(bds_start as u64),
541 crate::format_bytes(u64::from(num_virtual_docs) * 6),
542 crate::format_bytes(forward.as_ref().map_or(0, |f| f.encoded_bytes()) as u64),
543 );
544
545 Ok(Self {
546 bmp_block_size,
547 num_blocks,
548 num_virtual_docs,
549 max_weight_scale,
550 total_vectors,
551 segment_num_docs: total_docs,
552 dims,
553 total_terms,
554 total_postings,
555 grid_bits,
556 num_real_docs,
557 single_valued,
558 logically_ordered,
559 forward,
560 block_data_starts_bytes,
561 block_data_bytes,
562 block_grid,
563 superblock_grid,
564 num_superblocks,
565 coarse_grid,
566 num_coarse_groups,
567 doc_map_ids_bytes,
568 doc_map_ordinals_bytes,
569 source: handle,
570 blob_offset,
571 blob_len,
572 doc_map_offset,
573 })
574 }
575
576 #[cfg_attr(not(feature = "native"), allow(dead_code))]
581 pub(crate) fn read_raw_blob(&self) -> std::io::Result<OwnedBytes> {
582 self.source
583 .read_bytes_range_sync(self.blob_offset..self.blob_offset + self.blob_len)
584 }
585
586 pub(crate) fn logically_ordered(&self) -> bool {
587 self.logically_ordered
588 }
589
590 pub(crate) fn ordered_slots_for_document(
591 &self,
592 doc: u32,
593 ) -> impl Iterator<Item = (u16, u32)> + '_ {
594 crate::segment::logical_address::ordered_document_slots(
595 self.num_virtual_docs,
596 doc,
597 |slot| {
598 let (doc, ordinal) = self.virtual_to_doc(slot);
599 (doc != u32::MAX)
600 .then_some(crate::segment::logical_address::LogicalUnit { doc, ordinal })
601 },
602 )
603 }
604
605 #[inline(always)]
610 pub fn virtual_to_doc(&self, virtual_id: u32) -> (u32, u16) {
611 if virtual_id >= self.num_virtual_docs {
612 return (u32::MAX, 0);
613 }
614 let ids = self.doc_map_ids_bytes.as_slice();
615 let ords = self.doc_map_ordinals_bytes.as_slice();
616 debug_assert!((virtual_id as usize + 1) * 4 <= ids.len());
617 debug_assert!((virtual_id as usize + 1) * 2 <= ords.len());
618 unsafe {
619 let doc_id = read_u32_unchecked(ids.as_ptr(), virtual_id as usize);
620 if doc_id >= self.segment_num_docs {
621 return (u32::MAX, 0);
622 }
623 let p = ords.as_ptr().add(virtual_id as usize * 2);
624 let ordinal = u16::from_le((p as *const u16).read_unaligned());
625 (doc_id, ordinal)
626 }
627 }
628
629 #[inline(always)]
632 pub fn doc_id_for_virtual(&self, virtual_id: u32) -> u32 {
633 if virtual_id >= self.num_virtual_docs {
634 return u32::MAX;
635 }
636 let d = self.doc_map_ids_bytes.as_slice();
637 debug_assert!((virtual_id as usize + 1) * 4 <= d.len());
638 let doc_id = unsafe { read_u32_unchecked(d.as_ptr(), virtual_id as usize) };
639 if doc_id < self.segment_num_docs {
640 doc_id
641 } else {
642 u32::MAX
643 }
644 }
645
646 #[inline(always)]
650 pub(crate) fn block_data_range(&self, block_id: u32) -> (u64, u64) {
651 let d = self.block_data_starts_bytes.as_slice();
652 debug_assert!((block_id as usize + 2) * 8 <= d.len());
653 unsafe {
654 let start = read_u64_unchecked(d.as_ptr(), block_id as usize);
655 let end = read_u64_unchecked(d.as_ptr(), block_id as usize + 1);
656 (start, end)
657 }
658 }
659
660 #[cfg(feature = "native")]
663 pub(crate) fn pin_block_starts(
664 &mut self,
665 mode: crate::segment::pin::PinMode,
666 remaining: &mut u64,
667 report: &mut crate::segment::pin::PinReport,
668 ) {
669 crate::segment::pin::pin_section(
670 &mut self.block_data_starts_bytes,
671 "bmp block_data_starts",
672 mode,
673 remaining,
674 report,
675 );
676 self.block_grid
677 .pin_offsets("bmp block_grid row_offsets", mode, remaining, report);
678 }
679
680 #[cfg(feature = "native")]
683 pub(crate) fn pin_doc_maps(
684 &mut self,
685 mode: crate::segment::pin::PinMode,
686 remaining: &mut u64,
687 report: &mut crate::segment::pin::PinReport,
688 ) {
689 crate::segment::pin::pin_section(
690 &mut self.doc_map_ids_bytes,
691 "bmp doc_map_ids",
692 mode,
693 remaining,
694 report,
695 );
696 crate::segment::pin::pin_section(
697 &mut self.doc_map_ordinals_bytes,
698 "bmp doc_map_ordinals",
699 mode,
700 remaining,
701 report,
702 );
703 }
704
705 #[cfg(feature = "native")]
712 pub(crate) fn pin_query_hierarchy(
713 &mut self,
714 mode: crate::segment::pin::PinMode,
715 remaining: &mut u64,
716 report: &mut crate::segment::pin::PinReport,
717 ) {
718 self.superblock_grid
719 .pin_offsets("bmp sb_grid row_offsets", mode, remaining, report);
720 self.coarse_grid.pin_all(
721 "bmp coarse_grid row_offsets",
722 "bmp coarse_grid rows",
723 mode,
724 remaining,
725 report,
726 );
727 }
728
729 #[cfg(feature = "native")]
733 #[inline]
734 pub(crate) fn block_data_resident(&self) -> bool {
735 !self.block_data_bytes.is_mmap()
736 }
737
738 #[cfg(feature = "native")]
746 #[inline]
747 pub(crate) fn prefetch_block_data(&self, byte_start: u64, byte_end: u64) {
748 self.block_data_bytes
749 .madvise_range(byte_start as usize..byte_end as usize, libc::MADV_WILLNEED);
750 }
751
752 #[cfg(feature = "native")]
759 pub(crate) fn prefetch_block_data_ranges(
760 &self,
761 ranges: &mut Vec<std::ops::Range<u64>>,
762 ) -> (usize, usize) {
763 if ranges.is_empty() {
764 return (0, 0);
765 }
766 const PAGE_NEAR_BYTES: u64 = 4096;
767 ranges.sort_unstable_by_key(|range| (range.start, range.end));
768 let mut advised_bytes = 0usize;
769 let mut calls = 0usize;
770 let mut current = ranges[0].clone();
771 for range in &ranges[1..] {
772 if range.start <= current.end.saturating_add(PAGE_NEAR_BYTES) {
773 current.end = current.end.max(range.end);
774 continue;
775 }
776 advised_bytes = advised_bytes.saturating_add((current.end - current.start) as usize);
777 calls += 1;
778 self.prefetch_block_data(current.start, current.end);
779 current = range.clone();
780 }
781 advised_bytes = advised_bytes.saturating_add((current.end - current.start) as usize);
782 calls += 1;
783 self.prefetch_block_data(current.start, current.end);
784 ranges.clear();
785 (advised_bytes, calls)
786 }
787
788 #[inline(always)]
791 pub(crate) fn block_data_ptr(&self, block_id: u32) -> *const u8 {
792 let (start, _) = self.block_data_range(block_id);
793 unsafe {
794 self.block_data_bytes
795 .as_slice()
796 .as_ptr()
797 .add(start as usize)
798 }
799 }
800
801 #[inline(always)]
804 pub(crate) fn parse_block(&self, block_id: u32) -> Option<AdaptiveBlock<'_>> {
805 if block_id >= self.num_blocks {
806 return None;
807 }
808 let (start, end) = self.block_data_range(block_id);
809 if start == end {
810 return None;
811 }
812 let start = usize::try_from(start).ok()?;
813 let end = usize::try_from(end).ok()?;
814 let bytes = self.block_data_bytes.as_slice().get(start..end)?;
815 AdaptiveBlock::parse(bytes, self.bmp_block_size as usize)
816 }
817
818 #[inline(always)]
822 pub(crate) fn block_data_starts_ptr(&self, block_id: u32) -> *const u8 {
823 unsafe {
824 self.block_data_starts_bytes
825 .as_slice()
826 .as_ptr()
827 .add(block_id as usize * 8)
828 }
829 }
830
831 #[cfg_attr(not(any(feature = "native", feature = "wasm")), allow(dead_code))]
836 pub(crate) fn iter_block_terms(
837 &self,
838 block_id: u32,
839 ) -> impl Iterator<Item = (u32, u8, AdaptivePostings<'_>)> + '_ {
840 self.parse_block(block_id)
841 .into_iter()
842 .flat_map(AdaptiveBlock::terms)
843 }
844
845 pub fn dims(&self) -> u32 {
849 self.dims
850 }
851
852 #[cfg(any(feature = "native", test))]
859 pub(crate) fn validate_rewrite_layout(
860 &self,
861 context: &str,
862 expected_dims: u32,
863 expected_block_size: u32,
864 expected_grid_bits: u8,
865 expected_max_weight_scale: f32,
866 ) -> crate::Result<()> {
867 if expected_dims == 0 {
868 return Err(crate::Error::Corruption(format!(
869 "{context}: expected vocabulary is empty",
870 )));
871 }
872 if self.dims != expected_dims {
873 return Err(crate::Error::Corruption(format!(
874 "{context}: source dims={} != expected {expected_dims}",
875 self.dims,
876 )));
877 }
878 if self.bmp_block_size != expected_block_size {
879 return Err(crate::Error::Corruption(format!(
880 "{context}: source block_size={} != expected {expected_block_size}",
881 self.bmp_block_size,
882 )));
883 }
884 if self.grid_bits != expected_grid_bits {
885 return Err(crate::Error::Corruption(format!(
886 "{context}: source grid_bits={} != expected {expected_grid_bits}",
887 self.grid_bits,
888 )));
889 }
890 if !expected_max_weight_scale.is_finite() || expected_max_weight_scale <= 0.0 {
891 return Err(crate::Error::Corruption(format!(
892 "{context}: invalid expected max_weight_scale={expected_max_weight_scale}",
893 )));
894 }
895 if self.max_weight_scale.to_bits() != expected_max_weight_scale.to_bits() {
896 return Err(crate::Error::Corruption(format!(
897 "{context}: source max_weight_scale={:.4} != expected {:.4}",
898 self.max_weight_scale, expected_max_weight_scale,
899 )));
900 }
901 Ok(())
902 }
903
904 #[cfg(any(feature = "native", feature = "wasm", test))]
909 pub(crate) fn visit_real_slots_for_rewrite(
910 &self,
911 check_cancel: &(impl Fn() -> crate::Result<()> + Sync),
912 mut visitor: impl FnMut(usize),
913 ) -> crate::Result<()> {
914 let expected_real = self.num_real_docs as usize;
915 let mut real_slots = 0usize;
916 for (virtual_id, chunk) in self
917 .doc_map_ids_bytes
918 .as_slice()
919 .chunks_exact(4)
920 .enumerate()
921 {
922 if virtual_id.is_multiple_of(256) {
923 check_cancel()?;
924 }
925 let doc_id = u32::from_le_bytes(chunk.try_into().unwrap());
926 if doc_id == u32::MAX {
927 continue;
928 }
929 if doc_id >= self.segment_num_docs {
930 return Err(crate::Error::Corruption(format!(
931 "BMP document map contains doc id {doc_id} outside segment bound {}",
932 self.segment_num_docs,
933 )));
934 }
935 if real_slots == expected_real {
936 return Err(crate::Error::Corruption(format!(
937 "BMP document map contains more than the footer's {expected_real} real slots"
938 )));
939 }
940 visitor(virtual_id);
941 real_slots += 1;
942 }
943 if real_slots != expected_real {
944 return Err(crate::Error::Corruption(format!(
945 "BMP document map has {real_slots} real slots but footer declares {expected_real}",
946 )));
947 }
948 Ok(())
949 }
950
951 #[cfg(any(feature = "native", test))]
956 pub(crate) fn validate_block_for_rewrite(&self, block_id: u32) -> crate::Result<()> {
957 if block_id >= self.num_blocks {
958 return Err(crate::Error::Corruption(format!(
959 "BMP rewrite block {block_id} exceeds block count {}",
960 self.num_blocks,
961 )));
962 }
963 let (start, end) = self.block_data_range(block_id);
964 let start = usize::try_from(start)
965 .map_err(|_| crate::Error::Corruption("BMP block start exceeds usize".into()))?;
966 let end = usize::try_from(end)
967 .map_err(|_| crate::Error::Corruption("BMP block end exceeds usize".into()))?;
968 let block = self
969 .block_data_bytes
970 .as_slice()
971 .get(start..end)
972 .ok_or_else(|| {
973 crate::Error::Corruption(format!(
974 "BMP block {block_id} range {start}..{end} exceeds block data",
975 ))
976 })?;
977 if block.is_empty() {
978 return Ok(());
979 }
980 let parsed =
981 AdaptiveBlock::parse(block, self.bmp_block_size as usize).ok_or_else(|| {
982 crate::Error::Corruption(format!(
983 "BMP block {block_id} has an invalid adaptive envelope"
984 ))
985 })?;
986 parsed.validate(self.dims).map_err(|reason| {
987 crate::Error::Corruption(format!("BMP block {block_id} is invalid: {reason}"))
988 })
989 }
990
991 pub fn total_terms(&self) -> u64 {
993 self.total_terms
994 }
995
996 pub fn total_postings(&self) -> u64 {
998 self.total_postings
999 }
1000
1001 pub fn num_real_docs(&self) -> u32 {
1003 self.num_real_docs
1004 }
1005
1006 pub fn is_single_valued(&self) -> bool {
1011 self.single_valued
1012 }
1013
1014 pub(crate) fn forward(&self) -> Option<&crate::segment::bmp_forward::BmpForward> {
1015 self.forward.as_ref()
1016 }
1017
1018 #[cfg(feature = "native")]
1019 pub(crate) fn forward_payload_file_range(&self) -> std::ops::Range<u64> {
1020 let start = self.blob_offset + self.doc_map_offset + u64::from(self.num_virtual_docs) * 6;
1021 let bytes = self
1022 .forward
1023 .as_ref()
1024 .map_or(0, |forward| forward.payload_bytes());
1025 start..start + bytes as u64
1026 }
1027
1028 pub fn estimated_heap_bytes(&self) -> usize {
1031 std::mem::size_of::<Self>()
1032 }
1033
1034 pub fn grid_bits(&self) -> u8 {
1036 self.grid_bits
1037 }
1038
1039 pub fn dim_stats(&self, top: usize) -> BmpDimStats {
1048 let mut per_dim: rustc_hash::FxHashMap<u32, u64> = rustc_hash::FxHashMap::default();
1049 let mut total_postings = 0u64;
1050 let mut saturated = 0u64;
1051 for block_id in 0..self.num_blocks {
1052 for (dim, _, postings) in self.iter_block_terms(block_id) {
1053 let mut count = 0u64;
1054 for posting in postings {
1055 count += 1;
1056 if posting.impact == u8::MAX {
1057 saturated += 1;
1058 }
1059 }
1060 *per_dim.entry(dim).or_default() += count;
1061 total_postings += count;
1062 }
1063 }
1064 let mut counts: Vec<u64> = per_dim.values().copied().collect();
1065 counts.sort_unstable();
1066 let percentile = |fraction: f64| -> u64 {
1067 if counts.is_empty() {
1068 0
1069 } else {
1070 counts[((counts.len() - 1) as f64 * fraction) as usize]
1071 }
1072 };
1073 let mut top_dims: Vec<(u32, u64)> = per_dim.into_iter().collect();
1074 top_dims.sort_unstable_by_key(|&(dim, count)| (std::cmp::Reverse(count), dim));
1075 top_dims.truncate(top);
1076 let hot = counts.len().div_ceil(100);
1079 let top_1pct_postings: u64 = counts.iter().rev().take(hot).sum();
1080 BmpDimStats {
1081 nonzero_dims: counts.len() as u32,
1082 declared_dims: self.dims(),
1083 total_postings,
1084 p50_postings_per_dim: percentile(0.50),
1085 p99_postings_per_dim: percentile(0.99),
1086 max_postings_per_dim: counts.last().copied().unwrap_or(0),
1087 top_1pct_share: if total_postings == 0 {
1088 0.0
1089 } else {
1090 top_1pct_postings as f64 / total_postings as f64
1091 },
1092 saturated_impacts: saturated,
1093 top_dims,
1094 }
1095 }
1096
1097 #[inline]
1099 pub(crate) fn block_grid(&self) -> &CompressedGrid {
1100 &self.block_grid
1101 }
1102
1103 #[inline]
1105 pub(crate) fn superblock_grid(&self) -> &CompressedGrid {
1106 &self.superblock_grid
1107 }
1108
1109 #[inline]
1111 pub(crate) fn coarse_grid(&self) -> &CompressedGrid {
1112 &self.coarse_grid
1113 }
1114
1115 pub fn for_each_block_grid_chunk(
1121 &self,
1122 dimension: u32,
1123 mut visitor: impl FnMut(usize, usize, Option<&[u8]>),
1124 ) -> crate::Result<()> {
1125 let dimension = dimension as usize;
1126 if dimension >= self.block_grid.dims() {
1127 return Err(crate::Error::Query(format!(
1128 "BMP block-grid dimension {dimension} exceeds {}",
1129 self.block_grid.dims()
1130 )));
1131 }
1132 let mut decoded = [0u8; crate::segment::bmp_grid::GRID_GROUP_CELLS];
1133 self.block_grid
1134 .try_for_each_row_group(dimension, |group_id, group| {
1135 let start = group_id * crate::segment::bmp_grid::GRID_GROUP_CELLS;
1136 let count =
1137 crate::segment::bmp_grid::GRID_GROUP_CELLS.min(self.block_grid.cells() - start);
1138 if group.width() == 0 {
1139 visitor(start, count, None);
1140 } else {
1141 group.decode(0, count, &mut decoded);
1142 visitor(start, count, Some(&decoded[..count]));
1143 }
1144 Ok(())
1145 })
1146 }
1147
1148 #[inline]
1152 pub fn block_data_slice(&self) -> &[u8] {
1153 self.block_data_bytes.as_slice()
1154 }
1155
1156 #[inline]
1158 pub fn block_data_start(&self, block_id: u32) -> u64 {
1159 let d = self.block_data_starts_bytes.as_slice();
1160 let off = block_id as usize * 8;
1161 u64::from_le_bytes(d[off..off + 8].try_into().unwrap())
1162 }
1163
1164 #[inline]
1166 pub fn block_data_sentinel(&self) -> u64 {
1167 self.block_data_start(self.num_blocks)
1168 }
1169
1170 #[inline]
1173 pub fn doc_map_ids_slice(&self) -> &[u8] {
1174 self.doc_map_ids_bytes.as_slice()
1175 }
1176
1177 #[inline]
1180 pub fn doc_map_ordinals_slice(&self) -> &[u8] {
1181 self.doc_map_ordinals_bytes.as_slice()
1182 }
1183
1184 #[cfg(feature = "native")]
1186 pub(crate) fn block_data_file_range(&self) -> std::ops::Range<u64> {
1187 self.blob_offset..self.blob_offset + self.block_data_sentinel()
1188 }
1189
1190 #[cfg(feature = "native")]
1192 pub(crate) fn doc_map_ids_file_range(&self) -> std::ops::Range<u64> {
1193 let start = self.blob_offset + self.doc_map_offset;
1194 start..start + u64::from(self.num_virtual_docs) * 4
1195 }
1196
1197 #[cfg(feature = "native")]
1199 pub(crate) fn doc_map_ordinals_file_range(&self) -> std::ops::Range<u64> {
1200 let start = self.blob_offset + self.doc_map_offset + u64::from(self.num_virtual_docs) * 4;
1201 start..start + u64::from(self.num_virtual_docs) * 2
1202 }
1203
1204 #[cfg(feature = "native")]
1208 pub fn madvise_sequential(&self) {
1209 if let Some(forward) = &self.forward {
1210 forward.advise(libc::MADV_SEQUENTIAL);
1211 }
1212 Self::madvise_owned(&self.block_data_bytes, libc::MADV_SEQUENTIAL);
1213 Self::madvise_owned(&self.block_data_starts_bytes, libc::MADV_SEQUENTIAL);
1214 self.block_grid.madvise_rows(libc::MADV_SEQUENTIAL);
1215 self.superblock_grid.madvise_rows(libc::MADV_SEQUENTIAL);
1216 self.coarse_grid.madvise_rows(libc::MADV_SEQUENTIAL);
1217 Self::madvise_owned(&self.doc_map_ids_bytes, libc::MADV_SEQUENTIAL);
1218 Self::madvise_owned(&self.doc_map_ordinals_bytes, libc::MADV_SEQUENTIAL);
1219 }
1220
1221 #[cfg(feature = "native")]
1224 pub fn madvise_dontneed_block_data(&self) {
1225 if let Some(forward) = &self.forward {
1226 forward.advise(libc::MADV_DONTNEED);
1227 }
1228 Self::madvise_owned(&self.block_data_bytes, libc::MADV_DONTNEED);
1229 }
1230
1231 #[cfg(feature = "native")]
1235 pub fn madvise_random_query(&self) {
1236 if let Some(forward) = &self.forward {
1237 forward.advise(libc::MADV_RANDOM);
1238 }
1239 Self::madvise_owned(&self.block_data_bytes, libc::MADV_RANDOM);
1240 self.block_grid.madvise_rows(libc::MADV_RANDOM);
1241 self.superblock_grid.madvise_rows(libc::MADV_RANDOM);
1242 self.coarse_grid.madvise_rows(libc::MADV_SEQUENTIAL);
1243 Self::madvise_owned(&self.doc_map_ids_bytes, libc::MADV_RANDOM);
1244 Self::madvise_owned(&self.doc_map_ordinals_bytes, libc::MADV_RANDOM);
1245 }
1246
1247 #[cfg(feature = "native")]
1249 pub fn madvise_dontneed_grids(&self) {
1250 self.block_grid.madvise_rows(libc::MADV_DONTNEED);
1251 self.superblock_grid.madvise_rows(libc::MADV_DONTNEED);
1252 self.coarse_grid.madvise_rows(libc::MADV_DONTNEED);
1253 }
1254
1255 #[cfg(feature = "native")]
1259 pub fn madvise_dontneed_doc_maps(&self) {
1260 Self::madvise_owned(&self.doc_map_ids_bytes, libc::MADV_DONTNEED);
1261 Self::madvise_owned(&self.doc_map_ordinals_bytes, libc::MADV_DONTNEED);
1262 }
1263
1264 #[cfg(feature = "native")]
1271 fn madvise_owned(bytes: &crate::directories::OwnedBytes, advice: i32) {
1272 bytes.madvise(advice);
1273 }
1274}
1275
1276#[cfg(feature = "native")]
1283pub(crate) struct BmpScanPageGuard<'a> {
1284 indexes: Vec<&'a BmpIndex>,
1285}
1286
1287#[cfg(feature = "native")]
1288impl<'a> BmpScanPageGuard<'a> {
1289 pub(crate) fn new(indexes: impl IntoIterator<Item = &'a BmpIndex>) -> Self {
1290 let indexes: Vec<_> = indexes.into_iter().collect();
1291 for index in &indexes {
1292 index.madvise_sequential();
1293 }
1294 Self { indexes }
1295 }
1296
1297 pub(crate) fn switch_to_random(&self) {
1298 for index in &self.indexes {
1299 index.madvise_random_query();
1300 }
1301 }
1302}
1303
1304#[cfg(feature = "native")]
1305impl Drop for BmpScanPageGuard<'_> {
1306 fn drop(&mut self) {
1307 for index in &self.indexes {
1308 index.madvise_dontneed_block_data();
1309 index.madvise_dontneed_grids();
1310 index.madvise_dontneed_doc_maps();
1311 index.madvise_random_query();
1312 }
1313 }
1314}
1315
1316#[cfg(test)]
1317mod safety_tests {
1318 use super::BmpIndex;
1319 use crate::directories::{FileHandle, OwnedBytes};
1320 use crate::segment::format::BMP_BLOB_FOOTER_SIZE;
1321 use rustc_hash::FxHashMap;
1322
1323 fn test_blob() -> Vec<u8> {
1324 let mut postings = FxHashMap::default();
1325 postings.insert(3, vec![(0, 0, 1.0), (1, 0, 0.5)]);
1326 let mut blob = Vec::new();
1327 crate::segment::builder::bmp::build_bmp_blob(
1328 postings, 64, 4, 0.0, None, 16, 5.0, 0, true, &mut blob,
1329 )
1330 .unwrap();
1331 blob
1332 }
1333
1334 fn parse(blob: Vec<u8>) -> crate::Result<BmpIndex> {
1335 let len = blob.len() as u64;
1336 BmpIndex::parse(FileHandle::from_bytes(OwnedBytes::new(blob)), 0, len, 2, 2)
1337 }
1338
1339 #[test]
1340 fn parse_rejects_footer_section_underflow_without_panicking() {
1341 let mut blob = test_blob();
1342 let footer = blob.len() - BMP_BLOB_FOOTER_SIZE;
1343 blob[footer + 16..footer + 24].copy_from_slice(&0u64.to_le_bytes());
1344 assert!(matches!(parse(blob), Err(crate::Error::Corruption(_))));
1345 }
1346
1347 #[test]
1348 fn parse_rejects_nonzero_first_block_offset() {
1349 let mut blob = test_blob();
1350 let footer = blob.len() - BMP_BLOB_FOOTER_SIZE;
1351 let grid_offset =
1352 u64::from_le_bytes(blob[footer + 16..footer + 24].try_into().unwrap()) as usize;
1353 let num_blocks =
1354 u32::from_le_bytes(blob[footer + 40..footer + 44].try_into().unwrap()) as usize;
1355 let starts = grid_offset - (num_blocks + 1) * 8;
1356 blob[starts..starts + 8].copy_from_slice(&1u64.to_le_bytes());
1357 assert!(matches!(parse(blob), Err(crate::Error::Corruption(_))));
1358 }
1359
1360 #[test]
1361 fn physical_single_value_detection_uses_ordinal_map() {
1362 let single = parse(test_blob()).unwrap();
1363 assert!(single.is_single_valued());
1364
1365 let mut postings = FxHashMap::default();
1366 postings.insert(3, vec![(0, 0, 1.0), (0, 1, 0.8), (1, 0, 0.5)]);
1367 let mut blob = Vec::new();
1368 crate::segment::builder::bmp::build_bmp_blob(
1369 postings, 64, 4, 0.0, None, 16, 5.0, 0, true, &mut blob,
1370 )
1371 .unwrap();
1372 let multi = parse(blob).unwrap();
1373 assert!(!multi.is_single_valued());
1374 }
1375
1376 #[test]
1377 fn rewrite_validation_rejects_out_of_range_local_slot() {
1378 let mut blob = test_blob();
1379 blob[13] = 64;
1381 let index = parse(blob).unwrap();
1382 let error = index.validate_block_for_rewrite(0).unwrap_err();
1383 assert!(matches!(error, crate::Error::Corruption(_)));
1384 }
1385
1386 #[test]
1387 fn rewrite_validation_rejects_bad_dimension_and_maximum() {
1388 let mut bad_dimension = test_blob();
1389 bad_dimension[4..8].copy_from_slice(&16u32.to_le_bytes());
1390 let index = parse(bad_dimension).unwrap();
1391 assert!(matches!(
1392 index.validate_block_for_rewrite(0),
1393 Err(crate::Error::Corruption(_))
1394 ));
1395
1396 let mut bad_maximum = test_blob();
1397 bad_maximum[12] = 0;
1398 let index = parse(bad_maximum).unwrap();
1399 assert!(matches!(
1400 index.validate_block_for_rewrite(0),
1401 Err(crate::Error::Corruption(_))
1402 ));
1403 }
1404
1405 #[test]
1406 fn invalid_doc_map_id_is_bounded_and_rewrite_rejects_it() {
1407 let mut blob = test_blob();
1408 let footer = blob.len() - BMP_BLOB_FOOTER_SIZE;
1409 let doc_map =
1410 u64::from_le_bytes(blob[footer + 60..footer + 68].try_into().unwrap()) as usize;
1411 blob[doc_map..doc_map + 4].copy_from_slice(&2u32.to_le_bytes());
1412 let index = parse(blob).unwrap();
1413
1414 assert_eq!(index.doc_id_for_virtual(0), u32::MAX);
1415 assert!(matches!(
1416 crate::segment::builder::graph_bisection::build_vid_maps(&index, &|| Ok(())),
1417 Err(crate::Error::Corruption(_))
1418 ));
1419 }
1420
1421 #[test]
1422 fn rewrite_layout_requires_exact_finite_scale() {
1423 let index = parse(test_blob()).unwrap();
1424 let adjacent_scale = f32::from_bits(index.max_weight_scale.to_bits() + 1);
1425 assert!(matches!(
1426 index.validate_rewrite_layout("test", 16, 64, 4, adjacent_scale),
1427 Err(crate::Error::Corruption(_))
1428 ));
1429 assert!(matches!(
1430 index.validate_rewrite_layout("test", 16, 64, 4, f32::NAN),
1431 Err(crate::Error::Corruption(_))
1432 ));
1433 }
1434}