Skip to main content

lance_index/scalar/
bloomfilter.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Bloom Filter Index
5//!
6//! Bloom Filter is a probabilistic data structure that allows for fast membership testing.
7//! It is a space-efficient data structure that can be used to test whether an element is a member of a set.
8//! It's an inexact filter - they may include false positives that require rechecking.
9
10use crate::scalar::bloomfilter::sbbf::{Sbbf, SbbfBuilder};
11use crate::scalar::expression::{BloomFilterQueryParser, ScalarQueryParser};
12use crate::scalar::registry::{
13    ScalarIndexPlugin, TrainingCriteria, TrainingOrdering, TrainingRequest,
14};
15use crate::scalar::{
16    BloomFilterQuery, BuiltinIndexType, CreatedIndex, ScalarIndexParams, UpdateCriteria,
17};
18use crate::{Any, pb};
19use arrow_array::{Array, UInt64Array};
20mod as_bytes;
21pub mod sbbf;
22use arrow_schema::{DataType, Field};
23use serde::{Deserialize, Serialize};
24
25use std::sync::LazyLock;
26
27use datafusion::execution::SendableRecordBatchStream;
28use std::{collections::HashMap, sync::Arc};
29
30use crate::scalar::FragReuseIndex;
31use crate::scalar::{AnyQuery, IndexStore, MetricsCollector, ScalarIndex, SearchResult};
32use crate::vector::VectorIndex;
33use crate::{Index, IndexType};
34use arrow_array::{ArrayRef, RecordBatch};
35use async_trait::async_trait;
36use deepsize::DeepSizeOf;
37use lance_core::Error;
38use lance_core::Result;
39use lance_core::cache::LanceCache;
40use roaring::RoaringBitmap;
41
42use super::zoned::{ZoneBound, ZoneProcessor, ZoneTrainer, rebuild_zones, search_zones};
43
44const BLOOMFILTER_FILENAME: &str = "bloomfilter.lance";
45const BLOOMFILTER_ITEM_META_KEY: &str = "bloomfilter_item";
46const BLOOMFILTER_PROBABILITY_META_KEY: &str = "bloomfilter_probability";
47const BLOOMFILTER_INDEX_VERSION: u32 = 0;
48
49#[derive(Debug, Clone)]
50struct BloomFilterStatistics {
51    // Bound of this zone within the fragment. Persisted as three separate columns
52    // (fragment_id, zone_start, zone_length) in the index file.
53    bound: ZoneBound,
54    // Whether this zone contains any null values
55    has_null: bool,
56    // The actual bloom filter (SBBF) for efficient querying
57    bloom_filter: Sbbf,
58}
59
60impl DeepSizeOf for BloomFilterStatistics {
61    fn deep_size_of_children(&self, _context: &mut deepsize::Context) -> usize {
62        // Estimate the size of the bloom filter
63        // We could try to get the actual size from the Sbbf if it has a method for that,
64        // but for now we'll estimate based on the number of bytes it serializes to
65        self.bloom_filter.to_bytes().len()
66    }
67}
68
69impl AsRef<ZoneBound> for BloomFilterStatistics {
70    fn as_ref(&self) -> &ZoneBound {
71        &self.bound
72    }
73}
74
75#[derive(Debug, Clone)]
76pub struct BloomFilterIndex {
77    zones: Vec<BloomFilterStatistics>,
78    // Number of items in the filter
79    number_of_items: u64,
80    // Probability of false positives, fraction between 0 and 1
81    probability: f64,
82}
83
84impl DeepSizeOf for BloomFilterIndex {
85    fn deep_size_of_children(&self, context: &mut deepsize::Context) -> usize {
86        self.zones.deep_size_of_children(context)
87    }
88}
89
90impl BloomFilterIndex {
91    async fn load(
92        store: Arc<dyn IndexStore>,
93        _fri: Option<Arc<FragReuseIndex>>,
94        _index_cache: &LanceCache,
95    ) -> Result<Arc<Self>> {
96        let index_file = store.open_index_file(BLOOMFILTER_FILENAME).await?;
97        let bloom_data = index_file
98            .read_range(0..index_file.num_rows(), None)
99            .await?;
100        let file_schema = index_file.schema();
101
102        let number_of_items: u64 = file_schema
103            .metadata
104            .get(BLOOMFILTER_ITEM_META_KEY)
105            .and_then(|bs| bs.parse().ok())
106            .unwrap_or(*DEFAULT_NUMBER_OF_ITEMS);
107
108        let probability: f64 = file_schema
109            .metadata
110            .get(BLOOMFILTER_PROBABILITY_META_KEY)
111            .and_then(|bs| bs.parse().ok())
112            .unwrap_or(*DEFAULT_PROBABILITY);
113
114        Ok(Arc::new(Self::try_from_serialized(
115            bloom_data,
116            number_of_items,
117            probability,
118        )?))
119    }
120
121    fn try_from_serialized(
122        data: RecordBatch,
123        number_of_items: u64,
124        probability: f64,
125    ) -> Result<Self> {
126        if data.num_rows() == 0 {
127            // Return empty index for empty data
128            return Ok(Self {
129                zones: Vec::new(),
130                number_of_items,
131                probability,
132            });
133        }
134
135        let fragment_id_col = data
136            .column_by_name("fragment_id")
137            .ok_or_else(|| Error::invalid_input("BloomFilterIndex: missing 'fragment_id' column"))?
138            .as_any()
139            .downcast_ref::<arrow_array::UInt64Array>()
140            .ok_or_else(|| {
141                Error::invalid_input("BloomFilterIndex: 'fragment_id' column is not UInt64")
142            })?;
143
144        let zone_start_col = data
145            .column_by_name("zone_start")
146            .ok_or_else(|| Error::invalid_input("BloomFilterIndex: missing 'zone_start' column"))?
147            .as_any()
148            .downcast_ref::<arrow_array::UInt64Array>()
149            .ok_or_else(|| {
150                Error::invalid_input("BloomFilterIndex: 'zone_start' column is not UInt64")
151            })?;
152
153        let zone_length_col = data
154            .column_by_name("zone_length")
155            .ok_or_else(|| Error::invalid_input("BloomFilterIndex: missing 'zone_length' column"))?
156            .as_any()
157            .downcast_ref::<arrow_array::UInt64Array>()
158            .ok_or_else(|| {
159                Error::invalid_input("BloomFilterIndex: 'zone_length' column is not UInt64")
160            })?;
161
162        let bloom_filter_data_col = data
163            .column_by_name("bloom_filter_data")
164            .ok_or_else(|| {
165                Error::invalid_input("BloomFilterIndex: missing 'bloom_filter_data' column")
166            })?
167            .as_any()
168            .downcast_ref::<arrow_array::BinaryArray>()
169            .ok_or_else(|| {
170                Error::invalid_input("BloomFilterIndex: 'bloom_filter_data' column is not Binary")
171            })?;
172
173        let has_null_col = data
174            .column_by_name("has_null")
175            .ok_or_else(|| Error::invalid_input("BloomFilterIndex: missing 'has_null' column"))?
176            .as_any()
177            .downcast_ref::<arrow_array::BooleanArray>()
178            .ok_or_else(|| {
179                Error::invalid_input("BloomFilterIndex: 'has_null' column is not Boolean")
180            })?;
181
182        let num_blocks = data.num_rows();
183        let mut blocks = Vec::with_capacity(num_blocks);
184
185        for i in 0..num_blocks {
186            let bloom_filter_bytes = if bloom_filter_data_col.is_valid(i) {
187                bloom_filter_data_col.value(i).to_vec()
188            } else {
189                Vec::new()
190            };
191
192            // Convert bytes back to Sbbf
193            let bloom_filter = Sbbf::new(&bloom_filter_bytes).map_err(|e| {
194                Error::invalid_input(format!("Failed to deserialize bloom filter: {:?}", e))
195            })?;
196
197            blocks.push(BloomFilterStatistics {
198                bound: ZoneBound {
199                    fragment_id: fragment_id_col.value(i),
200                    start: zone_start_col.value(i),
201                    length: zone_length_col.value(i) as usize,
202                },
203                has_null: has_null_col.value(i),
204                bloom_filter,
205            });
206        }
207
208        Ok(Self {
209            zones: blocks,
210            number_of_items,
211            probability,
212        })
213    }
214
215    fn evaluate_block_against_query(
216        &self,
217        block: &BloomFilterStatistics,
218        query: &BloomFilterQuery,
219    ) -> Result<bool> {
220        let sbbf = &block.bloom_filter;
221
222        match query {
223            BloomFilterQuery::IsNull() => {
224                // Use the has_null information to determine if this block contains nulls
225                Ok(block.has_null)
226            }
227            BloomFilterQuery::Equals(target) => {
228                if target.is_null() {
229                    // Handle null values using has_null information
230                    return Ok(block.has_null);
231                }
232
233                // Check the bloom filter for the target value
234                match target {
235                    // Signed integers
236                    datafusion_common::ScalarValue::Int8(Some(val)) => Ok(sbbf.check(val)),
237                    datafusion_common::ScalarValue::Int16(Some(val)) => Ok(sbbf.check(val)),
238                    datafusion_common::ScalarValue::Int32(Some(val)) => Ok(sbbf.check(val)),
239                    datafusion_common::ScalarValue::Int64(Some(val)) => Ok(sbbf.check(val)),
240                    // Unsigned integers
241                    datafusion_common::ScalarValue::UInt8(Some(val)) => Ok(sbbf.check(val)),
242                    datafusion_common::ScalarValue::UInt16(Some(val)) => Ok(sbbf.check(val)),
243                    datafusion_common::ScalarValue::UInt32(Some(val)) => Ok(sbbf.check(val)),
244                    datafusion_common::ScalarValue::UInt64(Some(val)) => Ok(sbbf.check(val)),
245                    // Floating point
246                    datafusion_common::ScalarValue::Float32(Some(val)) => Ok(sbbf.check(val)),
247                    datafusion_common::ScalarValue::Float64(Some(val)) => Ok(sbbf.check(val)),
248                    // String types
249                    datafusion_common::ScalarValue::Utf8(Some(val)) => Ok(sbbf.check(val.as_str())),
250                    datafusion_common::ScalarValue::LargeUtf8(Some(val)) => {
251                        Ok(sbbf.check(val.as_str()))
252                    }
253                    // Binary types
254                    datafusion_common::ScalarValue::Binary(Some(val)) => {
255                        Ok(sbbf.check(val.as_slice()))
256                    }
257                    datafusion_common::ScalarValue::LargeBinary(Some(val)) => {
258                        Ok(sbbf.check(val.as_slice()))
259                    }
260                    // Date and time types
261                    datafusion_common::ScalarValue::Date32(Some(val)) => Ok(sbbf.check(val)),
262                    datafusion_common::ScalarValue::Date64(Some(val)) => Ok(sbbf.check(val)),
263                    datafusion_common::ScalarValue::Time32Second(Some(val)) => Ok(sbbf.check(val)),
264                    datafusion_common::ScalarValue::Time32Millisecond(Some(val)) => {
265                        Ok(sbbf.check(val))
266                    }
267                    datafusion_common::ScalarValue::Time64Microsecond(Some(val)) => {
268                        Ok(sbbf.check(val))
269                    }
270                    datafusion_common::ScalarValue::Time64Nanosecond(Some(val)) => {
271                        Ok(sbbf.check(val))
272                    }
273                    datafusion_common::ScalarValue::TimestampSecond(Some(val), _) => {
274                        Ok(sbbf.check(val))
275                    }
276                    datafusion_common::ScalarValue::TimestampMillisecond(Some(val), _) => {
277                        Ok(sbbf.check(val))
278                    }
279                    datafusion_common::ScalarValue::TimestampMicrosecond(Some(val), _) => {
280                        Ok(sbbf.check(val))
281                    }
282                    datafusion_common::ScalarValue::TimestampNanosecond(Some(val), _) => {
283                        Ok(sbbf.check(val))
284                    }
285                    _ => Err(Error::invalid_input_source(
286                        format!("Unsupported data type in bloom filter query: {:?}", target).into(),
287                    )),
288                }
289            }
290            BloomFilterQuery::IsIn(values) => {
291                // Check if any value in the set is in the bloom filter
292                for value in values {
293                    if value.is_null() {
294                        // Handle null values using has_null information
295                        if block.has_null {
296                            return Ok(true);
297                        }
298                        continue;
299                    }
300
301                    let found = match value {
302                        // Signed integers
303                        datafusion_common::ScalarValue::Int8(Some(val)) => sbbf.check(val),
304                        datafusion_common::ScalarValue::Int16(Some(val)) => sbbf.check(val),
305                        datafusion_common::ScalarValue::Int32(Some(val)) => sbbf.check(val),
306                        datafusion_common::ScalarValue::Int64(Some(val)) => sbbf.check(val),
307                        // Unsigned integers
308                        datafusion_common::ScalarValue::UInt8(Some(val)) => sbbf.check(val),
309                        datafusion_common::ScalarValue::UInt16(Some(val)) => sbbf.check(val),
310                        datafusion_common::ScalarValue::UInt32(Some(val)) => sbbf.check(val),
311                        datafusion_common::ScalarValue::UInt64(Some(val)) => sbbf.check(val),
312                        // Floating point
313                        datafusion_common::ScalarValue::Float32(Some(val)) => sbbf.check(val),
314                        datafusion_common::ScalarValue::Float64(Some(val)) => sbbf.check(val),
315                        // String types
316                        datafusion_common::ScalarValue::Utf8(Some(val)) => sbbf.check(val.as_str()),
317                        datafusion_common::ScalarValue::LargeUtf8(Some(val)) => {
318                            sbbf.check(val.as_str())
319                        }
320                        // Binary types
321                        datafusion_common::ScalarValue::Binary(Some(val)) => {
322                            sbbf.check(val.as_slice())
323                        }
324                        datafusion_common::ScalarValue::LargeBinary(Some(val)) => {
325                            sbbf.check(val.as_slice())
326                        }
327                        // Date and time types
328                        datafusion_common::ScalarValue::Date32(Some(val)) => sbbf.check(val),
329                        datafusion_common::ScalarValue::Date64(Some(val)) => sbbf.check(val),
330                        datafusion_common::ScalarValue::Time32Second(Some(val)) => sbbf.check(val),
331                        datafusion_common::ScalarValue::Time32Millisecond(Some(val)) => {
332                            sbbf.check(val)
333                        }
334                        datafusion_common::ScalarValue::Time64Microsecond(Some(val)) => {
335                            sbbf.check(val)
336                        }
337                        datafusion_common::ScalarValue::Time64Nanosecond(Some(val)) => {
338                            sbbf.check(val)
339                        }
340                        datafusion_common::ScalarValue::TimestampSecond(Some(val), _) => {
341                            sbbf.check(val)
342                        }
343                        datafusion_common::ScalarValue::TimestampMillisecond(Some(val), _) => {
344                            sbbf.check(val)
345                        }
346                        datafusion_common::ScalarValue::TimestampMicrosecond(Some(val), _) => {
347                            sbbf.check(val)
348                        }
349                        datafusion_common::ScalarValue::TimestampNanosecond(Some(val), _) => {
350                            sbbf.check(val)
351                        }
352                        _ => {
353                            return Err(Error::invalid_input_source(
354                                format!("Unsupported data type in bloom filter query: {:?}", value)
355                                    .into(),
356                            ));
357                        }
358                    };
359
360                    if found {
361                        return Ok(true);
362                    }
363                }
364                Ok(false) // None of the values were found
365            }
366        }
367    }
368}
369
370#[async_trait]
371impl Index for BloomFilterIndex {
372    fn as_any(&self) -> &dyn Any {
373        self
374    }
375
376    fn as_index(self: Arc<Self>) -> Arc<dyn Index> {
377        self
378    }
379
380    fn as_vector_index(self: Arc<Self>) -> Result<Arc<dyn VectorIndex>> {
381        Err(Error::invalid_input_source(
382            "BloomFilter is not a vector index".into(),
383        ))
384    }
385
386    async fn prewarm(&self) -> Result<()> {
387        Ok(())
388    }
389
390    fn statistics(&self) -> Result<serde_json::Value> {
391        Ok(serde_json::json!({
392            "type": "BloomFilter",
393            "num_blocks": self.zones.len(),
394            "number_of_items": self.number_of_items,
395            "probability": self.probability,
396        }))
397    }
398
399    fn index_type(&self) -> IndexType {
400        IndexType::BloomFilter
401    }
402
403    async fn calculate_included_frags(&self) -> Result<RoaringBitmap> {
404        let mut frag_ids = RoaringBitmap::new();
405
406        // Loop through zones and add unique fragment IDs to the bitmap
407        for block in &self.zones {
408            frag_ids.insert(block.bound.fragment_id as u32);
409        }
410
411        Ok(frag_ids)
412    }
413}
414
415#[async_trait]
416impl ScalarIndex for BloomFilterIndex {
417    async fn search(
418        &self,
419        query: &dyn AnyQuery,
420        metrics: &dyn MetricsCollector,
421    ) -> Result<SearchResult> {
422        let query = query.as_any().downcast_ref::<BloomFilterQuery>().unwrap();
423        search_zones(&self.zones, metrics, |block| {
424            self.evaluate_block_against_query(block, query)
425        })
426    }
427
428    fn can_remap(&self) -> bool {
429        false
430    }
431
432    async fn remap(
433        &self,
434        _mapping: &HashMap<u64, Option<u64>>,
435        _dest_store: &dyn IndexStore,
436    ) -> Result<CreatedIndex> {
437        Err(Error::invalid_input_source(
438            "BloomFilter does not support remap".into(),
439        ))
440    }
441
442    async fn update(
443        &self,
444        new_data: SendableRecordBatchStream,
445        dest_store: &dyn IndexStore,
446        _valid_old_fragments: Option<&RoaringBitmap>,
447    ) -> Result<CreatedIndex> {
448        // Re-train bloom filters for the appended data using the shared trainer
449        let params = BloomFilterIndexBuilderParams {
450            number_of_items: self.number_of_items,
451            probability: self.probability,
452        };
453
454        let processor = BloomFilterProcessor::new(params.clone())?;
455        let trainer = ZoneTrainer::new(processor, params.number_of_items)?;
456        let updated_blocks = rebuild_zones(&self.zones, trainer, new_data).await?;
457
458        // Write the combined zones back to storage
459        let mut builder = BloomFilterIndexBuilder::try_new(params)?;
460        builder.blocks = updated_blocks;
461        builder.write_index(dest_store).await?;
462
463        Ok(CreatedIndex {
464            index_details: prost_types::Any::from_msg(&pb::BloomFilterIndexDetails::default())
465                .unwrap(),
466            index_version: BLOOMFILTER_INDEX_VERSION,
467        })
468    }
469
470    fn update_criteria(&self) -> UpdateCriteria {
471        UpdateCriteria::only_new_data(
472            TrainingCriteria::new(TrainingOrdering::Addresses).with_row_addr(),
473        )
474    }
475
476    fn derive_index_params(&self) -> Result<ScalarIndexParams> {
477        let params = serde_json::to_value(BloomFilterIndexBuilderParams {
478            number_of_items: self.number_of_items,
479            probability: self.probability,
480        })?;
481        Ok(ScalarIndexParams::for_builtin(BuiltinIndexType::BloomFilter).with_params(&params))
482    }
483}
484
485fn default_number_of_items() -> u64 {
486    *DEFAULT_NUMBER_OF_ITEMS
487}
488
489fn default_probability() -> f64 {
490    *DEFAULT_PROBABILITY
491}
492
493// NumberOfItems: 8192 + Probability: 0.00057(1 in 1754) -> NumberOfBytes: 16384(16KiB) + 8 SALT values
494// reference: https://hur.st/bloomfilter/?n=8192&p=&m=16KiB&k=8
495static DEFAULT_NUMBER_OF_ITEMS: LazyLock<u64> = LazyLock::new(|| {
496    std::env::var("LANCE_BLOOMFILTER_DEFAULT_NUMBER_OF_ITEMS")
497        .unwrap_or_else(|_| "8192".to_string())
498        .parse()
499        .expect("failed to parse Lance_BLOOMFILTER_DEFAULT_NUMBER_OF_ITEMS")
500});
501
502#[allow(clippy::manual_inspect)]
503static DEFAULT_PROBABILITY: LazyLock<f64> = LazyLock::new(|| {
504    std::env::var("LANCE_BLOOMFILTER_DEFAULT_PROBABILITY")
505        // 0.00057 ≈ 1 in 1754 false positive rate
506        .unwrap_or_else(|_| "0.00057".to_string())
507        .parse()
508        .map(|prob: f64| {
509            assert!(
510                (0.0..=1.0).contains(&prob),
511                "LANCE_BLOOMFILTER_DEFAULT_PROBABILITY must be between 0 and 1, got {}",
512                prob
513            );
514            prob
515        })
516        .expect("failed to parse LANCE_BLOOMFILTER_DEFAULT_PROBABILITY")
517});
518
519#[derive(Debug, Clone, Serialize, Deserialize)]
520pub struct BloomFilterIndexBuilderParams {
521    #[serde(default = "default_number_of_items")]
522    number_of_items: u64,
523    #[serde(default = "default_probability")]
524    probability: f64,
525}
526
527impl Default for BloomFilterIndexBuilderParams {
528    fn default() -> Self {
529        Self {
530            number_of_items: *DEFAULT_NUMBER_OF_ITEMS,
531            probability: *DEFAULT_PROBABILITY,
532        }
533    }
534}
535
536impl BloomFilterIndexBuilderParams {
537    #[cfg(test)]
538    fn new(number_of_items: u64, probability: f64) -> Self {
539        Self {
540            number_of_items,
541            probability,
542        }
543    }
544}
545
546pub struct BloomFilterIndexBuilder {
547    params: BloomFilterIndexBuilderParams,
548    blocks: Vec<BloomFilterStatistics>,
549}
550
551impl BloomFilterIndexBuilder {
552    pub fn try_new(params: BloomFilterIndexBuilderParams) -> Result<Self> {
553        Ok(Self {
554            params,
555            blocks: Vec::new(),
556        })
557    }
558
559    /// Train the builder using the shared ZoneTrainer. The input stream is expected to
560    /// contain the value column followed by `_rowaddr`, matching the order emitted by
561    /// the scalar index training pipeline.
562    pub async fn train(&mut self, batches_source: SendableRecordBatchStream) -> Result<()> {
563        let processor = BloomFilterProcessor::new(self.params.clone())?;
564        let trainer = ZoneTrainer::new(processor, self.params.number_of_items)?;
565        self.blocks = trainer.train(batches_source).await?;
566        Ok(())
567    }
568
569    fn bloomfilter_stats_as_batch(&self) -> Result<RecordBatch> {
570        let fragment_ids =
571            UInt64Array::from_iter_values(self.blocks.iter().map(|block| block.bound.fragment_id));
572
573        let zone_starts =
574            UInt64Array::from_iter_values(self.blocks.iter().map(|block| block.bound.start));
575
576        let zone_lengths = UInt64Array::from_iter_values(
577            self.blocks.iter().map(|block| block.bound.length as u64),
578        );
579
580        let has_nulls = arrow_array::BooleanArray::from(
581            self.blocks
582                .iter()
583                .map(|block| block.has_null)
584                .collect::<Vec<bool>>(),
585        );
586
587        // Convert bloom filters to binary data for serialization
588        let bloom_filter_data = if self.blocks.is_empty() {
589            Arc::new(arrow_array::BinaryArray::new_null(0)) as ArrayRef
590        } else {
591            let binary_data: Vec<Vec<u8>> = self
592                .blocks
593                .iter()
594                .map(|block| block.bloom_filter.to_bytes())
595                .collect();
596            let binary_refs: Vec<Option<&[u8]>> = binary_data
597                .iter()
598                .map(|bytes| Some(bytes.as_slice()))
599                .collect();
600            Arc::new(arrow_array::BinaryArray::from_opt_vec(binary_refs)) as ArrayRef
601        };
602
603        let schema = Arc::new(arrow_schema::Schema::new(vec![
604            Field::new("fragment_id", DataType::UInt64, false),
605            Field::new("zone_start", DataType::UInt64, false),
606            Field::new("zone_length", DataType::UInt64, false),
607            Field::new("has_null", DataType::Boolean, false),
608            Field::new("bloom_filter_data", DataType::Binary, false),
609        ]));
610
611        let columns: Vec<ArrayRef> = vec![
612            Arc::new(fragment_ids) as ArrayRef,
613            Arc::new(zone_starts) as ArrayRef,
614            Arc::new(zone_lengths) as ArrayRef,
615            Arc::new(has_nulls) as ArrayRef,
616            bloom_filter_data,
617        ];
618
619        Ok(RecordBatch::try_new(schema, columns)?)
620    }
621
622    pub async fn write_index(self, index_store: &dyn IndexStore) -> Result<()> {
623        let record_batch = self.bloomfilter_stats_as_batch()?;
624
625        let mut file_schema = record_batch.schema().as_ref().clone();
626        file_schema.metadata.insert(
627            BLOOMFILTER_ITEM_META_KEY.to_string(),
628            self.params.number_of_items.to_string(),
629        );
630
631        file_schema.metadata.insert(
632            BLOOMFILTER_PROBABILITY_META_KEY.to_string(),
633            self.params.probability.to_string(),
634        );
635
636        let mut index_file = index_store
637            .new_index_file(BLOOMFILTER_FILENAME, Arc::new(file_schema))
638            .await?;
639        index_file.write_record_batch(record_batch).await?;
640        index_file.finish().await?;
641        Ok(())
642    }
643}
644
645/// Index-specific processor that inserts values into the split block Bloom filter.
646struct BloomFilterProcessor {
647    params: BloomFilterIndexBuilderParams,
648    sbbf: Option<Sbbf>,
649    cur_zone_has_null: bool,
650}
651
652impl BloomFilterProcessor {
653    fn new(params: BloomFilterIndexBuilderParams) -> Result<Self> {
654        let mut processor = Self {
655            params,
656            sbbf: None,
657            cur_zone_has_null: false,
658        };
659        processor.reset()?;
660        Ok(processor)
661    }
662
663    fn build_filter(params: &BloomFilterIndexBuilderParams) -> Result<Sbbf> {
664        SbbfBuilder::new()
665            .expected_items(params.number_of_items)
666            .false_positive_probability(params.probability)
667            .build()
668            .map_err(|e| {
669                Error::invalid_input_source(format!("Failed to build SBBF: {:?}", e).into())
670            })
671    }
672
673    fn process_primitive_array<T>(sbbf: &mut Sbbf, array: &arrow_array::PrimitiveArray<T>) -> bool
674    where
675        T: arrow_array::ArrowPrimitiveType,
676        T::Native: as_bytes::AsBytes,
677    {
678        let mut has_null = false;
679        for i in 0..array.len() {
680            if array.is_valid(i) {
681                sbbf.insert(&array.value(i));
682            } else {
683                has_null = true;
684            }
685        }
686        has_null
687    }
688
689    fn process_string_array(sbbf: &mut Sbbf, array: &arrow_array::StringArray) -> bool {
690        let mut has_null = false;
691        for i in 0..array.len() {
692            if array.is_valid(i) {
693                sbbf.insert(array.value(i));
694            } else {
695                has_null = true;
696            }
697        }
698        has_null
699    }
700
701    fn process_large_string_array(sbbf: &mut Sbbf, array: &arrow_array::LargeStringArray) -> bool {
702        let mut has_null = false;
703        for i in 0..array.len() {
704            if array.is_valid(i) {
705                sbbf.insert(array.value(i));
706            } else {
707                has_null = true;
708            }
709        }
710        has_null
711    }
712
713    fn process_binary_array(sbbf: &mut Sbbf, array: &arrow_array::BinaryArray) -> bool {
714        let mut has_null = false;
715        for i in 0..array.len() {
716            if array.is_valid(i) {
717                sbbf.insert(array.value(i));
718            } else {
719                has_null = true;
720            }
721        }
722        has_null
723    }
724
725    fn process_large_binary_array(sbbf: &mut Sbbf, array: &arrow_array::LargeBinaryArray) -> bool {
726        let mut has_null = false;
727        for i in 0..array.len() {
728            if array.is_valid(i) {
729                sbbf.insert(array.value(i));
730            } else {
731                has_null = true;
732            }
733        }
734        has_null
735    }
736}
737
738impl ZoneProcessor for BloomFilterProcessor {
739    type ZoneStatistics = BloomFilterStatistics;
740
741    fn process_chunk(&mut self, array: &ArrayRef) -> Result<()> {
742        let sbbf = self.sbbf.as_mut().ok_or_else(|| {
743            Error::invalid_input("BloomFilterProcessor did not initialize bloom filter")
744        })?;
745
746        let has_null = match array.data_type() {
747            // Signed integers
748            DataType::Int8 => {
749                let typed_array = array
750                    .as_any()
751                    .downcast_ref::<arrow_array::Int8Array>()
752                    .unwrap();
753                Self::process_primitive_array(sbbf, typed_array)
754            }
755            DataType::Int16 => {
756                let typed_array = array
757                    .as_any()
758                    .downcast_ref::<arrow_array::Int16Array>()
759                    .unwrap();
760                Self::process_primitive_array(sbbf, typed_array)
761            }
762            DataType::Int32 => {
763                let typed_array = array
764                    .as_any()
765                    .downcast_ref::<arrow_array::Int32Array>()
766                    .unwrap();
767                Self::process_primitive_array(sbbf, typed_array)
768            }
769            DataType::Int64 => {
770                let typed_array = array
771                    .as_any()
772                    .downcast_ref::<arrow_array::Int64Array>()
773                    .unwrap();
774                Self::process_primitive_array(sbbf, typed_array)
775            }
776            // Unsigned integers
777            DataType::UInt8 => {
778                let typed_array = array
779                    .as_any()
780                    .downcast_ref::<arrow_array::UInt8Array>()
781                    .unwrap();
782                Self::process_primitive_array(sbbf, typed_array)
783            }
784            DataType::UInt16 => {
785                let typed_array = array
786                    .as_any()
787                    .downcast_ref::<arrow_array::UInt16Array>()
788                    .unwrap();
789                Self::process_primitive_array(sbbf, typed_array)
790            }
791            DataType::UInt32 => {
792                let typed_array = array
793                    .as_any()
794                    .downcast_ref::<arrow_array::UInt32Array>()
795                    .unwrap();
796                Self::process_primitive_array(sbbf, typed_array)
797            }
798            DataType::UInt64 => {
799                let typed_array = array
800                    .as_any()
801                    .downcast_ref::<arrow_array::UInt64Array>()
802                    .unwrap();
803                Self::process_primitive_array(sbbf, typed_array)
804            }
805            // Floating point numbers
806            DataType::Float32 => {
807                let typed_array = array
808                    .as_any()
809                    .downcast_ref::<arrow_array::Float32Array>()
810                    .unwrap();
811                Self::process_primitive_array(sbbf, typed_array)
812            }
813            DataType::Float64 => {
814                let typed_array = array
815                    .as_any()
816                    .downcast_ref::<arrow_array::Float64Array>()
817                    .unwrap();
818                Self::process_primitive_array(sbbf, typed_array)
819            }
820            // Date and time types (stored as i32 internally)
821            DataType::Date32 => {
822                let typed_array = array
823                    .as_any()
824                    .downcast_ref::<arrow_array::Date32Array>()
825                    .unwrap();
826                Self::process_primitive_array(sbbf, typed_array)
827            }
828            DataType::Time32(time_unit) => match time_unit {
829                arrow_schema::TimeUnit::Second => {
830                    let typed_array = array
831                        .as_any()
832                        .downcast_ref::<arrow_array::Time32SecondArray>()
833                        .unwrap();
834                    Self::process_primitive_array(sbbf, typed_array)
835                }
836                arrow_schema::TimeUnit::Millisecond => {
837                    let typed_array = array
838                        .as_any()
839                        .downcast_ref::<arrow_array::Time32MillisecondArray>()
840                        .unwrap();
841                    Self::process_primitive_array(sbbf, typed_array)
842                }
843                _ => {
844                    return Err(Error::invalid_input_source(
845                        format!("Unsupported Time32 unit: {:?}", time_unit).into(),
846                    ));
847                }
848            },
849            // Date and time types (stored as i64 internally)
850            DataType::Date64 => {
851                let typed_array = array
852                    .as_any()
853                    .downcast_ref::<arrow_array::Date64Array>()
854                    .unwrap();
855                Self::process_primitive_array(sbbf, typed_array)
856            }
857            DataType::Time64(time_unit) => match time_unit {
858                arrow_schema::TimeUnit::Microsecond => {
859                    let typed_array = array
860                        .as_any()
861                        .downcast_ref::<arrow_array::Time64MicrosecondArray>()
862                        .unwrap();
863                    Self::process_primitive_array(sbbf, typed_array)
864                }
865                arrow_schema::TimeUnit::Nanosecond => {
866                    let typed_array = array
867                        .as_any()
868                        .downcast_ref::<arrow_array::Time64NanosecondArray>()
869                        .unwrap();
870                    Self::process_primitive_array(sbbf, typed_array)
871                }
872                _ => {
873                    return Err(Error::invalid_input_source(
874                        format!("Unsupported Time64 unit: {:?}", time_unit).into(),
875                    ));
876                }
877            },
878            DataType::Timestamp(time_unit, _) => match time_unit {
879                arrow_schema::TimeUnit::Second => {
880                    let typed_array = array
881                        .as_any()
882                        .downcast_ref::<arrow_array::TimestampSecondArray>()
883                        .unwrap();
884                    Self::process_primitive_array(sbbf, typed_array)
885                }
886                arrow_schema::TimeUnit::Millisecond => {
887                    let typed_array = array
888                        .as_any()
889                        .downcast_ref::<arrow_array::TimestampMillisecondArray>()
890                        .unwrap();
891                    Self::process_primitive_array(sbbf, typed_array)
892                }
893                arrow_schema::TimeUnit::Microsecond => {
894                    let typed_array = array
895                        .as_any()
896                        .downcast_ref::<arrow_array::TimestampMicrosecondArray>()
897                        .unwrap();
898                    Self::process_primitive_array(sbbf, typed_array)
899                }
900                arrow_schema::TimeUnit::Nanosecond => {
901                    let typed_array = array
902                        .as_any()
903                        .downcast_ref::<arrow_array::TimestampNanosecondArray>()
904                        .unwrap();
905                    Self::process_primitive_array(sbbf, typed_array)
906                }
907            },
908            DataType::Utf8 => {
909                let typed_array = array
910                    .as_any()
911                    .downcast_ref::<arrow_array::StringArray>()
912                    .unwrap();
913                Self::process_string_array(sbbf, typed_array)
914            }
915            DataType::LargeUtf8 => {
916                let typed_array = array
917                    .as_any()
918                    .downcast_ref::<arrow_array::LargeStringArray>()
919                    .unwrap();
920                Self::process_large_string_array(sbbf, typed_array)
921            }
922            DataType::Binary => {
923                let typed_array = array
924                    .as_any()
925                    .downcast_ref::<arrow_array::BinaryArray>()
926                    .unwrap();
927                Self::process_binary_array(sbbf, typed_array)
928            }
929            DataType::LargeBinary => {
930                let typed_array = array
931                    .as_any()
932                    .downcast_ref::<arrow_array::LargeBinaryArray>()
933                    .unwrap();
934                Self::process_large_binary_array(sbbf, typed_array)
935            }
936            _ => {
937                return Err(Error::invalid_input_source(
938                    format!(
939                        "Bloom filter does not support data type: {:?}",
940                        array.data_type()
941                    )
942                    .into(),
943                ));
944            }
945        };
946
947        // Update the current zone's null tracking
948        self.cur_zone_has_null = self.cur_zone_has_null || has_null;
949        Ok(())
950    }
951
952    fn finish_zone(&mut self, bound: ZoneBound) -> Result<Self::ZoneStatistics> {
953        let bloom_filter = self.sbbf.as_ref().ok_or_else(|| {
954            Error::invalid_input("BloomFilterProcessor did not initialize bloom filter")
955        })?;
956        Ok(BloomFilterStatistics {
957            bound,
958            has_null: self.cur_zone_has_null,
959            bloom_filter: bloom_filter.clone(),
960        })
961    }
962
963    fn reset(&mut self) -> Result<()> {
964        self.sbbf = Some(Self::build_filter(&self.params)?);
965        self.cur_zone_has_null = false;
966        Ok(())
967    }
968}
969
970#[derive(Debug, Default)]
971pub struct BloomFilterIndexPlugin;
972
973impl BloomFilterIndexPlugin {
974    async fn train_bloomfilter_index(
975        batches_source: SendableRecordBatchStream,
976        index_store: &dyn IndexStore,
977        options: Option<BloomFilterIndexBuilderParams>,
978    ) -> Result<()> {
979        let mut builder = BloomFilterIndexBuilder::try_new(options.unwrap_or_default())?;
980
981        builder.train(batches_source).await?;
982
983        builder.write_index(index_store).await?;
984        Ok(())
985    }
986}
987
988#[async_trait]
989impl ScalarIndexPlugin for BloomFilterIndexPlugin {
990    fn name(&self) -> &str {
991        "BloomFilter"
992    }
993
994    fn new_training_request(
995        &self,
996        params: &str,
997        field: &Field,
998    ) -> Result<Box<dyn TrainingRequest>> {
999        if field.data_type().is_nested() {
1000            return Err(Error::invalid_input_source(
1001                "A bloom filter index can only be created on a non-nested field.".into(),
1002            ));
1003        }
1004
1005        // Check if the data type is supported by bloom filter
1006        match field.data_type() {
1007            // Signed integers
1008            DataType::Int8
1009            | DataType::Int16
1010            | DataType::Int32
1011            | DataType::Int64
1012            // Unsigned integers
1013            | DataType::UInt8
1014            | DataType::UInt16
1015            | DataType::UInt32
1016            | DataType::UInt64
1017            // Floating point
1018            | DataType::Float32
1019            | DataType::Float64
1020            // String types
1021            | DataType::Utf8
1022            | DataType::LargeUtf8
1023            // Binary types
1024            | DataType::Binary
1025            | DataType::LargeBinary
1026            // Date and time types
1027            | DataType::Date32
1028            | DataType::Date64
1029            | DataType::Time32(_)
1030            | DataType::Time64(_)
1031            | DataType::Timestamp(_, _) => {
1032                // Type is supported, continue
1033            }
1034            _ => {
1035                return Err(Error::invalid_input_source(format!(
1036                    "Bloom filter index does not support data type: {:?}. Supported types: Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64, Float32, Float64, Utf8, LargeUtf8, Binary, LargeBinary, Date32, Date64, Time32, Time64, Timestamp",
1037                    field.data_type()
1038                ).into()));
1039            }
1040        }
1041
1042        let params = serde_json::from_str::<BloomFilterIndexBuilderParams>(params)?;
1043
1044        Ok(Box::new(BloomFilterIndexTrainingRequest::new(params)))
1045    }
1046
1047    async fn train_index(
1048        &self,
1049        data: SendableRecordBatchStream,
1050        index_store: &dyn IndexStore,
1051        request: Box<dyn TrainingRequest>,
1052        fragment_ids: Option<Vec<u32>>,
1053        _progress: Arc<dyn crate::progress::IndexBuildProgress>,
1054    ) -> Result<CreatedIndex> {
1055        if fragment_ids.is_some() {
1056            return Err(Error::invalid_input_source(
1057                "BloomFilter index does not support fragment training".into(),
1058            ));
1059        }
1060
1061        let request = (request as Box<dyn std::any::Any>)
1062            .downcast::<BloomFilterIndexTrainingRequest>()
1063            .map_err(|_| {
1064                Error::invalid_input_source(
1065                    "must provide training request created by new_training_request".into(),
1066                )
1067            })?;
1068        Self::train_bloomfilter_index(data, index_store, Some(request.params)).await?;
1069        Ok(CreatedIndex {
1070            index_details: prost_types::Any::from_msg(&pb::BloomFilterIndexDetails::default())
1071                .unwrap(),
1072            index_version: BLOOMFILTER_INDEX_VERSION,
1073        })
1074    }
1075
1076    fn provides_exact_answer(&self) -> bool {
1077        false
1078    }
1079
1080    fn version(&self) -> u32 {
1081        BLOOMFILTER_INDEX_VERSION
1082    }
1083
1084    fn new_query_parser(
1085        &self,
1086        index_name: String,
1087        _index_details: &prost_types::Any,
1088    ) -> Option<Box<dyn ScalarQueryParser>> {
1089        Some(Box::new(BloomFilterQueryParser::new(index_name, true)))
1090    }
1091
1092    async fn load_index(
1093        &self,
1094        index_store: Arc<dyn IndexStore>,
1095        _index_details: &prost_types::Any,
1096        frag_reuse_index: Option<Arc<FragReuseIndex>>,
1097        cache: &LanceCache,
1098    ) -> Result<Arc<dyn ScalarIndex>> {
1099        Ok(
1100            BloomFilterIndex::load(index_store, frag_reuse_index, cache).await?
1101                as Arc<dyn ScalarIndex>,
1102        )
1103    }
1104
1105    async fn load_statistics(
1106        &self,
1107        _index_store: Arc<dyn IndexStore>,
1108        _index_details: &prost_types::Any,
1109    ) -> Result<Option<serde_json::Value>> {
1110        Ok(None)
1111    }
1112}
1113
1114#[derive(Debug)]
1115pub struct BloomFilterIndexTrainingRequest {
1116    pub params: BloomFilterIndexBuilderParams,
1117    pub criteria: TrainingCriteria,
1118}
1119
1120impl BloomFilterIndexTrainingRequest {
1121    pub fn new(params: BloomFilterIndexBuilderParams) -> Self {
1122        Self {
1123            params,
1124            criteria: TrainingCriteria::new(TrainingOrdering::Addresses).with_row_addr(),
1125        }
1126    }
1127}
1128
1129impl TrainingRequest for BloomFilterIndexTrainingRequest {
1130    fn as_any(&self) -> &dyn std::any::Any {
1131        self
1132    }
1133
1134    fn criteria(&self) -> &TrainingCriteria {
1135        &self.criteria
1136    }
1137}
1138
1139#[cfg(test)]
1140mod tests {
1141    use crate::scalar::registry::VALUE_COLUMN_NAME;
1142    use std::sync::Arc;
1143
1144    use crate::scalar::bloomfilter::BloomFilterIndexPlugin;
1145    use arrow_array::{RecordBatch, UInt64Array, record_batch};
1146    use arrow_schema::{DataType, Field, Schema};
1147    use datafusion::execution::SendableRecordBatchStream;
1148    use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
1149    use datafusion_common::ScalarValue;
1150    use futures::{StreamExt, stream};
1151    use lance_core::{
1152        ROW_ADDR,
1153        cache::LanceCache,
1154        utils::{mask::RowAddrTreeMap, tempfile::TempObjDir},
1155    };
1156    use lance_io::object_store::ObjectStore;
1157
1158    use crate::scalar::{
1159        BloomFilterQuery, ScalarIndex, SearchResult,
1160        bloomfilter::{BloomFilterIndex, BloomFilterIndexBuilderParams},
1161        lance_format::LanceIndexStore,
1162    };
1163
1164    use crate::Index; // Import Index trait to access calculate_included_frags
1165    use crate::metrics::NoOpMetricsCollector;
1166    use roaring::RoaringBitmap; // Import RoaringBitmap for the test
1167
1168    // Adds a _rowaddr column emulating each batch as a new fragment
1169    fn add_row_addr(stream: SendableRecordBatchStream) -> SendableRecordBatchStream {
1170        let schema = stream.schema();
1171        let schema_with_row_addr = Arc::new(Schema::new(vec![
1172            schema.field(0).clone(),
1173            Field::new(ROW_ADDR, DataType::UInt64, false),
1174        ]));
1175        let schema = schema_with_row_addr.clone();
1176        let stream = stream.enumerate().map(move |(frag_id, batch)| {
1177            let batch = batch.unwrap();
1178            let row_addr = Arc::new(UInt64Array::from_iter_values(
1179                (0..batch.num_rows() as u64).map(|off| off + ((frag_id as u64) << 32)),
1180            ));
1181            Ok(RecordBatch::try_new(
1182                schema_with_row_addr.clone(),
1183                vec![batch.column(0).clone(), row_addr],
1184            )?)
1185        });
1186        Box::pin(RecordBatchStreamAdapter::new(schema, stream))
1187    }
1188
1189    #[tokio::test]
1190    async fn test_empty_bloomfilter_index() {
1191        let tmpdir = TempObjDir::default();
1192        let test_store = Arc::new(LanceIndexStore::new(
1193            Arc::new(ObjectStore::local()),
1194            tmpdir.clone(),
1195            Arc::new(LanceCache::no_cache()),
1196        ));
1197
1198        let data = arrow_array::Int32Array::from(Vec::<i32>::new());
1199        let schema = Arc::new(Schema::new(vec![Field::new(
1200            VALUE_COLUMN_NAME,
1201            DataType::Int32,
1202            false,
1203        )]));
1204        let data = RecordBatch::try_new(schema.clone(), vec![Arc::new(data)]).unwrap();
1205
1206        let data_stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
1207            schema,
1208            stream::once(std::future::ready(Ok(data))),
1209        ));
1210        let data_stream = add_row_addr(data_stream);
1211
1212        BloomFilterIndexPlugin::train_bloomfilter_index(data_stream, test_store.as_ref(), None)
1213            .await
1214            .unwrap();
1215
1216        log::debug!("Successfully wrote the index file");
1217
1218        // Read the index file back and check its contents
1219        let index = BloomFilterIndex::load(test_store.clone(), None, &LanceCache::no_cache())
1220            .await
1221            .expect("Failed to load BloomFilterIndex");
1222        assert_eq!(index.zones.len(), 0);
1223        assert_eq!(index.number_of_items, 8192);
1224        assert_eq!(index.probability, 0.00057); // Default probability
1225
1226        // Equals query: null (should match nothing, as there are no nulls in empty index)
1227        let query = BloomFilterQuery::Equals(ScalarValue::Int32(None));
1228        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1229        assert_eq!(result, SearchResult::at_most(RowAddrTreeMap::new()));
1230    }
1231
1232    #[tokio::test]
1233    async fn test_basic_bloomfilter_index() {
1234        let tmpdir = TempObjDir::default();
1235        let test_store = Arc::new(LanceIndexStore::new(
1236            Arc::new(ObjectStore::local()),
1237            tmpdir.clone(),
1238            Arc::new(LanceCache::no_cache()),
1239        ));
1240
1241        let data = arrow_array::Int32Array::from_iter_values(0..100);
1242        let schema = Arc::new(Schema::new(vec![Field::new(
1243            VALUE_COLUMN_NAME,
1244            DataType::Int32,
1245            false,
1246        )]));
1247        let data = RecordBatch::try_new(schema.clone(), vec![Arc::new(data)]).unwrap();
1248        let data_stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
1249            schema,
1250            stream::once(std::future::ready(Ok(data))),
1251        ));
1252        let data_stream = add_row_addr(data_stream);
1253
1254        BloomFilterIndexPlugin::train_bloomfilter_index(
1255            data_stream,
1256            test_store.as_ref(),
1257            Some(BloomFilterIndexBuilderParams::new(100, 0.01)), // ~1% false positive rate
1258        )
1259        .await
1260        .unwrap();
1261
1262        log::debug!("Successfully wrote the index file");
1263
1264        // Read the index file back and check its contents
1265        let index = BloomFilterIndex::load(test_store.clone(), None, &LanceCache::no_cache())
1266            .await
1267            .expect("Failed to load BloomFilterIndex");
1268
1269        assert_eq!(index.zones.len(), 1);
1270        assert_eq!(index.number_of_items, 100);
1271        assert_eq!(index.probability, 0.01);
1272
1273        // Check that we have one zone (since 100 items fit exactly in one zone of size 100)
1274        assert_eq!(index.zones[0].bound.fragment_id, 0u64);
1275        assert_eq!(index.zones[0].bound.start, 0u64);
1276        assert_eq!(index.zones[0].bound.length, 100);
1277
1278        // Test search functionality
1279        // The bloom filter should work correctly and find the value
1280        let query = BloomFilterQuery::Equals(ScalarValue::Int32(Some(50)));
1281        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1282
1283        // Should match the block since value 50 is in the range [0, 100)
1284        let mut expected = RowAddrTreeMap::new();
1285        expected.insert_range(0..100);
1286        assert_eq!(result, SearchResult::at_most(expected));
1287
1288        // Test search for a value that shouldn't exist
1289        let query = BloomFilterQuery::Equals(ScalarValue::Int32(Some(500))); // Value not in [0, 100)
1290        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1291
1292        // Should return empty result since bloom filter correctly filters out this value
1293        assert_eq!(result, SearchResult::at_most(RowAddrTreeMap::new()));
1294
1295        // Test calculate_included_frags
1296        assert_eq!(
1297            index.calculate_included_frags().await.unwrap(),
1298            RoaringBitmap::from_iter(0..1)
1299        );
1300    }
1301
1302    #[tokio::test]
1303    async fn test_multiple_fragments_bloomfilter() {
1304        let tmpdir = TempObjDir::default();
1305        let test_store = Arc::new(LanceIndexStore::new(
1306            Arc::new(ObjectStore::local()),
1307            tmpdir.clone(),
1308            Arc::new(LanceCache::no_cache()),
1309        ));
1310
1311        let schema = Arc::new(Schema::new(vec![Field::new(
1312            VALUE_COLUMN_NAME,
1313            DataType::Int64,
1314            false,
1315        )]));
1316
1317        // Create multiple fragments with data
1318        // Fragment 0: values 0-99
1319        let fragment0_data = arrow_array::Int64Array::from_iter_values(0..100);
1320        let fragment0_batch =
1321            RecordBatch::try_new(schema.clone(), vec![Arc::new(fragment0_data)]).unwrap();
1322
1323        // Fragment 1: values 100-199
1324        let fragment1_data = arrow_array::Int64Array::from_iter_values(100..200);
1325        let fragment1_batch =
1326            RecordBatch::try_new(schema.clone(), vec![Arc::new(fragment1_data)]).unwrap();
1327
1328        // Create a stream with multiple batches (fragments)
1329        let data_stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
1330            schema.clone(),
1331            stream::iter(vec![
1332                Ok(fragment0_batch.clone()),
1333                Ok(fragment1_batch.clone()),
1334            ]),
1335        ));
1336        let data_stream = add_row_addr(data_stream);
1337
1338        BloomFilterIndexPlugin::train_bloomfilter_index(
1339            data_stream,
1340            test_store.as_ref(),
1341            Some(BloomFilterIndexBuilderParams::new(50, 0.05)), // ~5% false positive rate
1342        )
1343        .await
1344        .unwrap();
1345
1346        // Read the index file back and check its contents
1347        let index = BloomFilterIndex::load(test_store.clone(), None, &LanceCache::no_cache())
1348            .await
1349            .expect("Failed to load BloomFilterIndex");
1350
1351        // Should have 4 zones total (2 zones per fragment)
1352        assert_eq!(index.zones.len(), 4);
1353
1354        // Check fragment 0 zones
1355        assert_eq!(index.zones[0].bound.fragment_id, 0u64);
1356        assert_eq!(index.zones[0].bound.start, 0u64);
1357        assert_eq!(index.zones[0].bound.length, 50);
1358
1359        assert_eq!(index.zones[1].bound.fragment_id, 0u64);
1360        assert_eq!(index.zones[1].bound.start, 50u64);
1361        assert_eq!(index.zones[1].bound.length, 50);
1362
1363        // Check fragment 1 zones
1364        assert_eq!(index.zones[2].bound.fragment_id, 1u64);
1365        assert_eq!(index.zones[2].bound.start, 0u64);
1366        assert_eq!(index.zones[2].bound.length, 50);
1367
1368        assert_eq!(index.zones[3].bound.fragment_id, 1u64);
1369        assert_eq!(index.zones[3].bound.start, 50u64);
1370        assert_eq!(index.zones[3].bound.length, 50);
1371
1372        // Test search functionality
1373        let query = BloomFilterQuery::Equals(ScalarValue::Int64(Some(150)));
1374        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1375
1376        // Should only match fragment 1 blocks since bloom filter correctly filters
1377        // Value 150 is only in fragment 1 (values 100-199), not in fragment 0 (values 0-99)
1378        let mut expected = RowAddrTreeMap::new();
1379        expected.insert_range((1u64 << 32) + 50..((1u64 << 32) + 100)); // Only the block containing 150
1380        assert_eq!(result, SearchResult::at_most(expected));
1381
1382        // Test calculate_included_frags
1383        assert_eq!(
1384            index.calculate_included_frags().await.unwrap(),
1385            RoaringBitmap::from_iter(0..2)
1386        );
1387    }
1388
1389    #[tokio::test]
1390    async fn test_nan_bloomfilter_index() {
1391        let tmpdir = TempObjDir::default();
1392        let test_store = Arc::new(LanceIndexStore::new(
1393            Arc::new(ObjectStore::local()),
1394            tmpdir.clone(),
1395            Arc::new(LanceCache::no_cache()),
1396        ));
1397
1398        // Create deterministic data with NaN values
1399        // Pattern: [1.0, 2.0, NaN, 3.0, 4.0, 5.0, NaN, 6.0, 7.0, 8.0, ...]
1400        let mut values = Vec::new();
1401        for i in 0..500 {
1402            if i % 5 == 2 {
1403                values.push(f32::NAN);
1404            } else {
1405                values.push(i as f32);
1406            }
1407        }
1408
1409        let float_data = arrow_array::Float32Array::from(values);
1410        let schema = Arc::new(Schema::new(vec![Field::new(
1411            VALUE_COLUMN_NAME,
1412            DataType::Float32,
1413            true,
1414        )]));
1415        let data =
1416            RecordBatch::try_new(schema.clone(), vec![Arc::new(float_data.clone())]).unwrap();
1417        let data_stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
1418            schema,
1419            stream::once(std::future::ready(Ok(data))),
1420        ));
1421        let data_stream = add_row_addr(data_stream);
1422
1423        BloomFilterIndexPlugin::train_bloomfilter_index(
1424            data_stream,
1425            test_store.as_ref(),
1426            Some(BloomFilterIndexBuilderParams::new(100, 0.01)), // ~1% false positive rate
1427        )
1428        .await
1429        .unwrap();
1430
1431        // Load the index
1432        let index = BloomFilterIndex::load(test_store.clone(), None, &LanceCache::no_cache())
1433            .await
1434            .expect("Failed to load BloomFilterIndex");
1435
1436        // Should have 5 zones since we have 500 rows and zone size is 100
1437        assert_eq!(index.zones.len(), 5);
1438
1439        // Test search for NaN values using Equals with NaN
1440        let query = BloomFilterQuery::Equals(ScalarValue::Float32(Some(f32::NAN)));
1441        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1442
1443        // Should match all blocks since they all contain NaN values
1444        let mut expected = RowAddrTreeMap::new();
1445        expected.insert_range(0..500); // All rows since NaN is in every block
1446        assert_eq!(result, SearchResult::at_most(expected));
1447
1448        // Test search for a specific finite value that exists in the data
1449        let query = BloomFilterQuery::Equals(ScalarValue::Float32(Some(5.0)));
1450        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1451
1452        // Should match only the first block since 5.0 only exists in rows 0-99
1453        let mut expected = RowAddrTreeMap::new();
1454        expected.insert_range(0..100);
1455        assert_eq!(result, SearchResult::at_most(expected));
1456
1457        // Test search for a value that doesn't exist but is within expected range
1458        let query = BloomFilterQuery::Equals(ScalarValue::Float32(Some(250.0)));
1459        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1460
1461        // Should match the third block since 250.0 would be in that range if it existed
1462        let mut expected = RowAddrTreeMap::new();
1463        expected.insert_range(200..300);
1464        assert_eq!(result, SearchResult::at_most(expected));
1465
1466        // Test search for a value way outside the range
1467        let query = BloomFilterQuery::Equals(ScalarValue::Float32(Some(10000.0)));
1468        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1469
1470        // Should return empty since bloom filter correctly filters out this value
1471        assert_eq!(result, SearchResult::at_most(RowAddrTreeMap::new()));
1472
1473        // Test IsIn query with NaN and finite values
1474        let query = BloomFilterQuery::IsIn(vec![
1475            ScalarValue::Float32(Some(f32::NAN)),
1476            ScalarValue::Float32(Some(5.0)),
1477            ScalarValue::Float32(Some(150.0)), // This value exists in the second block
1478        ]);
1479        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1480
1481        // Should match all blocks since they all contain NaN values
1482        let mut expected = RowAddrTreeMap::new();
1483        expected.insert_range(0..500);
1484        assert_eq!(result, SearchResult::at_most(expected));
1485    }
1486
1487    #[tokio::test]
1488    async fn test_complex_bloomfilter_index() {
1489        let tmpdir = TempObjDir::default();
1490        let test_store = Arc::new(LanceIndexStore::new(
1491            Arc::new(ObjectStore::local()),
1492            tmpdir.clone(),
1493            Arc::new(LanceCache::no_cache()),
1494        ));
1495
1496        // Create data that will produce multiple blocks
1497        let data_size = 10000;
1498        let data = arrow_array::Int64Array::from_iter_values(0..data_size as i64);
1499        let schema = Arc::new(Schema::new(vec![Field::new(
1500            VALUE_COLUMN_NAME,
1501            DataType::Int64,
1502            false,
1503        )]));
1504        let data = RecordBatch::try_new(schema.clone(), vec![Arc::new(data)]).unwrap();
1505        let data_stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
1506            schema,
1507            stream::once(std::future::ready(Ok(data))),
1508        ));
1509        let data_stream = add_row_addr(data_stream);
1510
1511        BloomFilterIndexPlugin::train_bloomfilter_index(
1512            data_stream,
1513            test_store.as_ref(),
1514            Some(BloomFilterIndexBuilderParams::new(1000, 0.001)), // 10 blocks total
1515        )
1516        .await
1517        .unwrap();
1518
1519        // Load the index
1520        let index = BloomFilterIndex::load(test_store.clone(), None, &LanceCache::no_cache())
1521            .await
1522            .expect("Failed to load BloomFilterIndex");
1523
1524        // Should have 10 zones since we have 10000 rows and zone size is 1000
1525        assert_eq!(index.zones.len(), 10);
1526        assert_eq!(index.number_of_items, 1000);
1527        assert_eq!(index.probability, 0.001);
1528
1529        // Verify zone structure
1530        for (i, block) in index.zones.iter().enumerate() {
1531            assert_eq!(block.bound.fragment_id, 0u64);
1532            assert_eq!(block.bound.start, (i * 1000) as u64);
1533            assert_eq!(block.bound.length, 1000);
1534            // Check that the bloom filter has some data (non-zero bytes when serialized)
1535            assert!(!block.bloom_filter.to_bytes().is_empty());
1536        }
1537
1538        // Test search for a value in a specific zone
1539        let query = BloomFilterQuery::Equals(ScalarValue::Int64(Some(2500))); // In zone 2 (2000-2999)
1540        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1541
1542        // Should match zone 2
1543        let mut expected = RowAddrTreeMap::new();
1544        expected.insert_range(2000..3000);
1545        assert_eq!(result, SearchResult::at_most(expected));
1546
1547        // Test search for a value way outside the range
1548        let query = BloomFilterQuery::Equals(ScalarValue::Int64(Some(50000)));
1549        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1550
1551        // Should return empty since bloom filter correctly filters out this value
1552        assert_eq!(result, SearchResult::at_most(RowAddrTreeMap::new()));
1553
1554        // Test IsIn query with values from different zones
1555        let query = BloomFilterQuery::IsIn(vec![
1556            ScalarValue::Int64(Some(500)),   // Zone 0 (0-999)
1557            ScalarValue::Int64(Some(2500)),  // Zone 2 (2000-2999)
1558            ScalarValue::Int64(Some(7500)),  // Zone 7 (7000-7999)
1559            ScalarValue::Int64(Some(50000)), // Not in any zone
1560        ]);
1561        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1562
1563        // Should match zones 0, 2, and 7
1564        let mut expected = RowAddrTreeMap::new();
1565        expected.insert_range(0..1000); // Zone 0
1566        expected.insert_range(2000..3000); // Zone 2
1567        expected.insert_range(7000..8000); // Zone 7
1568        assert_eq!(result, SearchResult::at_most(expected));
1569
1570        // Test calculate_included_frags
1571        assert_eq!(
1572            index.calculate_included_frags().await.unwrap(),
1573            RoaringBitmap::from_iter(0..1)
1574        );
1575    }
1576
1577    #[tokio::test]
1578    async fn test_string_bloomfilter_index() {
1579        let tmpdir = TempObjDir::default();
1580        let test_store = Arc::new(LanceIndexStore::new(
1581            Arc::new(ObjectStore::local()),
1582            tmpdir.clone(),
1583            Arc::new(LanceCache::no_cache()),
1584        ));
1585
1586        // Create string data
1587        let string_values: Vec<String> = (0..200).map(|i| format!("value_{:03}", i)).collect();
1588        let string_data = arrow_array::StringArray::from_iter_values(string_values.iter());
1589        let schema = Arc::new(Schema::new(vec![Field::new(
1590            VALUE_COLUMN_NAME,
1591            DataType::Utf8,
1592            false,
1593        )]));
1594        let data = RecordBatch::try_new(schema.clone(), vec![Arc::new(string_data)]).unwrap();
1595        let data_stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
1596            schema,
1597            stream::once(std::future::ready(Ok(data))),
1598        ));
1599        let data_stream = add_row_addr(data_stream);
1600
1601        BloomFilterIndexPlugin::train_bloomfilter_index(
1602            data_stream,
1603            test_store.as_ref(),
1604            Some(BloomFilterIndexBuilderParams::new(100, 0.01)), // ~1% false positive rate
1605        )
1606        .await
1607        .unwrap();
1608
1609        // Load the index
1610        let index = BloomFilterIndex::load(test_store.clone(), None, &LanceCache::no_cache())
1611            .await
1612            .expect("Failed to load BloomFilterIndex");
1613
1614        // Should have 2 zones since we have 200 rows and zone size is 100
1615        assert_eq!(index.zones.len(), 2);
1616
1617        // Test search for a value in the first zone
1618        let query = BloomFilterQuery::Equals(ScalarValue::Utf8(Some("value_050".to_string())));
1619        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1620
1621        // Should match the first zone
1622        let mut expected = RowAddrTreeMap::new();
1623        expected.insert_range(0..100);
1624        assert_eq!(result, SearchResult::at_most(expected));
1625
1626        // Test search for a value in the second zone
1627        let query = BloomFilterQuery::Equals(ScalarValue::Utf8(Some("value_150".to_string())));
1628        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1629
1630        // Should match the second zone
1631        let mut expected = RowAddrTreeMap::new();
1632        expected.insert_range(100..200);
1633        assert_eq!(result, SearchResult::at_most(expected));
1634
1635        // Test search for a value that doesn't exist
1636        let query =
1637            BloomFilterQuery::Equals(ScalarValue::Utf8(Some("nonexistent_value".to_string())));
1638        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1639
1640        // Should return empty since bloom filter correctly filters out this value
1641        assert_eq!(result, SearchResult::at_most(RowAddrTreeMap::new()));
1642
1643        // Test IsIn query with string values
1644        let query = BloomFilterQuery::IsIn(vec![
1645            ScalarValue::Utf8(Some("value_025".to_string())), // First zone
1646            ScalarValue::Utf8(Some("value_175".to_string())), // Second zone
1647            ScalarValue::Utf8(Some("nonexistent".to_string())), // Not present
1648        ]);
1649        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1650
1651        // Should match both zones
1652        let mut expected = RowAddrTreeMap::new();
1653        expected.insert_range(0..200);
1654        assert_eq!(result, SearchResult::at_most(expected));
1655    }
1656
1657    #[tokio::test]
1658    async fn test_binary_bloomfilter_index() {
1659        let tmpdir = TempObjDir::default();
1660        let test_store = Arc::new(LanceIndexStore::new(
1661            Arc::new(ObjectStore::local()),
1662            tmpdir.clone(),
1663            Arc::new(LanceCache::no_cache()),
1664        ));
1665
1666        // Create binary data
1667        let binary_values: Vec<Vec<u8>> = (0..100)
1668            .map(|i| vec![i as u8, (i + 1) as u8, (i + 2) as u8])
1669            .collect();
1670        let binary_data = arrow_array::BinaryArray::from_iter_values(binary_values.iter());
1671        let schema = Arc::new(Schema::new(vec![Field::new(
1672            VALUE_COLUMN_NAME,
1673            DataType::Binary,
1674            false,
1675        )]));
1676        let data = RecordBatch::try_new(schema.clone(), vec![Arc::new(binary_data)]).unwrap();
1677        let data_stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
1678            schema,
1679            stream::once(std::future::ready(Ok(data))),
1680        ));
1681        let data_stream = add_row_addr(data_stream);
1682
1683        BloomFilterIndexPlugin::train_bloomfilter_index(
1684            data_stream,
1685            test_store.as_ref(),
1686            Some(BloomFilterIndexBuilderParams::new(50, 0.05)),
1687        )
1688        .await
1689        .unwrap();
1690
1691        // Load the index
1692        let index = BloomFilterIndex::load(test_store.clone(), None, &LanceCache::no_cache())
1693            .await
1694            .expect("Failed to load BloomFilterIndex");
1695
1696        // Should have 2 zones since we have 100 rows and zone size is 50
1697        assert_eq!(index.zones.len(), 2);
1698
1699        // Test search for a value in the first zone
1700        let query = BloomFilterQuery::Equals(ScalarValue::Binary(Some(vec![25, 26, 27])));
1701        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1702
1703        // Should match the first zone
1704        let mut expected = RowAddrTreeMap::new();
1705        expected.insert_range(0..50);
1706        assert_eq!(result, SearchResult::at_most(expected));
1707
1708        // Test search for a value in the second zone
1709        let query = BloomFilterQuery::Equals(ScalarValue::Binary(Some(vec![75, 76, 77])));
1710        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1711
1712        // Should match the second zone
1713        let mut expected = RowAddrTreeMap::new();
1714        expected.insert_range(50..100);
1715        assert_eq!(result, SearchResult::at_most(expected));
1716
1717        // Test search for a value that doesn't exist
1718        let query = BloomFilterQuery::Equals(ScalarValue::Binary(Some(vec![255, 254, 253])));
1719        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1720
1721        // Should return empty since bloom filter correctly filters out this value
1722        assert_eq!(result, SearchResult::at_most(RowAddrTreeMap::new()));
1723    }
1724
1725    #[tokio::test]
1726    async fn test_large_data_types_bloomfilter_index() {
1727        let tmpdir = TempObjDir::default();
1728        let test_store = Arc::new(LanceIndexStore::new(
1729            Arc::new(ObjectStore::local()),
1730            tmpdir.clone(),
1731            Arc::new(LanceCache::no_cache()),
1732        ));
1733
1734        // Test LargeUtf8 data type
1735        let large_string_values: Vec<String> =
1736            (0..100).map(|i| format!("large_value_{:05}", i)).collect();
1737        let large_string_data =
1738            arrow_array::LargeStringArray::from_iter_values(large_string_values.iter());
1739        let schema = Arc::new(Schema::new(vec![Field::new(
1740            VALUE_COLUMN_NAME,
1741            DataType::LargeUtf8,
1742            false,
1743        )]));
1744        let data = RecordBatch::try_new(schema.clone(), vec![Arc::new(large_string_data)]).unwrap();
1745        let data_stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
1746            schema,
1747            stream::once(std::future::ready(Ok(data))),
1748        ));
1749        let data_stream = add_row_addr(data_stream);
1750
1751        BloomFilterIndexPlugin::train_bloomfilter_index(
1752            data_stream,
1753            test_store.as_ref(),
1754            Some(BloomFilterIndexBuilderParams::new(50, 0.05)),
1755        )
1756        .await
1757        .unwrap();
1758
1759        // Load the index
1760        let index = BloomFilterIndex::load(test_store.clone(), None, &LanceCache::no_cache())
1761            .await
1762            .expect("Failed to load BloomFilterIndex");
1763
1764        assert_eq!(index.zones.len(), 2);
1765
1766        // Test search functionality
1767        let query = BloomFilterQuery::Equals(ScalarValue::LargeUtf8(Some(
1768            "large_value_00025".to_string(),
1769        )));
1770        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1771
1772        // Should match the first zone
1773        let mut expected = RowAddrTreeMap::new();
1774        expected.insert_range(0..50);
1775        assert_eq!(result, SearchResult::at_most(expected));
1776
1777        // Test search for a value that doesn't exist
1778        let query = BloomFilterQuery::Equals(ScalarValue::LargeUtf8(Some(
1779            "nonexistent_large_value".to_string(),
1780        )));
1781        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1782
1783        // Should return empty since bloom filter correctly filters out this value
1784        assert_eq!(result, SearchResult::at_most(RowAddrTreeMap::new()));
1785    }
1786
1787    #[tokio::test]
1788    async fn test_timestamp_bloomfilter_index() {
1789        let tmpdir = TempObjDir::default();
1790        let test_store = Arc::new(LanceIndexStore::new(
1791            Arc::new(ObjectStore::local()),
1792            tmpdir.clone(),
1793            Arc::new(LanceCache::no_cache()),
1794        ));
1795
1796        // Test Date32 (days since Unix epoch)
1797        let date32_values: Vec<i32> = (0..100).collect(); // Days since Unix epoch
1798        let date32_data = arrow_array::Date32Array::from(date32_values.clone());
1799        let schema = Arc::new(Schema::new(vec![Field::new(
1800            VALUE_COLUMN_NAME,
1801            DataType::Date32,
1802            false,
1803        )]));
1804        let data = RecordBatch::try_new(schema.clone(), vec![Arc::new(date32_data)]).unwrap();
1805        let data_stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
1806            schema,
1807            stream::once(std::future::ready(Ok(data))),
1808        ));
1809        let data_stream = add_row_addr(data_stream);
1810
1811        BloomFilterIndexPlugin::train_bloomfilter_index(
1812            data_stream,
1813            test_store.as_ref(),
1814            Some(BloomFilterIndexBuilderParams::new(50, 0.01)),
1815        )
1816        .await
1817        .unwrap();
1818
1819        // Load the Date32 index
1820        let index = BloomFilterIndex::load(test_store.clone(), None, &LanceCache::no_cache())
1821            .await
1822            .expect("Failed to load Date32 BloomFilterIndex");
1823
1824        assert_eq!(index.zones.len(), 2); // 100 rows, zone size 50
1825
1826        // Test search for Date32 value in first zone
1827        let query = BloomFilterQuery::Equals(ScalarValue::Date32(Some(25)));
1828        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1829        let mut expected = RowAddrTreeMap::new();
1830        expected.insert_range(0..50);
1831        assert_eq!(result, SearchResult::at_most(expected));
1832
1833        // Test search for Date32 value in second zone
1834        let query = BloomFilterQuery::Equals(ScalarValue::Date32(Some(75)));
1835        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1836        let mut expected = RowAddrTreeMap::new();
1837        expected.insert_range(50..100);
1838        assert_eq!(result, SearchResult::at_most(expected));
1839
1840        // Test search for Date32 value that doesn't exist
1841        let query = BloomFilterQuery::Equals(ScalarValue::Date32(Some(500)));
1842        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1843        assert_eq!(result, SearchResult::at_most(RowAddrTreeMap::new()));
1844    }
1845
1846    #[tokio::test]
1847    async fn test_timestamp_types_bloomfilter_index() {
1848        let tmpdir = TempObjDir::default();
1849        let test_store = Arc::new(LanceIndexStore::new(
1850            Arc::new(ObjectStore::local()),
1851            tmpdir.clone(),
1852            Arc::new(LanceCache::no_cache()),
1853        ));
1854
1855        // Test Timestamp with nanosecond precision - use simple incrementing values
1856        let timestamp_values: Vec<i64> = (0..100).map(|i| 1_000_000_000i64 + (i as i64)).collect();
1857
1858        let timestamp_data = arrow_array::TimestampNanosecondArray::from(timestamp_values.clone());
1859        let schema = Arc::new(Schema::new(vec![Field::new(
1860            VALUE_COLUMN_NAME,
1861            DataType::Timestamp(arrow_schema::TimeUnit::Nanosecond, None),
1862            false,
1863        )]));
1864        let data = RecordBatch::try_new(schema.clone(), vec![Arc::new(timestamp_data)]).unwrap();
1865        let data_stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
1866            schema,
1867            stream::once(std::future::ready(Ok(data))),
1868        ));
1869        let data_stream = add_row_addr(data_stream);
1870
1871        BloomFilterIndexPlugin::train_bloomfilter_index(
1872            data_stream,
1873            test_store.as_ref(),
1874            Some(BloomFilterIndexBuilderParams::new(50, 0.01)),
1875        )
1876        .await
1877        .unwrap();
1878
1879        // Load the Timestamp index
1880        let index = BloomFilterIndex::load(test_store.clone(), None, &LanceCache::no_cache())
1881            .await
1882            .expect("Failed to load Timestamp BloomFilterIndex");
1883
1884        assert_eq!(index.zones.len(), 2); // 100 rows, zone size 50
1885
1886        // Test search for Timestamp value in first zone
1887        let first_timestamp = timestamp_values[25];
1888        let query = BloomFilterQuery::Equals(ScalarValue::TimestampNanosecond(
1889            Some(first_timestamp),
1890            None,
1891        ));
1892        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1893        let mut expected = RowAddrTreeMap::new();
1894        expected.insert_range(0..50);
1895        assert_eq!(result, SearchResult::at_most(expected));
1896
1897        // Test search for Timestamp value in second zone
1898        let second_timestamp = timestamp_values[75];
1899        let query = BloomFilterQuery::Equals(ScalarValue::TimestampNanosecond(
1900            Some(second_timestamp),
1901            None,
1902        ));
1903        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1904        let mut expected = RowAddrTreeMap::new();
1905        expected.insert_range(50..100);
1906        assert_eq!(result, SearchResult::at_most(expected));
1907
1908        // Test search for Timestamp value that doesn't exist
1909        let query =
1910            BloomFilterQuery::Equals(ScalarValue::TimestampNanosecond(Some(999_999_999i64), None));
1911        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1912        assert_eq!(result, SearchResult::at_most(RowAddrTreeMap::new()));
1913
1914        // Test IsIn query with multiple timestamp values
1915        let query = BloomFilterQuery::IsIn(vec![
1916            ScalarValue::TimestampNanosecond(Some(timestamp_values[10]), None), // First zone
1917            ScalarValue::TimestampNanosecond(Some(timestamp_values[85]), None), // Second zone
1918            ScalarValue::TimestampNanosecond(Some(999_999_999i64), None),       // Not present
1919        ]);
1920        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1921        let mut expected = RowAddrTreeMap::new();
1922        expected.insert_range(0..100); // Should match both zones
1923        assert_eq!(result, SearchResult::at_most(expected));
1924    }
1925
1926    #[tokio::test]
1927    async fn test_time_types_bloomfilter_index() {
1928        let tmpdir = TempObjDir::default();
1929        let test_store = Arc::new(LanceIndexStore::new(
1930            Arc::new(ObjectStore::local()),
1931            tmpdir.clone(),
1932            Arc::new(LanceCache::no_cache()),
1933        ));
1934
1935        // Test Time64 with microsecond precision (stored as i64)
1936        let time_values: Vec<i64> = (0..100)
1937            .map(|i| (i as i64) * 3_600_000_000) // Hours in microseconds
1938            .collect();
1939
1940        let time_data = arrow_array::Time64MicrosecondArray::from(time_values.clone());
1941        let schema = Arc::new(Schema::new(vec![Field::new(
1942            VALUE_COLUMN_NAME,
1943            DataType::Time64(arrow_schema::TimeUnit::Microsecond),
1944            false,
1945        )]));
1946        let data = RecordBatch::try_new(schema.clone(), vec![Arc::new(time_data)]).unwrap();
1947        let data_stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
1948            schema,
1949            stream::once(std::future::ready(Ok(data))),
1950        ));
1951        let data_stream = add_row_addr(data_stream);
1952
1953        BloomFilterIndexPlugin::train_bloomfilter_index(
1954            data_stream,
1955            test_store.as_ref(),
1956            Some(BloomFilterIndexBuilderParams::new(25, 0.05)),
1957        )
1958        .await
1959        .unwrap();
1960
1961        // Load the Time64 index
1962        let index = BloomFilterIndex::load(test_store.clone(), None, &LanceCache::no_cache())
1963            .await
1964            .expect("Failed to load Time64 BloomFilterIndex");
1965
1966        assert_eq!(index.zones.len(), 4); // 100 rows, zone size 25
1967
1968        // Test search for Time64 value in first zone
1969        let first_time = time_values[10];
1970        let query = BloomFilterQuery::Equals(ScalarValue::Time64Microsecond(Some(first_time)));
1971        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1972        let mut expected = RowAddrTreeMap::new();
1973        expected.insert_range(0..25);
1974        assert_eq!(result, SearchResult::at_most(expected));
1975
1976        // Test search for Time64 value that doesn't exist
1977        let query = BloomFilterQuery::Equals(ScalarValue::Time64Microsecond(Some(999_999_999i64)));
1978        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1979        assert_eq!(result, SearchResult::at_most(RowAddrTreeMap::new()));
1980    }
1981
1982    #[tokio::test]
1983    async fn test_bloomfilter_supported_operations() {
1984        let tmpdir = TempObjDir::default();
1985        let test_store = Arc::new(LanceIndexStore::new(
1986            Arc::new(ObjectStore::local()),
1987            tmpdir.clone(),
1988            Arc::new(LanceCache::no_cache()),
1989        ));
1990
1991        let data = arrow_array::Int32Array::from_iter_values(0..1000);
1992        let schema = Arc::new(Schema::new(vec![Field::new(
1993            VALUE_COLUMN_NAME,
1994            DataType::Int32,
1995            false,
1996        )]));
1997        let data = RecordBatch::try_new(schema.clone(), vec![Arc::new(data)]).unwrap();
1998        let data_stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
1999            schema,
2000            stream::once(std::future::ready(Ok(data))),
2001        ));
2002        let data_stream = add_row_addr(data_stream);
2003
2004        BloomFilterIndexPlugin::train_bloomfilter_index(
2005            data_stream,
2006            test_store.as_ref(),
2007            Some(BloomFilterIndexBuilderParams::new(250, 0.01)), // 4 zones total
2008        )
2009        .await
2010        .unwrap();
2011
2012        // Load the index
2013        let index = BloomFilterIndex::load(test_store.clone(), None, &LanceCache::no_cache())
2014            .await
2015            .expect("Failed to load BloomFilterIndex");
2016
2017        assert_eq!(index.zones.len(), 4);
2018
2019        // Test that bloom filters support the operations they are designed for
2020        // Test a specific equality query
2021        let query = BloomFilterQuery::Equals(ScalarValue::Int32(Some(500)));
2022        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2023        let mut expected = RowAddrTreeMap::new();
2024        expected.insert_range(500..750); // Should match the zone containing 500
2025        assert_eq!(result, SearchResult::at_most(expected));
2026
2027        // Test IsNull query
2028        let query = BloomFilterQuery::IsNull();
2029        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2030        assert_eq!(result, SearchResult::at_most(RowAddrTreeMap::new())); // No nulls in the data
2031
2032        // Test IsIn query
2033        let query = BloomFilterQuery::IsIn(vec![
2034            ScalarValue::Int32(Some(100)),
2035            ScalarValue::Int32(Some(600)),
2036        ]);
2037        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2038        let mut expected = RowAddrTreeMap::new();
2039        expected.insert_range(0..250); // Zone containing 100
2040        expected.insert_range(500..750); // Zone containing 600
2041        assert_eq!(result, SearchResult::at_most(expected));
2042    }
2043
2044    #[tokio::test]
2045    async fn test_bloomfilter_null_handling_in_queries() {
2046        // Test that bloomfilter index correctly returns null_list for queries
2047        let tmpdir = TempObjDir::default();
2048        let store = Arc::new(LanceIndexStore::new(
2049            Arc::new(ObjectStore::local()),
2050            tmpdir.clone(),
2051            Arc::new(LanceCache::no_cache()),
2052        ));
2053
2054        // Create test data: [0, 5, null]
2055        let batch = record_batch!(
2056            (VALUE_COLUMN_NAME, Int64, [Some(0), Some(5), None]),
2057            (ROW_ADDR, UInt64, [0, 1, 2])
2058        )
2059        .unwrap();
2060        let schema = batch.schema();
2061        let stream = stream::once(async move { Ok(batch) });
2062        let stream = Box::pin(RecordBatchStreamAdapter::new(schema, stream));
2063
2064        // Train and write the bloomfilter index
2065        BloomFilterIndexPlugin::train_bloomfilter_index(stream, store.as_ref(), None)
2066            .await
2067            .unwrap();
2068
2069        let cache = LanceCache::with_capacity(1024 * 1024);
2070        let index = BloomFilterIndex::load(store.clone(), None, &cache)
2071            .await
2072            .unwrap();
2073
2074        // Test 1: Search for value 5 - bloomfilter should return at_most with all rows
2075        // Like ZoneMap, BloomFilter returns AtMost (superset) and includes nulls
2076        let query = BloomFilterQuery::Equals(ScalarValue::Int64(Some(5)));
2077        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2078
2079        match result {
2080            SearchResult::AtMost(row_addrs) => {
2081                // Bloomfilter returns all rows in the zone including nulls
2082                let all_rows: Vec<u64> = row_addrs
2083                    .true_rows()
2084                    .row_addrs()
2085                    .unwrap()
2086                    .map(u64::from)
2087                    .collect();
2088                assert_eq!(
2089                    all_rows,
2090                    vec![0, 1, 2],
2091                    "Should return all rows (including nulls) since BloomFilter is inexact"
2092                );
2093
2094                // For AtMost results, nulls are included in the superset
2095            }
2096            _ => panic!("Expected AtMost search result from bloomfilter"),
2097        }
2098
2099        // Test 2: IsIn query - should also return all rows
2100        let query = BloomFilterQuery::IsIn(vec![
2101            ScalarValue::Int64(Some(0)),
2102            ScalarValue::Int64(Some(10)),
2103        ]);
2104        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2105
2106        match result {
2107            SearchResult::AtMost(row_addrs) => {
2108                let all_rows: Vec<u64> = row_addrs
2109                    .true_rows()
2110                    .row_addrs()
2111                    .unwrap()
2112                    .map(u64::from)
2113                    .collect();
2114                assert_eq!(
2115                    all_rows,
2116                    vec![0, 1, 2],
2117                    "Should return all rows in zone as possible matches"
2118                );
2119            }
2120            _ => panic!("Expected AtMost search result from bloomfilter"),
2121        }
2122    }
2123}