Skip to main content

lance_index/scalar/
bitmap.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::{
5    any::Any,
6    collections::{BTreeMap, HashMap},
7    fmt::Debug,
8    ops::Bound,
9    sync::Arc,
10};
11
12use arrow::array::BinaryBuilder;
13use arrow_array::{Array, BinaryArray, RecordBatch, UInt64Array, new_null_array};
14use arrow_schema::{DataType, Field, Schema};
15use async_trait::async_trait;
16use datafusion::physical_plan::SendableRecordBatchStream;
17use datafusion_common::ScalarValue;
18use deepsize::DeepSizeOf;
19use futures::{StreamExt, TryStreamExt, stream};
20use lance_core::utils::mask::RowSetOps;
21use lance_core::{
22    Error, ROW_ID, Result,
23    cache::{CacheKey, LanceCache, WeakLanceCache},
24    error::LanceOptionExt,
25    utils::{
26        mask::{NullableRowAddrSet, RowAddrTreeMap},
27        tokio::get_num_compute_intensive_cpus,
28    },
29};
30use roaring::RoaringBitmap;
31use serde::Serialize;
32use tracing::instrument;
33
34use super::{AnyQuery, IndexStore, ScalarIndex};
35use super::{
36    BuiltinIndexType, SargableQuery, ScalarIndexParams, SearchResult, btree::OrderableScalarValue,
37};
38use crate::pbold;
39use crate::{Index, IndexType, metrics::MetricsCollector};
40use crate::{
41    frag_reuse::FragReuseIndex,
42    scalar::{
43        CreatedIndex, UpdateCriteria,
44        expression::SargableQueryParser,
45        registry::{
46            DefaultTrainingRequest, ScalarIndexPlugin, TrainingCriteria, TrainingOrdering,
47            TrainingRequest, VALUE_COLUMN_NAME,
48        },
49    },
50};
51use crate::{scalar::IndexReader, scalar::expression::ScalarQueryParser};
52
53pub const BITMAP_LOOKUP_NAME: &str = "bitmap_page_lookup.lance";
54pub const INDEX_STATS_METADATA_KEY: &str = "lance:index_stats";
55
56const MAX_BITMAP_ARRAY_LENGTH: usize = i32::MAX as usize - 1024 * 1024; // leave headroom
57
58const MAX_ROWS_PER_CHUNK: usize = 2 * 1024;
59
60const BITMAP_INDEX_VERSION: u32 = 0;
61
62// We only need to open a file reader if we need to load a bitmap. If all
63// bitmaps are cached we don't open it. If we do open it we should only open it once.
64#[derive(Clone)]
65struct LazyIndexReader {
66    index_reader: Arc<tokio::sync::Mutex<Option<Arc<dyn IndexReader>>>>,
67    store: Arc<dyn IndexStore>,
68}
69
70impl std::fmt::Debug for LazyIndexReader {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        f.debug_struct("LazyIndexReader")
73            .field("store", &self.store)
74            .finish()
75    }
76}
77
78impl LazyIndexReader {
79    fn new(store: Arc<dyn IndexStore>) -> Self {
80        Self {
81            index_reader: Arc::new(tokio::sync::Mutex::new(None)),
82            store,
83        }
84    }
85
86    async fn get(&self) -> Result<Arc<dyn IndexReader>> {
87        let mut reader = self.index_reader.lock().await;
88        if reader.is_none() {
89            let index_reader = self.store.open_index_file(BITMAP_LOOKUP_NAME).await?;
90            *reader = Some(index_reader);
91        }
92        Ok(reader.as_ref().unwrap().clone())
93    }
94}
95
96/// A scalar index that stores a bitmap for each possible value
97///
98/// This index works best for low-cardinality columns, where the number of unique values is small.
99/// The bitmap stores a list of row ids where the value is present.
100#[derive(Clone, Debug)]
101pub struct BitmapIndex {
102    /// Maps each unique value to its bitmap location in the index file
103    /// The usize value is the row offset in the bitmap_page_lookup.lance file
104    /// for quickly locating the row and reading it out
105    index_map: BTreeMap<OrderableScalarValue, usize>,
106
107    null_map: Arc<RowAddrTreeMap>,
108
109    value_type: DataType,
110
111    store: Arc<dyn IndexStore>,
112
113    index_cache: WeakLanceCache,
114
115    frag_reuse_index: Option<Arc<FragReuseIndex>>,
116
117    lazy_reader: LazyIndexReader,
118}
119
120#[derive(Debug, Clone)]
121pub struct BitmapKey {
122    value: OrderableScalarValue,
123}
124
125impl CacheKey for BitmapKey {
126    type ValueType = RowAddrTreeMap;
127
128    fn key(&self) -> std::borrow::Cow<'_, str> {
129        format!("{}", self.value.0).into()
130    }
131}
132
133impl BitmapIndex {
134    fn new(
135        index_map: BTreeMap<OrderableScalarValue, usize>,
136        null_map: Arc<RowAddrTreeMap>,
137        value_type: DataType,
138        store: Arc<dyn IndexStore>,
139        index_cache: WeakLanceCache,
140        frag_reuse_index: Option<Arc<FragReuseIndex>>,
141    ) -> Self {
142        let lazy_reader = LazyIndexReader::new(store.clone());
143        Self {
144            index_map,
145            null_map,
146            value_type,
147            store,
148            index_cache,
149            frag_reuse_index,
150            lazy_reader,
151        }
152    }
153
154    pub(crate) async fn load(
155        store: Arc<dyn IndexStore>,
156        frag_reuse_index: Option<Arc<FragReuseIndex>>,
157        index_cache: &LanceCache,
158    ) -> Result<Arc<Self>> {
159        let page_lookup_file = store.open_index_file(BITMAP_LOOKUP_NAME).await?;
160        let total_rows = page_lookup_file.num_rows();
161
162        if total_rows == 0 {
163            let schema = page_lookup_file.schema();
164            let data_type = schema.fields[0].data_type();
165            return Ok(Arc::new(Self::new(
166                BTreeMap::new(),
167                Arc::new(RowAddrTreeMap::default()),
168                data_type,
169                store,
170                WeakLanceCache::from(index_cache),
171                frag_reuse_index,
172            )));
173        }
174
175        let mut index_map: BTreeMap<OrderableScalarValue, usize> = BTreeMap::new();
176        let mut null_map = Arc::new(RowAddrTreeMap::default());
177        let mut value_type: Option<DataType> = None;
178        let mut null_location: Option<usize> = None;
179        let mut row_offset = 0;
180
181        for start_row in (0..total_rows).step_by(MAX_ROWS_PER_CHUNK) {
182            let end_row = (start_row + MAX_ROWS_PER_CHUNK).min(total_rows);
183            let chunk = page_lookup_file
184                .read_range(start_row..end_row, Some(&["keys"]))
185                .await?;
186
187            if chunk.num_rows() == 0 {
188                continue;
189            }
190
191            if value_type.is_none() {
192                value_type = Some(chunk.schema().field(0).data_type().clone());
193            }
194
195            let dict_keys = chunk.column(0);
196
197            for idx in 0..chunk.num_rows() {
198                let key = OrderableScalarValue(ScalarValue::try_from_array(dict_keys, idx)?);
199
200                if key.0.is_null() {
201                    null_location = Some(row_offset);
202                } else {
203                    index_map.insert(key, row_offset);
204                }
205
206                row_offset += 1;
207            }
208        }
209
210        if let Some(null_loc) = null_location {
211            let batch = page_lookup_file
212                .read_range(null_loc..null_loc + 1, Some(&["bitmaps"]))
213                .await?;
214
215            let binary_bitmaps = batch
216                .column(0)
217                .as_any()
218                .downcast_ref::<BinaryArray>()
219                .ok_or_else(|| Error::internal("Invalid bitmap column type".to_string()))?;
220            let bitmap_bytes = binary_bitmaps.value(0);
221            let mut bitmap = RowAddrTreeMap::deserialize_from(bitmap_bytes).unwrap();
222
223            // Apply fragment remapping if needed
224            if let Some(fri) = &frag_reuse_index {
225                bitmap = fri.remap_row_addrs_tree_map(&bitmap);
226            }
227
228            null_map = Arc::new(bitmap);
229        }
230
231        let final_value_type = value_type.expect_ok()?;
232
233        Ok(Arc::new(Self::new(
234            index_map,
235            null_map,
236            final_value_type,
237            store,
238            WeakLanceCache::from(index_cache),
239            frag_reuse_index,
240        )))
241    }
242
243    async fn load_bitmap(
244        &self,
245        key: &OrderableScalarValue,
246        metrics: Option<&dyn MetricsCollector>,
247    ) -> Result<Arc<RowAddrTreeMap>> {
248        if key.0.is_null() {
249            return Ok(self.null_map.clone());
250        }
251
252        let cache_key = BitmapKey { value: key.clone() };
253
254        if let Some(cached) = self.index_cache.get_with_key(&cache_key).await {
255            return Ok(cached);
256        }
257
258        // Record that we're loading a partition from disk
259        if let Some(metrics) = metrics {
260            metrics.record_part_load();
261        }
262
263        let row_offset = match self.index_map.get(key) {
264            Some(loc) => *loc,
265            None => return Ok(Arc::new(RowAddrTreeMap::default())),
266        };
267
268        let page_lookup_file = self.lazy_reader.get().await?;
269        let batch = page_lookup_file
270            .read_range(row_offset..row_offset + 1, Some(&["bitmaps"]))
271            .await?;
272
273        let binary_bitmaps = batch
274            .column(0)
275            .as_any()
276            .downcast_ref::<BinaryArray>()
277            .ok_or_else(|| Error::internal("Invalid bitmap column type".to_string()))?;
278        let bitmap_bytes = binary_bitmaps.value(0); // First (and only) row
279        let mut bitmap = RowAddrTreeMap::deserialize_from(bitmap_bytes).unwrap();
280
281        if let Some(fri) = &self.frag_reuse_index {
282            bitmap = fri.remap_row_addrs_tree_map(&bitmap);
283        }
284
285        self.index_cache
286            .insert_with_key(&cache_key, Arc::new(bitmap.clone()))
287            .await;
288
289        Ok(Arc::new(bitmap))
290    }
291}
292
293impl DeepSizeOf for BitmapIndex {
294    fn deep_size_of_children(&self, context: &mut deepsize::Context) -> usize {
295        let mut total_size = 0;
296
297        total_size += self.index_map.deep_size_of_children(context);
298        total_size += self.store.deep_size_of_children(context);
299
300        total_size
301    }
302}
303
304#[derive(Serialize)]
305struct BitmapStatistics {
306    num_bitmaps: usize,
307}
308
309#[async_trait]
310impl Index for BitmapIndex {
311    fn as_any(&self) -> &dyn Any {
312        self
313    }
314
315    fn as_index(self: Arc<Self>) -> Arc<dyn Index> {
316        self
317    }
318
319    fn as_vector_index(self: Arc<Self>) -> Result<Arc<dyn crate::vector::VectorIndex>> {
320        Err(Error::not_supported_source(
321            "BitmapIndex is not a vector index".into(),
322        ))
323    }
324
325    async fn prewarm(&self) -> Result<()> {
326        let page_lookup_file = self.lazy_reader.get().await?;
327        let total_rows = page_lookup_file.num_rows();
328
329        if total_rows == 0 {
330            return Ok(());
331        }
332
333        for start_row in (0..total_rows).step_by(MAX_ROWS_PER_CHUNK) {
334            let end_row = (start_row + MAX_ROWS_PER_CHUNK).min(total_rows);
335            let chunk = page_lookup_file
336                .read_range(start_row..end_row, None)
337                .await?;
338
339            if chunk.num_rows() == 0 {
340                continue;
341            }
342
343            let dict_keys = chunk.column(0);
344            let binary_bitmaps = chunk.column(1);
345            let bitmap_binary_array = binary_bitmaps
346                .as_any()
347                .downcast_ref::<BinaryArray>()
348                .unwrap();
349
350            for idx in 0..chunk.num_rows() {
351                let key = OrderableScalarValue(ScalarValue::try_from_array(dict_keys, idx)?);
352
353                if key.0.is_null() {
354                    continue;
355                }
356
357                let bitmap_bytes = bitmap_binary_array.value(idx);
358                let mut bitmap = RowAddrTreeMap::deserialize_from(bitmap_bytes).unwrap();
359
360                if let Some(frag_reuse_index_ref) = self.frag_reuse_index.as_ref() {
361                    bitmap = frag_reuse_index_ref.remap_row_addrs_tree_map(&bitmap);
362                }
363
364                let cache_key = BitmapKey { value: key };
365                self.index_cache
366                    .insert_with_key(&cache_key, Arc::new(bitmap))
367                    .await;
368            }
369        }
370
371        Ok(())
372    }
373
374    fn index_type(&self) -> IndexType {
375        IndexType::Bitmap
376    }
377
378    fn statistics(&self) -> Result<serde_json::Value> {
379        let stats = BitmapStatistics {
380            num_bitmaps: self.index_map.len() + if !self.null_map.is_empty() { 1 } else { 0 },
381        };
382        serde_json::to_value(stats).map_err(|e| {
383            Error::internal(format!(
384                "failed to serialize bitmap index statistics: {}",
385                e
386            ))
387        })
388    }
389
390    async fn calculate_included_frags(&self) -> Result<RoaringBitmap> {
391        unimplemented!()
392    }
393}
394
395#[async_trait]
396impl ScalarIndex for BitmapIndex {
397    #[instrument(name = "bitmap_search", level = "debug", skip_all)]
398    async fn search(
399        &self,
400        query: &dyn AnyQuery,
401        metrics: &dyn MetricsCollector,
402    ) -> Result<SearchResult> {
403        let query = query.as_any().downcast_ref::<SargableQuery>().unwrap();
404
405        let (row_ids, null_row_ids) = match query {
406            SargableQuery::Equals(val) => {
407                metrics.record_comparisons(1);
408                if val.is_null() {
409                    // Querying FOR nulls - they are the TRUE result, not NULL result
410                    ((*self.null_map).clone(), None)
411                } else {
412                    let key = OrderableScalarValue(val.clone());
413                    let bitmap = self.load_bitmap(&key, Some(metrics)).await?;
414                    let null_rows = if !self.null_map.is_empty() {
415                        Some((*self.null_map).clone())
416                    } else {
417                        None
418                    };
419                    ((*bitmap).clone(), null_rows)
420                }
421            }
422            SargableQuery::Range(start, end) => {
423                let range_start = match start {
424                    Bound::Included(val) => Bound::Included(OrderableScalarValue(val.clone())),
425                    Bound::Excluded(val) => Bound::Excluded(OrderableScalarValue(val.clone())),
426                    Bound::Unbounded => Bound::Unbounded,
427                };
428
429                let range_end = match end {
430                    Bound::Included(val) => Bound::Included(OrderableScalarValue(val.clone())),
431                    Bound::Excluded(val) => Bound::Excluded(OrderableScalarValue(val.clone())),
432                    Bound::Unbounded => Bound::Unbounded,
433                };
434
435                // Empty range if lower > upper, or if any bound is excluded and lower >= upper.
436                let empty_range = match (&range_start, &range_end) {
437                    (Bound::Included(lower), Bound::Included(upper)) => lower > upper,
438                    (Bound::Included(lower), Bound::Excluded(upper))
439                    | (Bound::Excluded(lower), Bound::Included(upper))
440                    | (Bound::Excluded(lower), Bound::Excluded(upper)) => lower >= upper,
441                    _ => false,
442                };
443
444                let keys: Vec<_> = if empty_range {
445                    Vec::new()
446                } else {
447                    self.index_map
448                        .range((range_start, range_end))
449                        .map(|(k, _v)| k.clone())
450                        .collect()
451                };
452
453                metrics.record_comparisons(keys.len());
454
455                let result = if keys.is_empty() {
456                    RowAddrTreeMap::default()
457                } else {
458                    let bitmaps: Vec<_> = stream::iter(
459                        keys.into_iter()
460                            .map(|key| async move { self.load_bitmap(&key, None).await }),
461                    )
462                    .buffer_unordered(get_num_compute_intensive_cpus())
463                    .try_collect()
464                    .await?;
465
466                    let bitmap_refs: Vec<_> = bitmaps.iter().map(|b| b.as_ref()).collect();
467                    RowAddrTreeMap::union_all(&bitmap_refs)
468                };
469
470                let null_rows = if !self.null_map.is_empty() {
471                    Some((*self.null_map).clone())
472                } else {
473                    None
474                };
475                (result, null_rows)
476            }
477            SargableQuery::IsIn(values) => {
478                metrics.record_comparisons(values.len());
479
480                // Collect keys that exist in the index, tracking if we need nulls
481                let mut has_null = false;
482                let keys: Vec<_> = values
483                    .iter()
484                    .filter_map(|val| {
485                        if val.is_null() {
486                            has_null = true;
487                            None
488                        } else {
489                            let key = OrderableScalarValue(val.clone());
490                            if self.index_map.contains_key(&key) {
491                                Some(key)
492                            } else {
493                                None
494                            }
495                        }
496                    })
497                    .collect();
498
499                // Load bitmaps in parallel
500                let mut bitmaps: Vec<_> = stream::iter(
501                    keys.into_iter()
502                        .map(|key| async move { self.load_bitmap(&key, None).await }),
503                )
504                .buffer_unordered(get_num_compute_intensive_cpus())
505                .try_collect()
506                .await?;
507
508                // Add null bitmap if needed
509                if has_null && !self.null_map.is_empty() {
510                    bitmaps.push(self.null_map.clone());
511                }
512
513                let result = if bitmaps.is_empty() {
514                    RowAddrTreeMap::default()
515                } else {
516                    // Convert Arc<RowAddrTreeMap> to &RowAddrTreeMap for union_all
517                    let bitmap_refs: Vec<_> = bitmaps.iter().map(|b| b.as_ref()).collect();
518                    RowAddrTreeMap::union_all(&bitmap_refs)
519                };
520
521                // If the query explicitly includes null, then nulls are TRUE (not NULL)
522                // Otherwise, nulls remain NULL (unknown)
523                let null_rows = if !has_null && !self.null_map.is_empty() {
524                    Some((*self.null_map).clone())
525                } else {
526                    None
527                };
528                (result, null_rows)
529            }
530            SargableQuery::IsNull() => {
531                metrics.record_comparisons(1);
532                // Querying FOR nulls - they are the TRUE result, not NULL result
533                ((*self.null_map).clone(), None)
534            }
535            SargableQuery::FullTextSearch(_) => {
536                return Err(Error::not_supported_source(
537                    "full text search is not supported for bitmap indexes".into(),
538                ));
539            }
540        };
541
542        let selection = NullableRowAddrSet::new(row_ids, null_row_ids.unwrap_or_default());
543        Ok(SearchResult::Exact(selection))
544    }
545
546    fn can_remap(&self) -> bool {
547        true
548    }
549
550    /// Remap the row ids, creating a new remapped version of this index in `dest_store`
551    async fn remap(
552        &self,
553        mapping: &HashMap<u64, Option<u64>>,
554        dest_store: &dyn IndexStore,
555    ) -> Result<CreatedIndex> {
556        let mut state = HashMap::new();
557
558        for key in self.index_map.keys() {
559            let bitmap = self.load_bitmap(key, None).await?;
560            let remapped_bitmap =
561                RowAddrTreeMap::from_iter(bitmap.row_addrs().unwrap().filter_map(|addr| {
562                    let addr_as_u64 = u64::from(addr);
563                    mapping
564                        .get(&addr_as_u64)
565                        .copied()
566                        .unwrap_or(Some(addr_as_u64))
567                }));
568            state.insert(key.0.clone(), remapped_bitmap);
569        }
570
571        if !self.null_map.is_empty() {
572            let remapped_null =
573                RowAddrTreeMap::from_iter(self.null_map.row_addrs().unwrap().filter_map(|addr| {
574                    let addr_as_u64 = u64::from(addr);
575                    mapping
576                        .get(&addr_as_u64)
577                        .copied()
578                        .unwrap_or(Some(addr_as_u64))
579                }));
580            state.insert(ScalarValue::try_from(&self.value_type)?, remapped_null);
581        }
582
583        BitmapIndexPlugin::write_bitmap_index(state, dest_store, &self.value_type).await?;
584
585        Ok(CreatedIndex {
586            index_details: prost_types::Any::from_msg(&pbold::BitmapIndexDetails::default())
587                .unwrap(),
588            index_version: BITMAP_INDEX_VERSION,
589        })
590    }
591
592    /// Add the new data into the index, creating an updated version of the index in `dest_store`
593    async fn update(
594        &self,
595        new_data: SendableRecordBatchStream,
596        dest_store: &dyn IndexStore,
597        _valid_old_fragments: Option<&RoaringBitmap>,
598    ) -> Result<CreatedIndex> {
599        let mut state = HashMap::new();
600
601        // Initialize builder with existing data
602        for key in self.index_map.keys() {
603            let bitmap = self.load_bitmap(key, None).await?;
604            state.insert(key.0.clone(), (*bitmap).clone());
605        }
606
607        if !self.null_map.is_empty() {
608            let ex_null = new_null_array(&self.value_type, 1);
609            let ex_null = ScalarValue::try_from_array(ex_null.as_ref(), 0)?;
610            state.insert(ex_null, (*self.null_map).clone());
611        }
612
613        BitmapIndexPlugin::do_train_bitmap_index(new_data, state, dest_store).await?;
614
615        Ok(CreatedIndex {
616            index_details: prost_types::Any::from_msg(&pbold::BitmapIndexDetails::default())
617                .unwrap(),
618            index_version: BITMAP_INDEX_VERSION,
619        })
620    }
621
622    fn update_criteria(&self) -> UpdateCriteria {
623        UpdateCriteria::only_new_data(TrainingCriteria::new(TrainingOrdering::None).with_row_id())
624    }
625
626    fn derive_index_params(&self) -> Result<ScalarIndexParams> {
627        Ok(ScalarIndexParams::for_builtin(BuiltinIndexType::Bitmap))
628    }
629}
630
631#[derive(Debug, Default)]
632pub struct BitmapIndexPlugin;
633
634impl BitmapIndexPlugin {
635    fn get_batch_from_arrays(
636        keys: Arc<dyn Array>,
637        binary_bitmaps: Arc<dyn Array>,
638    ) -> Result<RecordBatch> {
639        let schema = Arc::new(Schema::new(vec![
640            Field::new("keys", keys.data_type().clone(), true),
641            Field::new("bitmaps", binary_bitmaps.data_type().clone(), true),
642        ]));
643
644        let columns = vec![keys, binary_bitmaps];
645
646        Ok(RecordBatch::try_new(schema, columns)?)
647    }
648
649    async fn write_bitmap_index(
650        state: HashMap<ScalarValue, RowAddrTreeMap>,
651        index_store: &dyn IndexStore,
652        value_type: &DataType,
653    ) -> Result<()> {
654        let num_bitmaps = state.len();
655        let schema = Arc::new(Schema::new(vec![
656            Field::new("keys", value_type.clone(), true),
657            Field::new("bitmaps", DataType::Binary, true),
658        ]));
659
660        let mut bitmap_index_file = index_store
661            .new_index_file(BITMAP_LOOKUP_NAME, schema)
662            .await?;
663
664        let mut cur_keys = Vec::new();
665        let mut cur_bitmaps = Vec::new();
666        let mut cur_bytes = 0;
667
668        for (key, bitmap) in state.into_iter() {
669            let mut bytes = Vec::new();
670            bitmap.serialize_into(&mut bytes).unwrap();
671            let bitmap_size = bytes.len();
672
673            if cur_bytes + bitmap_size > MAX_BITMAP_ARRAY_LENGTH {
674                let keys_array = ScalarValue::iter_to_array(cur_keys.clone().into_iter()).unwrap();
675                let mut binary_builder = BinaryBuilder::new();
676                for b in &cur_bitmaps {
677                    binary_builder.append_value(b);
678                }
679                let bitmaps_array = Arc::new(binary_builder.finish()) as Arc<dyn Array>;
680
681                let record_batch = Self::get_batch_from_arrays(keys_array, bitmaps_array)?;
682                bitmap_index_file.write_record_batch(record_batch).await?;
683
684                cur_keys.clear();
685                cur_bitmaps.clear();
686                cur_bytes = 0;
687            }
688
689            cur_keys.push(key);
690            cur_bitmaps.push(bytes);
691            cur_bytes += bitmap_size;
692        }
693
694        // Flush any remaining
695        if !cur_keys.is_empty() {
696            let keys_array = ScalarValue::iter_to_array(cur_keys).unwrap();
697            let mut binary_builder = BinaryBuilder::new();
698            for b in &cur_bitmaps {
699                binary_builder.append_value(b);
700            }
701            let bitmaps_array = Arc::new(binary_builder.finish()) as Arc<dyn Array>;
702
703            let record_batch = Self::get_batch_from_arrays(keys_array, bitmaps_array)?;
704            bitmap_index_file.write_record_batch(record_batch).await?;
705        }
706
707        // Finish file with metadata that allows lightweight statistics reads
708        let stats_json = serde_json::to_string(&BitmapStatistics { num_bitmaps })
709            .map_err(|e| Error::internal(format!("failed to serialize bitmap statistics: {e}")))?;
710        let mut metadata = HashMap::new();
711        metadata.insert(INDEX_STATS_METADATA_KEY.to_string(), stats_json);
712
713        bitmap_index_file.finish_with_metadata(metadata).await?;
714
715        Ok(())
716    }
717
718    async fn do_train_bitmap_index(
719        mut data_source: SendableRecordBatchStream,
720        mut state: HashMap<ScalarValue, RowAddrTreeMap>,
721        index_store: &dyn IndexStore,
722    ) -> Result<()> {
723        let value_type = data_source.schema().field(0).data_type().clone();
724        while let Some(batch) = data_source.try_next().await? {
725            let values = batch.column_by_name(VALUE_COLUMN_NAME).expect_ok()?;
726            let row_ids = batch.column_by_name(ROW_ID).expect_ok()?;
727            debug_assert_eq!(row_ids.data_type(), &DataType::UInt64);
728
729            let row_id_column = row_ids.as_any().downcast_ref::<UInt64Array>().unwrap();
730
731            for i in 0..values.len() {
732                let row_id = row_id_column.value(i);
733                let key = ScalarValue::try_from_array(values.as_ref(), i)?;
734                state.entry(key.clone()).or_default().insert(row_id);
735            }
736        }
737
738        Self::write_bitmap_index(state, index_store, &value_type).await
739    }
740
741    pub async fn train_bitmap_index(
742        data: SendableRecordBatchStream,
743        index_store: &dyn IndexStore,
744    ) -> Result<()> {
745        // mapping from item to list of the row ids where it is present
746        let dictionary: HashMap<ScalarValue, RowAddrTreeMap> = HashMap::new();
747
748        Self::do_train_bitmap_index(data, dictionary, index_store).await
749    }
750}
751
752#[async_trait]
753impl ScalarIndexPlugin for BitmapIndexPlugin {
754    fn name(&self) -> &str {
755        "Bitmap"
756    }
757
758    fn new_training_request(
759        &self,
760        _params: &str,
761        field: &Field,
762    ) -> Result<Box<dyn TrainingRequest>> {
763        if field.data_type().is_nested() {
764            return Err(Error::invalid_input_source(
765                "A bitmap index can only be created on a non-nested field.".into(),
766            ));
767        }
768        Ok(Box::new(DefaultTrainingRequest::new(
769            TrainingCriteria::new(TrainingOrdering::None).with_row_id(),
770        )))
771    }
772
773    fn provides_exact_answer(&self) -> bool {
774        true
775    }
776
777    fn version(&self) -> u32 {
778        BITMAP_INDEX_VERSION
779    }
780
781    fn new_query_parser(
782        &self,
783        index_name: String,
784        _index_details: &prost_types::Any,
785    ) -> Option<Box<dyn ScalarQueryParser>> {
786        Some(Box::new(SargableQueryParser::new(index_name, false)))
787    }
788
789    async fn train_index(
790        &self,
791        data: SendableRecordBatchStream,
792        index_store: &dyn IndexStore,
793        _request: Box<dyn TrainingRequest>,
794        fragment_ids: Option<Vec<u32>>,
795        _progress: Arc<dyn crate::progress::IndexBuildProgress>,
796    ) -> Result<CreatedIndex> {
797        if fragment_ids.is_some() {
798            return Err(Error::invalid_input_source(
799                "Bitmap index does not support fragment training".into(),
800            ));
801        }
802
803        Self::train_bitmap_index(data, index_store).await?;
804        Ok(CreatedIndex {
805            index_details: prost_types::Any::from_msg(&pbold::BitmapIndexDetails::default())
806                .unwrap(),
807            index_version: BITMAP_INDEX_VERSION,
808        })
809    }
810
811    /// Load an index from storage
812    async fn load_index(
813        &self,
814        index_store: Arc<dyn IndexStore>,
815        _index_details: &prost_types::Any,
816        frag_reuse_index: Option<Arc<FragReuseIndex>>,
817        cache: &LanceCache,
818    ) -> Result<Arc<dyn ScalarIndex>> {
819        Ok(BitmapIndex::load(index_store, frag_reuse_index, cache).await? as Arc<dyn ScalarIndex>)
820    }
821
822    async fn load_statistics(
823        &self,
824        index_store: Arc<dyn IndexStore>,
825        _index_details: &prost_types::Any,
826    ) -> Result<Option<serde_json::Value>> {
827        let reader = index_store.open_index_file(BITMAP_LOOKUP_NAME).await?;
828        if let Some(value) = reader.schema().metadata.get(INDEX_STATS_METADATA_KEY) {
829            let stats = serde_json::from_str(value).map_err(|e| {
830                Error::internal(format!("failed to parse bitmap statistics metadata: {e}"))
831            })?;
832            Ok(Some(stats))
833        } else {
834            Ok(None)
835        }
836    }
837}
838
839#[cfg(test)]
840pub mod tests {
841    use super::*;
842    use crate::metrics::NoOpMetricsCollector;
843    use crate::scalar::lance_format::LanceIndexStore;
844    use arrow_array::{RecordBatch, StringArray, UInt64Array, record_batch};
845    use arrow_schema::{DataType, Field, Schema};
846    use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
847    use futures::stream;
848    use lance_core::utils::mask::RowSetOps;
849    use lance_core::utils::{address::RowAddress, tempfile::TempObjDir};
850    use lance_io::object_store::ObjectStore;
851    use std::collections::HashMap;
852
853    #[tokio::test]
854    async fn test_bitmap_lazy_loading_and_cache() {
855        // Create a temporary directory for the index
856        let tmpdir = TempObjDir::default();
857        let store = Arc::new(LanceIndexStore::new(
858            Arc::new(ObjectStore::local()),
859            tmpdir.clone(),
860            Arc::new(LanceCache::no_cache()),
861        ));
862
863        // Create test data with low cardinality column
864        let colors = vec![
865            "red", "blue", "green", "red", "yellow", "blue", "red", "green", "blue", "yellow",
866            "red", "red", "blue", "green", "yellow",
867        ];
868
869        let row_ids = (0u64..15u64).collect::<Vec<_>>();
870
871        let schema = Arc::new(Schema::new(vec![
872            Field::new("value", DataType::Utf8, false),
873            Field::new("_rowid", DataType::UInt64, false),
874        ]));
875
876        let batch = RecordBatch::try_new(
877            schema.clone(),
878            vec![
879                Arc::new(StringArray::from(colors.clone())),
880                Arc::new(UInt64Array::from(row_ids.clone())),
881            ],
882        )
883        .unwrap();
884
885        let stream = stream::once(async move { Ok(batch) });
886        let stream = Box::pin(RecordBatchStreamAdapter::new(schema, stream));
887
888        // Train and write the bitmap index
889        BitmapIndexPlugin::train_bitmap_index(stream, store.as_ref())
890            .await
891            .unwrap();
892
893        // Create a cache with limited capacity
894        let cache = LanceCache::with_capacity(1024 * 1024); // 1MB cache
895
896        // Load the index (should only load metadata, not bitmaps)
897        let index = BitmapIndex::load(store.clone(), None, &cache)
898            .await
899            .unwrap();
900
901        assert_eq!(index.index_map.len(), 4); // 4 non-null unique values (red, blue, green, yellow)
902        assert!(index.null_map.is_empty()); // No nulls in test data
903
904        // Test 1: Search for "red"
905        let query = SargableQuery::Equals(ScalarValue::Utf8(Some("red".to_string())));
906        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
907
908        // Verify results
909        let expected_red_rows = vec![0u64, 3, 6, 10, 11];
910        if let SearchResult::Exact(row_ids) = result {
911            let mut actual: Vec<u64> = row_ids
912                .true_rows()
913                .row_addrs()
914                .unwrap()
915                .map(|id| id.into())
916                .collect();
917            actual.sort();
918            assert_eq!(actual, expected_red_rows);
919        } else {
920            panic!("Expected exact search result");
921        }
922
923        // Test 2: Search for "red" again - should hit cache
924        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
925        if let SearchResult::Exact(row_ids) = result {
926            let mut actual: Vec<u64> = row_ids
927                .true_rows()
928                .row_addrs()
929                .unwrap()
930                .map(|id| id.into())
931                .collect();
932            actual.sort();
933            assert_eq!(actual, expected_red_rows);
934        }
935
936        // Test 3: Range query
937        let query = SargableQuery::Range(
938            std::ops::Bound::Included(ScalarValue::Utf8(Some("blue".to_string()))),
939            std::ops::Bound::Included(ScalarValue::Utf8(Some("green".to_string()))),
940        );
941        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
942
943        let expected_range_rows = vec![1u64, 2, 5, 7, 8, 12, 13];
944        if let SearchResult::Exact(row_ids) = result {
945            let mut actual: Vec<u64> = row_ids
946                .true_rows()
947                .row_addrs()
948                .unwrap()
949                .map(|id| id.into())
950                .collect();
951            actual.sort();
952            assert_eq!(actual, expected_range_rows);
953        }
954
955        // Test 3b: Inverted range query should return empty result
956        let query = SargableQuery::Range(
957            std::ops::Bound::Included(ScalarValue::Utf8(Some("green".to_string()))),
958            std::ops::Bound::Included(ScalarValue::Utf8(Some("blue".to_string()))),
959        );
960        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
961        if let SearchResult::Exact(row_ids) = result {
962            assert!(row_ids.true_rows().is_empty());
963        } else {
964            panic!("Expected exact search result");
965        }
966
967        // Test 4: IsIn query
968        let query = SargableQuery::IsIn(vec![
969            ScalarValue::Utf8(Some("red".to_string())),
970            ScalarValue::Utf8(Some("yellow".to_string())),
971        ]);
972        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
973
974        let expected_in_rows = vec![0u64, 3, 4, 6, 9, 10, 11, 14];
975        if let SearchResult::Exact(row_ids) = result {
976            let mut actual: Vec<u64> = row_ids
977                .true_rows()
978                .row_addrs()
979                .unwrap()
980                .map(|id| id.into())
981                .collect();
982            actual.sort();
983            assert_eq!(actual, expected_in_rows);
984        }
985    }
986
987    #[tokio::test]
988    #[ignore]
989    async fn test_big_bitmap_index() {
990        // WARNING: This test allocates a huge state to force overflow over int32 on BinaryArray
991        // You must run it only on a machine with enough resources (or skip it normally).
992        use super::{BITMAP_LOOKUP_NAME, BitmapIndex};
993        use crate::scalar::IndexStore;
994        use crate::scalar::lance_format::LanceIndexStore;
995        use arrow_schema::DataType;
996        use datafusion_common::ScalarValue;
997        use lance_core::cache::LanceCache;
998        use lance_core::utils::mask::RowAddrTreeMap;
999        use lance_io::object_store::ObjectStore;
1000        use std::collections::HashMap;
1001        use std::sync::Arc;
1002
1003        // Adjust these numbers so that:
1004        //     m * (serialized size per bitmap) > 2^31 bytes.
1005        //
1006        // For example, if we assume each bitmap serializes to ~1000 bytes,
1007        // you need m > 2.1e6.
1008        let m: u32 = 2_500_000;
1009        let per_bitmap_size = 1000; // assumed bytes per bitmap
1010
1011        let mut state = HashMap::new();
1012        for i in 0..m {
1013            // Create a bitmap that contains, say, 1000 row IDs.
1014            let bitmap = RowAddrTreeMap::from_iter(0..per_bitmap_size);
1015
1016            let key = ScalarValue::UInt32(Some(i));
1017            state.insert(key, bitmap);
1018        }
1019
1020        // Create a temporary store.
1021        let tmpdir = TempObjDir::default();
1022        let test_store = LanceIndexStore::new(
1023            Arc::new(ObjectStore::local()),
1024            tmpdir.clone(),
1025            Arc::new(LanceCache::no_cache()),
1026        );
1027
1028        // This call should never trigger a "byte array offset overflow" error since now the code supports
1029        // read by chunks
1030        let result =
1031            BitmapIndexPlugin::write_bitmap_index(state, &test_store, &DataType::UInt32).await;
1032
1033        assert!(
1034            result.is_ok(),
1035            "Failed to write bitmap index: {:?}",
1036            result.err()
1037        );
1038
1039        // Verify the index file exists
1040        let index_file = test_store.open_index_file(BITMAP_LOOKUP_NAME).await;
1041        assert!(
1042            index_file.is_ok(),
1043            "Failed to open index file: {:?}",
1044            index_file.err()
1045        );
1046        let index_file = index_file.unwrap();
1047
1048        // Print stats about the index file
1049        tracing::info!(
1050            "Index file contains {} rows in total",
1051            index_file.num_rows()
1052        );
1053
1054        // Load the index using BitmapIndex::load
1055        tracing::info!("Loading index from disk...");
1056        let loaded_index = BitmapIndex::load(Arc::new(test_store), None, &LanceCache::no_cache())
1057            .await
1058            .expect("Failed to load bitmap index");
1059
1060        // Verify the loaded index has the correct number of entries
1061        assert_eq!(
1062            loaded_index.index_map.len(),
1063            m as usize,
1064            "Loaded index has incorrect number of keys (expected {}, got {})",
1065            m,
1066            loaded_index.index_map.len()
1067        );
1068
1069        // Manually verify specific keys without using search()
1070        let test_keys = [0, m / 2, m - 1]; // Beginning, middle, and end
1071        for &key_val in &test_keys {
1072            let key = OrderableScalarValue(ScalarValue::UInt32(Some(key_val)));
1073            // Load the bitmap for this key
1074            let bitmap = loaded_index
1075                .load_bitmap(&key, None)
1076                .await
1077                .unwrap_or_else(|_| panic!("Key {} should exist", key_val));
1078
1079            // Convert RowAddrTreeMap to a vector for easier assertion
1080            let row_addrs: Vec<u64> = bitmap.row_addrs().unwrap().map(u64::from).collect();
1081
1082            // Verify length
1083            assert_eq!(
1084                row_addrs.len(),
1085                per_bitmap_size as usize,
1086                "Bitmap for key {} has wrong size",
1087                key_val
1088            );
1089
1090            // Verify first few and last few elements
1091            for i in 0..5.min(per_bitmap_size) {
1092                assert!(
1093                    row_addrs.contains(&i),
1094                    "Bitmap for key {} should contain row_id {}",
1095                    key_val,
1096                    i
1097                );
1098            }
1099
1100            for i in (per_bitmap_size - 5)..per_bitmap_size {
1101                assert!(
1102                    row_addrs.contains(&i),
1103                    "Bitmap for key {} should contain row_id {}",
1104                    key_val,
1105                    i
1106                );
1107            }
1108
1109            // Verify exact range
1110            let expected_range: Vec<u64> = (0..per_bitmap_size).collect();
1111            assert_eq!(
1112                row_addrs, expected_range,
1113                "Bitmap for key {} doesn't contain expected values",
1114                key_val
1115            );
1116
1117            tracing::info!(
1118                "✓ Verified bitmap for key {}: {} rows as expected",
1119                key_val,
1120                row_addrs.len()
1121            );
1122        }
1123
1124        tracing::info!("Test successful! Index properly contains {} keys", m);
1125    }
1126
1127    #[tokio::test]
1128    async fn test_bitmap_prewarm() {
1129        // Create a temporary directory for the index
1130        let tmpdir = TempObjDir::default();
1131        let store = Arc::new(LanceIndexStore::new(
1132            Arc::new(ObjectStore::local()),
1133            tmpdir.clone(),
1134            Arc::new(LanceCache::no_cache()),
1135        ));
1136
1137        // Create test data with low cardinality
1138        let colors = vec![
1139            "red", "blue", "green", "red", "yellow", "blue", "red", "green", "blue", "yellow",
1140            "red", "red", "blue", "green", "yellow",
1141        ];
1142
1143        let row_ids = (0u64..15u64).collect::<Vec<_>>();
1144
1145        let schema = Arc::new(Schema::new(vec![
1146            Field::new("value", DataType::Utf8, false),
1147            Field::new("_rowid", DataType::UInt64, false),
1148        ]));
1149
1150        let batch = RecordBatch::try_new(
1151            schema.clone(),
1152            vec![
1153                Arc::new(StringArray::from(colors.clone())),
1154                Arc::new(UInt64Array::from(row_ids.clone())),
1155            ],
1156        )
1157        .unwrap();
1158
1159        let stream = stream::once(async move { Ok(batch) });
1160        let stream = Box::pin(RecordBatchStreamAdapter::new(schema, stream));
1161
1162        // Train and write the bitmap index
1163        BitmapIndexPlugin::train_bitmap_index(stream, store.as_ref())
1164            .await
1165            .unwrap();
1166
1167        // Create a cache with metrics tracking
1168        let cache = LanceCache::with_capacity(1024 * 1024); // 1MB cache
1169
1170        // Load the index (should only load metadata, not bitmaps)
1171        let index = BitmapIndex::load(store.clone(), None, &cache)
1172            .await
1173            .unwrap();
1174
1175        // Verify no bitmaps are cached yet
1176        let cache_key_red = BitmapKey {
1177            value: OrderableScalarValue(ScalarValue::Utf8(Some("red".to_string()))),
1178        };
1179        let cache_key_blue = BitmapKey {
1180            value: OrderableScalarValue(ScalarValue::Utf8(Some("blue".to_string()))),
1181        };
1182
1183        assert!(
1184            cache
1185                .get_with_key::<BitmapKey>(&cache_key_red)
1186                .await
1187                .is_none()
1188        );
1189        assert!(
1190            cache
1191                .get_with_key::<BitmapKey>(&cache_key_blue)
1192                .await
1193                .is_none()
1194        );
1195
1196        // Call prewarm
1197        index.prewarm().await.unwrap();
1198
1199        // Verify all bitmaps are now cached
1200        assert!(
1201            cache
1202                .get_with_key::<BitmapKey>(&cache_key_red)
1203                .await
1204                .is_some()
1205        );
1206        assert!(
1207            cache
1208                .get_with_key::<BitmapKey>(&cache_key_blue)
1209                .await
1210                .is_some()
1211        );
1212
1213        // Verify cached bitmaps have correct content
1214        let cached_red = cache
1215            .get_with_key::<BitmapKey>(&cache_key_red)
1216            .await
1217            .unwrap();
1218        let red_rows: Vec<u64> = cached_red.row_addrs().unwrap().map(u64::from).collect();
1219        assert_eq!(red_rows, vec![0, 3, 6, 10, 11]);
1220
1221        // Call prewarm again - should be idempotent
1222        index.prewarm().await.unwrap();
1223
1224        // Verify cache still contains the same items
1225        let cached_red_2 = cache
1226            .get_with_key::<BitmapKey>(&cache_key_red)
1227            .await
1228            .unwrap();
1229        let red_rows_2: Vec<u64> = cached_red_2.row_addrs().unwrap().map(u64::from).collect();
1230        assert_eq!(red_rows_2, vec![0, 3, 6, 10, 11]);
1231    }
1232
1233    #[tokio::test]
1234    async fn test_remap_bitmap_with_null() {
1235        use arrow_array::UInt32Array;
1236
1237        // Create a temporary store.
1238        let tmpdir = TempObjDir::default();
1239        let test_store = Arc::new(LanceIndexStore::new(
1240            Arc::new(ObjectStore::local()),
1241            tmpdir.clone(),
1242            Arc::new(LanceCache::no_cache()),
1243        ));
1244
1245        // Create test data that simulates:
1246        // frag 1 - { 0: null, 1: null, 2: 1 }
1247        // frag 2 - { 0: 1, 1: 2, 2: 2 }
1248        // We'll create this data with specific row addresses
1249        let values = vec![
1250            None,       // row 0: null (will be at address (1,0))
1251            None,       // row 1: null (will be at address (1,1))
1252            Some(1u32), // row 2: 1    (will be at address (1,2))
1253            Some(1u32), // row 3: 1    (will be at address (2,0))
1254            Some(2u32), // row 4: 2    (will be at address (2,1))
1255            Some(2u32), // row 5: 2    (will be at address (2,2))
1256        ];
1257
1258        // Create row IDs with specific fragment addresses
1259        let row_ids: Vec<u64> = vec![
1260            RowAddress::new_from_parts(1, 0).into(),
1261            RowAddress::new_from_parts(1, 1).into(),
1262            RowAddress::new_from_parts(1, 2).into(),
1263            RowAddress::new_from_parts(2, 0).into(),
1264            RowAddress::new_from_parts(2, 1).into(),
1265            RowAddress::new_from_parts(2, 2).into(),
1266        ];
1267
1268        let schema = Arc::new(Schema::new(vec![
1269            Field::new("value", DataType::UInt32, true),
1270            Field::new("_rowid", DataType::UInt64, false),
1271        ]));
1272
1273        let batch = RecordBatch::try_new(
1274            schema.clone(),
1275            vec![
1276                Arc::new(UInt32Array::from(values)),
1277                Arc::new(UInt64Array::from(row_ids)),
1278            ],
1279        )
1280        .unwrap();
1281
1282        let stream = stream::once(async move { Ok(batch) });
1283        let stream = Box::pin(RecordBatchStreamAdapter::new(schema, stream));
1284
1285        // Create the bitmap index
1286        BitmapIndexPlugin::train_bitmap_index(stream, test_store.as_ref())
1287            .await
1288            .unwrap();
1289
1290        // Load the index
1291        let index = BitmapIndex::load(test_store.clone(), None, &LanceCache::no_cache())
1292            .await
1293            .expect("Failed to load bitmap index");
1294
1295        // Verify initial state
1296        assert_eq!(index.index_map.len(), 2); // 2 non-null values (1 and 2)
1297        assert!(!index.null_map.is_empty()); // Should have null values
1298
1299        // Create a remap that simulates compaction of frags 1 and 2 into frag 3
1300        let mut row_addr_map = HashMap::<u64, Option<u64>>::new();
1301        row_addr_map.insert(
1302            RowAddress::new_from_parts(1, 0).into(),
1303            Some(RowAddress::new_from_parts(3, 0).into()),
1304        );
1305        row_addr_map.insert(
1306            RowAddress::new_from_parts(1, 1).into(),
1307            Some(RowAddress::new_from_parts(3, 1).into()),
1308        );
1309        row_addr_map.insert(
1310            RowAddress::new_from_parts(1, 2).into(),
1311            Some(RowAddress::new_from_parts(3, 2).into()),
1312        );
1313        row_addr_map.insert(
1314            RowAddress::new_from_parts(2, 0).into(),
1315            Some(RowAddress::new_from_parts(3, 3).into()),
1316        );
1317        row_addr_map.insert(
1318            RowAddress::new_from_parts(2, 1).into(),
1319            Some(RowAddress::new_from_parts(3, 4).into()),
1320        );
1321        row_addr_map.insert(
1322            RowAddress::new_from_parts(2, 2).into(),
1323            Some(RowAddress::new_from_parts(3, 5).into()),
1324        );
1325
1326        // Perform remap
1327        index
1328            .remap(&row_addr_map, test_store.as_ref())
1329            .await
1330            .unwrap();
1331
1332        // Reload and check
1333        let reloaded_idx = BitmapIndex::load(test_store, None, &LanceCache::no_cache())
1334            .await
1335            .expect("Failed to load remapped bitmap index");
1336
1337        // Verify the null bitmap was remapped correctly
1338        let expected_null_addrs: Vec<u64> = vec![
1339            RowAddress::new_from_parts(3, 0).into(),
1340            RowAddress::new_from_parts(3, 1).into(),
1341        ];
1342        let actual_null_addrs: Vec<u64> = reloaded_idx
1343            .null_map
1344            .row_addrs()
1345            .unwrap()
1346            .map(u64::from)
1347            .collect();
1348        assert_eq!(
1349            actual_null_addrs, expected_null_addrs,
1350            "Null bitmap not remapped correctly"
1351        );
1352
1353        // Search for value 1 and verify remapped addresses
1354        let query = SargableQuery::Equals(ScalarValue::UInt32(Some(1)));
1355        let result = reloaded_idx
1356            .search(&query, &NoOpMetricsCollector)
1357            .await
1358            .unwrap();
1359        if let crate::scalar::SearchResult::Exact(row_ids) = result {
1360            let mut actual: Vec<u64> = row_ids
1361                .true_rows()
1362                .row_addrs()
1363                .unwrap()
1364                .map(u64::from)
1365                .collect();
1366            actual.sort();
1367            let expected: Vec<u64> = vec![
1368                RowAddress::new_from_parts(3, 2).into(),
1369                RowAddress::new_from_parts(3, 3).into(),
1370            ];
1371            assert_eq!(actual, expected, "Value 1 bitmap not remapped correctly");
1372        }
1373
1374        // Search for value 2 and verify remapped addresses
1375        let query = SargableQuery::Equals(ScalarValue::UInt32(Some(2)));
1376        let result = reloaded_idx
1377            .search(&query, &NoOpMetricsCollector)
1378            .await
1379            .unwrap();
1380        if let crate::scalar::SearchResult::Exact(row_ids) = result {
1381            let mut actual: Vec<u64> = row_ids
1382                .true_rows()
1383                .row_addrs()
1384                .unwrap()
1385                .map(u64::from)
1386                .collect();
1387            actual.sort();
1388            let expected: Vec<u64> = vec![
1389                RowAddress::new_from_parts(3, 4).into(),
1390                RowAddress::new_from_parts(3, 5).into(),
1391            ];
1392            assert_eq!(actual, expected, "Value 2 bitmap not remapped correctly");
1393        }
1394
1395        // Search for null values
1396        let query = SargableQuery::IsNull();
1397        let result = reloaded_idx
1398            .search(&query, &NoOpMetricsCollector)
1399            .await
1400            .unwrap();
1401        if let crate::scalar::SearchResult::Exact(row_ids) = result {
1402            let mut actual: Vec<u64> = row_ids
1403                .true_rows()
1404                .row_addrs()
1405                .unwrap()
1406                .map(u64::from)
1407                .collect();
1408            actual.sort();
1409            assert_eq!(
1410                actual, expected_null_addrs,
1411                "Null search results not correct"
1412            );
1413        }
1414    }
1415
1416    #[tokio::test]
1417    async fn test_bitmap_null_handling_in_queries() {
1418        // Test that bitmap index correctly returns null_list for queries
1419        let tmpdir = TempObjDir::default();
1420        let store = Arc::new(LanceIndexStore::new(
1421            Arc::new(ObjectStore::local()),
1422            tmpdir.clone(),
1423            Arc::new(LanceCache::no_cache()),
1424        ));
1425
1426        // Create test data: [0, 5, null]
1427        let batch = record_batch!(
1428            ("value", Int64, [Some(0), Some(5), None]),
1429            ("_rowid", UInt64, [0, 1, 2])
1430        )
1431        .unwrap();
1432        let schema = batch.schema();
1433        let stream = stream::once(async move { Ok(batch) });
1434        let stream = Box::pin(RecordBatchStreamAdapter::new(schema, stream));
1435
1436        // Train and write the bitmap index
1437        BitmapIndexPlugin::train_bitmap_index(stream, store.as_ref())
1438            .await
1439            .unwrap();
1440
1441        let cache = LanceCache::with_capacity(1024 * 1024);
1442        let index = BitmapIndex::load(store.clone(), None, &cache)
1443            .await
1444            .unwrap();
1445
1446        // Test 1: Search for value 5 - should return allow=[1], null=[2]
1447        let query = SargableQuery::Equals(ScalarValue::Int64(Some(5)));
1448        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1449
1450        match result {
1451            SearchResult::Exact(row_ids) => {
1452                let actual_rows: Vec<u64> = row_ids
1453                    .true_rows()
1454                    .row_addrs()
1455                    .unwrap()
1456                    .map(u64::from)
1457                    .collect();
1458                assert_eq!(actual_rows, vec![1], "Should find row 1 where value == 5");
1459
1460                let null_row_ids = row_ids.null_rows();
1461                // Check that null_row_ids contains row 2
1462                assert!(!null_row_ids.is_empty(), "null_row_ids should be Some");
1463                let null_rows: Vec<u64> =
1464                    null_row_ids.row_addrs().unwrap().map(u64::from).collect();
1465                assert_eq!(null_rows, vec![2], "Should report row 2 as null");
1466            }
1467            _ => panic!("Expected Exact search result"),
1468        }
1469
1470        // Test 2: Search for null values - should return allow=[2], null=None
1471        let query = SargableQuery::IsNull();
1472        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1473
1474        match result {
1475            SearchResult::Exact(row_addrs) => {
1476                let actual_rows: Vec<u64> = row_addrs
1477                    .true_rows()
1478                    .row_addrs()
1479                    .unwrap()
1480                    .map(u64::from)
1481                    .collect();
1482                assert_eq!(
1483                    actual_rows,
1484                    vec![2],
1485                    "IsNull should find row 2 where value is null"
1486                );
1487
1488                let null_row_ids = row_addrs.null_rows();
1489                // When querying FOR nulls, null_row_ids should be None (nulls are the TRUE result)
1490                assert!(
1491                    null_row_ids.is_empty(),
1492                    "null_row_ids should be None for IsNull query"
1493                );
1494            }
1495            _ => panic!("Expected Exact search result"),
1496        }
1497
1498        // Test 3: Range query - should return matching rows and null_list
1499        let query = SargableQuery::Range(
1500            std::ops::Bound::Included(ScalarValue::Int64(Some(0))),
1501            std::ops::Bound::Included(ScalarValue::Int64(Some(3))),
1502        );
1503        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1504
1505        match result {
1506            SearchResult::Exact(row_addrs) => {
1507                let actual_rows: Vec<u64> = row_addrs
1508                    .true_rows()
1509                    .row_addrs()
1510                    .unwrap()
1511                    .map(u64::from)
1512                    .collect();
1513                assert_eq!(actual_rows, vec![0], "Should find row 0 where value == 0");
1514
1515                // Should report row 2 as null
1516                let null_row_ids = row_addrs.null_rows();
1517                assert!(!null_row_ids.is_empty(), "null_row_ids should be Some");
1518                let null_rows: Vec<u64> =
1519                    null_row_ids.row_addrs().unwrap().map(u64::from).collect();
1520                assert_eq!(null_rows, vec![2], "Should report row 2 as null");
1521            }
1522            _ => panic!("Expected Exact search result"),
1523        }
1524    }
1525}