1use lance_core::utils::row_addr_remap::RowAddrRemap;
5use std::{
6 any::Any,
7 cmp::Reverse,
8 collections::{BTreeMap, BinaryHeap, HashMap},
9 fmt::Debug,
10 ops::Bound,
11 sync::Arc,
12};
13
14use arrow::array::BinaryBuilder;
15use arrow_array::{Array, BinaryArray, RecordBatch, UInt64Array, new_null_array};
16use arrow_schema::{DataType, Field, Schema};
17use async_trait::async_trait;
18use bytes::Bytes;
19use datafusion::physical_plan::SendableRecordBatchStream;
20use datafusion_common::ScalarValue;
21use futures::{StreamExt, TryStreamExt, stream};
22use lance_core::deepsize::DeepSizeOf;
23use lance_core::{
24 Error, ROW_ID, Result,
25 cache::{
26 CacheCodec, CacheCodecImpl, CacheEntryReader, CacheEntryWriter, CacheKey, CacheKeySchema,
27 KeyBuilder, LanceCache, WeakLanceCache,
28 },
29 error::LanceOptionExt,
30 utils::tokio::get_num_compute_intensive_cpus,
31};
32use lance_io::object_store::ObjectStore;
33use lance_select::{NullableRowAddrSet, RowAddrTreeMap, RowSetOps};
34use object_store::path::Path;
35use roaring::RoaringBitmap;
36use serde::{Deserialize, Serialize};
37use tracing::{instrument, warn};
38
39use super::{AnyQuery, IndexFile, IndexStore, ScalarIndex};
40use super::{
41 BuiltinIndexType, SargableQuery, ScalarIndexParams, SearchResult, btree::OrderableScalarValue,
42};
43use crate::pbold;
44use crate::{Index, IndexType, metrics::MetricsCollector};
45use crate::{
46 progress::IndexBuildProgress,
47 scalar::{
48 CreatedIndex, RowIdRemapper, UpdateCriteria,
49 expression::SargableQueryParser,
50 registry::{
51 BasicTrainer, ScalarIndexLoad, ScalarIndexPlugin, TrainingCriteria, TrainingOrdering,
52 TrainingRequest, VALUE_COLUMN_NAME, single_flight_open,
53 },
54 },
55};
56use crate::{scalar::IndexReader, scalar::expression::ScalarQueryParser};
57
58pub const BITMAP_LOOKUP_NAME: &str = "bitmap_page_lookup.lance";
59pub const INDEX_STATS_METADATA_KEY: &str = "lance:index_stats";
60const BITMAP_PART_LOOKUP_PREFIX: &str = "part_";
61const BITMAP_PART_LOOKUP_SUFFIX: &str = "_bitmap_page_lookup.lance";
62const EXPLICIT_SHARD_ID_TAG: u64 = 0;
63const IMPLICIT_FRAGMENT_ID_TAG: u64 = 1;
64
65const MAX_BITMAP_ARRAY_LENGTH: usize = i32::MAX as usize - 1024 * 1024; const MAX_ROWS_PER_CHUNK: usize = 2 * 1024;
68const MERGE_ROWS_PER_CHUNK: usize = 512;
72
73const BITMAP_INDEX_VERSION: u32 = 0;
74
75#[derive(Clone)]
78struct LazyIndexReader {
79 index_reader: Arc<tokio::sync::Mutex<Option<Arc<dyn IndexReader>>>>,
80 store: Arc<dyn IndexStore>,
81}
82
83impl std::fmt::Debug for LazyIndexReader {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 f.debug_struct("LazyIndexReader")
86 .field("store", &self.store)
87 .finish()
88 }
89}
90
91impl LazyIndexReader {
92 fn new(store: Arc<dyn IndexStore>) -> Self {
93 Self {
94 index_reader: Arc::new(tokio::sync::Mutex::new(None)),
95 store,
96 }
97 }
98
99 async fn get(&self) -> Result<Arc<dyn IndexReader>> {
100 let mut reader = self.index_reader.lock().await;
101 if reader.is_none() {
102 let index_reader = self.store.open_index_file(BITMAP_LOOKUP_NAME).await?;
103 *reader = Some(index_reader);
104 }
105 Ok(reader.as_ref().unwrap().clone())
106 }
107}
108
109#[derive(Clone, Debug)]
114pub struct BitmapIndex {
115 index_map: Arc<BTreeMap<OrderableScalarValue, usize>>,
119
120 null_map: Arc<RowAddrTreeMap>,
121
122 value_type: DataType,
123
124 store: Arc<dyn IndexStore>,
125
126 index_cache: WeakLanceCache,
127
128 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
129
130 lazy_reader: LazyIndexReader,
131}
132
133#[derive(Debug, Clone)]
134pub struct BitmapKey {
135 row_offset: u64,
136}
137
138impl BitmapKey {
139 fn try_new(row_offset: usize) -> Result<Self> {
140 let row_offset = u64::try_from(row_offset).map_err(|_| {
141 Error::internal(format!(
142 "bitmap row offset {row_offset} does not fit in u64"
143 ))
144 })?;
145 Ok(Self { row_offset })
146 }
147}
148
149impl CacheKey for BitmapKey {
150 type ValueType = RowAddrTreeMap;
151
152 fn key(&self) -> std::borrow::Cow<'_, str> {
153 self.row_offset.to_string().into()
154 }
155
156 fn type_name() -> &'static str {
157 "Bitmap"
158 }
159
160 fn schema() -> CacheKeySchema {
161 CacheKeySchema::new("lance.scalar.bitmap-row-offset-key", 1)
162 }
163
164 fn write_key(&self, builder: &mut KeyBuilder) {
165 builder.write_u64(self.row_offset);
166 }
167
168 fn codec() -> Option<CacheCodec> {
169 Some(CacheCodec::from_impl::<RowAddrTreeMap>())
170 }
171}
172
173#[derive(Debug, Clone)]
180pub struct BitmapIndexState {
181 lookup_batch: RecordBatch,
188 null_map: Arc<RowAddrTreeMap>,
191 value_type: DataType,
194 index_map: Arc<BTreeMap<OrderableScalarValue, usize>>,
198}
199
200impl DeepSizeOf for BitmapIndexState {
201 fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
202 self.lookup_batch.get_array_memory_size()
203 + self.null_map.deep_size_of_children(context)
204 + self.index_map.deep_size_of_children(context)
205 }
206}
207
208impl BitmapIndexState {
209 pub(crate) fn from_index(index: &BitmapIndex) -> Result<Self> {
210 Ok(Self {
211 lookup_batch: build_lookup_batch(&index.index_map, &index.value_type)?,
212 null_map: index.null_map.clone(),
213 value_type: index.value_type.clone(),
214 index_map: index.index_map.clone(),
215 })
216 }
217
218 fn from_scalar_index(index: &dyn ScalarIndex) -> Result<Self> {
219 let bitmap = index
220 .as_any()
221 .downcast_ref::<BitmapIndex>()
222 .ok_or_else(|| {
223 Error::internal(
224 "BitmapIndexState::from_scalar_index called with a non-bitmap index",
225 )
226 })?;
227 Self::from_index(bitmap)
228 }
229
230 pub(crate) fn to_bitmap_index(
231 &self,
232 store: Arc<dyn IndexStore>,
233 index_cache: &LanceCache,
234 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
235 ) -> Result<Arc<BitmapIndex>> {
236 Ok(Arc::new(BitmapIndex::new(
237 self.index_map.clone(),
238 self.null_map.clone(),
239 self.value_type.clone(),
240 store,
241 WeakLanceCache::from(index_cache),
242 frag_reuse_index,
243 )))
244 }
245
246 #[cfg(test)]
249 pub(crate) fn new_for_test(
250 index_map: BTreeMap<OrderableScalarValue, usize>,
251 null_map: RowAddrTreeMap,
252 value_type: DataType,
253 ) -> Result<Self> {
254 Ok(Self {
255 lookup_batch: build_lookup_batch(&index_map, &value_type)?,
256 null_map: Arc::new(null_map),
257 value_type,
258 index_map: Arc::new(index_map),
259 })
260 }
261
262 #[cfg(test)]
263 pub(crate) fn lookup_batch(&self) -> &RecordBatch {
264 &self.lookup_batch
265 }
266
267 #[cfg(test)]
268 pub(crate) fn null_map(&self) -> &RowAddrTreeMap {
269 &self.null_map
270 }
271}
272
273fn build_lookup_batch(
274 index_map: &BTreeMap<OrderableScalarValue, usize>,
275 value_type: &DataType,
276) -> Result<RecordBatch> {
277 let keys = if index_map.is_empty() {
278 arrow_array::new_empty_array(value_type)
279 } else {
280 ScalarValue::iter_to_array(index_map.keys().map(|k| k.0.clone()))?
281 };
282 let offsets = Arc::new(UInt64Array::from_iter_values(
283 index_map.values().map(|v| *v as u64),
284 ));
285 let schema = Arc::new(Schema::new(vec![
286 Field::new("keys", value_type.clone(), true),
287 Field::new("offsets", DataType::UInt64, false),
288 ]));
289 Ok(RecordBatch::try_new(schema, vec![keys, offsets])?)
290}
291
292fn parse_lookup_batch(batch: &RecordBatch) -> Result<BTreeMap<OrderableScalarValue, usize>> {
293 let keys = batch.column(0);
294 let offsets = batch
295 .column(1)
296 .as_any()
297 .downcast_ref::<UInt64Array>()
298 .ok_or_else(|| {
299 Error::internal("BitmapIndexState: expected UInt64 offsets column".to_string())
300 })?;
301 let mut index_map = BTreeMap::new();
302 for idx in 0..batch.num_rows() {
303 let value = OrderableScalarValue(ScalarValue::try_from_array(keys, idx)?);
304 index_map.insert(value, offsets.value(idx) as usize);
305 }
306 Ok(index_map)
307}
308
309impl CacheCodecImpl for BitmapIndexState {
310 const TYPE_ID: &'static str = "lance.scalar.BitmapIndexState";
311 const CURRENT_VERSION: u32 = 1;
312
313 fn serialize(&self, w: &mut CacheEntryWriter<'_>) -> Result<()> {
320 let mut null_bytes = Vec::with_capacity(self.null_map.serialized_size());
321 self.null_map.serialize_into(&mut null_bytes)?;
322 w.write_raw(&null_bytes)?;
323 w.write_ipc(&self.lookup_batch)?;
324 Ok(())
325 }
326
327 fn deserialize(r: &mut CacheEntryReader<'_>) -> Result<Self> {
328 let null_bytes = r.read_raw()?;
329 let null_map = Arc::new(RowAddrTreeMap::deserialize_from(null_bytes.as_ref())?);
330 let lookup_batch = r.read_ipc()?;
331 let value_type = lookup_batch.schema().field(0).data_type().clone();
332 let index_map = Arc::new(parse_lookup_batch(&lookup_batch)?);
333 Ok(Self {
334 lookup_batch,
335 null_map,
336 value_type,
337 index_map,
338 })
339 }
340}
341
342struct BitmapIndexStateKey;
345
346impl CacheKey for BitmapIndexStateKey {
347 type ValueType = BitmapIndexState;
348
349 fn key(&self) -> std::borrow::Cow<'_, str> {
350 "state".into()
351 }
352
353 fn type_name() -> &'static str {
354 "BitmapIndexState"
355 }
356
357 fn schema() -> CacheKeySchema {
358 CacheKeySchema::new("lance.scalar.bitmap-index-state-key", 1)
359 }
360
361 fn write_key(&self, builder: &mut KeyBuilder) {
362 builder.write_variant(0);
363 }
364
365 fn codec() -> Option<CacheCodec> {
366 Some(CacheCodec::from_impl::<BitmapIndexState>())
367 }
368}
369
370impl BitmapIndex {
371 fn new(
372 index_map: Arc<BTreeMap<OrderableScalarValue, usize>>,
373 null_map: Arc<RowAddrTreeMap>,
374 value_type: DataType,
375 store: Arc<dyn IndexStore>,
376 index_cache: WeakLanceCache,
377 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
378 ) -> Self {
379 let lazy_reader = LazyIndexReader::new(store.clone());
380 Self {
381 index_map,
382 null_map,
383 value_type,
384 store,
385 index_cache,
386 frag_reuse_index,
387 lazy_reader,
388 }
389 }
390
391 pub(crate) async fn load(
392 store: Arc<dyn IndexStore>,
393 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
394 index_cache: &LanceCache,
395 ) -> Result<Arc<Self>> {
396 let page_lookup_file = store.open_index_file(BITMAP_LOOKUP_NAME).await?;
397 let total_rows = page_lookup_file.num_rows();
398
399 if total_rows == 0 {
400 let schema = page_lookup_file.schema();
401 let data_type = schema.fields[0].data_type();
402 return Ok(Arc::new(Self::new(
403 Arc::new(BTreeMap::new()),
404 Arc::new(RowAddrTreeMap::default()),
405 data_type,
406 store,
407 WeakLanceCache::from(index_cache),
408 frag_reuse_index,
409 )));
410 }
411
412 let mut index_map: BTreeMap<OrderableScalarValue, usize> = BTreeMap::new();
413 let mut null_map = Arc::new(RowAddrTreeMap::default());
414 let mut null_location: Option<usize> = None;
415 let value_type = page_lookup_file.schema().fields[0].data_type();
416
417 let mut keys_stream = page_lookup_file
420 .read_range_stream(0..total_rows, Some(&["keys"]))
421 .await?;
422 let mut row_offset: usize = 0;
423 while let Some(keys_batch) = keys_stream.try_next().await? {
424 let dict_keys = keys_batch.column(0);
425 for idx in 0..keys_batch.num_rows() {
426 let key = OrderableScalarValue(ScalarValue::try_from_array(dict_keys, idx)?);
427 if key.0.is_null() {
428 null_location = Some(row_offset);
429 } else {
430 index_map.insert(key, row_offset);
431 }
432 row_offset += 1;
433 }
434 }
435
436 if let Some(null_loc) = null_location {
437 let batch = page_lookup_file
438 .read_range(null_loc..null_loc + 1, Some(&["bitmaps"]))
439 .await?;
440
441 let binary_bitmaps = batch
442 .column(0)
443 .as_any()
444 .downcast_ref::<BinaryArray>()
445 .ok_or_else(|| Error::internal("Invalid bitmap column type".to_string()))?;
446 let bitmap_bytes = binary_bitmaps.value(0);
447 let mut bitmap = RowAddrTreeMap::deserialize_from(bitmap_bytes).unwrap();
448
449 if let Some(fri) = &frag_reuse_index {
451 bitmap = fri.remap_row_addrs_tree_map(&bitmap);
452 }
453
454 null_map = Arc::new(bitmap);
455 }
456
457 Ok(Arc::new(Self::new(
458 Arc::new(index_map),
459 null_map,
460 value_type,
461 store,
462 WeakLanceCache::from(index_cache),
463 frag_reuse_index,
464 )))
465 }
466
467 async fn load_bitmap(
468 &self,
469 key: &OrderableScalarValue,
470 metrics: Option<&dyn MetricsCollector>,
471 ) -> Result<Arc<RowAddrTreeMap>> {
472 if key.0.is_null() {
473 return Ok(self.null_map.clone());
474 }
475
476 let row_offset = match self.index_map.get(key) {
482 Some(loc) => *loc,
483 None => return Ok(Arc::new(RowAddrTreeMap::default())),
484 };
485 let cache_key = BitmapKey::try_new(row_offset)?;
486
487 if let Some(cached) = self.index_cache.get_with_key(&cache_key).await {
488 if let Some(metrics) = metrics {
489 metrics.record_index_cache_hit();
490 }
491 return Ok(cached);
492 }
493
494 if let Some(metrics) = metrics {
496 metrics.record_index_cache_miss();
497 metrics.record_part_load();
498 }
499
500 let page_lookup_file = self.lazy_reader.get().await?;
501 let batch = page_lookup_file
502 .read_range(row_offset..row_offset + 1, Some(&["bitmaps"]))
503 .await?;
504
505 let binary_bitmaps = batch
506 .column(0)
507 .as_any()
508 .downcast_ref::<BinaryArray>()
509 .ok_or_else(|| Error::internal("Invalid bitmap column type".to_string()))?;
510 let bitmap_bytes = binary_bitmaps.value(0); let mut bitmap = RowAddrTreeMap::deserialize_from(bitmap_bytes).unwrap();
512
513 if let Some(fri) = &self.frag_reuse_index {
514 bitmap = fri.remap_row_addrs_tree_map(&bitmap);
515 }
516
517 self.index_cache
518 .insert_with_key(&cache_key, Arc::new(bitmap.clone()))
519 .await;
520
521 Ok(Arc::new(bitmap))
522 }
523
524 pub(crate) fn value_type(&self) -> &DataType {
525 &self.value_type
526 }
527
528 pub(crate) async fn load_bitmap_index_state(
530 &self,
531 ) -> Result<HashMap<ScalarValue, RowAddrTreeMap>> {
532 let mut state = HashMap::new();
533
534 for key in self.index_map.keys() {
535 let bitmap = self.load_bitmap(key, None).await?;
536 state.insert(key.0.clone(), (*bitmap).clone());
537 }
538
539 if !self.null_map.is_empty() {
540 let existing_null = new_null_array(&self.value_type, 1);
541 let existing_null = ScalarValue::try_from_array(existing_null.as_ref(), 0)?;
542 state.insert(existing_null, (*self.null_map).clone());
543 }
544
545 Ok(state)
546 }
547}
548
549impl DeepSizeOf for BitmapIndex {
550 fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
551 self.index_map.deep_size_of_children(context) + self.store.deep_size_of_children(context)
552 }
553}
554
555#[derive(Serialize)]
556struct BitmapStatistics {
557 num_bitmaps: usize,
558}
559
560#[derive(Debug, Clone, Default, Serialize, Deserialize)]
561pub struct BitmapParameters {
562 pub shard_id: Option<u32>,
565}
566
567struct BitmapTrainingRequest {
568 parameters: BitmapParameters,
569 criteria: TrainingCriteria,
570}
571
572impl BitmapTrainingRequest {
573 fn new(parameters: BitmapParameters) -> Self {
574 Self {
575 parameters,
576 criteria: TrainingCriteria::new(TrainingOrdering::Values).with_row_id(),
577 }
578 }
579}
580
581impl TrainingRequest for BitmapTrainingRequest {
582 fn as_any(&self) -> &dyn std::any::Any {
583 self
584 }
585
586 fn criteria(&self) -> &TrainingCriteria {
587 &self.criteria
588 }
589}
590
591#[async_trait]
592impl Index for BitmapIndex {
593 fn as_any(&self) -> &dyn Any {
594 self
595 }
596
597 fn as_index(self: Arc<Self>) -> Arc<dyn Index> {
598 self
599 }
600
601 async fn prewarm(&self) -> Result<()> {
602 let page_lookup_file = self.lazy_reader.get().await?;
603 let total_rows = page_lookup_file.num_rows();
604
605 if total_rows == 0 {
606 return Ok(());
607 }
608
609 for start_row in (0..total_rows).step_by(MAX_ROWS_PER_CHUNK) {
610 let end_row = (start_row + MAX_ROWS_PER_CHUNK).min(total_rows);
611 let chunk = page_lookup_file
612 .read_range(start_row..end_row, None)
613 .await?;
614
615 if chunk.num_rows() == 0 {
616 continue;
617 }
618
619 let dict_keys = chunk.column(0);
620 let binary_bitmaps = chunk.column(1);
621 let bitmap_binary_array = binary_bitmaps
622 .as_any()
623 .downcast_ref::<BinaryArray>()
624 .unwrap();
625
626 for idx in 0..chunk.num_rows() {
627 let key = OrderableScalarValue(ScalarValue::try_from_array(dict_keys, idx)?);
628
629 if key.0.is_null() {
630 continue;
631 }
632
633 let bitmap_bytes = bitmap_binary_array.value(idx);
634 let mut bitmap = RowAddrTreeMap::deserialize_from(bitmap_bytes).unwrap();
635
636 if let Some(frag_reuse_index_ref) = self.frag_reuse_index.as_ref() {
637 bitmap = frag_reuse_index_ref.remap_row_addrs_tree_map(&bitmap);
638 }
639
640 let row_offset = start_row.checked_add(idx).ok_or_else(|| {
641 Error::internal(format!(
642 "bitmap row offset overflow: start_row={start_row}, idx={idx}"
643 ))
644 })?;
645 let cache_key = BitmapKey::try_new(row_offset)?;
646 self.index_cache
647 .insert_with_key(&cache_key, Arc::new(bitmap))
648 .await;
649 }
650 }
651
652 Ok(())
653 }
654
655 fn index_type(&self) -> IndexType {
656 IndexType::Bitmap
657 }
658
659 fn statistics(&self) -> Result<serde_json::Value> {
660 let stats = BitmapStatistics {
661 num_bitmaps: self.index_map.len() + if !self.null_map.is_empty() { 1 } else { 0 },
662 };
663 serde_json::to_value(stats).map_err(|e| {
664 Error::internal(format!(
665 "failed to serialize bitmap index statistics: {}",
666 e
667 ))
668 })
669 }
670
671 async fn calculate_included_frags(&self) -> Result<RoaringBitmap> {
672 unimplemented!()
673 }
674}
675
676#[async_trait]
677impl ScalarIndex for BitmapIndex {
678 #[instrument(name = "bitmap_search", level = "debug", skip_all)]
679 async fn search(
680 &self,
681 query: &dyn AnyQuery,
682 metrics: &dyn MetricsCollector,
683 ) -> Result<SearchResult> {
684 let query = query.as_any().downcast_ref::<SargableQuery>().unwrap();
685
686 let (row_ids, null_row_ids) = match query {
687 SargableQuery::Equals(val) => {
688 metrics.record_comparisons(1);
689 if val.is_null() {
690 ((*self.null_map).clone(), None)
692 } else {
693 let key = OrderableScalarValue(val.clone());
694 let bitmap = self.load_bitmap(&key, Some(metrics)).await?;
695 let null_rows = if !self.null_map.is_empty() {
696 Some((*self.null_map).clone())
697 } else {
698 None
699 };
700 ((*bitmap).clone(), null_rows)
701 }
702 }
703 SargableQuery::Range(start, end) => {
704 let range_start = match start {
705 Bound::Included(val) => Bound::Included(OrderableScalarValue(val.clone())),
706 Bound::Excluded(val) => Bound::Excluded(OrderableScalarValue(val.clone())),
707 Bound::Unbounded => Bound::Unbounded,
708 };
709
710 let range_end = match end {
711 Bound::Included(val) => Bound::Included(OrderableScalarValue(val.clone())),
712 Bound::Excluded(val) => Bound::Excluded(OrderableScalarValue(val.clone())),
713 Bound::Unbounded => Bound::Unbounded,
714 };
715
716 let empty_range = match (&range_start, &range_end) {
718 (Bound::Included(lower), Bound::Included(upper)) => lower > upper,
719 (Bound::Included(lower), Bound::Excluded(upper))
720 | (Bound::Excluded(lower), Bound::Included(upper))
721 | (Bound::Excluded(lower), Bound::Excluded(upper)) => lower >= upper,
722 _ => false,
723 };
724
725 let keys: Vec<_> = if empty_range {
726 Vec::new()
727 } else {
728 self.index_map
729 .range((range_start, range_end))
730 .map(|(k, _v)| k.clone())
731 .collect()
732 };
733
734 metrics.record_comparisons(keys.len());
735
736 let result = if keys.is_empty() {
737 RowAddrTreeMap::default()
738 } else {
739 let bitmaps: Vec<_> = stream::iter(
740 keys.into_iter()
741 .map(|key| async move { self.load_bitmap(&key, Some(metrics)).await }),
742 )
743 .buffer_unordered(get_num_compute_intensive_cpus())
744 .try_collect()
745 .await?;
746
747 let bitmap_refs: Vec<_> = bitmaps.iter().map(|b| b.as_ref()).collect();
748 RowAddrTreeMap::union_all(&bitmap_refs)
749 };
750
751 let null_rows = if !self.null_map.is_empty() {
752 Some((*self.null_map).clone())
753 } else {
754 None
755 };
756 (result, null_rows)
757 }
758 SargableQuery::IsIn(values) => {
759 metrics.record_comparisons(values.len());
760
761 let mut has_null = false;
763 let keys: Vec<_> = values
764 .iter()
765 .filter_map(|val| {
766 if val.is_null() {
767 has_null = true;
768 None
769 } else {
770 let key = OrderableScalarValue(val.clone());
771 if self.index_map.contains_key(&key) {
772 Some(key)
773 } else {
774 None
775 }
776 }
777 })
778 .collect();
779
780 let mut bitmaps: Vec<_> = stream::iter(
782 keys.into_iter()
783 .map(|key| async move { self.load_bitmap(&key, Some(metrics)).await }),
784 )
785 .buffer_unordered(get_num_compute_intensive_cpus())
786 .try_collect()
787 .await?;
788
789 if has_null && !self.null_map.is_empty() {
791 bitmaps.push(self.null_map.clone());
792 }
793
794 let result = if bitmaps.is_empty() {
795 RowAddrTreeMap::default()
796 } else {
797 let bitmap_refs: Vec<_> = bitmaps.iter().map(|b| b.as_ref()).collect();
799 RowAddrTreeMap::union_all(&bitmap_refs)
800 };
801
802 let null_rows = if !has_null && !self.null_map.is_empty() {
805 Some((*self.null_map).clone())
806 } else {
807 None
808 };
809 (result, null_rows)
810 }
811 SargableQuery::IsNull() => {
812 metrics.record_comparisons(1);
813 ((*self.null_map).clone(), None)
815 }
816 SargableQuery::FullTextSearch(_) => {
817 return Err(Error::not_supported_source(
818 "full text search is not supported for bitmap indexes".into(),
819 ));
820 }
821 SargableQuery::LikePrefix(_) => {
822 return Err(Error::not_supported_source(
823 "LIKE prefix queries are not supported for bitmap indexes".into(),
824 ));
825 }
826 };
827
828 let selection = NullableRowAddrSet::new(row_ids, null_row_ids.unwrap_or_default());
829 Ok(SearchResult::Exact(selection))
830 }
831
832 fn can_remap(&self) -> bool {
833 true
834 }
835
836 async fn remap(
838 &self,
839 mapping: &RowAddrRemap,
840 dest_store: &dyn IndexStore,
841 ) -> Result<CreatedIndex> {
842 let state = self.load_bitmap_index_state().await?;
843 let remapped_state = BitmapIndexPlugin::remap_bitmap_state(state, mapping);
844 let file =
845 BitmapIndexPlugin::write_bitmap_index(remapped_state, dest_store, &self.value_type)
846 .await?;
847
848 Ok(CreatedIndex {
849 index_details: prost_types::Any::from_msg(&pbold::BitmapIndexDetails::default())
850 .unwrap(),
851 index_version: BITMAP_INDEX_VERSION,
852 files: vec![file],
853 })
854 }
855
856 async fn update(
858 &self,
859 new_data: SendableRecordBatchStream,
860 dest_store: &dyn IndexStore,
861 old_data_filter: Option<super::OldIndexDataFilter>,
862 ) -> Result<CreatedIndex> {
863 let file = BitmapIndexPlugin::streaming_build_and_write(
864 new_data,
865 Some(self),
866 dest_store,
867 BITMAP_LOOKUP_NAME,
868 old_data_filter.as_ref(),
869 )
870 .await?;
871
872 Ok(CreatedIndex {
873 index_details: prost_types::Any::from_msg(&pbold::BitmapIndexDetails::default())
874 .unwrap(),
875 index_version: BITMAP_INDEX_VERSION,
876 files: vec![file],
877 })
878 }
879
880 fn update_criteria(&self) -> UpdateCriteria {
881 UpdateCriteria::only_new_data(TrainingCriteria::new(TrainingOrdering::Values).with_row_id())
882 }
883
884 fn derive_index_params(&self) -> Result<ScalarIndexParams> {
885 Ok(ScalarIndexParams::for_builtin(BuiltinIndexType::Bitmap))
886 }
887}
888
889struct BitmapBatchWriter {
892 file: Box<dyn super::IndexWriter>,
893 keys: Vec<ScalarValue>,
894 serialized: Vec<Vec<u8>>,
895 bytes: usize,
896 num_bitmaps: usize,
897}
898
899impl BitmapBatchWriter {
900 fn new(file: Box<dyn super::IndexWriter>) -> Self {
901 Self {
902 file,
903 keys: Vec::new(),
904 serialized: Vec::new(),
905 bytes: 0,
906 num_bitmaps: 0,
907 }
908 }
909
910 async fn emit(&mut self, key: ScalarValue, bitmap: &RowAddrTreeMap) -> Result<()> {
913 let mut buf = Vec::new();
914 bitmap.serialize_into(&mut buf).unwrap();
915 let size = buf.len();
916
917 if self.bytes + size > MAX_BITMAP_ARRAY_LENGTH {
918 self.flush().await?;
919 }
920
921 self.keys.push(key);
922 self.serialized.push(buf);
923 self.bytes += size;
924 self.num_bitmaps += 1;
925 Ok(())
926 }
927
928 async fn flush(&mut self) -> Result<()> {
930 if self.keys.is_empty() {
931 return Ok(());
932 }
933 let keys_array =
934 ScalarValue::iter_to_array(self.keys.drain(..).collect::<Vec<_>>()).unwrap();
935 let total_size: usize = self.serialized.iter().map(|b| b.len()).sum();
936 let mut binary_builder = BinaryBuilder::with_capacity(self.serialized.len(), total_size);
937 for b in self.serialized.drain(..) {
938 binary_builder.append_value(&b);
939 }
940 let bitmaps_array = Arc::new(binary_builder.finish()) as Arc<dyn Array>;
941 let batch = BitmapIndexPlugin::get_batch_from_arrays(keys_array, bitmaps_array)?;
942 self.file.write_record_batch(batch).await?;
943 self.bytes = 0;
944 Ok(())
945 }
946
947 async fn finish(mut self) -> Result<IndexFile> {
949 self.flush().await?;
950 let stats_json = serde_json::to_string(&BitmapStatistics {
951 num_bitmaps: self.num_bitmaps,
952 })
953 .map_err(|e| Error::internal(format!("failed to serialize bitmap statistics: {e}")))?;
954 let mut metadata = HashMap::new();
955 metadata.insert(INDEX_STATS_METADATA_KEY.to_string(), stats_json);
956 self.file.finish_with_metadata(metadata).await
957 }
958}
959
960fn bitmap_shard_file_name(partition_id: u64) -> String {
961 format!("{BITMAP_PART_LOOKUP_PREFIX}{partition_id}{BITMAP_PART_LOOKUP_SUFFIX}")
962}
963
964fn tagged_bitmap_partition_id(id: u32, tag: u64) -> u64 {
965 ((id as u64) << 32) | tag
966}
967
968fn bitmap_shard_partition_id(fragment_ids: &[u32], shard_id: Option<u32>) -> Result<u64> {
969 if fragment_ids.is_empty() {
970 return Err(Error::invalid_input(
971 "Bitmap shard build requires at least one fragment id".to_string(),
972 ));
973 }
974
975 if let Some(shard_id) = shard_id {
976 return Ok(tagged_bitmap_partition_id(shard_id, EXPLICIT_SHARD_ID_TAG));
977 }
978
979 let [fragment_id] = fragment_ids else {
980 return Err(Error::invalid_input(format!(
981 "Bitmap distributed build over multiple fragments requires an explicit shard_id. \
982 Received {} fragment ids: {:?}. Please assign mutually exclusive shard_id values \
983 to disjoint fragment groups.",
984 fragment_ids.len(),
985 fragment_ids
986 )));
987 };
988
989 Ok(tagged_bitmap_partition_id(
990 *fragment_id,
991 IMPLICIT_FRAGMENT_ID_TAG,
992 ))
993}
994
995fn extract_bitmap_shard_id(filename: &str) -> Result<u64> {
996 let partition_id = filename
997 .strip_prefix(BITMAP_PART_LOOKUP_PREFIX)
998 .and_then(|name| name.strip_suffix(BITMAP_PART_LOOKUP_SUFFIX))
999 .ok_or_else(|| {
1000 Error::internal(format!("Invalid bitmap shard file name format: {filename}"))
1001 })?;
1002 partition_id.parse::<u64>().map_err(|_| {
1003 Error::internal(format!(
1004 "Failed to parse bitmap partition id from file name: {filename}"
1005 ))
1006 })
1007}
1008
1009fn deserialize_bitmap(bitmap_bytes: &[u8], file_name: &str) -> Result<RowAddrTreeMap> {
1010 RowAddrTreeMap::deserialize_from(bitmap_bytes).map_err(|error| {
1011 Error::corrupt_file(
1012 Path::from(file_name),
1013 format!("Failed to deserialize bitmap bytes: {error}"),
1014 )
1015 })
1016}
1017
1018async fn new_bitmap_batch_writer(
1019 index_store: &dyn IndexStore,
1020 file_name: &str,
1021 value_type: &DataType,
1022) -> Result<BitmapBatchWriter> {
1023 let schema = Arc::new(Schema::new(vec![
1024 Field::new("keys", value_type.clone(), true),
1025 Field::new("bitmaps", DataType::Binary, true),
1026 ]));
1027 let index_file = index_store.new_index_file(file_name, schema).await?;
1028 Ok(BitmapBatchWriter::new(index_file))
1029}
1030
1031#[derive(Clone, Debug, Eq, PartialEq)]
1032struct BitmapHeapItem {
1033 key: OrderableScalarValue,
1034 shard_idx: usize,
1035}
1036
1037impl Ord for BitmapHeapItem {
1038 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1039 self.key
1040 .cmp(&other.key)
1041 .then_with(|| self.shard_idx.cmp(&other.shard_idx))
1042 }
1043}
1044
1045impl PartialOrd for BitmapHeapItem {
1046 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1047 Some(self.cmp(other))
1048 }
1049}
1050
1051struct BitmapShardCursor {
1052 file_name: String,
1053 reader: Arc<dyn IndexReader>,
1054 total_rows: usize,
1055 next_row_offset: usize,
1056 batch: Option<RecordBatch>,
1057 batch_row_idx: usize,
1058}
1059
1060impl BitmapShardCursor {
1061 async fn try_new(file_name: String, reader: Arc<dyn IndexReader>) -> Result<Option<Self>> {
1062 let total_rows = reader.num_rows();
1063 if total_rows == 0 {
1064 return Ok(None);
1065 }
1066
1067 let mut cursor = Self {
1068 file_name,
1069 reader,
1070 total_rows,
1071 next_row_offset: 0,
1072 batch: None,
1073 batch_row_idx: 0,
1074 };
1075 if cursor.advance().await? {
1076 Ok(Some(cursor))
1077 } else {
1078 Ok(None)
1079 }
1080 }
1081
1082 fn peek_key(&self) -> Result<OrderableScalarValue> {
1083 let batch = self.batch.as_ref().ok_or_else(|| {
1084 Error::internal(format!(
1085 "Bitmap shard {} has no active batch",
1086 self.file_name
1087 ))
1088 })?;
1089 let key = ScalarValue::try_from_array(batch.column(0), self.batch_row_idx)?;
1090 Ok(OrderableScalarValue(key))
1091 }
1092
1093 fn take_current(&mut self) -> Result<(ScalarValue, RowAddrTreeMap)> {
1094 let batch = self.batch.as_ref().ok_or_else(|| {
1095 Error::internal(format!(
1096 "Bitmap shard {} has no active batch",
1097 self.file_name
1098 ))
1099 })?;
1100 let keys = batch.column(0);
1101 let binary_bitmaps = batch
1102 .column(1)
1103 .as_any()
1104 .downcast_ref::<BinaryArray>()
1105 .ok_or_else(|| {
1106 Error::corrupt_file(
1107 Path::from(self.file_name.as_str()),
1108 "Bitmap shard batch has non-binary bitmap column".to_string(),
1109 )
1110 })?;
1111 let key = ScalarValue::try_from_array(keys, self.batch_row_idx)?;
1112 let bitmap = deserialize_bitmap(binary_bitmaps.value(self.batch_row_idx), &self.file_name)?;
1113 self.batch_row_idx += 1;
1114 Ok((key, bitmap))
1115 }
1116
1117 async fn advance(&mut self) -> Result<bool> {
1118 loop {
1119 if let Some(batch) = &self.batch
1120 && self.batch_row_idx < batch.num_rows()
1121 {
1122 return Ok(true);
1123 }
1124
1125 if self.next_row_offset >= self.total_rows {
1126 self.batch = None;
1127 return Ok(false);
1128 }
1129
1130 let end_row = (self.next_row_offset + MERGE_ROWS_PER_CHUNK).min(self.total_rows);
1131 let batch = self
1132 .reader
1133 .read_range(self.next_row_offset..end_row, None)
1134 .await?;
1135 self.next_row_offset = end_row;
1136 self.batch = Some(batch);
1137 self.batch_row_idx = 0;
1138 }
1139 }
1140}
1141
1142async fn advance_cursor_and_push(
1143 cursors: &mut [BitmapShardCursor],
1144 heap: &mut BinaryHeap<Reverse<BitmapHeapItem>>,
1145 shard_idx: usize,
1146) -> Result<()> {
1147 if cursors[shard_idx].advance().await? {
1148 heap.push(Reverse(BitmapHeapItem {
1149 key: cursors[shard_idx].peek_key()?,
1150 shard_idx,
1151 }));
1152 }
1153 Ok(())
1154}
1155
1156async fn drain_same_key_bitmaps(
1157 cursors: &mut [BitmapShardCursor],
1158 heap: &mut BinaryHeap<Reverse<BitmapHeapItem>>,
1159 item: BitmapHeapItem,
1160) -> Result<(ScalarValue, RowAddrTreeMap)> {
1161 let (key, mut merged_bitmap) = cursors[item.shard_idx].take_current()?;
1162 let merged_key = OrderableScalarValue(key);
1163 advance_cursor_and_push(cursors, heap, item.shard_idx).await?;
1164
1165 while let Some(Reverse(next_item)) = heap.peek() {
1166 if next_item.key != merged_key {
1167 break;
1168 }
1169
1170 let shard_idx = next_item.shard_idx;
1171 let _ = heap.pop();
1172 let (_, bitmap) = cursors[shard_idx].take_current()?;
1173 merged_bitmap |= &bitmap;
1174 advance_cursor_and_push(cursors, heap, shard_idx).await?;
1175 }
1176
1177 Ok((merged_key.0, merged_bitmap))
1178}
1179
1180async fn list_bitmap_shard_files(
1181 object_store: &ObjectStore,
1182 index_dir: &Path,
1183 progress: &dyn IndexBuildProgress,
1184) -> Result<Vec<String>> {
1185 let mut shard_files = Vec::new();
1186 let mut list_stream = object_store.list(Some(index_dir.clone()));
1187 while let Some(item) = list_stream.next().await {
1188 match item {
1189 Ok(meta) => {
1190 let file_name = meta.location.filename().unwrap_or_default();
1191 if file_name.starts_with(BITMAP_PART_LOOKUP_PREFIX)
1192 && file_name.ends_with(BITMAP_PART_LOOKUP_SUFFIX)
1193 {
1194 shard_files.push(file_name.to_string());
1195 progress
1196 .stage_progress("scan_bitmap_shards", shard_files.len() as u64)
1197 .await?;
1198 }
1199 }
1200 Err(err) => {
1201 return Err(Error::io(format!(
1202 "Failed to list bitmap shard files in {}: {err}",
1203 index_dir
1204 )));
1205 }
1206 }
1207 }
1208 let mut shard_files = shard_files
1209 .into_iter()
1210 .map(|file_name| extract_bitmap_shard_id(&file_name).map(|shard_id| (shard_id, file_name)))
1211 .collect::<Result<Vec<_>>>()?;
1212 shard_files.sort_unstable_by_key(|(shard_id, _)| *shard_id);
1213 let shard_files = shard_files
1214 .into_iter()
1215 .map(|(_, file_name)| file_name)
1216 .collect::<Vec<_>>();
1217 if shard_files.is_empty() {
1218 return Err(Error::invalid_input(format!(
1219 "No bitmap shard files found in index directory: {}; \
1220 call build_index for each fragment before calling merge_index_metadata",
1221 index_dir
1222 )));
1223 }
1224 Ok(shard_files)
1225}
1226
1227async fn cleanup_bitmap_shard_files(store: &dyn IndexStore, shard_files: &[String]) {
1228 for file_name in shard_files {
1229 if let Err(error) = store.delete_index_file(file_name).await {
1230 warn!(
1231 "Failed to delete bitmap shard file '{}': {}. \
1232 This does not affect the merged bitmap index, but the shard file \
1233 may need manual cleanup.",
1234 file_name, error
1235 );
1236 }
1237 }
1238}
1239
1240#[derive(Debug, Default)]
1241pub struct BitmapIndexPlugin;
1242
1243fn retain_valid(
1247 mut bitmap: RowAddrTreeMap,
1248 filter: Option<&super::OldIndexDataFilter>,
1249) -> RowAddrTreeMap {
1250 if let Some(filter) = filter {
1251 filter.retain_old_rows(&mut bitmap);
1252 }
1253 bitmap
1254}
1255
1256impl BitmapIndexPlugin {
1257 fn get_batch_from_arrays(
1258 keys: Arc<dyn Array>,
1259 binary_bitmaps: Arc<dyn Array>,
1260 ) -> Result<RecordBatch> {
1261 let schema = Arc::new(Schema::new(vec![
1262 Field::new("keys", keys.data_type().clone(), true),
1263 Field::new("bitmaps", binary_bitmaps.data_type().clone(), true),
1264 ]));
1265
1266 let columns = vec![keys, binary_bitmaps];
1267
1268 Ok(RecordBatch::try_new(schema, columns)?)
1269 }
1270
1271 async fn write_bitmap_index(
1272 state: HashMap<ScalarValue, RowAddrTreeMap>,
1273 index_store: &dyn IndexStore,
1274 value_type: &DataType,
1275 ) -> Result<IndexFile> {
1276 Self::write_bitmap_index_with_extras(
1277 state,
1278 index_store,
1279 value_type,
1280 HashMap::new(),
1281 Vec::new(),
1282 )
1283 .await
1284 }
1285
1286 pub(crate) async fn write_bitmap_index_with_extras(
1288 state: HashMap<ScalarValue, RowAddrTreeMap>,
1289 index_store: &dyn IndexStore,
1290 value_type: &DataType,
1291 mut metadata: HashMap<String, String>,
1292 global_buffers: Vec<(String, Bytes)>,
1293 ) -> Result<IndexFile> {
1294 let num_bitmaps = state.len();
1295 let schema = Arc::new(Schema::new(vec![
1296 Field::new("keys", value_type.clone(), true),
1297 Field::new("bitmaps", DataType::Binary, true),
1298 ]));
1299
1300 let mut bitmap_index_file = index_store
1301 .new_index_file(BITMAP_LOOKUP_NAME, schema)
1302 .await?;
1303
1304 for (metadata_key, data) in global_buffers {
1305 let buffer_idx = bitmap_index_file.add_global_buffer(data).await?;
1306 metadata.insert(metadata_key, buffer_idx.to_string());
1307 }
1308
1309 let mut cur_keys = Vec::new();
1310 let mut cur_bitmaps = Vec::new();
1311 let mut cur_bytes = 0;
1312
1313 for (key, bitmap) in state.into_iter() {
1314 let mut bytes = Vec::new();
1315 bitmap.serialize_into(&mut bytes).unwrap();
1316 let bitmap_size = bytes.len();
1317
1318 if cur_bytes + bitmap_size > MAX_BITMAP_ARRAY_LENGTH {
1319 let keys_array = ScalarValue::iter_to_array(cur_keys.clone()).unwrap();
1320 let mut binary_builder = BinaryBuilder::new();
1321 for b in &cur_bitmaps {
1322 binary_builder.append_value(b);
1323 }
1324 let bitmaps_array = Arc::new(binary_builder.finish()) as Arc<dyn Array>;
1325
1326 let record_batch = Self::get_batch_from_arrays(keys_array, bitmaps_array)?;
1327 bitmap_index_file.write_record_batch(record_batch).await?;
1328
1329 cur_keys.clear();
1330 cur_bitmaps.clear();
1331 cur_bytes = 0;
1332 }
1333
1334 cur_keys.push(key);
1335 cur_bitmaps.push(bytes);
1336 cur_bytes += bitmap_size;
1337 }
1338
1339 if !cur_keys.is_empty() {
1341 let keys_array = ScalarValue::iter_to_array(cur_keys).unwrap();
1342 let mut binary_builder = BinaryBuilder::new();
1343 for b in &cur_bitmaps {
1344 binary_builder.append_value(b);
1345 }
1346 let bitmaps_array = Arc::new(binary_builder.finish()) as Arc<dyn Array>;
1347
1348 let record_batch = Self::get_batch_from_arrays(keys_array, bitmaps_array)?;
1349 bitmap_index_file.write_record_batch(record_batch).await?;
1350 }
1351
1352 let stats_json = serde_json::to_string(&BitmapStatistics { num_bitmaps })
1354 .map_err(|e| Error::internal(format!("failed to serialize bitmap statistics: {e}")))?;
1355 metadata.insert(INDEX_STATS_METADATA_KEY.to_string(), stats_json);
1356
1357 bitmap_index_file.finish_with_metadata(metadata).await
1358 }
1359
1360 pub(crate) async fn build_bitmap_index_state(
1362 mut data_source: SendableRecordBatchStream,
1363 mut state: HashMap<ScalarValue, RowAddrTreeMap>,
1364 ) -> Result<(HashMap<ScalarValue, RowAddrTreeMap>, DataType)> {
1365 let value_type = data_source.schema().field(0).data_type().clone();
1366 while let Some(batch) = data_source.try_next().await? {
1367 let values = batch.column_by_name(VALUE_COLUMN_NAME).expect_ok()?;
1368 let row_ids = batch.column_by_name(ROW_ID).expect_ok()?;
1369 debug_assert_eq!(row_ids.data_type(), &DataType::UInt64);
1370
1371 let row_id_column = row_ids.as_any().downcast_ref::<UInt64Array>().unwrap();
1372
1373 for i in 0..values.len() {
1374 let row_id = row_id_column.value(i);
1375 let key = ScalarValue::try_from_array(values.as_ref(), i)?;
1376 state.entry(key.clone()).or_default().insert(row_id);
1377 }
1378 }
1379
1380 Ok((state, value_type))
1381 }
1382
1383 pub async fn train_bitmap_index(
1384 data: SendableRecordBatchStream,
1385 index_store: &dyn IndexStore,
1386 ) -> Result<IndexFile> {
1387 Self::streaming_build_and_write(data, None, index_store, BITMAP_LOOKUP_NAME, None).await
1388 }
1389
1390 async fn train_bitmap_shard(
1391 data: SendableRecordBatchStream,
1392 index_store: &dyn IndexStore,
1393 fragment_ids: &[u32],
1394 shard_id: Option<u32>,
1395 progress: Arc<dyn crate::progress::IndexBuildProgress>,
1396 ) -> Result<IndexFile> {
1397 let partition_id = bitmap_shard_partition_id(fragment_ids, shard_id)?;
1398 let file_name = bitmap_shard_file_name(partition_id);
1399 progress
1400 .stage_start("build_bitmap_shard", None, "rows")
1401 .await?;
1402 let file =
1403 Self::streaming_build_and_write(data, None, index_store, &file_name, None).await?;
1404 progress.stage_complete("build_bitmap_shard").await?;
1405 Ok(file)
1406 }
1407
1408 async fn streaming_build_and_write(
1416 mut data_source: SendableRecordBatchStream,
1417 old_index: Option<&BitmapIndex>,
1418 index_store: &dyn IndexStore,
1419 output_file_name: &str,
1420 old_data_filter: Option<&super::OldIndexDataFilter>,
1421 ) -> Result<IndexFile> {
1422 let value_type = data_source.schema().field(0).data_type().clone();
1423
1424 let mut writer =
1425 new_bitmap_batch_writer(index_store, output_file_name, &value_type).await?;
1426
1427 let old_keys: Vec<OrderableScalarValue> = old_index
1430 .map(|idx| idx.index_map.keys().cloned().collect())
1431 .unwrap_or_default();
1432 let mut old_pos: usize = 0;
1433
1434 let mut current_key: Option<ScalarValue> = None;
1436 let mut current_bitmap = RowAddrTreeMap::default();
1437 let mut emitted_null = false;
1440
1441 while let Some(batch) = data_source.try_next().await? {
1442 let values = batch.column_by_name(VALUE_COLUMN_NAME).expect_ok()?;
1443 let row_ids = batch.column_by_name(ROW_ID).expect_ok()?;
1444 debug_assert_eq!(row_ids.data_type(), &DataType::UInt64);
1445 let row_id_column = row_ids.as_any().downcast_ref::<UInt64Array>().unwrap();
1446
1447 for i in 0..values.len() {
1448 let row_id = row_id_column.value(i);
1449 let key = ScalarValue::try_from_array(values.as_ref(), i)?;
1450
1451 match ¤t_key {
1452 Some(cur) if *cur == key => {
1453 current_bitmap.insert(row_id);
1454 }
1455 _ => {
1456 if let Some(prev_key) = current_key.take() {
1458 let mut prev_bitmap = std::mem::take(&mut current_bitmap);
1459 Self::finish_run(
1460 prev_key,
1461 &mut prev_bitmap,
1462 old_index,
1463 &old_keys,
1464 &mut old_pos,
1465 &mut emitted_null,
1466 &mut writer,
1467 old_data_filter,
1468 )
1469 .await?;
1470 }
1471 current_key = Some(key);
1472 current_bitmap = RowAddrTreeMap::default();
1473 current_bitmap.insert(row_id);
1474 }
1475 }
1476 }
1477 }
1478
1479 if let Some(last_key) = current_key.take() {
1481 let mut last_bitmap = std::mem::take(&mut current_bitmap);
1482 Self::finish_run(
1483 last_key,
1484 &mut last_bitmap,
1485 old_index,
1486 &old_keys,
1487 &mut old_pos,
1488 &mut emitted_null,
1489 &mut writer,
1490 old_data_filter,
1491 )
1492 .await?;
1493 }
1494
1495 if let Some(idx) = old_index {
1497 while old_pos < old_keys.len() {
1498 let old_bitmap = retain_valid(
1499 idx.load_bitmap(&old_keys[old_pos], None)
1500 .await?
1501 .as_ref()
1502 .clone(),
1503 old_data_filter,
1504 );
1505 writer
1506 .emit(old_keys[old_pos].0.clone(), &old_bitmap)
1507 .await?;
1508 old_pos += 1;
1509 }
1510 }
1511
1512 if !emitted_null
1514 && let Some(idx) = old_index
1515 && !idx.null_map.is_empty()
1516 {
1517 let null_key = new_null_array(&value_type, 1);
1518 let null_key = ScalarValue::try_from_array(null_key.as_ref(), 0)?;
1519 let null_bitmap = retain_valid((*idx.null_map).clone(), old_data_filter);
1520 writer.emit(null_key, &null_bitmap).await?;
1521 }
1522
1523 writer.finish().await
1524 }
1525
1526 #[allow(clippy::too_many_arguments)]
1530 async fn finish_run(
1531 key: ScalarValue,
1532 bitmap: &mut RowAddrTreeMap,
1533 old_index: Option<&BitmapIndex>,
1534 old_keys: &[OrderableScalarValue],
1535 old_pos: &mut usize,
1536 emitted_null: &mut bool,
1537 writer: &mut BitmapBatchWriter,
1538 old_data_filter: Option<&super::OldIndexDataFilter>,
1539 ) -> Result<()> {
1540 if key.is_null() {
1541 if let Some(idx) = old_index
1543 && !idx.null_map.is_empty()
1544 {
1545 *bitmap |= &retain_valid((*idx.null_map).clone(), old_data_filter);
1546 }
1547 *emitted_null = true;
1548 writer.emit(key, bitmap).await?;
1549 } else if let Some(idx) = old_index {
1550 let orderable = OrderableScalarValue(key.clone());
1551
1552 while *old_pos < old_keys.len() && old_keys[*old_pos] < orderable {
1554 let old_bitmap = retain_valid(
1555 idx.load_bitmap(&old_keys[*old_pos], None)
1556 .await?
1557 .as_ref()
1558 .clone(),
1559 old_data_filter,
1560 );
1561 writer
1562 .emit(old_keys[*old_pos].0.clone(), &old_bitmap)
1563 .await?;
1564 *old_pos += 1;
1565 }
1566
1567 if *old_pos < old_keys.len() && old_keys[*old_pos] == orderable {
1569 *bitmap |= &retain_valid(
1570 idx.load_bitmap(&old_keys[*old_pos], None)
1571 .await?
1572 .as_ref()
1573 .clone(),
1574 old_data_filter,
1575 );
1576 *old_pos += 1;
1577 }
1578
1579 writer.emit(key, bitmap).await?;
1580 } else {
1581 writer.emit(key, bitmap).await?;
1582 }
1583 Ok(())
1584 }
1585
1586 pub(crate) fn remap_bitmap_state(
1588 state: HashMap<ScalarValue, RowAddrTreeMap>,
1589 mapping: &RowAddrRemap,
1590 ) -> HashMap<ScalarValue, RowAddrTreeMap> {
1591 state
1592 .into_iter()
1593 .map(|(key, bitmap)| {
1594 let remapped_bitmap =
1595 RowAddrTreeMap::from_iter(bitmap.row_addrs().unwrap().filter_map(|addr| {
1596 let addr_as_u64 = u64::from(addr);
1597 mapping.get(addr_as_u64).unwrap_or(Some(addr_as_u64))
1598 }));
1599 (key, remapped_bitmap)
1600 })
1601 .collect()
1602 }
1603
1604 async fn merge_shards(
1621 store: &dyn IndexStore,
1622 shard_files: &[String],
1623 progress: Arc<dyn IndexBuildProgress>,
1624 ) -> Result<IndexFile> {
1625 progress
1626 .stage_start("merge_bitmap_shards", None, "bitmaps")
1627 .await?;
1628
1629 let mut cursors = Vec::with_capacity(shard_files.len());
1630 let mut heap = BinaryHeap::with_capacity(shard_files.len());
1631 let mut value_type: Option<DataType> = None;
1632
1633 for file_name in shard_files {
1634 let reader = store.open_index_file(file_name).await?;
1635 let shard_value_type = reader.schema().fields[0].data_type().clone();
1636 if let Some(existing_type) = &value_type {
1637 if existing_type != &shard_value_type {
1638 return Err(Error::invalid_input(format!(
1639 "Bitmap shard {} has value type {:?}, expected {:?}",
1640 file_name, shard_value_type, existing_type
1641 )));
1642 }
1643 } else {
1644 value_type = Some(shard_value_type);
1645 }
1646 if let Some(cursor) = BitmapShardCursor::try_new(file_name.clone(), reader).await? {
1647 let key = cursor.peek_key()?;
1648 let shard_idx = cursors.len();
1649 cursors.push(cursor);
1650 heap.push(Reverse(BitmapHeapItem { key, shard_idx }));
1651 }
1652 }
1653
1654 let value_type = value_type.ok_or_else(|| {
1655 Error::invalid_input("Bitmap shard merge requires at least one shard file".to_string())
1656 })?;
1657 let mut writer = new_bitmap_batch_writer(store, BITMAP_LOOKUP_NAME, &value_type).await?;
1658 let mut merged_keys = 0u64;
1659
1660 while let Some(Reverse(item)) = heap.pop() {
1661 let (key, merged_bitmap) =
1662 drain_same_key_bitmaps(&mut cursors, &mut heap, item).await?;
1663 writer.emit(key, &merged_bitmap).await?;
1664 merged_keys += 1;
1665 progress
1666 .stage_progress("merge_bitmap_shards", merged_keys)
1667 .await?;
1668 }
1669
1670 progress.stage_complete("merge_bitmap_shards").await?;
1671 progress
1672 .stage_start("write_bitmap_index", Some(1), "files")
1673 .await?;
1674 let file = writer.finish().await?;
1675 progress.stage_progress("write_bitmap_index", 1).await?;
1676 progress.stage_complete("write_bitmap_index").await?;
1677 Ok(file)
1678 }
1679}
1680
1681pub async fn merge_index_files(
1682 object_store: &ObjectStore,
1683 index_dir: &Path,
1684 store: Arc<dyn IndexStore>,
1685 progress: Arc<dyn IndexBuildProgress>,
1686) -> Result<()> {
1687 progress
1688 .stage_start("scan_bitmap_shards", None, "files")
1689 .await?;
1690 let shard_files = list_bitmap_shard_files(object_store, index_dir, progress.as_ref()).await?;
1691 progress.stage_complete("scan_bitmap_shards").await?;
1692
1693 BitmapIndexPlugin::merge_shards(store.as_ref(), &shard_files, progress).await?;
1694 cleanup_bitmap_shard_files(store.as_ref(), &shard_files).await;
1695 Ok(())
1696}
1697
1698pub async fn merge_bitmap_indices(
1699 source_indices: &[Arc<BitmapIndex>],
1700 dest_store: &dyn IndexStore,
1701 progress: Arc<dyn IndexBuildProgress>,
1702) -> Result<CreatedIndex> {
1703 if source_indices.is_empty() {
1704 return Err(Error::invalid_input(
1705 "Bitmap segment merge requires at least one source segment".to_string(),
1706 ));
1707 }
1708
1709 let value_type = source_indices[0].value_type().clone();
1710 let mut merged_state = HashMap::<ScalarValue, RowAddrTreeMap>::new();
1711
1712 progress
1713 .stage_start(
1714 "merge_bitmap_segments",
1715 Some(source_indices.len() as u64),
1716 "segments",
1717 )
1718 .await?;
1719 for (idx, source_index) in source_indices.iter().enumerate() {
1720 if source_index.value_type() != &value_type {
1721 return Err(Error::invalid_input(format!(
1722 "Bitmap segment has value type {:?}, expected {:?}",
1723 source_index.value_type(),
1724 value_type
1725 )));
1726 }
1727
1728 let state = source_index.load_bitmap_index_state().await?;
1729 for (key, bitmap) in state {
1730 merged_state
1731 .entry(key)
1732 .and_modify(|existing| *existing |= &bitmap)
1733 .or_insert(bitmap);
1734 }
1735 progress
1736 .stage_progress("merge_bitmap_segments", (idx + 1) as u64)
1737 .await?;
1738 }
1739 progress.stage_complete("merge_bitmap_segments").await?;
1740
1741 progress
1742 .stage_start("write_bitmap_index", Some(1), "files")
1743 .await?;
1744 let file = BitmapIndexPlugin::write_bitmap_index(merged_state, dest_store, &value_type).await?;
1745 progress.stage_progress("write_bitmap_index", 1).await?;
1746 progress.stage_complete("write_bitmap_index").await?;
1747
1748 Ok(CreatedIndex {
1749 index_details: prost_types::Any::from_msg(&pbold::BitmapIndexDetails::default()).unwrap(),
1750 index_version: BITMAP_INDEX_VERSION,
1751 files: vec![file],
1752 })
1753}
1754
1755#[async_trait]
1756impl BasicTrainer for BitmapIndexPlugin {
1757 fn new_training_request(
1758 &self,
1759 params: &str,
1760 field: &Field,
1761 ) -> Result<Box<dyn TrainingRequest>> {
1762 if field.data_type().is_nested() {
1763 return Err(Error::invalid_input_source(
1764 "A bitmap index can only be created on a non-nested field.".into(),
1765 ));
1766 }
1767 let params = if params.is_empty() {
1768 BitmapParameters::default()
1769 } else {
1770 serde_json::from_str::<BitmapParameters>(params)?
1771 };
1772 Ok(Box::new(BitmapTrainingRequest::new(params)))
1773 }
1774
1775 async fn train_index(
1776 &self,
1777 data: SendableRecordBatchStream,
1778 index_store: &dyn IndexStore,
1779 request: Box<dyn TrainingRequest>,
1780 fragment_ids: Option<Vec<u32>>,
1781 progress: Arc<dyn crate::progress::IndexBuildProgress>,
1782 ) -> Result<CreatedIndex> {
1783 let request = request
1784 .as_any()
1785 .downcast_ref::<BitmapTrainingRequest>()
1786 .ok_or_else(|| {
1787 Error::internal(
1788 "BitmapIndexPlugin::train_index received a non-bitmap training request"
1789 .to_string(),
1790 )
1791 })?;
1792 let file = if let Some(fragment_ids) = fragment_ids.as_ref() {
1793 Self::train_bitmap_shard(
1794 data,
1795 index_store,
1796 fragment_ids,
1797 request.parameters.shard_id,
1798 progress,
1799 )
1800 .await?
1801 } else if request.parameters.shard_id.is_some() {
1802 return Err(Error::invalid_input(
1803 "Bitmap shard_id requires fragment_ids and is only supported for distributed shard builds"
1804 .to_string(),
1805 ));
1806 } else {
1807 Self::train_bitmap_index(data, index_store).await?
1808 };
1809 Ok(CreatedIndex {
1810 index_details: prost_types::Any::from_msg(&pbold::BitmapIndexDetails::default())
1811 .unwrap(),
1812 index_version: BITMAP_INDEX_VERSION,
1813 files: vec![file],
1814 })
1815 }
1816}
1817
1818#[async_trait]
1819impl ScalarIndexPlugin for BitmapIndexPlugin {
1820 fn basic_trainer(&self) -> Option<&dyn BasicTrainer> {
1821 Some(self)
1822 }
1823
1824 fn name(&self) -> &str {
1825 "Bitmap"
1826 }
1827
1828 fn provides_exact_answer(&self) -> bool {
1829 true
1830 }
1831
1832 fn version(&self) -> u32 {
1833 BITMAP_INDEX_VERSION
1834 }
1835
1836 fn new_query_parser(
1837 &self,
1838 index_name: String,
1839 _index_details: &prost_types::Any,
1840 ) -> Option<Box<dyn ScalarQueryParser>> {
1841 Some(Box::new(
1844 SargableQueryParser::new(index_name, self.name().to_string(), false)
1845 .without_like_prefix(),
1846 ))
1847 }
1848
1849 async fn load_index(
1851 &self,
1852 index_store: Arc<dyn IndexStore>,
1853 _index_details: &prost_types::Any,
1854 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
1855 cache: &LanceCache,
1856 ) -> Result<Arc<dyn ScalarIndex>> {
1857 Ok(BitmapIndex::load(index_store, frag_reuse_index, cache).await? as Arc<dyn ScalarIndex>)
1858 }
1859
1860 async fn get_from_cache(
1861 &self,
1862 index_store: Arc<dyn IndexStore>,
1863 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
1864 cache: &LanceCache,
1865 ) -> Result<Option<Arc<dyn ScalarIndex>>> {
1866 let Some(state) = cache.get_with_key(&BitmapIndexStateKey).await else {
1867 return Ok(None);
1868 };
1869 let index = state.to_bitmap_index(index_store, cache, frag_reuse_index)?;
1870 Ok(Some(index as Arc<dyn ScalarIndex>))
1871 }
1872
1873 async fn put_in_cache(&self, cache: &LanceCache, index: Arc<dyn ScalarIndex>) -> Result<()> {
1874 let state = BitmapIndexState::from_scalar_index(index.as_ref())?;
1875 cache
1876 .insert_with_key(&BitmapIndexStateKey, Arc::new(state))
1877 .await;
1878 Ok(())
1879 }
1880
1881 async fn get_or_insert_in_cache(
1882 &self,
1883 index_store: Arc<dyn IndexStore>,
1884 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
1885 cache: &LanceCache,
1886 load: ScalarIndexLoad<'_>,
1887 ) -> Result<Arc<dyn ScalarIndex>> {
1888 single_flight_open(
1889 cache,
1890 BitmapIndexStateKey,
1891 load,
1892 BitmapIndexState::from_scalar_index,
1893 move |state| {
1894 Ok(state.to_bitmap_index(index_store, cache, frag_reuse_index)?
1895 as Arc<dyn ScalarIndex>)
1896 },
1897 )
1898 .await
1899 }
1900
1901 async fn load_statistics(
1902 &self,
1903 index_store: Arc<dyn IndexStore>,
1904 _index_details: &prost_types::Any,
1905 ) -> Result<Option<serde_json::Value>> {
1906 let reader = index_store.open_index_file(BITMAP_LOOKUP_NAME).await?;
1907 if let Some(value) = reader.schema().metadata.get(INDEX_STATS_METADATA_KEY) {
1908 let stats = serde_json::from_str(value).map_err(|e| {
1909 Error::internal(format!("failed to parse bitmap statistics metadata: {e}"))
1910 })?;
1911 Ok(Some(stats))
1912 } else {
1913 Ok(None)
1914 }
1915 }
1916}
1917
1918#[cfg(test)]
1919mod tests {
1920 use super::*;
1921 use crate::metrics::{LocalMetricsCollector, NoOpMetricsCollector};
1922 use crate::scalar::lance_format::LanceIndexStore;
1923 use arrow_array::{RecordBatch, StringArray, UInt64Array, record_batch};
1924 use arrow_schema::{DataType, Field, Schema};
1925
1926 fn sort_batch_by_value(batch: &RecordBatch) -> RecordBatch {
1929 use arrow::compute::SortOptions;
1930 let values = batch.column(0);
1931 let row_ids = batch.column(1);
1932 let options = SortOptions {
1933 descending: false,
1934 nulls_first: true,
1935 };
1936 let indices = arrow::compute::sort_to_indices(values, Some(options), None).unwrap();
1937 let sorted_values = arrow::compute::take(values.as_ref(), &indices, None).unwrap();
1938 let sorted_row_ids = arrow::compute::take(row_ids.as_ref(), &indices, None).unwrap();
1939 RecordBatch::try_new(batch.schema(), vec![sorted_values, sorted_row_ids]).unwrap()
1940 }
1941 use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
1942 use futures::stream;
1943 use lance_core::utils::{address::RowAddress, tempfile::TempObjDir};
1944 use lance_io::object_store::ObjectStore;
1945 use lance_select::RowSetOps;
1946 use rstest::rstest;
1947
1948 fn assert_state_roundtrips(state: &BitmapIndexState) {
1949 let mut buf = Vec::new();
1950 state
1951 .serialize(&mut CacheEntryWriter::new(&mut buf))
1952 .unwrap();
1953 let data = bytes::Bytes::from(buf);
1954 let mut reader = CacheEntryReader::new(&data, 0, BitmapIndexState::CURRENT_VERSION);
1955 let restored = BitmapIndexState::deserialize(&mut reader).unwrap();
1956 assert_eq!(restored.lookup_batch, state.lookup_batch);
1957 assert_eq!(&*restored.null_map, &*state.null_map);
1958 assert_eq!(restored.value_type, state.value_type);
1959 }
1960
1961 #[test]
1962 fn test_bitmap_index_state_codec_roundtrip() {
1963 let mut index_map = BTreeMap::new();
1965 index_map.insert(OrderableScalarValue(ScalarValue::Int32(Some(1))), 0);
1966 index_map.insert(OrderableScalarValue(ScalarValue::Int32(Some(7))), 1);
1967 index_map.insert(OrderableScalarValue(ScalarValue::Int32(Some(42))), 2);
1968 let mut null_map = RowAddrTreeMap::new();
1969 null_map.insert(RowAddress::new_from_parts(0, 3).into());
1970 null_map.insert(RowAddress::new_from_parts(0, 5).into());
1971 let state = BitmapIndexState {
1972 lookup_batch: build_lookup_batch(&index_map, &DataType::Int32).unwrap(),
1973 null_map: Arc::new(null_map),
1974 value_type: DataType::Int32,
1975 index_map: Arc::new(index_map),
1976 };
1977 assert_state_roundtrips(&state);
1978
1979 let empty_state = BitmapIndexState {
1981 lookup_batch: build_lookup_batch(&BTreeMap::new(), &DataType::Utf8).unwrap(),
1982 null_map: Arc::new(RowAddrTreeMap::new()),
1983 value_type: DataType::Utf8,
1984 index_map: Arc::new(BTreeMap::new()),
1985 };
1986 assert_state_roundtrips(&empty_state);
1987 }
1988
1989 #[test]
1993 fn test_bitmap_index_state_lookup_is_zero_copy() {
1994 const ALIGN: usize = 64;
1995 let mut index_map = BTreeMap::new();
1996 for k in 0..32i32 {
1997 index_map.insert(
1998 OrderableScalarValue(ScalarValue::Int32(Some(k))),
1999 k as usize,
2000 );
2001 }
2002 let state = BitmapIndexState {
2003 lookup_batch: build_lookup_batch(&index_map, &DataType::Int32).unwrap(),
2004 null_map: Arc::new(RowAddrTreeMap::new()),
2005 value_type: DataType::Int32,
2006 index_map: Arc::new(index_map),
2007 };
2008
2009 let codec = CacheCodec::from_impl::<BitmapIndexState>();
2010 let any: Arc<dyn std::any::Any + Send + Sync> = Arc::new(state);
2011 let mut buf = Vec::new();
2012 codec.serialize(&any, &mut buf).unwrap();
2013
2014 let mut v = vec![0u8; buf.len() + ALIGN];
2016 let pad = (ALIGN - (v.as_ptr() as usize % ALIGN)) % ALIGN;
2017 v[pad..pad + buf.len()].copy_from_slice(&buf);
2018 let data = bytes::Bytes::from(v).slice(pad..pad + buf.len());
2019
2020 let restored = codec.deserialize(&data).hit().unwrap();
2021 let restored = restored.downcast::<BitmapIndexState>().unwrap();
2022
2023 let base = data.as_ptr() as usize;
2024 let end = base + data.len();
2025 for col in restored.lookup_batch.columns() {
2026 for buffer in col.to_data().buffers() {
2027 let ptr = buffer.as_ptr() as usize;
2028 assert!(
2029 ptr >= base && ptr < end,
2030 "lookup batch buffer was realigned out of the input — misaligned IPC section",
2031 );
2032 }
2033 }
2034 }
2035
2036 #[tokio::test]
2037 async fn test_bitmap_cache_key_uses_row_offset_identity() {
2038 let cache = LanceCache::with_capacity(1024);
2039 let first = BitmapKey::try_new(3).unwrap();
2040 let second = BitmapKey::try_new(4).unwrap();
2041
2042 cache
2043 .insert_with_key(&first, Arc::new(RowAddrTreeMap::default()))
2044 .await;
2045
2046 assert!(cache.get_with_key(&second).await.is_none());
2047 }
2048
2049 #[tokio::test]
2050 async fn test_bitmap_lazy_loading_and_cache() {
2051 let tmpdir = TempObjDir::default();
2053 let store = Arc::new(LanceIndexStore::new(
2054 Arc::new(ObjectStore::local()),
2055 tmpdir.clone(),
2056 Arc::new(LanceCache::no_cache()),
2057 ));
2058
2059 let colors = vec![
2061 "red", "blue", "green", "red", "yellow", "blue", "red", "green", "blue", "yellow",
2062 "red", "red", "blue", "green", "yellow",
2063 ];
2064
2065 let row_ids = (0u64..15u64).collect::<Vec<_>>();
2066
2067 let schema = Arc::new(Schema::new(vec![
2068 Field::new("value", DataType::Utf8, false),
2069 Field::new("_rowid", DataType::UInt64, false),
2070 ]));
2071
2072 let batch = RecordBatch::try_new(
2073 schema.clone(),
2074 vec![
2075 Arc::new(StringArray::from(colors.clone())),
2076 Arc::new(UInt64Array::from(row_ids.clone())),
2077 ],
2078 )
2079 .unwrap();
2080
2081 let batch = sort_batch_by_value(&batch);
2082 let stream = stream::once(async move { Ok(batch) });
2083 let stream = Box::pin(RecordBatchStreamAdapter::new(schema, stream));
2084
2085 BitmapIndexPlugin::train_bitmap_index(stream, store.as_ref())
2087 .await
2088 .unwrap();
2089
2090 let cache = LanceCache::with_capacity(1024 * 1024); let index = BitmapIndex::load(store.clone(), None, &cache)
2095 .await
2096 .unwrap();
2097
2098 assert_eq!(index.index_map.len(), 4); assert!(index.null_map.is_empty()); let query = SargableQuery::Equals(ScalarValue::Utf8(Some("red".to_string())));
2103 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2104
2105 let expected_red_rows = vec![0u64, 3, 6, 10, 11];
2107 if let SearchResult::Exact(row_ids) = result {
2108 let mut actual: Vec<u64> = row_ids
2109 .true_rows()
2110 .row_addrs()
2111 .unwrap()
2112 .map(|id| id.into())
2113 .collect();
2114 actual.sort();
2115 assert_eq!(actual, expected_red_rows);
2116 } else {
2117 panic!("Expected exact search result");
2118 }
2119
2120 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2122 if let SearchResult::Exact(row_ids) = result {
2123 let mut actual: Vec<u64> = row_ids
2124 .true_rows()
2125 .row_addrs()
2126 .unwrap()
2127 .map(|id| id.into())
2128 .collect();
2129 actual.sort();
2130 assert_eq!(actual, expected_red_rows);
2131 }
2132
2133 let query = SargableQuery::Range(
2135 std::ops::Bound::Included(ScalarValue::Utf8(Some("blue".to_string()))),
2136 std::ops::Bound::Included(ScalarValue::Utf8(Some("green".to_string()))),
2137 );
2138 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2139
2140 let expected_range_rows = vec![1u64, 2, 5, 7, 8, 12, 13];
2141 if let SearchResult::Exact(row_ids) = result {
2142 let mut actual: Vec<u64> = row_ids
2143 .true_rows()
2144 .row_addrs()
2145 .unwrap()
2146 .map(|id| id.into())
2147 .collect();
2148 actual.sort();
2149 assert_eq!(actual, expected_range_rows);
2150 }
2151
2152 let query = SargableQuery::Range(
2154 std::ops::Bound::Included(ScalarValue::Utf8(Some("green".to_string()))),
2155 std::ops::Bound::Included(ScalarValue::Utf8(Some("blue".to_string()))),
2156 );
2157 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2158 if let SearchResult::Exact(row_ids) = result {
2159 assert!(row_ids.true_rows().is_empty());
2160 } else {
2161 panic!("Expected exact search result");
2162 }
2163
2164 let query = SargableQuery::IsIn(vec![
2166 ScalarValue::Utf8(Some("red".to_string())),
2167 ScalarValue::Utf8(Some("yellow".to_string())),
2168 ]);
2169 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2170
2171 let expected_in_rows = vec![0u64, 3, 4, 6, 9, 10, 11, 14];
2172 if let SearchResult::Exact(row_ids) = result {
2173 let mut actual: Vec<u64> = row_ids
2174 .true_rows()
2175 .row_addrs()
2176 .unwrap()
2177 .map(|id| id.into())
2178 .collect();
2179 actual.sort();
2180 assert_eq!(actual, expected_in_rows);
2181 }
2182 }
2183
2184 #[tokio::test]
2191 async fn test_bitmap_absent_value_records_no_cache_activity() {
2192 let tmpdir = TempObjDir::default();
2193 let store = Arc::new(LanceIndexStore::new(
2194 Arc::new(ObjectStore::local()),
2195 tmpdir.clone(),
2196 Arc::new(LanceCache::no_cache()),
2197 ));
2198
2199 let colors = vec!["red", "blue", "green", "yellow"];
2200 let row_ids = (0u64..4u64).collect::<Vec<_>>();
2201 let schema = Arc::new(Schema::new(vec![
2202 Field::new("value", DataType::Utf8, false),
2203 Field::new("_rowid", DataType::UInt64, false),
2204 ]));
2205 let batch = RecordBatch::try_new(
2206 schema.clone(),
2207 vec![
2208 Arc::new(StringArray::from(colors)),
2209 Arc::new(UInt64Array::from(row_ids)),
2210 ],
2211 )
2212 .unwrap();
2213 let batch = sort_batch_by_value(&batch);
2214 let stream = stream::once(async move { Ok(batch) });
2215 let stream = Box::pin(RecordBatchStreamAdapter::new(schema, stream));
2216 BitmapIndexPlugin::train_bitmap_index(stream, store.as_ref())
2217 .await
2218 .unwrap();
2219
2220 let cache = LanceCache::with_capacity(1024 * 1024);
2223 let index = BitmapIndex::load(store.clone(), None, &cache)
2224 .await
2225 .unwrap();
2226
2227 let metrics = LocalMetricsCollector::default();
2230 let query = SargableQuery::Equals(ScalarValue::Utf8(Some("purple".to_string())));
2231 let result = index.search(&query, &metrics).await.unwrap();
2232 if let SearchResult::Exact(row_ids) = result {
2233 assert!(row_ids.true_rows().is_empty());
2234 } else {
2235 panic!("Expected exact search result");
2236 }
2237 assert_eq!(
2238 metrics.index_cache_hits(),
2239 0,
2240 "absent value must not record any cache hits",
2241 );
2242 assert_eq!(
2243 metrics.index_cache_misses(),
2244 0,
2245 "absent value must not record a cache miss (no loader ran)",
2246 );
2247
2248 let metrics = LocalMetricsCollector::default();
2250 let query = SargableQuery::IsIn(vec![
2251 ScalarValue::Utf8(Some("purple".to_string())),
2252 ScalarValue::Utf8(Some("teal".to_string())),
2253 ]);
2254 let result = index.search(&query, &metrics).await.unwrap();
2255 if let SearchResult::Exact(row_ids) = result {
2256 assert!(row_ids.true_rows().is_empty());
2257 } else {
2258 panic!("Expected exact search result");
2259 }
2260 assert_eq!(metrics.index_cache_hits(), 0);
2261 assert_eq!(metrics.index_cache_misses(), 0);
2262
2263 let metrics = LocalMetricsCollector::default();
2267 let query = SargableQuery::Equals(ScalarValue::Utf8(Some("red".to_string())));
2268 index.search(&query, &metrics).await.unwrap();
2269 assert_eq!(metrics.index_cache_hits(), 0);
2270 assert_eq!(metrics.index_cache_misses(), 1);
2271 }
2272
2273 #[tokio::test]
2279 async fn test_bitmap_cache_fast_path() {
2280 use arrow_array::Int32Array;
2281
2282 let tmpdir = TempObjDir::default();
2283 let store = Arc::new(LanceIndexStore::new(
2284 Arc::new(ObjectStore::local()),
2285 tmpdir.clone(),
2286 Arc::new(LanceCache::no_cache()),
2287 ));
2288
2289 const N: u64 = 1_000;
2291 const NULL_COUNT: u64 = 5;
2292 let null_values: Vec<Option<i32>> =
2294 std::iter::repeat_n(None, NULL_COUNT as usize).collect();
2295 let non_null_values: Vec<Option<i32>> = (0..N as i32).map(Some).collect();
2296 let all_values: Vec<Option<i32>> = null_values.into_iter().chain(non_null_values).collect();
2297 let all_row_ids: Vec<u64> = (0..N + NULL_COUNT).collect();
2298
2299 let schema = Arc::new(Schema::new(vec![
2300 Field::new("value", DataType::Int32, true),
2301 Field::new("_rowid", DataType::UInt64, false),
2302 ]));
2303 let batch = RecordBatch::try_new(
2304 schema.clone(),
2305 vec![
2306 Arc::new(Int32Array::from(all_values)),
2307 Arc::new(UInt64Array::from(all_row_ids)),
2308 ],
2309 )
2310 .unwrap();
2311 let stream = stream::once(async move { Ok(batch) });
2312 let stream = Box::pin(RecordBatchStreamAdapter::new(schema, stream));
2313 BitmapIndexPlugin::train_bitmap_index(stream, store.as_ref())
2314 .await
2315 .unwrap();
2316
2317 let cache = LanceCache::with_capacity(16 * 1024 * 1024);
2318 let index = BitmapIndex::load(store.clone(), None, &cache)
2319 .await
2320 .unwrap();
2321
2322 let plugin = BitmapIndexPlugin;
2323 let index_arc: Arc<dyn ScalarIndex> = index.clone() as Arc<dyn ScalarIndex>;
2324 plugin.put_in_cache(&cache, index_arc).await.unwrap();
2325
2326 let cached = plugin
2329 .get_from_cache(store.clone(), None, &cache)
2330 .await
2331 .unwrap()
2332 .expect("get_from_cache must return Some after put_in_cache");
2333
2334 let query = SargableQuery::IsNull();
2336 match cached.search(&query, &NoOpMetricsCollector).await.unwrap() {
2337 SearchResult::Exact(row_set) => {
2338 let mut null_rows: Vec<u64> = row_set
2339 .true_rows()
2340 .row_addrs()
2341 .unwrap()
2342 .map(u64::from)
2343 .collect();
2344 null_rows.sort();
2345 let expected: Vec<u64> = (0..NULL_COUNT).collect();
2346 assert_eq!(null_rows, expected);
2347 }
2348 _ => panic!("Expected Exact result for IS NULL"),
2349 }
2350 }
2351
2352 #[tokio::test]
2353 #[ignore]
2354 async fn test_big_bitmap_index() {
2355 use super::{BITMAP_LOOKUP_NAME, BitmapIndex};
2358 use crate::scalar::IndexStore;
2359 use crate::scalar::lance_format::LanceIndexStore;
2360 use arrow_schema::DataType;
2361 use datafusion_common::ScalarValue;
2362 use lance_core::cache::LanceCache;
2363 use lance_io::object_store::ObjectStore;
2364 use lance_select::RowAddrTreeMap;
2365 use std::collections::HashMap;
2366 use std::sync::Arc;
2367
2368 let m: u32 = 2_500_000;
2374 let per_bitmap_size = 1000; let mut state = HashMap::new();
2377 for i in 0..m {
2378 let bitmap = RowAddrTreeMap::from_iter(0..per_bitmap_size);
2380
2381 let key = ScalarValue::UInt32(Some(i));
2382 state.insert(key, bitmap);
2383 }
2384
2385 let tmpdir = TempObjDir::default();
2387 let test_store = LanceIndexStore::new(
2388 Arc::new(ObjectStore::local()),
2389 tmpdir.clone(),
2390 Arc::new(LanceCache::no_cache()),
2391 );
2392
2393 let result =
2396 BitmapIndexPlugin::write_bitmap_index(state, &test_store, &DataType::UInt32).await;
2397
2398 assert!(
2399 result.is_ok(),
2400 "Failed to write bitmap index: {:?}",
2401 result.err()
2402 );
2403
2404 let index_file = test_store.open_index_file(BITMAP_LOOKUP_NAME).await;
2406 assert!(
2407 index_file.is_ok(),
2408 "Failed to open index file: {:?}",
2409 index_file.err()
2410 );
2411 let index_file = index_file.unwrap();
2412
2413 tracing::info!(
2415 "Index file contains {} rows in total",
2416 index_file.num_rows()
2417 );
2418
2419 tracing::info!("Loading index from disk...");
2421 let loaded_index = BitmapIndex::load(Arc::new(test_store), None, &LanceCache::no_cache())
2422 .await
2423 .expect("Failed to load bitmap index");
2424
2425 assert_eq!(
2427 loaded_index.index_map.len(),
2428 m as usize,
2429 "Loaded index has incorrect number of keys (expected {}, got {})",
2430 m,
2431 loaded_index.index_map.len()
2432 );
2433
2434 let test_keys = [0, m / 2, m - 1]; for &key_val in &test_keys {
2437 let key = OrderableScalarValue(ScalarValue::UInt32(Some(key_val)));
2438 let bitmap = loaded_index
2440 .load_bitmap(&key, None)
2441 .await
2442 .unwrap_or_else(|_| panic!("Key {} should exist", key_val));
2443
2444 let row_addrs: Vec<u64> = bitmap.row_addrs().unwrap().map(u64::from).collect();
2446
2447 assert_eq!(
2449 row_addrs.len(),
2450 per_bitmap_size as usize,
2451 "Bitmap for key {} has wrong size",
2452 key_val
2453 );
2454
2455 for i in 0..5.min(per_bitmap_size) {
2457 assert!(
2458 row_addrs.contains(&i),
2459 "Bitmap for key {} should contain row_id {}",
2460 key_val,
2461 i
2462 );
2463 }
2464
2465 for i in (per_bitmap_size - 5)..per_bitmap_size {
2466 assert!(
2467 row_addrs.contains(&i),
2468 "Bitmap for key {} should contain row_id {}",
2469 key_val,
2470 i
2471 );
2472 }
2473
2474 let expected_range: Vec<u64> = (0..per_bitmap_size).collect();
2476 assert_eq!(
2477 row_addrs, expected_range,
2478 "Bitmap for key {} doesn't contain expected values",
2479 key_val
2480 );
2481
2482 tracing::info!(
2483 "✓ Verified bitmap for key {}: {} rows as expected",
2484 key_val,
2485 row_addrs.len()
2486 );
2487 }
2488
2489 tracing::info!("Test successful! Index properly contains {} keys", m);
2490 }
2491
2492 #[tokio::test]
2493 async fn test_bitmap_prewarm() {
2494 let tmpdir = TempObjDir::default();
2496 let store = Arc::new(LanceIndexStore::new(
2497 Arc::new(ObjectStore::local()),
2498 tmpdir.clone(),
2499 Arc::new(LanceCache::no_cache()),
2500 ));
2501
2502 let colors = vec![
2504 "red", "blue", "green", "red", "yellow", "blue", "red", "green", "blue", "yellow",
2505 "red", "red", "blue", "green", "yellow",
2506 ];
2507
2508 let row_ids = (0u64..15u64).collect::<Vec<_>>();
2509
2510 let schema = Arc::new(Schema::new(vec![
2511 Field::new("value", DataType::Utf8, false),
2512 Field::new("_rowid", DataType::UInt64, false),
2513 ]));
2514
2515 let batch = RecordBatch::try_new(
2516 schema.clone(),
2517 vec![
2518 Arc::new(StringArray::from(colors.clone())),
2519 Arc::new(UInt64Array::from(row_ids.clone())),
2520 ],
2521 )
2522 .unwrap();
2523
2524 let batch = sort_batch_by_value(&batch);
2525 let stream = stream::once(async move { Ok(batch) });
2526 let stream = Box::pin(RecordBatchStreamAdapter::new(schema, stream));
2527
2528 BitmapIndexPlugin::train_bitmap_index(stream, store.as_ref())
2530 .await
2531 .unwrap();
2532
2533 let cache = LanceCache::with_capacity(1024 * 1024); let index = BitmapIndex::load(store.clone(), None, &cache)
2538 .await
2539 .unwrap();
2540
2541 let red = OrderableScalarValue(ScalarValue::Utf8(Some("red".to_string())));
2543 let blue = OrderableScalarValue(ScalarValue::Utf8(Some("blue".to_string())));
2544 let cache_key_red = BitmapKey::try_new(*index.index_map.get(&red).unwrap()).unwrap();
2545 let cache_key_blue = BitmapKey::try_new(*index.index_map.get(&blue).unwrap()).unwrap();
2546
2547 assert!(
2548 cache
2549 .get_with_key::<BitmapKey>(&cache_key_red)
2550 .await
2551 .is_none()
2552 );
2553 assert!(
2554 cache
2555 .get_with_key::<BitmapKey>(&cache_key_blue)
2556 .await
2557 .is_none()
2558 );
2559
2560 index.prewarm().await.unwrap();
2562
2563 assert!(
2565 cache
2566 .get_with_key::<BitmapKey>(&cache_key_red)
2567 .await
2568 .is_some()
2569 );
2570 assert!(
2571 cache
2572 .get_with_key::<BitmapKey>(&cache_key_blue)
2573 .await
2574 .is_some()
2575 );
2576
2577 let cached_red = cache
2579 .get_with_key::<BitmapKey>(&cache_key_red)
2580 .await
2581 .unwrap();
2582 let red_rows: Vec<u64> = cached_red.row_addrs().unwrap().map(u64::from).collect();
2583 assert_eq!(red_rows, vec![0, 3, 6, 10, 11]);
2584
2585 index.prewarm().await.unwrap();
2587
2588 let cached_red_2 = cache
2590 .get_with_key::<BitmapKey>(&cache_key_red)
2591 .await
2592 .unwrap();
2593 let red_rows_2: Vec<u64> = cached_red_2.row_addrs().unwrap().map(u64::from).collect();
2594 assert_eq!(red_rows_2, vec![0, 3, 6, 10, 11]);
2595 }
2596
2597 fn bitmap_remap_compact() -> RowAddrRemap {
2600 use lance_core::utils::row_addr_remap::GroupInput;
2601 use roaring::RoaringTreemap;
2602 RowAddrRemap::compact([GroupInput {
2603 rewritten_old_row_addrs: RoaringTreemap::from_iter(
2604 (0..3)
2605 .map(|o| u64::from(RowAddress::new_from_parts(1, o)))
2606 .chain((0..3).map(|o| u64::from(RowAddress::new_from_parts(2, o)))),
2607 ),
2608 old_frag_ids: vec![1, 2],
2609 new_frags: vec![(3, 6)],
2610 }])
2611 .unwrap()
2612 }
2613
2614 fn bitmap_remap_explicit() -> RowAddrRemap {
2615 RowAddrRemap::direct(
2617 (0..6u32)
2618 .map(|i| {
2619 let (f, o) = if i < 3 { (1, i) } else { (2, i - 3) };
2620 (
2621 u64::from(RowAddress::new_from_parts(f, o)),
2622 Some(u64::from(RowAddress::new_from_parts(3, i))),
2623 )
2624 })
2625 .collect(),
2626 )
2627 }
2628
2629 #[rstest]
2631 #[case(bitmap_remap_compact())]
2632 #[case(bitmap_remap_explicit())]
2633 #[tokio::test]
2634 async fn test_remap_bitmap_with_null(#[case] remap: RowAddrRemap) {
2635 use arrow_array::UInt32Array;
2636
2637 let tmpdir = TempObjDir::default();
2639 let test_store = Arc::new(LanceIndexStore::new(
2640 Arc::new(ObjectStore::local()),
2641 tmpdir.clone(),
2642 Arc::new(LanceCache::no_cache()),
2643 ));
2644
2645 let values = vec![
2650 None, None, Some(1u32), Some(1u32), Some(2u32), Some(2u32), ];
2657
2658 let row_ids: Vec<u64> = vec![
2660 RowAddress::new_from_parts(1, 0).into(),
2661 RowAddress::new_from_parts(1, 1).into(),
2662 RowAddress::new_from_parts(1, 2).into(),
2663 RowAddress::new_from_parts(2, 0).into(),
2664 RowAddress::new_from_parts(2, 1).into(),
2665 RowAddress::new_from_parts(2, 2).into(),
2666 ];
2667
2668 let schema = Arc::new(Schema::new(vec![
2669 Field::new("value", DataType::UInt32, true),
2670 Field::new("_rowid", DataType::UInt64, false),
2671 ]));
2672
2673 let batch = RecordBatch::try_new(
2674 schema.clone(),
2675 vec![
2676 Arc::new(UInt32Array::from(values)),
2677 Arc::new(UInt64Array::from(row_ids)),
2678 ],
2679 )
2680 .unwrap();
2681
2682 let stream = stream::once(async move { Ok(batch) });
2683 let stream = Box::pin(RecordBatchStreamAdapter::new(schema, stream));
2684
2685 BitmapIndexPlugin::train_bitmap_index(stream, test_store.as_ref())
2687 .await
2688 .unwrap();
2689
2690 let index = BitmapIndex::load(test_store.clone(), None, &LanceCache::no_cache())
2692 .await
2693 .expect("Failed to load bitmap index");
2694
2695 assert_eq!(index.index_map.len(), 2); assert!(!index.null_map.is_empty()); index.remap(&remap, test_store.as_ref()).await.unwrap();
2701
2702 let reloaded_idx = BitmapIndex::load(test_store, None, &LanceCache::no_cache())
2704 .await
2705 .expect("Failed to load remapped bitmap index");
2706
2707 let expected_null_addrs: Vec<u64> = vec![
2709 RowAddress::new_from_parts(3, 0).into(),
2710 RowAddress::new_from_parts(3, 1).into(),
2711 ];
2712 let actual_null_addrs: Vec<u64> = reloaded_idx
2713 .null_map
2714 .row_addrs()
2715 .unwrap()
2716 .map(u64::from)
2717 .collect();
2718 assert_eq!(
2719 actual_null_addrs, expected_null_addrs,
2720 "Null bitmap not remapped correctly"
2721 );
2722
2723 let query = SargableQuery::Equals(ScalarValue::UInt32(Some(1)));
2725 let result = reloaded_idx
2726 .search(&query, &NoOpMetricsCollector)
2727 .await
2728 .unwrap();
2729 if let crate::scalar::SearchResult::Exact(row_ids) = result {
2730 let mut actual: Vec<u64> = row_ids
2731 .true_rows()
2732 .row_addrs()
2733 .unwrap()
2734 .map(u64::from)
2735 .collect();
2736 actual.sort();
2737 let expected: Vec<u64> = vec![
2738 RowAddress::new_from_parts(3, 2).into(),
2739 RowAddress::new_from_parts(3, 3).into(),
2740 ];
2741 assert_eq!(actual, expected, "Value 1 bitmap not remapped correctly");
2742 }
2743
2744 let query = SargableQuery::Equals(ScalarValue::UInt32(Some(2)));
2746 let result = reloaded_idx
2747 .search(&query, &NoOpMetricsCollector)
2748 .await
2749 .unwrap();
2750 if let crate::scalar::SearchResult::Exact(row_ids) = result {
2751 let mut actual: Vec<u64> = row_ids
2752 .true_rows()
2753 .row_addrs()
2754 .unwrap()
2755 .map(u64::from)
2756 .collect();
2757 actual.sort();
2758 let expected: Vec<u64> = vec![
2759 RowAddress::new_from_parts(3, 4).into(),
2760 RowAddress::new_from_parts(3, 5).into(),
2761 ];
2762 assert_eq!(actual, expected, "Value 2 bitmap not remapped correctly");
2763 }
2764
2765 let query = SargableQuery::IsNull();
2767 let result = reloaded_idx
2768 .search(&query, &NoOpMetricsCollector)
2769 .await
2770 .unwrap();
2771 if let crate::scalar::SearchResult::Exact(row_ids) = result {
2772 let mut actual: Vec<u64> = row_ids
2773 .true_rows()
2774 .row_addrs()
2775 .unwrap()
2776 .map(u64::from)
2777 .collect();
2778 actual.sort();
2779 assert_eq!(
2780 actual, expected_null_addrs,
2781 "Null search results not correct"
2782 );
2783 }
2784 }
2785
2786 #[tokio::test]
2787 async fn test_bitmap_null_handling_in_queries() {
2788 let tmpdir = TempObjDir::default();
2790 let store = Arc::new(LanceIndexStore::new(
2791 Arc::new(ObjectStore::local()),
2792 tmpdir.clone(),
2793 Arc::new(LanceCache::no_cache()),
2794 ));
2795
2796 let batch = record_batch!(
2798 ("value", Int64, [Some(0), Some(5), None]),
2799 ("_rowid", UInt64, [0, 1, 2])
2800 )
2801 .unwrap();
2802 let schema = batch.schema();
2803 let stream = stream::once(async move { Ok(batch) });
2804 let stream = Box::pin(RecordBatchStreamAdapter::new(schema, stream));
2805
2806 BitmapIndexPlugin::train_bitmap_index(stream, store.as_ref())
2808 .await
2809 .unwrap();
2810
2811 let cache = LanceCache::with_capacity(1024 * 1024);
2812 let index = BitmapIndex::load(store.clone(), None, &cache)
2813 .await
2814 .unwrap();
2815
2816 let query = SargableQuery::Equals(ScalarValue::Int64(Some(5)));
2818 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2819
2820 match result {
2821 SearchResult::Exact(row_ids) => {
2822 let actual_rows: Vec<u64> = row_ids
2823 .true_rows()
2824 .row_addrs()
2825 .unwrap()
2826 .map(u64::from)
2827 .collect();
2828 assert_eq!(actual_rows, vec![1], "Should find row 1 where value == 5");
2829
2830 let null_row_ids = row_ids.null_rows();
2831 assert!(!null_row_ids.is_empty(), "null_row_ids should be Some");
2833 let null_rows: Vec<u64> =
2834 null_row_ids.row_addrs().unwrap().map(u64::from).collect();
2835 assert_eq!(null_rows, vec![2], "Should report row 2 as null");
2836 }
2837 _ => panic!("Expected Exact search result"),
2838 }
2839 let entries_after_value_lookup = cache.size().await;
2840
2841 let query = SargableQuery::IsNull();
2843 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2844
2845 match result {
2846 SearchResult::Exact(row_addrs) => {
2847 let actual_rows: Vec<u64> = row_addrs
2848 .true_rows()
2849 .row_addrs()
2850 .unwrap()
2851 .map(u64::from)
2852 .collect();
2853 assert_eq!(
2854 actual_rows,
2855 vec![2],
2856 "IsNull should find row 2 where value is null"
2857 );
2858
2859 let null_row_ids = row_addrs.null_rows();
2860 assert!(
2862 null_row_ids.is_empty(),
2863 "null_row_ids should be None for IsNull query"
2864 );
2865 }
2866 _ => panic!("Expected Exact search result"),
2867 }
2868 assert_eq!(
2869 cache.size().await,
2870 entries_after_value_lookup,
2871 "null bitmap lookup should bypass the per-value cache"
2872 );
2873
2874 let query = SargableQuery::Range(
2876 std::ops::Bound::Included(ScalarValue::Int64(Some(0))),
2877 std::ops::Bound::Included(ScalarValue::Int64(Some(3))),
2878 );
2879 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2880
2881 match result {
2882 SearchResult::Exact(row_addrs) => {
2883 let actual_rows: Vec<u64> = row_addrs
2884 .true_rows()
2885 .row_addrs()
2886 .unwrap()
2887 .map(u64::from)
2888 .collect();
2889 assert_eq!(actual_rows, vec![0], "Should find row 0 where value == 0");
2890
2891 let null_row_ids = row_addrs.null_rows();
2893 assert!(!null_row_ids.is_empty(), "null_row_ids should be Some");
2894 let null_rows: Vec<u64> =
2895 null_row_ids.row_addrs().unwrap().map(u64::from).collect();
2896 assert_eq!(null_rows, vec![2], "Should report row 2 as null");
2897 }
2898 _ => panic!("Expected Exact search result"),
2899 }
2900 }
2901}