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
132 block_data_starts_bytes: OwnedBytes,
135 block_data_bytes: OwnedBytes,
137 block_grid: CompressedGrid,
140 superblock_grid: CompressedGrid,
142 pub num_superblocks: u32,
144 coarse_grid: CompressedGrid,
146 pub num_coarse_groups: u32,
148 doc_map_ids_bytes: OwnedBytes,
150 doc_map_ordinals_bytes: OwnedBytes,
152
153 #[cfg_attr(not(feature = "native"), allow(dead_code))]
158 source: FileHandle,
159 #[cfg_attr(not(feature = "native"), allow(dead_code))]
160 blob_offset: u64,
161 #[cfg_attr(not(feature = "native"), allow(dead_code))]
162 blob_len: u64,
163 #[cfg_attr(not(feature = "native"), allow(dead_code))]
167 doc_map_offset: u64,
168}
169
170impl BmpIndex {
176 pub fn parse(
184 handle: FileHandle,
185 blob_offset: u64,
186 blob_len: u64,
187 total_docs: u32,
188 total_vectors: u32,
189 ) -> crate::Result<Self> {
190 use crate::segment::format::{BMP_BLOB_FOOTER_SIZE, BMP_BLOB_MAGIC};
191
192 if blob_len < BMP_BLOB_FOOTER_SIZE as u64 {
193 return Err(crate::Error::Corruption(
194 "BMP blob too small for V19 footer".into(),
195 ));
196 }
197
198 let blob_end = blob_offset
200 .checked_add(blob_len)
201 .ok_or_else(|| crate::Error::Corruption("BMP blob range overflows u64".into()))?;
202 let footer_start = blob_end - BMP_BLOB_FOOTER_SIZE as u64;
203 let footer_bytes = handle
204 .read_bytes_range_sync(footer_start..blob_end)
205 .map_err(crate::Error::Io)?;
206 let fb = footer_bytes.as_slice();
207
208 let total_terms = u64::from_le_bytes(fb[0..8].try_into().unwrap());
209 let total_postings = u64::from_le_bytes(fb[8..16].try_into().unwrap());
210 let grid_offset = u64::from_le_bytes(fb[16..24].try_into().unwrap());
211 let sb_grid_offset = u64::from_le_bytes(fb[24..32].try_into().unwrap());
212 let coarse_grid_offset = u64::from_le_bytes(fb[32..40].try_into().unwrap());
213 let num_blocks = u32::from_le_bytes(fb[40..44].try_into().unwrap());
214 let dims = u32::from_le_bytes(fb[44..48].try_into().unwrap());
215 let bmp_block_size = u32::from_le_bytes(fb[48..52].try_into().unwrap());
216 let num_virtual_docs = u32::from_le_bytes(fb[52..56].try_into().unwrap());
217 let max_weight_scale = f32::from_le_bytes(fb[56..60].try_into().unwrap());
218 let doc_map_offset = u64::from_le_bytes(fb[60..68].try_into().unwrap());
219 let num_real_docs = u32::from_le_bytes(fb[68..72].try_into().unwrap());
220 let grid_bits_raw = u32::from_le_bytes(fb[72..76].try_into().unwrap());
221 let magic = u32::from_le_bytes(fb[76..80].try_into().unwrap());
222
223 if magic != BMP_BLOB_MAGIC {
224 return Err(crate::Error::Corruption(format!(
225 "Invalid BMP blob magic: {:#x} (expected BMP9 {:#x}); rebuild \
226 the index with this version.",
227 magic, BMP_BLOB_MAGIC
228 )));
229 }
230 let grid_bits: u8 = match grid_bits_raw {
231 4 => 4,
232 2 => 2,
233 other => {
234 return Err(crate::Error::Corruption(format!(
235 "Unsupported BMP grid_bits {} (expected 2 or 4) — data too new to read?",
236 other
237 )));
238 }
239 };
240
241 if num_blocks == 0 {
243 if num_virtual_docs != 0 || num_real_docs != 0 {
244 return Err(crate::Error::Corruption(format!(
245 "empty BMP index has non-zero document counts (virtual={}, real={})",
246 num_virtual_docs, num_real_docs
247 )));
248 }
249 return Ok(Self {
250 bmp_block_size,
251 num_blocks,
252 num_virtual_docs,
253 max_weight_scale,
254 total_vectors,
255 segment_num_docs: total_docs,
256 dims,
257 total_terms: 0,
258 total_postings: 0,
259 grid_bits,
260 num_real_docs,
261 single_valued: true,
262 block_data_starts_bytes: OwnedBytes::empty(),
263 block_data_bytes: OwnedBytes::empty(),
264 block_grid: CompressedGrid::empty(),
265 superblock_grid: CompressedGrid::empty(),
266 num_superblocks: 0,
267 coarse_grid: CompressedGrid::empty(),
268 num_coarse_groups: 0,
269 doc_map_ids_bytes: OwnedBytes::empty(),
270 doc_map_ordinals_bytes: OwnedBytes::empty(),
271 source: handle,
272 blob_offset,
273 blob_len,
274 doc_map_offset,
275 });
276 }
277
278 if !(1..=256).contains(&bmp_block_size) {
279 return Err(crate::Error::Corruption(format!(
280 "invalid BMP block size {} (expected 1..=256)",
281 bmp_block_size
282 )));
283 }
284 let expected_virtual_docs = u64::from(num_blocks) * u64::from(bmp_block_size);
285 if expected_virtual_docs != u64::from(num_virtual_docs) {
286 return Err(crate::Error::Corruption(format!(
287 "BMP block/document mismatch: {} blocks × {} != {} virtual docs",
288 num_blocks, bmp_block_size, num_virtual_docs
289 )));
290 }
291 if num_real_docs > num_virtual_docs {
292 return Err(crate::Error::Corruption(format!(
293 "BMP real document count {} exceeds virtual count {}",
294 num_real_docs, num_virtual_docs
295 )));
296 }
297 if !max_weight_scale.is_finite() || max_weight_scale <= 0.0 {
298 return Err(crate::Error::Corruption(format!(
299 "invalid BMP max-weight scale {}",
300 max_weight_scale
301 )));
302 }
303
304 let data_len = blob_len - BMP_BLOB_FOOTER_SIZE as u64;
306 let data_len_usize = usize::try_from(data_len).map_err(|_| {
307 crate::Error::Corruption("BMP blob is too large for this platform".into())
308 })?;
309 let blob = handle
310 .read_bytes_range_sync(blob_offset..footer_start)
311 .map_err(crate::Error::Io)?;
312
313 let num_blocks_usize = num_blocks as usize;
316 let section_a_size = num_blocks_usize
317 .checked_add(1)
318 .and_then(|count| count.checked_mul(8))
319 .ok_or_else(|| {
320 crate::Error::Corruption("BMP block-offset table size overflows usize".into())
321 })?;
322 let grid_start = usize::try_from(grid_offset).map_err(|_| {
323 crate::Error::Corruption("BMP grid offset is too large for this platform".into())
324 })?;
325 let bds_start = grid_start.checked_sub(section_a_size).ok_or_else(|| {
326 crate::Error::Corruption(format!(
327 "BMP grid offset {} precedes {}-byte block-offset table",
328 grid_offset, section_a_size
329 ))
330 })?;
331 if grid_start > data_len_usize {
332 return Err(crate::Error::Corruption(format!(
333 "BMP grid offset {} exceeds data length {}",
334 grid_start, data_len_usize
335 )));
336 }
337
338 let block_data_bytes = blob.slice(0..bds_start);
340 let block_data_starts_bytes = blob.slice(bds_start..grid_start);
342
343 let num_superblocks = num_blocks.div_ceil(BMP_SUPERBLOCK_SIZE);
348 let num_coarse_groups = num_superblocks.div_ceil(BMP_COARSE_SUPERBLOCKS);
349 let sb_grid_start = usize::try_from(sb_grid_offset).map_err(|_| {
350 crate::Error::Corruption("BMP superblock-grid offset is too large".into())
351 })?;
352 if sb_grid_start < grid_start || sb_grid_start > data_len_usize {
353 return Err(crate::Error::Corruption(format!(
354 "BMP section order mismatch: block grid starts at {}, superblock grid at {}, data ends at {}",
355 grid_start, sb_grid_start, data_len_usize
356 )));
357 }
358 let coarse_grid_start = usize::try_from(coarse_grid_offset)
359 .map_err(|_| crate::Error::Corruption("BMP coarse-grid offset is too large".into()))?;
360 if coarse_grid_start < sb_grid_start || coarse_grid_start > data_len_usize {
361 return Err(crate::Error::Corruption(format!(
362 "BMP section order mismatch: superblock grid starts at {}, coarse grid at {}, data ends at {}",
363 sb_grid_start, coarse_grid_start, data_len_usize
364 )));
365 }
366
367 let dm_start = usize::try_from(doc_map_offset)
368 .map_err(|_| crate::Error::Corruption("BMP document-map offset is too large".into()))?;
369 if dm_start < coarse_grid_start || dm_start > data_len_usize {
370 return Err(crate::Error::Corruption(format!(
371 "BMP section order mismatch: coarse grid starts at {}, document map at {}, data ends at {}",
372 coarse_grid_start, dm_start, data_len_usize
373 )));
374 }
375 let dm_ids_len = (num_virtual_docs as usize).checked_mul(4).ok_or_else(|| {
376 crate::Error::Corruption("BMP document-id map size overflows usize".into())
377 })?;
378 let dm_ords_len = (num_virtual_docs as usize).checked_mul(2).ok_or_else(|| {
379 crate::Error::Corruption("BMP ordinal map size overflows usize".into())
380 })?;
381 let dm_ids_end = dm_start.checked_add(dm_ids_len).ok_or_else(|| {
382 crate::Error::Corruption("BMP document-id map end overflows usize".into())
383 })?;
384 let dm_ords_end = dm_ids_end.checked_add(dm_ords_len).ok_or_else(|| {
385 crate::Error::Corruption("BMP ordinal map end overflows usize".into())
386 })?;
387 if dm_ords_end != data_len_usize {
388 return Err(crate::Error::Corruption(format!(
389 "BMP data length mismatch: sections end at {}, blob data ends at {}",
390 dm_ords_end, data_len_usize
391 )));
392 }
393
394 let block_grid = CompressedGrid::parse(
396 blob.slice(grid_start..sb_grid_start),
397 dims as usize,
398 num_blocks as usize,
399 grid_bits,
400 "BMP block grid",
401 )?;
402 let superblock_grid = CompressedGrid::parse(
403 blob.slice(sb_grid_start..coarse_grid_start),
404 dims as usize,
405 num_superblocks as usize,
406 4,
407 "BMP superblock grid",
408 )?;
409 let coarse_grid = CompressedGrid::parse(
410 blob.slice(coarse_grid_start..dm_start),
411 dims as usize,
412 num_coarse_groups as usize,
413 4,
414 "BMP coarse grid",
415 )?;
416 let doc_map_ids_bytes = blob.slice(dm_start..dm_ids_end);
417 let doc_map_ordinals_bytes = blob.slice(dm_ids_end..dm_ords_end);
418 let single_valued = doc_map_ordinals_bytes
419 .as_slice()
420 .chunks_exact(2)
421 .all(|ordinal| ordinal == [0, 0]);
422
423 let starts = block_data_starts_bytes.as_slice();
426 let mut previous = 0u64;
427 for index in 0..=num_blocks_usize {
428 let offset = index * 8;
429 let current = u64::from_le_bytes(starts[offset..offset + 8].try_into().unwrap());
430 if (index == 0 && current != 0) || current < previous || current > bds_start as u64 {
431 return Err(crate::Error::Corruption(format!(
432 "invalid BMP block offset at {}: {} (previous={}, data_limit={})",
433 index, current, previous, bds_start
434 )));
435 }
436 if current > previous && current - previous < 8 {
437 return Err(crate::Error::Corruption(format!(
438 "BMP block {} is too small for a header ({} bytes)",
439 index - 1,
440 current - previous
441 )));
442 }
443 previous = current;
444 }
445
446 #[cfg(feature = "native")]
458 {
459 block_data_bytes.madvise(libc::MADV_RANDOM);
460 doc_map_ids_bytes.madvise(libc::MADV_RANDOM);
461 doc_map_ordinals_bytes.madvise(libc::MADV_RANDOM);
462 block_grid.madvise_rows(libc::MADV_RANDOM);
463 superblock_grid.madvise_rows(libc::MADV_RANDOM);
464 coarse_grid.madvise_rows(libc::MADV_SEQUENTIAL);
465 }
466
467 log::debug!(
468 "BMP V19 index loaded: num_blocks={}, num_superblocks={}, coarse_groups={}, dims={}, bmp_block_size={}, \
469 num_virtual_docs={}, num_real_docs={}, max_weight_scale={:.4}, postings={}, \
470 block_grid={}, superblock_grid={}, coarse_grid={}, single_valued={}, block_data={}, doc_map={}",
471 num_blocks,
472 num_superblocks,
473 num_coarse_groups,
474 dims,
475 bmp_block_size,
476 num_virtual_docs,
477 num_real_docs,
478 max_weight_scale,
479 total_postings,
480 crate::format_bytes(block_grid.encoded_bytes() as u64),
481 crate::format_bytes(superblock_grid.encoded_bytes() as u64),
482 crate::format_bytes(coarse_grid.encoded_bytes() as u64),
483 single_valued,
484 crate::format_bytes(bds_start as u64),
485 crate::format_bytes(u64::from(num_virtual_docs) * 6),
486 );
487
488 Ok(Self {
489 bmp_block_size,
490 num_blocks,
491 num_virtual_docs,
492 max_weight_scale,
493 total_vectors,
494 segment_num_docs: total_docs,
495 dims,
496 total_terms,
497 total_postings,
498 grid_bits,
499 num_real_docs,
500 single_valued,
501 block_data_starts_bytes,
502 block_data_bytes,
503 block_grid,
504 superblock_grid,
505 num_superblocks,
506 coarse_grid,
507 num_coarse_groups,
508 doc_map_ids_bytes,
509 doc_map_ordinals_bytes,
510 source: handle,
511 blob_offset,
512 blob_len,
513 doc_map_offset,
514 })
515 }
516
517 #[cfg_attr(not(feature = "native"), allow(dead_code))]
522 pub(crate) fn read_raw_blob(&self) -> std::io::Result<OwnedBytes> {
523 self.source
524 .read_bytes_range_sync(self.blob_offset..self.blob_offset + self.blob_len)
525 }
526
527 #[inline(always)]
532 pub fn virtual_to_doc(&self, virtual_id: u32) -> (u32, u16) {
533 if virtual_id >= self.num_virtual_docs {
534 return (u32::MAX, 0);
535 }
536 let ids = self.doc_map_ids_bytes.as_slice();
537 let ords = self.doc_map_ordinals_bytes.as_slice();
538 debug_assert!((virtual_id as usize + 1) * 4 <= ids.len());
539 debug_assert!((virtual_id as usize + 1) * 2 <= ords.len());
540 unsafe {
541 let doc_id = read_u32_unchecked(ids.as_ptr(), virtual_id as usize);
542 if doc_id >= self.segment_num_docs {
543 return (u32::MAX, 0);
544 }
545 let p = ords.as_ptr().add(virtual_id as usize * 2);
546 let ordinal = u16::from_le((p as *const u16).read_unaligned());
547 (doc_id, ordinal)
548 }
549 }
550
551 #[inline(always)]
554 pub fn doc_id_for_virtual(&self, virtual_id: u32) -> u32 {
555 if virtual_id >= self.num_virtual_docs {
556 return u32::MAX;
557 }
558 let d = self.doc_map_ids_bytes.as_slice();
559 debug_assert!((virtual_id as usize + 1) * 4 <= d.len());
560 let doc_id = unsafe { read_u32_unchecked(d.as_ptr(), virtual_id as usize) };
561 if doc_id < self.segment_num_docs {
562 doc_id
563 } else {
564 u32::MAX
565 }
566 }
567
568 #[inline(always)]
572 pub(crate) fn block_data_range(&self, block_id: u32) -> (u64, u64) {
573 let d = self.block_data_starts_bytes.as_slice();
574 debug_assert!((block_id as usize + 2) * 8 <= d.len());
575 unsafe {
576 let start = read_u64_unchecked(d.as_ptr(), block_id as usize);
577 let end = read_u64_unchecked(d.as_ptr(), block_id as usize + 1);
578 (start, end)
579 }
580 }
581
582 #[cfg(feature = "native")]
585 pub(crate) fn pin_block_starts(
586 &mut self,
587 mode: crate::segment::pin::PinMode,
588 remaining: &mut u64,
589 report: &mut crate::segment::pin::PinReport,
590 ) {
591 crate::segment::pin::pin_section(
592 &mut self.block_data_starts_bytes,
593 "bmp block_data_starts",
594 mode,
595 remaining,
596 report,
597 );
598 self.block_grid
599 .pin_offsets("bmp block_grid row_offsets", mode, remaining, report);
600 }
601
602 #[cfg(feature = "native")]
605 pub(crate) fn pin_doc_maps(
606 &mut self,
607 mode: crate::segment::pin::PinMode,
608 remaining: &mut u64,
609 report: &mut crate::segment::pin::PinReport,
610 ) {
611 crate::segment::pin::pin_section(
612 &mut self.doc_map_ids_bytes,
613 "bmp doc_map_ids",
614 mode,
615 remaining,
616 report,
617 );
618 crate::segment::pin::pin_section(
619 &mut self.doc_map_ordinals_bytes,
620 "bmp doc_map_ordinals",
621 mode,
622 remaining,
623 report,
624 );
625 }
626
627 #[cfg(feature = "native")]
634 pub(crate) fn pin_query_hierarchy(
635 &mut self,
636 mode: crate::segment::pin::PinMode,
637 remaining: &mut u64,
638 report: &mut crate::segment::pin::PinReport,
639 ) {
640 self.superblock_grid
641 .pin_offsets("bmp sb_grid row_offsets", mode, remaining, report);
642 self.coarse_grid.pin_all(
643 "bmp coarse_grid row_offsets",
644 "bmp coarse_grid rows",
645 mode,
646 remaining,
647 report,
648 );
649 }
650
651 #[cfg(feature = "native")]
659 #[inline]
660 pub(crate) fn prefetch_block_data(&self, byte_start: u64, byte_end: u64) {
661 self.block_data_bytes
662 .madvise_range(byte_start as usize..byte_end as usize, libc::MADV_WILLNEED);
663 }
664
665 #[cfg(feature = "native")]
672 pub(crate) fn prefetch_block_data_ranges(
673 &self,
674 ranges: &mut Vec<std::ops::Range<u64>>,
675 ) -> (usize, usize) {
676 if ranges.is_empty() {
677 return (0, 0);
678 }
679 const PAGE_NEAR_BYTES: u64 = 4096;
680 ranges.sort_unstable_by_key(|range| (range.start, range.end));
681 let mut advised_bytes = 0usize;
682 let mut calls = 0usize;
683 let mut current = ranges[0].clone();
684 for range in &ranges[1..] {
685 if range.start <= current.end.saturating_add(PAGE_NEAR_BYTES) {
686 current.end = current.end.max(range.end);
687 continue;
688 }
689 advised_bytes = advised_bytes.saturating_add((current.end - current.start) as usize);
690 calls += 1;
691 self.prefetch_block_data(current.start, current.end);
692 current = range.clone();
693 }
694 advised_bytes = advised_bytes.saturating_add((current.end - current.start) as usize);
695 calls += 1;
696 self.prefetch_block_data(current.start, current.end);
697 ranges.clear();
698 (advised_bytes, calls)
699 }
700
701 #[inline(always)]
704 pub(crate) fn block_data_ptr(&self, block_id: u32) -> *const u8 {
705 let (start, _) = self.block_data_range(block_id);
706 unsafe {
707 self.block_data_bytes
708 .as_slice()
709 .as_ptr()
710 .add(start as usize)
711 }
712 }
713
714 #[inline(always)]
717 pub(crate) fn parse_block(&self, block_id: u32) -> Option<AdaptiveBlock<'_>> {
718 if block_id >= self.num_blocks {
719 return None;
720 }
721 let (start, end) = self.block_data_range(block_id);
722 if start == end {
723 return None;
724 }
725 let start = usize::try_from(start).ok()?;
726 let end = usize::try_from(end).ok()?;
727 let bytes = self.block_data_bytes.as_slice().get(start..end)?;
728 AdaptiveBlock::parse(bytes, self.bmp_block_size as usize)
729 }
730
731 #[inline(always)]
735 pub(crate) fn block_data_starts_ptr(&self, block_id: u32) -> *const u8 {
736 unsafe {
737 self.block_data_starts_bytes
738 .as_slice()
739 .as_ptr()
740 .add(block_id as usize * 8)
741 }
742 }
743
744 #[cfg_attr(not(any(feature = "native", feature = "wasm")), allow(dead_code))]
749 pub(crate) fn iter_block_terms(
750 &self,
751 block_id: u32,
752 ) -> impl Iterator<Item = (u32, u8, AdaptivePostings<'_>)> + '_ {
753 self.parse_block(block_id)
754 .into_iter()
755 .flat_map(AdaptiveBlock::terms)
756 }
757
758 pub fn dims(&self) -> u32 {
762 self.dims
763 }
764
765 #[cfg(any(feature = "native", test))]
772 pub(crate) fn validate_rewrite_layout(
773 &self,
774 context: &str,
775 expected_dims: u32,
776 expected_block_size: u32,
777 expected_grid_bits: u8,
778 expected_max_weight_scale: f32,
779 ) -> crate::Result<()> {
780 if expected_dims == 0 {
781 return Err(crate::Error::Corruption(format!(
782 "{context}: expected vocabulary is empty",
783 )));
784 }
785 if self.dims != expected_dims {
786 return Err(crate::Error::Corruption(format!(
787 "{context}: source dims={} != expected {expected_dims}",
788 self.dims,
789 )));
790 }
791 if self.bmp_block_size != expected_block_size {
792 return Err(crate::Error::Corruption(format!(
793 "{context}: source block_size={} != expected {expected_block_size}",
794 self.bmp_block_size,
795 )));
796 }
797 if self.grid_bits != expected_grid_bits {
798 return Err(crate::Error::Corruption(format!(
799 "{context}: source grid_bits={} != expected {expected_grid_bits}",
800 self.grid_bits,
801 )));
802 }
803 if !expected_max_weight_scale.is_finite() || expected_max_weight_scale <= 0.0 {
804 return Err(crate::Error::Corruption(format!(
805 "{context}: invalid expected max_weight_scale={expected_max_weight_scale}",
806 )));
807 }
808 if self.max_weight_scale.to_bits() != expected_max_weight_scale.to_bits() {
809 return Err(crate::Error::Corruption(format!(
810 "{context}: source max_weight_scale={:.4} != expected {:.4}",
811 self.max_weight_scale, expected_max_weight_scale,
812 )));
813 }
814 Ok(())
815 }
816
817 #[cfg(any(feature = "native", feature = "wasm", test))]
822 pub(crate) fn visit_real_slots_for_rewrite(
823 &self,
824 mut visitor: impl FnMut(usize),
825 ) -> crate::Result<()> {
826 let expected_real = self.num_real_docs as usize;
827 let mut real_slots = 0usize;
828 for (virtual_id, chunk) in self
829 .doc_map_ids_bytes
830 .as_slice()
831 .chunks_exact(4)
832 .enumerate()
833 {
834 let doc_id = u32::from_le_bytes(chunk.try_into().unwrap());
835 if doc_id == u32::MAX {
836 continue;
837 }
838 if doc_id >= self.segment_num_docs {
839 return Err(crate::Error::Corruption(format!(
840 "BMP document map contains doc id {doc_id} outside segment bound {}",
841 self.segment_num_docs,
842 )));
843 }
844 if real_slots == expected_real {
845 return Err(crate::Error::Corruption(format!(
846 "BMP document map contains more than the footer's {expected_real} real slots"
847 )));
848 }
849 visitor(virtual_id);
850 real_slots += 1;
851 }
852 if real_slots != expected_real {
853 return Err(crate::Error::Corruption(format!(
854 "BMP document map has {real_slots} real slots but footer declares {expected_real}",
855 )));
856 }
857 Ok(())
858 }
859
860 #[cfg(any(feature = "native", test))]
865 pub(crate) fn validate_block_for_rewrite(&self, block_id: u32) -> crate::Result<()> {
866 if block_id >= self.num_blocks {
867 return Err(crate::Error::Corruption(format!(
868 "BMP rewrite block {block_id} exceeds block count {}",
869 self.num_blocks,
870 )));
871 }
872 let (start, end) = self.block_data_range(block_id);
873 let start = usize::try_from(start)
874 .map_err(|_| crate::Error::Corruption("BMP block start exceeds usize".into()))?;
875 let end = usize::try_from(end)
876 .map_err(|_| crate::Error::Corruption("BMP block end exceeds usize".into()))?;
877 let block = self
878 .block_data_bytes
879 .as_slice()
880 .get(start..end)
881 .ok_or_else(|| {
882 crate::Error::Corruption(format!(
883 "BMP block {block_id} range {start}..{end} exceeds block data",
884 ))
885 })?;
886 if block.is_empty() {
887 return Ok(());
888 }
889 let parsed =
890 AdaptiveBlock::parse(block, self.bmp_block_size as usize).ok_or_else(|| {
891 crate::Error::Corruption(format!(
892 "BMP block {block_id} has an invalid adaptive envelope"
893 ))
894 })?;
895 parsed.validate(self.dims).map_err(|reason| {
896 crate::Error::Corruption(format!("BMP block {block_id} is invalid: {reason}"))
897 })
898 }
899
900 pub fn total_terms(&self) -> u64 {
902 self.total_terms
903 }
904
905 pub fn total_postings(&self) -> u64 {
907 self.total_postings
908 }
909
910 pub fn num_real_docs(&self) -> u32 {
912 self.num_real_docs
913 }
914
915 pub fn is_single_valued(&self) -> bool {
920 self.single_valued
921 }
922
923 pub fn estimated_heap_bytes(&self) -> usize {
926 std::mem::size_of::<Self>()
927 }
928
929 pub fn grid_bits(&self) -> u8 {
931 self.grid_bits
932 }
933
934 pub fn dim_stats(&self, top: usize) -> BmpDimStats {
943 let mut per_dim: rustc_hash::FxHashMap<u32, u64> = rustc_hash::FxHashMap::default();
944 let mut total_postings = 0u64;
945 let mut saturated = 0u64;
946 for block_id in 0..self.num_blocks {
947 for (dim, _, postings) in self.iter_block_terms(block_id) {
948 let mut count = 0u64;
949 for posting in postings {
950 count += 1;
951 if posting.impact == u8::MAX {
952 saturated += 1;
953 }
954 }
955 *per_dim.entry(dim).or_default() += count;
956 total_postings += count;
957 }
958 }
959 let mut counts: Vec<u64> = per_dim.values().copied().collect();
960 counts.sort_unstable();
961 let percentile = |fraction: f64| -> u64 {
962 if counts.is_empty() {
963 0
964 } else {
965 counts[((counts.len() - 1) as f64 * fraction) as usize]
966 }
967 };
968 let mut top_dims: Vec<(u32, u64)> = per_dim.into_iter().collect();
969 top_dims.sort_unstable_by_key(|&(dim, count)| (std::cmp::Reverse(count), dim));
970 top_dims.truncate(top);
971 let hot = counts.len().div_ceil(100);
974 let top_1pct_postings: u64 = counts.iter().rev().take(hot).sum();
975 BmpDimStats {
976 nonzero_dims: counts.len() as u32,
977 declared_dims: self.dims(),
978 total_postings,
979 p50_postings_per_dim: percentile(0.50),
980 p99_postings_per_dim: percentile(0.99),
981 max_postings_per_dim: counts.last().copied().unwrap_or(0),
982 top_1pct_share: if total_postings == 0 {
983 0.0
984 } else {
985 top_1pct_postings as f64 / total_postings as f64
986 },
987 saturated_impacts: saturated,
988 top_dims,
989 }
990 }
991
992 #[inline]
994 pub(crate) fn block_grid(&self) -> &CompressedGrid {
995 &self.block_grid
996 }
997
998 #[inline]
1000 pub(crate) fn superblock_grid(&self) -> &CompressedGrid {
1001 &self.superblock_grid
1002 }
1003
1004 #[inline]
1006 pub(crate) fn coarse_grid(&self) -> &CompressedGrid {
1007 &self.coarse_grid
1008 }
1009
1010 pub fn for_each_block_grid_chunk(
1016 &self,
1017 dimension: u32,
1018 mut visitor: impl FnMut(usize, usize, Option<&[u8]>),
1019 ) -> crate::Result<()> {
1020 let dimension = dimension as usize;
1021 if dimension >= self.block_grid.dims() {
1022 return Err(crate::Error::Query(format!(
1023 "BMP block-grid dimension {dimension} exceeds {}",
1024 self.block_grid.dims()
1025 )));
1026 }
1027 let mut decoded = [0u8; crate::segment::bmp_grid::GRID_GROUP_CELLS];
1028 self.block_grid
1029 .try_for_each_row_group(dimension, |group_id, group| {
1030 let start = group_id * crate::segment::bmp_grid::GRID_GROUP_CELLS;
1031 let count =
1032 crate::segment::bmp_grid::GRID_GROUP_CELLS.min(self.block_grid.cells() - start);
1033 if group.width() == 0 {
1034 visitor(start, count, None);
1035 } else {
1036 group.decode(0, count, &mut decoded);
1037 visitor(start, count, Some(&decoded[..count]));
1038 }
1039 Ok(())
1040 })
1041 }
1042
1043 #[inline]
1047 pub fn block_data_slice(&self) -> &[u8] {
1048 self.block_data_bytes.as_slice()
1049 }
1050
1051 #[inline]
1053 pub fn block_data_start(&self, block_id: u32) -> u64 {
1054 let d = self.block_data_starts_bytes.as_slice();
1055 let off = block_id as usize * 8;
1056 u64::from_le_bytes(d[off..off + 8].try_into().unwrap())
1057 }
1058
1059 #[inline]
1061 pub fn block_data_sentinel(&self) -> u64 {
1062 self.block_data_start(self.num_blocks)
1063 }
1064
1065 #[inline]
1068 pub fn doc_map_ids_slice(&self) -> &[u8] {
1069 self.doc_map_ids_bytes.as_slice()
1070 }
1071
1072 #[inline]
1075 pub fn doc_map_ordinals_slice(&self) -> &[u8] {
1076 self.doc_map_ordinals_bytes.as_slice()
1077 }
1078
1079 #[cfg(feature = "native")]
1081 pub(crate) fn block_data_file_range(&self) -> std::ops::Range<u64> {
1082 self.blob_offset..self.blob_offset + self.block_data_sentinel()
1083 }
1084
1085 #[cfg(feature = "native")]
1087 pub(crate) fn doc_map_ids_file_range(&self) -> std::ops::Range<u64> {
1088 let start = self.blob_offset + self.doc_map_offset;
1089 start..start + u64::from(self.num_virtual_docs) * 4
1090 }
1091
1092 #[cfg(feature = "native")]
1094 pub(crate) fn doc_map_ordinals_file_range(&self) -> std::ops::Range<u64> {
1095 let start = self.blob_offset + self.doc_map_offset + u64::from(self.num_virtual_docs) * 4;
1096 start..start + u64::from(self.num_virtual_docs) * 2
1097 }
1098
1099 #[cfg(feature = "native")]
1103 pub fn madvise_sequential(&self) {
1104 Self::madvise_owned(&self.block_data_bytes, libc::MADV_SEQUENTIAL);
1105 Self::madvise_owned(&self.block_data_starts_bytes, libc::MADV_SEQUENTIAL);
1106 self.block_grid.madvise_rows(libc::MADV_SEQUENTIAL);
1107 self.superblock_grid.madvise_rows(libc::MADV_SEQUENTIAL);
1108 self.coarse_grid.madvise_rows(libc::MADV_SEQUENTIAL);
1109 Self::madvise_owned(&self.doc_map_ids_bytes, libc::MADV_SEQUENTIAL);
1110 Self::madvise_owned(&self.doc_map_ordinals_bytes, libc::MADV_SEQUENTIAL);
1111 }
1112
1113 #[cfg(feature = "native")]
1116 pub fn madvise_dontneed_block_data(&self) {
1117 Self::madvise_owned(&self.block_data_bytes, libc::MADV_DONTNEED);
1118 }
1119
1120 #[cfg(feature = "native")]
1124 pub fn madvise_random_query(&self) {
1125 Self::madvise_owned(&self.block_data_bytes, libc::MADV_RANDOM);
1126 self.block_grid.madvise_rows(libc::MADV_RANDOM);
1127 self.superblock_grid.madvise_rows(libc::MADV_RANDOM);
1128 self.coarse_grid.madvise_rows(libc::MADV_SEQUENTIAL);
1129 Self::madvise_owned(&self.doc_map_ids_bytes, libc::MADV_RANDOM);
1130 Self::madvise_owned(&self.doc_map_ordinals_bytes, libc::MADV_RANDOM);
1131 }
1132
1133 #[cfg(feature = "native")]
1135 pub fn madvise_dontneed_grids(&self) {
1136 self.block_grid.madvise_rows(libc::MADV_DONTNEED);
1137 self.superblock_grid.madvise_rows(libc::MADV_DONTNEED);
1138 self.coarse_grid.madvise_rows(libc::MADV_DONTNEED);
1139 }
1140
1141 #[cfg(feature = "native")]
1145 pub fn madvise_dontneed_doc_maps(&self) {
1146 Self::madvise_owned(&self.doc_map_ids_bytes, libc::MADV_DONTNEED);
1147 Self::madvise_owned(&self.doc_map_ordinals_bytes, libc::MADV_DONTNEED);
1148 }
1149
1150 #[cfg(feature = "native")]
1157 fn madvise_owned(bytes: &crate::directories::OwnedBytes, advice: i32) {
1158 bytes.madvise(advice);
1159 }
1160}
1161
1162#[cfg(feature = "native")]
1169pub(crate) struct BmpScanPageGuard<'a> {
1170 indexes: Vec<&'a BmpIndex>,
1171}
1172
1173#[cfg(feature = "native")]
1174impl<'a> BmpScanPageGuard<'a> {
1175 pub(crate) fn new(indexes: impl IntoIterator<Item = &'a BmpIndex>) -> Self {
1176 let indexes: Vec<_> = indexes.into_iter().collect();
1177 for index in &indexes {
1178 index.madvise_sequential();
1179 }
1180 Self { indexes }
1181 }
1182
1183 pub(crate) fn switch_to_random(&self) {
1184 for index in &self.indexes {
1185 index.madvise_random_query();
1186 }
1187 }
1188}
1189
1190#[cfg(feature = "native")]
1191impl Drop for BmpScanPageGuard<'_> {
1192 fn drop(&mut self) {
1193 for index in &self.indexes {
1194 index.madvise_dontneed_block_data();
1195 index.madvise_dontneed_grids();
1196 index.madvise_dontneed_doc_maps();
1197 index.madvise_random_query();
1198 }
1199 }
1200}
1201
1202#[cfg(test)]
1203mod safety_tests {
1204 use super::BmpIndex;
1205 use crate::directories::{FileHandle, OwnedBytes};
1206 use crate::segment::format::BMP_BLOB_FOOTER_SIZE;
1207 use rustc_hash::FxHashMap;
1208
1209 fn test_blob() -> Vec<u8> {
1210 let mut postings = FxHashMap::default();
1211 postings.insert(3, vec![(0, 0, 1.0), (1, 0, 0.5)]);
1212 let mut blob = Vec::new();
1213 crate::segment::builder::bmp::build_bmp_blob(
1214 postings, 64, 4, 0.0, None, 16, 5.0, 0, &mut blob,
1215 )
1216 .unwrap();
1217 blob
1218 }
1219
1220 fn parse(blob: Vec<u8>) -> crate::Result<BmpIndex> {
1221 let len = blob.len() as u64;
1222 BmpIndex::parse(FileHandle::from_bytes(OwnedBytes::new(blob)), 0, len, 2, 2)
1223 }
1224
1225 #[test]
1226 fn parse_rejects_footer_section_underflow_without_panicking() {
1227 let mut blob = test_blob();
1228 let footer = blob.len() - BMP_BLOB_FOOTER_SIZE;
1229 blob[footer + 16..footer + 24].copy_from_slice(&0u64.to_le_bytes());
1230 assert!(matches!(parse(blob), Err(crate::Error::Corruption(_))));
1231 }
1232
1233 #[test]
1234 fn parse_rejects_nonzero_first_block_offset() {
1235 let mut blob = test_blob();
1236 let footer = blob.len() - BMP_BLOB_FOOTER_SIZE;
1237 let grid_offset =
1238 u64::from_le_bytes(blob[footer + 16..footer + 24].try_into().unwrap()) as usize;
1239 let num_blocks =
1240 u32::from_le_bytes(blob[footer + 40..footer + 44].try_into().unwrap()) as usize;
1241 let starts = grid_offset - (num_blocks + 1) * 8;
1242 blob[starts..starts + 8].copy_from_slice(&1u64.to_le_bytes());
1243 assert!(matches!(parse(blob), Err(crate::Error::Corruption(_))));
1244 }
1245
1246 #[test]
1247 fn physical_single_value_detection_uses_ordinal_map() {
1248 let single = parse(test_blob()).unwrap();
1249 assert!(single.is_single_valued());
1250
1251 let mut postings = FxHashMap::default();
1252 postings.insert(3, vec![(0, 0, 1.0), (0, 1, 0.8), (1, 0, 0.5)]);
1253 let mut blob = Vec::new();
1254 crate::segment::builder::bmp::build_bmp_blob(
1255 postings, 64, 4, 0.0, None, 16, 5.0, 0, &mut blob,
1256 )
1257 .unwrap();
1258 let multi = parse(blob).unwrap();
1259 assert!(!multi.is_single_valued());
1260 }
1261
1262 #[test]
1263 fn rewrite_validation_rejects_out_of_range_local_slot() {
1264 let mut blob = test_blob();
1265 blob[13] = 64;
1267 let index = parse(blob).unwrap();
1268 let error = index.validate_block_for_rewrite(0).unwrap_err();
1269 assert!(matches!(error, crate::Error::Corruption(_)));
1270 }
1271
1272 #[test]
1273 fn rewrite_validation_rejects_bad_dimension_and_maximum() {
1274 let mut bad_dimension = test_blob();
1275 bad_dimension[4..8].copy_from_slice(&16u32.to_le_bytes());
1276 let index = parse(bad_dimension).unwrap();
1277 assert!(matches!(
1278 index.validate_block_for_rewrite(0),
1279 Err(crate::Error::Corruption(_))
1280 ));
1281
1282 let mut bad_maximum = test_blob();
1283 bad_maximum[12] = 0;
1284 let index = parse(bad_maximum).unwrap();
1285 assert!(matches!(
1286 index.validate_block_for_rewrite(0),
1287 Err(crate::Error::Corruption(_))
1288 ));
1289 }
1290
1291 #[test]
1292 fn invalid_doc_map_id_is_bounded_and_rewrite_rejects_it() {
1293 let mut blob = test_blob();
1294 let footer = blob.len() - BMP_BLOB_FOOTER_SIZE;
1295 let doc_map =
1296 u64::from_le_bytes(blob[footer + 60..footer + 68].try_into().unwrap()) as usize;
1297 blob[doc_map..doc_map + 4].copy_from_slice(&2u32.to_le_bytes());
1298 let index = parse(blob).unwrap();
1299
1300 assert_eq!(index.doc_id_for_virtual(0), u32::MAX);
1301 assert!(matches!(
1302 crate::segment::builder::graph_bisection::build_vid_maps(&index),
1303 Err(crate::Error::Corruption(_))
1304 ));
1305 }
1306
1307 #[test]
1308 fn rewrite_layout_requires_exact_finite_scale() {
1309 let index = parse(test_blob()).unwrap();
1310 let adjacent_scale = f32::from_bits(index.max_weight_scale.to_bits() + 1);
1311 assert!(matches!(
1312 index.validate_rewrite_layout("test", 16, 64, 4, adjacent_scale),
1313 Err(crate::Error::Corruption(_))
1314 ));
1315 assert!(matches!(
1316 index.validate_rewrite_layout("test", 16, 64, 4, f32::NAN),
1317 Err(crate::Error::Corruption(_))
1318 ));
1319 }
1320}