1use 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; const MAX_ROWS_PER_CHUNK: usize = 2 * 1024;
59
60const BITMAP_INDEX_VERSION: u32 = 0;
61
62#[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#[derive(Clone, Debug)]
101pub struct BitmapIndex {
102 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 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 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); 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 ((*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 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 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 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 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 let bitmap_refs: Vec<_> = bitmaps.iter().map(|b| b.as_ref()).collect();
518 RowAddrTreeMap::union_all(&bitmap_refs)
519 };
520
521 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 ((*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 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 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 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 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 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 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 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 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 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 BitmapIndexPlugin::train_bitmap_index(stream, store.as_ref())
890 .await
891 .unwrap();
892
893 let cache = LanceCache::with_capacity(1024 * 1024); let index = BitmapIndex::load(store.clone(), None, &cache)
898 .await
899 .unwrap();
900
901 assert_eq!(index.index_map.len(), 4); assert!(index.null_map.is_empty()); let query = SargableQuery::Equals(ScalarValue::Utf8(Some("red".to_string())));
906 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
907
908 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 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 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 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 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 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 let m: u32 = 2_500_000;
1009 let per_bitmap_size = 1000; let mut state = HashMap::new();
1012 for i in 0..m {
1013 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 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 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 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 tracing::info!(
1050 "Index file contains {} rows in total",
1051 index_file.num_rows()
1052 );
1053
1054 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 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 let test_keys = [0, m / 2, m - 1]; for &key_val in &test_keys {
1072 let key = OrderableScalarValue(ScalarValue::UInt32(Some(key_val)));
1073 let bitmap = loaded_index
1075 .load_bitmap(&key, None)
1076 .await
1077 .unwrap_or_else(|_| panic!("Key {} should exist", key_val));
1078
1079 let row_addrs: Vec<u64> = bitmap.row_addrs().unwrap().map(u64::from).collect();
1081
1082 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 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 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 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 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 BitmapIndexPlugin::train_bitmap_index(stream, store.as_ref())
1164 .await
1165 .unwrap();
1166
1167 let cache = LanceCache::with_capacity(1024 * 1024); let index = BitmapIndex::load(store.clone(), None, &cache)
1172 .await
1173 .unwrap();
1174
1175 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 index.prewarm().await.unwrap();
1198
1199 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 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 index.prewarm().await.unwrap();
1223
1224 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 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 let values = vec![
1250 None, None, Some(1u32), Some(1u32), Some(2u32), Some(2u32), ];
1257
1258 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 BitmapIndexPlugin::train_bitmap_index(stream, test_store.as_ref())
1287 .await
1288 .unwrap();
1289
1290 let index = BitmapIndex::load(test_store.clone(), None, &LanceCache::no_cache())
1292 .await
1293 .expect("Failed to load bitmap index");
1294
1295 assert_eq!(index.index_map.len(), 2); assert!(!index.null_map.is_empty()); 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 index
1328 .remap(&row_addr_map, test_store.as_ref())
1329 .await
1330 .unwrap();
1331
1332 let reloaded_idx = BitmapIndex::load(test_store, None, &LanceCache::no_cache())
1334 .await
1335 .expect("Failed to load remapped bitmap index");
1336
1337 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 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 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 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 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 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 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 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 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 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 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 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 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}