This is an automated email from the ASF dual-hosted git repository.

tustvold pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/master by this push:
     new fa1f6112f Set bloom filter on byte array (#3284)
fa1f6112f is described below

commit fa1f6112f091d7493f8911a47a893048ce0e4e02
Author: Liang-Chi Hsieh <[email protected]>
AuthorDate: Thu Dec 8 08:50:24 2022 -0800

    Set bloom filter on byte array (#3284)
    
    * Set bloom filter on byte array
    
    * Check positive values
    
    * For review
    
    * Clippy
    
    Co-authored-by: Raphael Taylor-Davies <[email protected]>
---
 parquet/src/arrow/arrow_writer/byte_array.rs |  18 +++-
 parquet/src/arrow/arrow_writer/mod.rs        | 119 +++++++++++++++++++++++++--
 parquet/src/bloom_filter/mod.rs              |   2 +-
 3 files changed, 131 insertions(+), 8 deletions(-)

diff --git a/parquet/src/arrow/arrow_writer/byte_array.rs 
b/parquet/src/arrow/arrow_writer/byte_array.rs
index d870ac54f..c3a9f83d1 100644
--- a/parquet/src/arrow/arrow_writer/byte_array.rs
+++ b/parquet/src/arrow/arrow_writer/byte_array.rs
@@ -429,6 +429,7 @@ struct ByteArrayEncoder {
     dict_encoder: Option<DictEncoder>,
     min_value: Option<ByteArray>,
     max_value: Option<ByteArray>,
+    bloom_filter: Option<Sbbf>,
 }
 
 impl ColumnValueEncoder for ByteArrayEncoder {
@@ -453,8 +454,7 @@ impl ColumnValueEncoder for ByteArrayEncoder {
     }
 
     fn flush_bloom_filter(&mut self) -> Option<Sbbf> {
-        // TODO FIX ME need to handle bloom filter in arrow writer
-        None
+        self.bloom_filter.take()
     }
 
     fn try_new(descr: &ColumnDescPtr, props: &WriterProperties) -> Result<Self>
@@ -467,11 +467,17 @@ impl ColumnValueEncoder for ByteArrayEncoder {
 
         let fallback = FallbackEncoder::new(descr, props)?;
 
+        let bloom_filter = props
+            .bloom_filter_properties(descr.path())
+            .map(|props| Sbbf::new_with_ndv_fpp(props.ndv, props.fpp))
+            .transpose()?;
+
         Ok(Self {
             fallback,
             dict_encoder: dictionary,
             min_value: None,
             max_value: None,
+            bloom_filter,
         })
     }
 
@@ -555,6 +561,14 @@ where
         }
     }
 
+    // encode the values into bloom filter if enabled
+    if let Some(bloom_filter) = &mut encoder.bloom_filter {
+        let valid = indices.iter().cloned();
+        for idx in valid {
+            bloom_filter.insert(values.value(idx).as_ref());
+        }
+    }
+
     match &mut encoder.dict_encoder {
         Some(dict_encoder) => dict_encoder.encode(values, indices),
         None => encoder.fallback.encode(values, indices),
diff --git a/parquet/src/arrow/arrow_writer/mod.rs 
b/parquet/src/arrow/arrow_writer/mod.rs
index ecb59e93e..a609b992a 100644
--- a/parquet/src/arrow/arrow_writer/mod.rs
+++ b/parquet/src/arrow/arrow_writer/mod.rs
@@ -622,7 +622,8 @@ mod tests {
     use crate::basic::Encoding;
     use crate::file::metadata::ParquetMetaData;
     use crate::file::page_index::index_reader::read_pages_locations;
-    use crate::file::properties::WriterVersion;
+    use crate::file::properties::{ReaderProperties, WriterVersion};
+    use crate::file::serialized_reader::ReadOptionsBuilder;
     use crate::file::{
         reader::{FileReader, SerializedFileReader},
         statistics::Statistics,
@@ -1269,6 +1270,7 @@ mod tests {
                             .set_dictionary_enabled(dictionary_size != 0)
                             
.set_dictionary_pagesize_limit(dictionary_size.max(1))
                             .set_encoding(*encoding)
+                            .set_bloom_filter_enabled(true)
                             .build();
 
                         files.push(roundtrip_opts(&expected_batch, props))
@@ -1279,17 +1281,17 @@ mod tests {
         files
     }
 
-    fn values_required<A, I>(iter: I)
+    fn values_required<A, I>(iter: I) -> Vec<File>
     where
         A: From<Vec<I::Item>> + Array + 'static,
         I: IntoIterator,
     {
         let raw_values: Vec<_> = iter.into_iter().collect();
         let values = Arc::new(A::from(raw_values));
-        one_column_roundtrip(values, false);
+        one_column_roundtrip(values, false)
     }
 
-    fn values_optional<A, I>(iter: I)
+    fn values_optional<A, I>(iter: I) -> Vec<File>
     where
         A: From<Vec<Option<I::Item>>> + Array + 'static,
         I: IntoIterator,
@@ -1300,7 +1302,7 @@ mod tests {
             .map(|(i, v)| if i % 2 == 0 { None } else { Some(v) })
             .collect();
         let optional_values = Arc::new(A::from(optional_raw_values));
-        one_column_roundtrip(optional_values, true);
+        one_column_roundtrip(optional_values, true)
     }
 
     fn required_and_optional<A, I>(iter: I)
@@ -1312,6 +1314,70 @@ mod tests {
         values_optional::<A, I>(iter);
     }
 
+    fn check_bloom_filter<T: AsBytes>(
+        files: Vec<File>,
+        file_column: String,
+        positive_values: Vec<T>,
+        negative_values: Vec<T>,
+    ) {
+        files.into_iter().take(1).for_each(|file| {
+            let file_reader = SerializedFileReader::new_with_options(
+                file,
+                ReadOptionsBuilder::new()
+                    .with_reader_properties(
+                        ReaderProperties::builder()
+                            .set_read_bloom_filter(true)
+                            .build(),
+                    )
+                    .build(),
+            )
+            .expect("Unable to open file as Parquet");
+            let metadata = file_reader.metadata();
+
+            // Gets bloom filters from all row groups.
+            let mut bloom_filters: Vec<_> = vec![];
+            for (ri, row_group) in metadata.row_groups().iter().enumerate() {
+                if let Some((column_index, _)) = row_group
+                    .columns()
+                    .iter()
+                    .enumerate()
+                    .find(|(_, column)| column.column_path().string() == 
file_column)
+                {
+                    let row_group_reader = file_reader
+                        .get_row_group(ri)
+                        .expect("Unable to read row group");
+                    if let Some(sbbf) =
+                        row_group_reader.get_column_bloom_filter(column_index)
+                    {
+                        bloom_filters.push(sbbf.clone());
+                    } else {
+                        panic!("No bloom filter for column named {} found", 
file_column);
+                    }
+                } else {
+                    panic!("No column named {} found", file_column);
+                }
+            }
+
+            positive_values.iter().for_each(|value| {
+                let found = bloom_filters.iter().find(|sbbf| 
sbbf.check(value));
+                assert!(
+                    found.is_some(),
+                    "{}",
+                    format!("Value {:?} should be in bloom filter", 
value.as_bytes())
+                );
+            });
+
+            negative_values.iter().for_each(|value| {
+                let found = bloom_filters.iter().find(|sbbf| 
sbbf.check(value));
+                assert!(
+                    found.is_none(),
+                    "{}",
+                    format!("Value {:?} should not be in bloom filter", 
value.as_bytes())
+                );
+            });
+        });
+    }
+
     #[test]
     fn all_null_primitive_single_column() {
         let values = Arc::new(Int32Array::from(vec![None; SMALL_SIZE]));
@@ -1528,6 +1594,49 @@ mod tests {
         values_required::<BinaryArray, _>(many_vecs_iter);
     }
 
+    #[test]
+    fn i32_column_bloom_filter() {
+        let positive_values: Vec<i32> = (0..SMALL_SIZE as i32).collect();
+        let files = values_required::<Int32Array, _>(positive_values);
+        check_bloom_filter(
+            files,
+            "col".to_string(),
+            (0..SMALL_SIZE as i32).collect(),
+            (SMALL_SIZE as i32 + 1..SMALL_SIZE as i32 + 10).collect(),
+        );
+    }
+
+    #[test]
+    fn binary_column_bloom_filter() {
+        let one_vec: Vec<u8> = (0..SMALL_SIZE as u8).collect();
+        let many_vecs: Vec<_> = 
std::iter::repeat(one_vec).take(SMALL_SIZE).collect();
+        let many_vecs_iter = many_vecs.iter().map(|v| v.as_slice());
+
+        let files = values_required::<BinaryArray, _>(many_vecs_iter);
+        check_bloom_filter(
+            files,
+            "col".to_string(),
+            many_vecs,
+            vec![vec![(SMALL_SIZE + 1) as u8]],
+        );
+    }
+
+    #[test]
+    fn empty_string_null_column_bloom_filter() {
+        let raw_values: Vec<_> = (0..SMALL_SIZE).map(|i| 
i.to_string()).collect();
+        let raw_strs = raw_values.iter().map(|s| s.as_str());
+
+        let files = values_optional::<StringArray, _>(raw_strs);
+
+        let optional_raw_values: Vec<_> = raw_values
+            .iter()
+            .enumerate()
+            .filter_map(|(i, v)| if i % 2 == 0 { None } else { 
Some(v.as_str()) })
+            .collect();
+        // For null slots, empty string should not be in bloom filter.
+        check_bloom_filter(files, "col".to_string(), optional_raw_values, 
vec![""]);
+    }
+
     #[test]
     fn large_binary_single_column() {
         let one_vec: Vec<u8> = (0..SMALL_SIZE as u8).collect();
diff --git a/parquet/src/bloom_filter/mod.rs b/parquet/src/bloom_filter/mod.rs
index 15c38cf59..5bb89bf3f 100644
--- a/parquet/src/bloom_filter/mod.rs
+++ b/parquet/src/bloom_filter/mod.rs
@@ -261,7 +261,7 @@ impl Sbbf {
     }
 
     /// Insert an [AsBytes] value into the filter
-    pub fn insert<T: AsBytes>(&mut self, value: &T) {
+    pub fn insert<T: AsBytes + ?Sized>(&mut self, value: &T) {
         self.insert_hash(hash_as_bytes(value));
     }
 

Reply via email to