From bea99c91f93b5b8f3374122a80851d05b815df4b Mon Sep 17 00:00:00 2001 From: Mahadevuni Naveen Kumar Date: Thu, 6 Mar 2025 16:25:55 +0530 Subject: [PATCH] Add parquet bloom filter read support for int,bigint,string columns --- velox/connectors/hive/HiveConfig.cpp | 7 + velox/connectors/hive/HiveConfig.h | 9 + velox/connectors/hive/HiveConnectorUtil.cpp | 5 + .../connectors/hive/tests/HiveConfigTest.cpp | 4 +- velox/dwio/common/Options.h | 10 + velox/dwio/parquet/common/BloomFilter.h | 83 ++--- velox/dwio/parquet/common/CMakeLists.txt | 3 +- velox/dwio/parquet/common/Hasher.h | 38 +- .../dwio/parquet/common/ParquetBloomFilter.h | 47 +++ velox/dwio/parquet/common/XxHasher.cpp | 36 +- velox/dwio/parquet/common/XxHasher.h | 24 +- velox/dwio/parquet/reader/Metadata.cpp | 14 + velox/dwio/parquet/reader/Metadata.h | 8 + velox/dwio/parquet/reader/ParquetData.cpp | 129 ++++++- velox/dwio/parquet/reader/ParquetData.h | 32 +- velox/dwio/parquet/reader/ParquetReader.cpp | 27 +- velox/dwio/parquet/reader/ParquetReader.h | 6 + velox/dwio/parquet/tests/ParquetTestBase.h | 64 ++-- velox/dwio/parquet/tests/ParquetTpchTest.cpp | 8 +- ...int64_string_int32_bloom_1k.snappy.parquet | Bin 0 -> 59361 bytes .../parquet/tests/reader/BloomFilterTest.cpp | 31 +- .../tests/reader/ParquetReaderTest.cpp | 349 +++++++++++++++++- velox/type/Filter.cpp | 30 ++ velox/type/Filter.h | 119 ++++++ 24 files changed, 936 insertions(+), 147 deletions(-) create mode 100644 velox/dwio/parquet/common/ParquetBloomFilter.h create mode 100644 velox/dwio/parquet/tests/examples/sample_int64_string_int32_bloom_1k.snappy.parquet diff --git a/velox/connectors/hive/HiveConfig.cpp b/velox/connectors/hive/HiveConfig.cpp index 74b5872aa404..70fe15fe0874 100644 --- a/velox/connectors/hive/HiveConfig.cpp +++ b/velox/connectors/hive/HiveConfig.cpp @@ -107,6 +107,13 @@ bool HiveConfig::isFileColumnNamesReadAsLowerCase( config_->get(kFileColumnNamesReadAsLowerCase, false)); } +bool HiveConfig::isParquetReadBloomFilter( + const config::ConfigBase* session) const { + return session->get( + kParquetReadBloomFilterSession, + config_->get(kParquetReadBloomFilter, false)); +} + bool HiveConfig::isPartitionPathAsLowerCase( const config::ConfigBase* session) const { return session->get(kPartitionPathAsLowerCaseSession, true); diff --git a/velox/connectors/hive/HiveConfig.h b/velox/connectors/hive/HiveConfig.h index 38becf08415d..996b9fc7de7b 100644 --- a/velox/connectors/hive/HiveConfig.h +++ b/velox/connectors/hive/HiveConfig.h @@ -80,6 +80,13 @@ class HiveConfig { static constexpr const char* kParquetUseColumnNamesSession = "parquet_use_column_names"; + // Read bloom filters from parquet files to filter row groups. + static constexpr const char* kParquetReadBloomFilter = + "hive.parquet.read-bloom-filter"; + + static constexpr const char* kParquetReadBloomFilterSession = + "hive_parquet_read_bloom_filter"; + /// Reads the source file column name as lower case. static constexpr const char* kFileColumnNamesReadAsLowerCase = "file-column-names-read-as-lower-case"; @@ -199,6 +206,8 @@ class HiveConfig { bool isFileColumnNamesReadAsLowerCase( const config::ConfigBase* session) const; + bool isParquetReadBloomFilter(const config::ConfigBase* session) const; + bool isPartitionPathAsLowerCase(const config::ConfigBase* session) const; bool allowNullPartitionKeys(const config::ConfigBase* session) const; diff --git a/velox/connectors/hive/HiveConnectorUtil.cpp b/velox/connectors/hive/HiveConnectorUtil.cpp index 1ccc4adb9c36..113443eefd7a 100644 --- a/velox/connectors/hive/HiveConnectorUtil.cpp +++ b/velox/connectors/hive/HiveConnectorUtil.cpp @@ -603,6 +603,11 @@ void configureReaderOptions( readerOptions.setFileFormat(hiveSplit->fileFormat); } + + if (readerOptions.fileFormat() == dwio::common::FileFormat::PARQUET) { + readerOptions.setReadBloomFilter( + hiveConfig->isParquetReadBloomFilter(sessionProperties)); + } } void configureRowReaderOptions( diff --git a/velox/connectors/hive/tests/HiveConfigTest.cpp b/velox/connectors/hive/tests/HiveConfigTest.cpp index 3522eec64abb..deea5ab518e4 100644 --- a/velox/connectors/hive/tests/HiveConfigTest.cpp +++ b/velox/connectors/hive/tests/HiveConfigTest.cpp @@ -37,7 +37,7 @@ TEST(HiveConfigTest, defaultConfig) { ASSERT_EQ(hiveConfig.gcsCredentialsPath(), ""); ASSERT_FALSE(hiveConfig.isOrcUseColumnNames(emptySession.get())); ASSERT_FALSE(hiveConfig.isFileColumnNamesReadAsLowerCase(emptySession.get())); - + ASSERT_FALSE(hiveConfig.isParquetReadBloomFilter(emptySession.get())); ASSERT_EQ(hiveConfig.maxCoalescedBytes(emptySession.get()), 128 << 20); ASSERT_EQ( hiveConfig.maxCoalescedDistanceBytes(emptySession.get()), 512 << 10); @@ -64,6 +64,7 @@ TEST(HiveConfigTest, overrideConfig) { {HiveConfig::kGcsCredentialsPath, "hey"}, {HiveConfig::kOrcUseColumnNames, "true"}, {HiveConfig::kFileColumnNamesReadAsLowerCase, "true"}, + {HiveConfig::kParquetReadBloomFilter, "true"}, {HiveConfig::kAllowNullPartitionKeys, "false"}, {HiveConfig::kMaxCoalescedBytes, "100"}, {HiveConfig::kMaxCoalescedDistance, "100kB"}, @@ -92,6 +93,7 @@ TEST(HiveConfigTest, overrideConfig) { ASSERT_EQ(hiveConfig.maxCoalescedBytes(emptySession.get()), 100); ASSERT_EQ( hiveConfig.maxCoalescedDistanceBytes(emptySession.get()), 100 << 10); + ASSERT_TRUE(hiveConfig.isParquetReadBloomFilter(emptySession.get())); ASSERT_EQ(hiveConfig.numCacheFileHandles(), 100); ASSERT_FALSE(hiveConfig.isFileHandleCacheEnabled()); ASSERT_EQ(hiveConfig.sortWriterMaxOutputRows(emptySession.get()), 100); diff --git a/velox/dwio/common/Options.h b/velox/dwio/common/Options.h index 987e0eb76462..2d112a7b6a2b 100644 --- a/velox/dwio/common/Options.h +++ b/velox/dwio/common/Options.h @@ -497,6 +497,11 @@ class ReaderOptions : public io::ReaderOptions { return *this; } + ReaderOptions& setReadBloomFilter(bool flag) { + readBloomFilter_ = flag; + return *this; + } + ReaderOptions& setIOExecutor(std::shared_ptr executor) { ioExecutor_ = std::move(executor); return *this; @@ -567,6 +572,10 @@ class ReaderOptions : public io::ReaderOptions { return useColumnNamesForColumnMapping_; } + bool readBloomFilter() const { + return readBloomFilter_; + } + const std::shared_ptr& randomSkip() const { return randomSkip_; } @@ -609,6 +618,7 @@ class ReaderOptions : public io::ReaderOptions { uint64_t filePreloadThreshold_{kDefaultFilePreloadThreshold}; bool fileColumnNamesReadAsLowerCase_{false}; bool useColumnNamesForColumnMapping_{false}; + bool readBloomFilter_{false}; std::shared_ptr ioExecutor_; std::shared_ptr randomSkip_; std::shared_ptr scanSpec_; diff --git a/velox/dwio/parquet/common/BloomFilter.h b/velox/dwio/parquet/common/BloomFilter.h index 9a2d9d47fec0..5eb196684bab 100644 --- a/velox/dwio/parquet/common/BloomFilter.h +++ b/velox/dwio/parquet/common/BloomFilter.h @@ -68,31 +68,31 @@ class BloomFilter { /// /// @param value the value to hash. /// @return hash result. - virtual uint64_t hash(int32_t value) const = 0; + virtual uint64_t hashInt32(int32_t value) const = 0; /// Compute hash for 64 bits value by using its plain encoding result. /// /// @param value the value to hash. /// @return hash result. - virtual uint64_t hash(int64_t value) const = 0; + virtual uint64_t hashInt64(int64_t value) const = 0; /// Compute hash for float value by using its plain encoding result. /// /// @param value the value to hash. /// @return hash result. - virtual uint64_t hash(float value) const = 0; + virtual uint64_t hashFloat(float value) const = 0; /// Compute hash for double value by using its plain encoding result. /// /// @param value the value to hash. /// @return hash result. - virtual uint64_t hash(double value) const = 0; + virtual uint64_t hashDouble(double value) const = 0; /// Compute hash for bytearray by using its plain encoding result. /// /// @param value the value to hash. /// @return hash result. - virtual uint64_t hash(const ByteArray* value) const = 0; + virtual uint64_t hashByteArray(const ByteArray* value) const = 0; /// Batch compute hashes for 32 bits values by using its plain encoding /// result. @@ -101,8 +101,8 @@ class BloomFilter { /// @param num_values the number of values to hash. /// @param hashes a pointer to the output hash values, its length should be /// equal to num_values. - virtual void hashes(const int32_t* values, int numValues, uint64_t* hashes) - const = 0; + virtual void + hashesInt32(const int32_t* values, int numValues, uint64_t* hashes) const = 0; /// Batch compute hashes for 64 bits values by using its plain encoding /// result. @@ -111,8 +111,8 @@ class BloomFilter { /// @param num_values the number of values to hash. /// @param hashes a pointer to the output hash values, its length should be /// equal to num_values. - virtual void hashes(const int64_t* values, int numValues, uint64_t* hashes) - const = 0; + virtual void + hashesInt64(const int64_t* values, int numValues, uint64_t* hashes) const = 0; /// Batch compute hashes for float values by using its plain encoding result. /// @@ -120,7 +120,7 @@ class BloomFilter { /// @param num_values the number of values to hash. /// @param hashes a pointer to the output hash values, its length should be /// equal to num_values. - virtual void hashes(const float* values, int numValues, uint64_t* hashes) + virtual void hashesFloat(const float* values, int numValues, uint64_t* hashes) const = 0; /// Batch compute hashes for double values by using its plain encoding result. @@ -129,8 +129,8 @@ class BloomFilter { /// @param num_values the number of values to hash. /// @param hashes a pointer to the output hash values, its length should be /// equal to num_values. - virtual void hashes(const double* values, int numValues, uint64_t* hashes) - const = 0; + virtual void + hashesDouble(const double* values, int numValues, uint64_t* hashes) const = 0; /// Batch compute hashes for bytearray values by using its plain encoding /// result. @@ -139,8 +139,10 @@ class BloomFilter { /// @param num_values the number of values to hash. /// @param hashes a pointer to the output hash values, its length should be /// equal to num_values. - virtual void hashes(const ByteArray* values, int numValues, uint64_t* hashes) - const = 0; + virtual void hashesByteArray( + const ByteArray* values, + int numValues, + uint64_t* hashes) const = 0; virtual ~BloomFilter() = default; @@ -248,54 +250,41 @@ class BlockSplitBloomFilter : public BloomFilter { return numBytes_; } - uint64_t hash(int32_t value) const override { - return hasher_->hash(value); + uint64_t hashInt32(int32_t value) const override { + return hasher_->hashInt32(value); } - uint64_t hash(int64_t value) const override { - return hasher_->hash(value); + uint64_t hashInt64(int64_t value) const override { + return hasher_->hashInt64(value); } - uint64_t hash(float value) const override { - return hasher_->hash(value); + uint64_t hashFloat(float value) const override { + return hasher_->hashFloat(value); } - uint64_t hash(double value) const override { - return hasher_->hash(value); + uint64_t hashDouble(double value) const override { + return hasher_->hashDouble(value); } - uint64_t hash(const ByteArray* value) const override { - return hasher_->hash(value); + uint64_t hashByteArray(const ByteArray* value) const override { + return hasher_->hashByteArray(value); } - void hashes(const int32_t* values, int numValues, uint64_t* hashes) + void hashesInt32(const int32_t* values, int numValues, uint64_t* hashes) const override { - hasher_->hashes(values, numValues, hashes); + hasher_->hashesInt32(values, numValues, hashes); } - void hashes(const int64_t* values, int numValues, uint64_t* hashes) + void hashesInt64(const int64_t* values, int numValues, uint64_t* hashes) const override { - hasher_->hashes(values, numValues, hashes); + hasher_->hashesInt64(values, numValues, hashes); } - void hashes(const float* values, int numValues, uint64_t* hashes) + void hashesFloat(const float* values, int numValues, uint64_t* hashes) const override { - hasher_->hashes(values, numValues, hashes); + hasher_->hashesFloat(values, numValues, hashes); } - void hashes(const double* values, int numValues, uint64_t* hashes) + void hashesDouble(const double* values, int numValues, uint64_t* hashes) const override { - hasher_->hashes(values, numValues, hashes); + hasher_->hashesDouble(values, numValues, hashes); } - void hashes(const ByteArray* values, int numValues, uint64_t* hashes) + void hashesByteArray(const ByteArray* values, int numValues, uint64_t* hashes) const override { - hasher_->hashes(values, numValues, hashes); - } - - uint64_t hash(const int32_t* value) const { - return hasher_->hash(*value); - } - uint64_t hash(const int64_t* value) const { - return hasher_->hash(*value); - } - uint64_t hash(const float* value) const { - return hasher_->hash(*value); - } - uint64_t hash(const double* value) const { - return hasher_->hash(*value); + hasher_->hashesByteArray(values, numValues, hashes); } /// Deserialize the Bloom filter from an input stream. It is used when diff --git a/velox/dwio/parquet/common/CMakeLists.txt b/velox/dwio/parquet/common/CMakeLists.txt index 4e3edf6687ce..fb6f4d4577c4 100644 --- a/velox/dwio/parquet/common/CMakeLists.txt +++ b/velox/dwio/parquet/common/CMakeLists.txt @@ -17,7 +17,8 @@ velox_add_library( BloomFilter.cpp XxHasher.cpp LevelComparison.cpp - LevelConversion.cpp) + LevelConversion.cpp + ParquetBloomFilter.h) velox_link_libraries( velox_dwio_parquet_common diff --git a/velox/dwio/parquet/common/Hasher.h b/velox/dwio/parquet/common/Hasher.h index 3f3a907d06b4..b40b1167b624 100644 --- a/velox/dwio/parquet/common/Hasher.h +++ b/velox/dwio/parquet/common/Hasher.h @@ -47,31 +47,31 @@ class Hasher { /// /// @param value the value to hash. /// @return hash result. - virtual uint64_t hash(int32_t value) const = 0; + virtual uint64_t hashInt32(int32_t value) const = 0; /// Compute hash for 64 bits value by using its plain encoding result. /// /// @param value the value to hash. /// @return hash result. - virtual uint64_t hash(int64_t value) const = 0; + virtual uint64_t hashInt64(int64_t value) const = 0; /// Compute hash for float value by using its plain encoding result. /// /// @param value the value to hash. /// @return hash result. - virtual uint64_t hash(float value) const = 0; + virtual uint64_t hashFloat(float value) const = 0; /// Compute hash for double value by using its plain encoding result. /// /// @param value the value to hash. /// @return hash result. - virtual uint64_t hash(double value) const = 0; + virtual uint64_t hashDouble(double value) const = 0; /// Compute hash for ByteArray value by using its plain encoding result. /// /// @param value the value to hash. /// @return hash result. - virtual uint64_t hash(const ByteArray* value) const = 0; + virtual uint64_t hashByteArray(const ByteArray* value) const = 0; /// Batch compute hashes for 32 bits values by using its plain encoding /// result. @@ -80,8 +80,10 @@ class Hasher { /// @param num_values the number of values to hash. /// @param hashes a pointer to the output hash values, its length should be /// equal to num_values. - virtual void hashes(const int32_t* values, int num_values, uint64_t* hashes) - const = 0; + virtual void hashesInt32( + const int32_t* values, + int num_values, + uint64_t* hashes) const = 0; /// Batch compute hashes for 64 bits values by using its plain encoding /// result. @@ -90,8 +92,10 @@ class Hasher { /// @param num_values the number of values to hash. /// @param hashes a pointer to the output hash values, its length should be /// equal to num_values. - virtual void hashes(const int64_t* values, int num_values, uint64_t* hashes) - const = 0; + virtual void hashesInt64( + const int64_t* values, + int num_values, + uint64_t* hashes) const = 0; /// Batch compute hashes for float values by using its plain encoding result. /// @@ -99,8 +103,8 @@ class Hasher { /// @param num_values the number of values to hash. /// @param hashes a pointer to the output hash values, its length should be /// equal to num_values. - virtual void hashes(const float* values, int num_values, uint64_t* hashes) - const = 0; + virtual void + hashesFloat(const float* values, int num_values, uint64_t* hashes) const = 0; /// Batch compute hashes for double values by using its plain encoding result. /// @@ -108,8 +112,10 @@ class Hasher { /// @param num_values the number of values to hash. /// @param hashes a pointer to the output hash values, its length should be /// equal to num_values. - virtual void hashes(const double* values, int num_values, uint64_t* hashes) - const = 0; + virtual void hashesDouble( + const double* values, + int num_values, + uint64_t* hashes) const = 0; /// Batch compute hashes for ByteArray values by using its plain encoding /// result. @@ -118,8 +124,10 @@ class Hasher { /// @param num_values the number of values to hash. /// @param hashes a pointer to the output hash values, its length should be /// equal to num_values. - virtual void hashes(const ByteArray* values, int num_values, uint64_t* hashes) - const = 0; + virtual void hashesByteArray( + const ByteArray* values, + int num_values, + uint64_t* hashes) const = 0; virtual ~Hasher() = default; }; diff --git a/velox/dwio/parquet/common/ParquetBloomFilter.h b/velox/dwio/parquet/common/ParquetBloomFilter.h new file mode 100644 index 000000000000..1cf4f0ffea27 --- /dev/null +++ b/velox/dwio/parquet/common/ParquetBloomFilter.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "velox/dwio/parquet/common/BloomFilter.h" +#include "velox/type/Filter.h" + +namespace facebook::velox::parquet { + +class ParquetBloomFilter final : public common::AbstractBloomFilter { + public: + ParquetBloomFilter( + std::shared_ptr bloomFilter) + : bloomFilter_(bloomFilter) {} + + bool mightContainInt32(int32_t value) const override { + return bloomFilter_->findHash(bloomFilter_->hashInt32(value)); + } + + bool mightContainInt64(int64_t value) const override { + return bloomFilter_->findHash(bloomFilter_->hashInt64(value)); + } + + bool mightContainString(const std::string& value) const override { + ByteArray byteArray{value}; + return bloomFilter_->findHash(bloomFilter_->hashByteArray(&byteArray)); + } + + private: + std::shared_ptr bloomFilter_; +}; + +} // namespace facebook::velox::parquet diff --git a/velox/dwio/parquet/common/XxHasher.cpp b/velox/dwio/parquet/common/XxHasher.cpp index df189a972b6e..8904b3d601b5 100644 --- a/velox/dwio/parquet/common/XxHasher.cpp +++ b/velox/dwio/parquet/common/XxHasher.cpp @@ -42,51 +42,59 @@ void XxHashesHelper( } // namespace -uint64_t XxHasher::hash(int32_t value) const { +uint64_t XxHasher::hashInt32(int32_t value) const { return XxHashHelper(value, kParquetBloomXxHashSeed); } -uint64_t XxHasher::hash(int64_t value) const { +uint64_t XxHasher::hashInt64(int64_t value) const { return XxHashHelper(value, kParquetBloomXxHashSeed); } -uint64_t XxHasher::hash(float value) const { +uint64_t XxHasher::hashFloat(float value) const { return XxHashHelper(value, kParquetBloomXxHashSeed); } -uint64_t XxHasher::hash(double value) const { +uint64_t XxHasher::hashDouble(double value) const { return XxHashHelper(value, kParquetBloomXxHashSeed); } -uint64_t XxHasher::hash(const ByteArray* value) const { +uint64_t XxHasher::hashByteArray(const ByteArray* value) const { return XXH64( reinterpret_cast(value->ptr), value->len, kParquetBloomXxHashSeed); } -void XxHasher::hashes(const int32_t* values, int numValues, uint64_t* hashes) - const { +void XxHasher::hashesInt32( + const int32_t* values, + int numValues, + uint64_t* hashes) const { XxHashesHelper(values, kParquetBloomXxHashSeed, numValues, hashes); } -void XxHasher::hashes(const int64_t* values, int numValues, uint64_t* hashes) - const { +void XxHasher::hashesInt64( + const int64_t* values, + int numValues, + uint64_t* hashes) const { XxHashesHelper(values, kParquetBloomXxHashSeed, numValues, hashes); } -void XxHasher::hashes(const float* values, int numValues, uint64_t* hashes) +void XxHasher::hashesFloat(const float* values, int numValues, uint64_t* hashes) const { XxHashesHelper(values, kParquetBloomXxHashSeed, numValues, hashes); } -void XxHasher::hashes(const double* values, int numValues, uint64_t* hashes) - const { +void XxHasher::hashesDouble( + const double* values, + int numValues, + uint64_t* hashes) const { XxHashesHelper(values, kParquetBloomXxHashSeed, numValues, hashes); } -void XxHasher::hashes(const ByteArray* values, int numValues, uint64_t* hashes) - const { +void XxHasher::hashesByteArray( + const ByteArray* values, + int numValues, + uint64_t* hashes) const { for (int i = 0; i < numValues; ++i) { hashes[i] = XXH64( reinterpret_cast(values[i].ptr), diff --git a/velox/dwio/parquet/common/XxHasher.h b/velox/dwio/parquet/common/XxHasher.h index 07c37f762f36..0d830eaf4722 100644 --- a/velox/dwio/parquet/common/XxHasher.h +++ b/velox/dwio/parquet/common/XxHasher.h @@ -26,22 +26,24 @@ namespace facebook::velox::parquet { class XxHasher : public Hasher { public: - uint64_t hash(int32_t value) const override; - uint64_t hash(int64_t value) const override; - uint64_t hash(float value) const override; - uint64_t hash(double value) const override; - uint64_t hash(const ByteArray* value) const override; + uint64_t hashInt32(int32_t value) const override; + uint64_t hashInt64(int64_t value) const override; + uint64_t hashFloat(float value) const override; + uint64_t hashDouble(double value) const override; + uint64_t hashByteArray(const ByteArray* value) const override; - void hashes(const int32_t* values, int numValues, uint64_t* hashes) + void hashesInt32(const int32_t* values, int numValues, uint64_t* hashes) const override; - void hashes(const int64_t* values, int numValues, uint64_t* hashes) + void hashesInt64(const int64_t* values, int numValues, uint64_t* hashes) const override; - void hashes(const float* values, int numValues, uint64_t* hashes) + void hashesFloat(const float* values, int numValues, uint64_t* hashes) const override; - void hashes(const double* values, int numValues, uint64_t* hashes) - const override; - virtual void hashes(const ByteArray* values, int numValues, uint64_t* hashes) + void hashesDouble(const double* values, int numValues, uint64_t* hashes) const override; + virtual void hashesByteArray( + const ByteArray* values, + int numValues, + uint64_t* hashes) const override; static constexpr int kParquetBloomXxHashSeed = 0; }; diff --git a/velox/dwio/parquet/reader/Metadata.cpp b/velox/dwio/parquet/reader/Metadata.cpp index 771e68e8a595..b67be36f28c4 100644 --- a/velox/dwio/parquet/reader/Metadata.cpp +++ b/velox/dwio/parquet/reader/Metadata.cpp @@ -210,6 +210,15 @@ bool ColumnChunkMetaDataPtr::hasDictionaryPageOffset() const { thriftColumnChunkPtr(ptr_)->meta_data.__isset.dictionary_page_offset; } +bool ColumnChunkMetaDataPtr::hasBloomFilterOffset() const { + return hasMetadata() && + thriftColumnChunkPtr(ptr_)->meta_data.__isset.bloom_filter_offset; +} + +bool ColumnChunkMetaDataPtr::hasCryptoMetadata() const { + return thriftColumnChunkPtr(ptr_)->__isset.crypto_metadata; +} + std::unique_ptr ColumnChunkMetaDataPtr::getColumnStatistics( const TypePtr type, @@ -228,6 +237,11 @@ int64_t ColumnChunkMetaDataPtr::dictionaryPageOffset() const { return thriftColumnChunkPtr(ptr_)->meta_data.dictionary_page_offset; } +int64_t ColumnChunkMetaDataPtr::bloomFilterOffset() const { + VELOX_CHECK(hasBloomFilterOffset()); + return thriftColumnChunkPtr(ptr_)->meta_data.bloom_filter_offset; +} + common::CompressionKind ColumnChunkMetaDataPtr::compression() const { return thriftCodecToCompressionKind( thriftColumnChunkPtr(ptr_)->meta_data.codec); diff --git a/velox/dwio/parquet/reader/Metadata.h b/velox/dwio/parquet/reader/Metadata.h index f99d46656d8c..b32cf512fe8f 100644 --- a/velox/dwio/parquet/reader/Metadata.h +++ b/velox/dwio/parquet/reader/Metadata.h @@ -37,6 +37,12 @@ class ColumnChunkMetaDataPtr { /// Check the presence of the dictionary page offset in ColumnChunk metadata. bool hasDictionaryPageOffset() const; + // Check the presence of the bloom filter offset in ColumnChunk metadata + bool hasBloomFilterOffset() const; + + // Check the presence of crypto metadata in ColumnChunk metadata + bool hasCryptoMetadata() const; + /// Return the ColumnChunk statistics. std::unique_ptr getColumnStatistics( const TypePtr type, @@ -52,6 +58,8 @@ class ColumnChunkMetaDataPtr { /// Must check for its presence using hasDictionaryPageOffset(). int64_t dictionaryPageOffset() const; + int64_t bloomFilterOffset() const; + /// The compression. common::CompressionKind compression() const; diff --git a/velox/dwio/parquet/reader/ParquetData.cpp b/velox/dwio/parquet/reader/ParquetData.cpp index 29a593da414c..875c4ac15860 100644 --- a/velox/dwio/parquet/reader/ParquetData.cpp +++ b/velox/dwio/parquet/reader/ParquetData.cpp @@ -17,15 +17,62 @@ #include "velox/dwio/parquet/reader/ParquetData.h" #include "velox/dwio/common/BufferedInput.h" +#include "velox/dwio/parquet/common/ParquetBloomFilter.h" #include "velox/dwio/parquet/reader/ParquetStatsContext.h" namespace facebook::velox::parquet { +using thrift::RowGroup; + +namespace { +bool isFilterRangeCoversStatsRange( + common::Filter* filter, + dwio::common::ColumnStatistics* stats, + const TypePtr& type) { + switch (type->kind()) { + case TypeKind::BIGINT: + case TypeKind::INTEGER: + case TypeKind::SMALLINT: + case TypeKind::TINYINT: { + auto intStats = + dynamic_cast(stats); + if (!intStats) + return false; + + int64_t min = + intStats->getMinimum().value_or(std::numeric_limits::min()); + int64_t max = + intStats->getMaximum().value_or(std::numeric_limits::max()); + + switch (filter->kind()) { + case common::FilterKind::kBigintRange: + return static_cast(filter)->lower() <= min && + max <= static_cast(filter)->upper(); + case common::FilterKind::kBigintMultiRange: { + common::BigintMultiRange* multiRangeFilter = + static_cast(filter); + auto numRanges = multiRangeFilter->ranges().size(); + if (numRanges > 0) { + return multiRangeFilter->ranges()[0]->lower() <= min && + max <= multiRangeFilter->ranges()[numRanges - 1]->upper(); + } + } break; + default: + return false; + } + } break; + default: + return false; + } + return false; +} +} // namespace + std::unique_ptr ParquetParams::toFormatData( const std::shared_ptr& type, const common::ScanSpec& /*scanSpec*/) { return std::make_unique( - type, metaData_, pool(), sessionTimezone_); + type, metaData_, pool(), sessionTimezone_, parquetReadBloomFilter_); } void ParquetData::filterRowGroups( @@ -47,6 +94,7 @@ void ParquetData::filterRowGroups( result.filterResult.resize(nwords); } auto metadataFiltersStartIndex = result.metadataFilterResults.size(); + for (int i = 0; i < scanSpec.numMetadataFilters(); ++i) { result.metadataFilterResults.emplace_back( scanSpec.metadataFilterNodeAt(i), std::vector(nwords)); @@ -80,12 +128,31 @@ bool ParquetData::rowGroupMatches(uint32_t rowGroupId, common::Filter* filter) { return true; } + bool needsToCheckBloomFilter = true; auto columnChunk = rowGroup.columnChunk(column); if (columnChunk.hasStatistics()) { auto columnStats = columnChunk.getColumnStatistics(type, rowGroup.numRows()); - return testFilter(filter, columnStats.get(), rowGroup.numRows(), type); + if (!testFilter(filter, columnStats.get(), rowGroup.numRows(), type)) { + return false; + } + + // We can avoid testing bloom filter unnecessarily if we know that the + // filter (min,max) range is a superset of the stats (min,max) range. For + // example, if the filter is "COL between 1 and 20" and the column stats + // range is (5,10), then we have to read the whole row group and hence avoid + // bloom filter test. + needsToCheckBloomFilter = parquetReadBloomFilter_ && + !isFilterRangeCoversStatsRange(filter, columnStats.get(), type); + } + + if (needsToCheckBloomFilter && + rowGroup.columnChunk(column).hasBloomFilterOffset()) { + std::unique_ptr parquetBloomFilter = + std::make_unique(getBloomFilter(rowGroupId)); + return filter->testBloomFilter(*parquetBloomFilter, *type); } + return true; } @@ -148,4 +215,62 @@ std::pair ParquetData::getRowGroupRegion( return {fileOffset, length}; } +void ParquetData::setBloomFilterInputStream( + uint32_t rowGroupId, + dwio::common::BufferedInput& bufferedInput) { + bloomFilterInputStreams_.resize(fileMetaDataPtr_.numRowGroups()); + if (bloomFilterInputStreams_[rowGroupId] != nullptr) { + return; + } + auto rowGroup = fileMetaDataPtr_.rowGroup(rowGroupId); + auto colChunk = rowGroup.columnChunk(type_->column()); + + if (!colChunk.hasBloomFilterOffset()) { + return; + } + + VELOX_CHECK( + !colChunk.hasCryptoMetadata(), "Cannot read encrypted bloom filter yet"); + + auto bloomFilterOffset = colChunk.bloomFilterOffset(); + auto fileSize = bufferedInput.getInputStream()->getLength(); + VELOX_CHECK_GT( + fileSize, + bloomFilterOffset, + "file size {} less or equal than bloom offset {}", + fileSize, + bloomFilterOffset); + + auto id = dwio::common::StreamIdentifier(type_->column()); + bloomFilterInputStreams_[rowGroupId] = bufferedInput.enqueue( + {static_cast(bloomFilterOffset), fileSize - bloomFilterOffset}, + &id); +} + +std::shared_ptr ParquetData::getBloomFilter( + const uint32_t rowGroupId) { + auto columnBloomFilterIter = columnBloomFilterMap_.find(rowGroupId); + if (columnBloomFilterIter != columnBloomFilterMap_.end()) { + return columnBloomFilterIter->second; + } + + VELOX_CHECK_LT( + rowGroupId, + fileMetaDataPtr_.numRowGroups(), + "Invalid row group ordinal: {}", + rowGroupId); + + if (bloomFilterInputStreams_[rowGroupId] == nullptr) { + return nullptr; + } + + auto bloomFilter = BlockSplitBloomFilter::deserialize( + bloomFilterInputStreams_[rowGroupId].get(), pool_); + + auto blockSplitBloomFilter = + std::make_shared(std::move(bloomFilter)); + columnBloomFilterMap_[rowGroupId] = blockSplitBloomFilter; + return blockSplitBloomFilter; +} + } // namespace facebook::velox::parquet diff --git a/velox/dwio/parquet/reader/ParquetData.h b/velox/dwio/parquet/reader/ParquetData.h index fe8020f57c65..d5cab43a5dfd 100644 --- a/velox/dwio/parquet/reader/ParquetData.h +++ b/velox/dwio/parquet/reader/ParquetData.h @@ -17,6 +17,9 @@ #pragma once #include "velox/dwio/common/BufferUtil.h" +#include "velox/dwio/common/BufferedInput.h" +#include "velox/dwio/common/ScanSpec.h" +#include "velox/dwio/parquet/common/BloomFilter.h" #include "velox/dwio/parquet/reader/Metadata.h" #include "velox/dwio/parquet/reader/PageReader.h" @@ -37,11 +40,14 @@ class ParquetParams : public dwio::common::FormatParams { dwio::common::ColumnReaderStatistics& stats, const FileMetaDataPtr metaData, const tz::TimeZone* sessionTimezone, - TimestampPrecision timestampPrecision) + TimestampPrecision timestampPrecision, + bool parquetReadBloomFilter) : FormatParams(pool, stats), metaData_(metaData), sessionTimezone_(sessionTimezone), - timestampPrecision_(timestampPrecision) {} + timestampPrecision_(timestampPrecision), + parquetReadBloomFilter_(parquetReadBloomFilter) {} + std::unique_ptr toFormatData( const std::shared_ptr& type, const common::ScanSpec& scanSpec) override; @@ -54,6 +60,7 @@ class ParquetParams : public dwio::common::FormatParams { const FileMetaDataPtr metaData_; const tz::TimeZone* sessionTimezone_; const TimestampPrecision timestampPrecision_; + bool parquetReadBloomFilter_; }; /// Format-specific data created for each leaf column of a Parquet rowgroup. @@ -63,14 +70,16 @@ class ParquetData : public dwio::common::FormatData { const std::shared_ptr& type, const FileMetaDataPtr fileMetadataPtr, memory::MemoryPool& pool, - const tz::TimeZone* sessionTimezone) + const tz::TimeZone* sessionTimezone, + bool parquetReadBloomFilter) : pool_(pool), type_(std::static_pointer_cast(type)), fileMetaDataPtr_(fileMetadataPtr), maxDefine_(type_->maxDefine_), maxRepeat_(type_->maxRepeat_), rowsInRowGroup_(-1), - sessionTimezone_(sessionTimezone) {} + sessionTimezone_(sessionTimezone), + parquetReadBloomFilter_(parquetReadBloomFilter) {} /// Prepares to read data for 'index'th row group. void enqueueRowGroup(uint32_t index, dwio::common::BufferedInput& input); @@ -90,6 +99,8 @@ class ParquetData : public dwio::common::FormatData { return reader_.get(); } + std::shared_ptr getBloomFilter(const uint32_t rowGroupId); + // Reads null flags for 'numValues' next top level rows. The first 'numValues' // bits of 'nulls' are set and the reader is advanced by numValues'. void readNullsOnly(int32_t numValues, BufferPtr& nulls) { @@ -200,6 +211,10 @@ class ParquetData : public dwio::common::FormatData { return true; } + void setBloomFilterInputStream( + uint32_t rowGroupId, + dwio::common::BufferedInput& bufferedInput); + // Returns the of the row group. std::pair getRowGroupRegion(uint32_t index) const; @@ -222,6 +237,14 @@ class ParquetData : public dwio::common::FormatData { const tz::TimeZone* sessionTimezone_; std::unique_ptr reader_; + bool parquetReadBloomFilter_; + std::vector> + bloomFilterInputStreams_; + + // RowGroup+Column to BloomFilter map + std::unordered_map> + columnBloomFilterMap_; + // Nulls derived from leaf repdefs for non-leaf readers. BufferPtr presetNulls_; @@ -231,5 +254,4 @@ class ParquetData : public dwio::common::FormatData { // Count of leading skipped positions in 'presetNulls_' int32_t presetNullsConsumed_{0}; }; - } // namespace facebook::velox::parquet diff --git a/velox/dwio/parquet/reader/ParquetReader.cpp b/velox/dwio/parquet/reader/ParquetReader.cpp index 35702028c75e..b8aaa07e8455 100644 --- a/velox/dwio/parquet/reader/ParquetReader.cpp +++ b/velox/dwio/parquet/reader/ParquetReader.cpp @@ -17,7 +17,8 @@ #include "velox/dwio/parquet/reader/ParquetReader.h" #include //@manual - +#include "velox/dwio/common/MetricsLog.h" +#include "velox/dwio/common/TypeUtils.h" #include "velox/dwio/parquet/reader/ParquetColumnReader.h" #include "velox/dwio/parquet/reader/StructColumnReader.h" #include "velox/dwio/parquet/thrift/ThriftTransport.h" @@ -87,6 +88,10 @@ class ReaderBase { return version_; } + bool isReadBloomFilter() const { + return options_.readBloomFilter(); + } + /// Ensures that streams are enqueued and loading for the row group at /// 'currentGroup'. May start loading one or more subsequent groups. void scheduleRowGroups( @@ -952,7 +957,8 @@ class ParquetRowReader::Impl { columnReaderStats_, readerBase_->fileMetaData(), readerBase->sessionTimezone(), - options_.timestampPrecision()); + options_.timestampPrecision(), + readerBase_->isReadBloomFilter()); requestedType_ = options_.requestedType() ? options_.requestedType() : readerBase_->schema(); columnReader_ = ParquetColumnReader::build( @@ -971,10 +977,23 @@ class ParquetRowReader::Impl { } } + const thrift::FileMetaData& fileMetaData() const { + return readerBase_->thriftFileMetaData(); + } + void filterRowGroups() { rowGroupIds_.reserve(rowGroups_.size()); firstRowOfRowGroup_.reserve(rowGroups_.size()); + for (auto child : columnReader_->children()) { + auto& parquetData = child->formatData().as(); + for (auto i = 0; i < rowGroups_.size(); ++i) { + parquetData.setBloomFilterInputStream(i, readerBase_->bufferedInput()); + } + } + + readerBase_->bufferedInput().load(dwio::common::LogType::STRIPE_FOOTER); + ParquetData::FilterRowGroupsResult res; columnReader_->filterRowGroups(0, parquetStatsContext_, res); if (auto& metadataFilter = options_.metadataFilter()) { @@ -1158,6 +1177,10 @@ std::optional ParquetRowReader::estimatedRowSize() const { return impl_->estimatedRowSize(); } +const thrift::FileMetaData& ParquetRowReader::fileMetaData() const { + return impl_->fileMetaData(); +} + ParquetReader::ParquetReader( std::unique_ptr input, const dwio::common::ReaderOptions& options) diff --git a/velox/dwio/parquet/reader/ParquetReader.h b/velox/dwio/parquet/reader/ParquetReader.h index de6d7a9966dc..fca8f043b33e 100644 --- a/velox/dwio/parquet/reader/ParquetReader.h +++ b/velox/dwio/parquet/reader/ParquetReader.h @@ -18,8 +18,10 @@ #include "velox/dwio/common/Reader.h" #include "velox/dwio/common/ReaderFactory.h" +#include "velox/dwio/parquet/common/BloomFilter.h" #include "velox/dwio/parquet/reader/Metadata.h" #include "velox/dwio/parquet/reader/ParquetStatsContext.h" +#include "velox/dwio/parquet/reader/ParquetTypeWithId.h" namespace facebook::velox::dwio::common { @@ -36,6 +38,8 @@ class StructColumnReader; class ReaderBase; +class BloomFilterReader; + /// Implements the RowReader interface for Parquet. class ParquetRowReader : public dwio::common::RowReader { public: @@ -60,6 +64,8 @@ class ParquetRowReader : public dwio::common::RowReader { std::optional estimatedRowSize() const override; + const thrift::FileMetaData& fileMetaData() const; + bool allPrefetchIssued() const override { // Allow opening the next split while this is reading. return true; diff --git a/velox/dwio/parquet/tests/ParquetTestBase.h b/velox/dwio/parquet/tests/ParquetTestBase.h index 9b04ad56b500..bd0317517e0c 100644 --- a/velox/dwio/parquet/tests/ParquetTestBase.h +++ b/velox/dwio/parquet/tests/ParquetTestBase.h @@ -33,6 +33,33 @@ namespace facebook::velox::parquet { class ParquetTestBase : public testing::Test, public velox::test::VectorTestBase { + public: + static dwio::common::RowReaderOptions getReaderOpts( + const RowTypePtr& rowType, + bool fileColumnNamesReadAsLowerCase = false) { + dwio::common::RowReaderOptions rowReaderOpts; + rowReaderOpts.select( + std::make_shared( + rowType, + rowType->names(), + nullptr, + fileColumnNamesReadAsLowerCase)); + + return rowReaderOpts; + } + + static std::string getExampleFilePath(const std::string& fileName) { + return test::getDataFilePath( + "velox/dwio/parquet/tests/reader", "../examples/" + fileName); + } + + static std::shared_ptr makeScanSpec( + const RowTypePtr& rowType) { + auto scanSpec = std::make_shared(""); + scanSpec->addAllChildFields(*rowType); + return scanSpec; + } + protected: static void SetUpTestCase() { memory::MemoryManager::testingSetInstance({}); @@ -70,27 +97,6 @@ class ParquetTestBase : public testing::Test, std::move(input), opts); } - dwio::common::RowReaderOptions getReaderOpts( - const RowTypePtr& rowType, - bool fileColumnNamesReadAsLowerCase = false) { - dwio::common::RowReaderOptions rowReaderOpts; - rowReaderOpts.select( - std::make_shared( - rowType, - rowType->names(), - nullptr, - fileColumnNamesReadAsLowerCase)); - - return rowReaderOpts; - } - - std::shared_ptr makeScanSpec( - const RowTypePtr& rowType) { - auto scanSpec = std::make_shared(""); - scanSpec->addAllChildFields(*rowType); - return scanSpec; - } - using FilterMap = std::unordered_map>; @@ -115,7 +121,7 @@ class ParquetTestBase : public testing::Test, memory::MemoryPool& memoryPool) { uint64_t total = 0; VectorPtr result = BaseVector::create(outputType, 0, &memoryPool); - while (total < expected->size()) { + do { auto part = reader.next(1000, result); if (part > 0) { assertEqualVectorPart(expected, result, total); @@ -123,7 +129,7 @@ class ParquetTestBase : public testing::Test, } else { break; } - } + } while (total < expected->size()); EXPECT_EQ(total, expected->size()); EXPECT_EQ(reader.next(1000, result), 0); } @@ -133,7 +139,9 @@ class ParquetTestBase : public testing::Test, const std::string& /* fileName */, const RowTypePtr& fileSchema, FilterMap filters, - const RowVectorPtr& expected) { + const RowVectorPtr& expected, + std::shared_ptr + runtimeStats = nullptr) { auto scanSpec = makeScanSpec(fileSchema); for (auto&& [column, filter] : filters) { scanSpec->getOrCreateChild(velox::common::Subfield(column)) @@ -145,6 +153,9 @@ class ParquetTestBase : public testing::Test, auto rowReader = reader->createRowReader(rowReaderOpts); assertReadWithReaderAndExpected( fileSchema, *rowReader, expected, *leafPool_); + if (runtimeStats != nullptr) { + rowReader->updateRuntimeStats(*runtimeStats); + } } std::unique_ptr createSink( @@ -186,11 +197,6 @@ class ParquetTestBase : public testing::Test, return batches; } - std::string getExampleFilePath(const std::string& fileName) { - return test::getDataFilePath( - "velox/dwio/parquet/tests/reader", "../examples/" + fileName); - } - static constexpr uint64_t kRowsInRowGroup = 10'000; static constexpr uint64_t kBytesInRowGroup = 128 * 1'024 * 1'024; std::shared_ptr rootPool_; diff --git a/velox/dwio/parquet/tests/ParquetTpchTest.cpp b/velox/dwio/parquet/tests/ParquetTpchTest.cpp index 300b17b8ac2e..221ee8b23cfa 100644 --- a/velox/dwio/parquet/tests/ParquetTpchTest.cpp +++ b/velox/dwio/parquet/tests/ParquetTpchTest.cpp @@ -51,8 +51,8 @@ class ParquetTpchTest : public testing::Test { filesystems::registerLocalFileSystem(); dwio::common::registerFileSinks(); - parquet::registerParquetReaderFactory(); - parquet::registerParquetWriterFactory(); + facebook::velox::parquet::registerParquetReaderFactory(); + facebook::velox::parquet::registerParquetWriterFactory(); connector::registerConnectorFactory( std::make_shared()); @@ -87,8 +87,8 @@ class ParquetTpchTest : public testing::Test { connector::tpch::TpchConnectorFactory::kTpchConnectorName); connector::unregisterConnector(kHiveConnectorId); connector::unregisterConnector(kTpchConnectorId); - parquet::unregisterParquetReaderFactory(); - parquet::unregisterParquetWriterFactory(); + facebook::velox::parquet::unregisterParquetReaderFactory(); + facebook::velox::parquet::unregisterParquetWriterFactory(); } static void saveTpchTablesAsParquet() { diff --git a/velox/dwio/parquet/tests/examples/sample_int64_string_int32_bloom_1k.snappy.parquet b/velox/dwio/parquet/tests/examples/sample_int64_string_int32_bloom_1k.snappy.parquet new file mode 100644 index 0000000000000000000000000000000000000000..8e6beac8df0b8b0fdd8dc6eec2fc061dea30e04b GIT binary patch literal 59361 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 IDqHb?0K+N}EC2ui literal 0 HcmV?d00001 diff --git a/velox/dwio/parquet/tests/reader/BloomFilterTest.cpp b/velox/dwio/parquet/tests/reader/BloomFilterTest.cpp index 91ba224d0c81..5ddbdc8b432e 100644 --- a/velox/dwio/parquet/tests/reader/BloomFilterTest.cpp +++ b/velox/dwio/parquet/tests/reader/BloomFilterTest.cpp @@ -76,32 +76,32 @@ TEST_F(BloomFilterTest, BasicTest) { // Empty bloom filter deterministically returns false for (const auto v : kIntInserts) { - EXPECT_FALSE(bloomFilter.findHash(bloomFilter.hash(v))); + EXPECT_FALSE(bloomFilter.findHash(bloomFilter.hashInt64(v))); } for (const auto v : kFloatInserts) { - EXPECT_FALSE(bloomFilter.findHash(bloomFilter.hash(v))); + EXPECT_FALSE(bloomFilter.findHash(bloomFilter.hashFloat(v))); } // Insert all values for (const auto v : kIntInserts) { - bloomFilter.insertHash(bloomFilter.hash(v)); + bloomFilter.insertHash(bloomFilter.hashInt64(v)); } for (const auto v : kFloatInserts) { - bloomFilter.insertHash(bloomFilter.hash(v)); + bloomFilter.insertHash(bloomFilter.hashFloat(v)); } // They should always lookup successfully for (const auto v : kIntInserts) { - EXPECT_TRUE(bloomFilter.findHash(bloomFilter.hash(v))); + EXPECT_TRUE(bloomFilter.findHash(bloomFilter.hashInt64(v))); } for (const auto v : kFloatInserts) { - EXPECT_TRUE(bloomFilter.findHash(bloomFilter.hash(v))); + EXPECT_TRUE(bloomFilter.findHash(bloomFilter.hashFloat(v))); } // Values not inserted in the filter should only rarely lookup successfully int falsePositives = 0; for (const auto v : kNegativeIntLookups) { - falsePositives += bloomFilter.findHash(bloomFilter.hash(v)); + falsePositives += bloomFilter.findHash(bloomFilter.hashInt64(v)); } // (this is a crude check, see FPPTest below for a more rigorous formula) EXPECT_LE(falsePositives, 2); @@ -130,14 +130,14 @@ TEST_F(BloomFilterTest, BasicTest) { // Lookup previously inserted values for (const auto v : kIntInserts) { - EXPECT_TRUE(deBloom.findHash(deBloom.hash(v))); + EXPECT_TRUE(deBloom.findHash(deBloom.hashInt64(v))); } for (const auto v : kFloatInserts) { - EXPECT_TRUE(deBloom.findHash(deBloom.hash(v))); + EXPECT_TRUE(deBloom.findHash(deBloom.hashFloat(v))); } falsePositives = 0; for (const auto v : kNegativeIntLookups) { - falsePositives += deBloom.findHash(deBloom.hash(v)); + falsePositives += deBloom.findHash(deBloom.hashInt64(v)); } EXPECT_LE(falsePositives, 2); } @@ -185,18 +185,18 @@ TEST_F(BloomFilterTest, FPPTest) { const ByteArray byte_array( 8, reinterpret_cast(tmp.c_str())); members.push_back(tmp); - bloomFilter.insertHash(bloomFilter.hash(&byte_array)); + bloomFilter.insertHash(bloomFilter.hashByteArray(&byte_array)); } for (int i = 0; i < totalCount; i++) { const ByteArray byte_array1( 8, reinterpret_cast(members[i].c_str())); - ASSERT_TRUE(bloomFilter.findHash(bloomFilter.hash(&byte_array1))); + ASSERT_TRUE(bloomFilter.findHash(bloomFilter.hashByteArray(&byte_array1))); std::string tmp = GetRandomString(7); const ByteArray byte_array2( 7, reinterpret_cast(tmp.c_str())); - if (bloomFilter.findHash(bloomFilter.hash(&byte_array2))) { + if (bloomFilter.findHash(bloomFilter.hashByteArray(&byte_array2))) { exist++; } } @@ -368,7 +368,8 @@ TEST_F(BloomFilterTest, XxHashTest) { auto hasherSeed0 = std::make_unique(); EXPECT_EQ( - HASHES_OF_LOOPING_BYTES_WITH_SEED_0[i], hasherSeed0->hash(&byteArray)) + HASHES_OF_LOOPING_BYTES_WITH_SEED_0[i], + hasherSeed0->hashByteArray(&byteArray)) << "Hash with seed 0 Error: " << i; } } @@ -386,7 +387,7 @@ TEST_F(BloomFilterTest, TestBloomFilterHashes) { auto hasherSeed0 = std::make_unique(); std::vector hashes; hashes.resize(kNumValues); - hasherSeed0->hashes( + hasherSeed0->hashesByteArray( byteArrayVector.data(), static_cast(byteArrayVector.size()), hashes.data()); diff --git a/velox/dwio/parquet/tests/reader/ParquetReaderTest.cpp b/velox/dwio/parquet/tests/reader/ParquetReaderTest.cpp index 79251e2b381f..7c3cd6498297 100644 --- a/velox/dwio/parquet/tests/reader/ParquetReaderTest.cpp +++ b/velox/dwio/parquet/tests/reader/ParquetReaderTest.cpp @@ -57,9 +57,27 @@ class ParquetReaderTest : public ParquetTestBase { const RowVectorPtr& expected) { const auto filePath(getExampleFilePath(fileName)); dwio::common::ReaderOptions readerOpts{leafPool_.get()}; + assertReadWithFilters( + fileName, fileSchema, std::move(filters), expected, readerOpts); + } + + void assertReadWithFilters( + const std::string& fileName, + const RowTypePtr& fileSchema, + FilterMap filters, + const RowVectorPtr& expected, + const facebook::velox::dwio::common::ReaderOptions& readerOpts, + std::shared_ptr + runtimeStats = nullptr) { + const auto filePath(getExampleFilePath(fileName)); auto reader = createReader(filePath, readerOpts); assertReadWithReaderAndFilters( - std::move(reader), fileName, fileSchema, std::move(filters), expected); + std::move(reader), + fileName, + fileSchema, + std::move(filters), + expected, + runtimeStats); } }; @@ -837,6 +855,335 @@ TEST_F(ParquetReaderTest, intMultipleFilters) { "int.parquet", intSchema(), std::move(filters), expected); } +TEST_F(ParquetReaderTest, bloomFilterBigint) { + std::string fileName = "sample_int64_string_int32_bloom_1k.snappy.parquet"; + // Using the below row from the parquet file for testing + // 7824166607706395581 | c74ddef8-b260-44f9-8889-752b0aafb2c1 | 607 + auto schema = ROW( + {"id", "i64", "uuid", "i32"}, {BIGINT(), BIGINT(), VARCHAR(), INTEGER()}); + auto expected = makeRowVector({ + makeFlatVector(std::vector{958}), + makeFlatVector(std::vector{7824166607706395581}), + makeFlatVector({"c74ddef8-b260-44f9-8889-752b0aafb2c1"}), + makeFlatVector(std::vector{607}), + }); + + facebook::velox::dwio::common::ReaderOptions readerOpts{leafPool_.get()}; + readerOpts.setReadBloomFilter(true); + + // Test equality filter + FilterMap bigintFilters; + bigintFilters.insert({"i64", exec::equal(7824166607706395581)}); + ParquetReaderTest::assertReadWithFilters( + fileName, schema, std::move(bigintFilters), expected, readerOpts); + + // Test IN filter with at least one value present in the column value. + // 607 is present in the column, 2000 and 4000 are not present. + bigintFilters.insert({"i64", exec::in({7824166607706395581, 2000, 4000})}); + assertReadWithFilters( + fileName, schema, std::move(bigintFilters), expected, readerOpts); + + readerOpts.setReadBloomFilter(false); + // Test IN filter with values not present in the column but are within the + // stats min/max range. With bloom filter disabled, no row groups should be + // skipped. + bigintFilters.insert( + {"i64", exec::in({7824166607706395582, 7824166607706395590})}); + std::shared_ptr runtimeStats = + std::make_shared(); + assertReadWithFilters( + fileName, + schema, + std::move(bigintFilters), + makeRowVector({}), + readerOpts, + runtimeStats); + EXPECT_EQ(runtimeStats->skippedStrides, 0); + + readerOpts.setReadBloomFilter(true); + + // Test IN filter with values not present in the column but are within the + // stats min/max range. With bloom filter enabled, row groups should be + // skipped. + bigintFilters.insert( + {"i64", exec::in({7824166607706395582, 7824166607706395590})}); + std::shared_ptr runtimeStats1 = + std::make_shared(); + assertReadWithFilters( + fileName, + schema, + std::move(bigintFilters), + makeRowVector({}), + readerOpts, + runtimeStats1); + EXPECT_EQ(runtimeStats1->skippedStrides, 1); + + // Test BETWEEN filter, at least one value in the range is present in the + // column. + bigintFilters.insert( + {"i64", exec::between(7824166607706395581, 7824166607706395582)}); + assertReadWithFilters( + fileName, schema, std::move(bigintFilters), expected, readerOpts); + + readerOpts.setReadBloomFilter(false); + // Test BETWEEN filter, None of the values in the range are present in the + // column but are within stats min/max range. With bloom filter disabled, no + // row groups should be skipped + bigintFilters.insert( + {"i64", exec::between(7824166607706395582, 7824166607706395590)}); + std::shared_ptr runtimeStats2 = + std::make_shared(); + assertReadWithFilters( + fileName, + schema, + std::move(bigintFilters), + makeRowVector({}), + readerOpts, + runtimeStats2); + EXPECT_EQ(runtimeStats2->skippedStrides, 0); + + readerOpts.setReadBloomFilter(true); + // Test BETWEEN filter, None of the values in the range are present in the + // column but are within stats min/max range. With bloom filter enabled, row + // groups should be skipped + bigintFilters.insert( + {"i64", exec::between(7824166607706395582, 7824166607706395590)}); + std::shared_ptr runtimeStats3 = + std::make_shared(); + assertReadWithFilters( + fileName, + schema, + std::move(bigintFilters), + makeRowVector({}), + readerOpts, + runtimeStats3); + EXPECT_EQ(runtimeStats3->skippedStrides, 1); +} + +TEST_F(ParquetReaderTest, bloomFilterString) { + std::string fileName = "sample_int64_string_int32_bloom_1k.snappy.parquet"; + // Using the below row from the parquet file for testing + // 7824166607706395581 | c74ddef8-b260-44f9-8889-752b0aafb2c1 | 607 + auto schema = ROW( + {"id", "i64", "uuid", "i32"}, {BIGINT(), BIGINT(), VARCHAR(), INTEGER()}); + auto expected = makeRowVector({ + makeFlatVector(std::vector{958}), + makeFlatVector(std::vector{7824166607706395581}), + makeFlatVector({"c74ddef8-b260-44f9-8889-752b0aafb2c1"}), + makeFlatVector(std::vector{607}), + }); + + facebook::velox::dwio::common::ReaderOptions readerOpts{leafPool_.get()}; + readerOpts.setReadBloomFilter(true); + + // Test equality filter + FilterMap stringFilters; + stringFilters.insert( + {"uuid", exec::equal("c74ddef8-b260-44f9-8889-752b0aafb2c1")}); + assertReadWithFilters( + fileName, schema, std::move(stringFilters), expected, readerOpts); + + // Test IN filter with at least one value present in the column + stringFilters.insert( + {"uuid", + exec::in( + {"c74ddef8-b260-44f9-8889-752b0aafb2c1", + "not_exists1", + "not_exists2"})}); + assertReadWithFilters( + fileName, schema, std::move(stringFilters), expected, readerOpts); + + readerOpts.setReadBloomFilter(false); + // Test IN filter with none of the values present in the column. + // With bloom filter disabled, no row groups should be skipped. + stringFilters.insert( + {"uuid", + exec::in( + {"c74ddef8-b260-44f9-8889-notexists", + "not_exists1", + "not_exists2"})}); + std::shared_ptr runtimeStats = + std::make_shared(); + assertReadWithFilters( + fileName, + schema, + std::move(stringFilters), + makeRowVector({}), + readerOpts, + runtimeStats); + EXPECT_EQ(runtimeStats->skippedStrides, 0); + + readerOpts.setReadBloomFilter(true); + // Test IN filter with none of the values present in the column. + // With bloom filter enabled, row groups should be skipped. + stringFilters.insert( + {"uuid", + exec::in( + {"c74ddef8-b260-44f9-8889-notexists", + "not_exists1", + "not_exists2"})}); + std::shared_ptr runtimeStats1 = + std::make_shared(); + assertReadWithFilters( + fileName, + schema, + std::move(stringFilters), + makeRowVector({}), + readerOpts, + runtimeStats1); + EXPECT_EQ(runtimeStats1->skippedStrides, 1); + + // Test BETWEEN filter with lower and upper being same value. Bloom filter for + // string range is applicable only when lower = upper + stringFilters.insert( + {"uuid", + exec::between( + "c74ddef8-b260-44f9-8889-752b0aafb2c1", + "c74ddef8-b260-44f9-8889-752b0aafb2c1")}); + assertReadWithFilters( + fileName, schema, std::move(stringFilters), expected, readerOpts); + + // Test BETWEEN filter with lower = upper, value is not present in the column, + // but within stats min/max range.. With bloom filter disabled, no row groups + // should be skipped. + readerOpts.setReadBloomFilter(false); + stringFilters.insert( + {"uuid", + exec::between( + "c74ddef8-b260-44f9-8889-notb0aafb2c1", + "c74ddef8-b260-44f9-8889-notb0aafb2c1")}); + std::shared_ptr runtimeStats2 = + std::make_shared(); + assertReadWithFilters( + fileName, + schema, + std::move(stringFilters), + makeRowVector({}), + readerOpts, + runtimeStats2); + EXPECT_EQ(runtimeStats2->skippedStrides, 0); + + // Test BETWEEN filter with lower = upper, value is not present in the column, + // but within stats min/max range.. With bloom filter enabled, row groups + // should be skipped. + readerOpts.setReadBloomFilter(true); + stringFilters.insert( + {"uuid", + exec::between( + "c74ddef8-b260-44f9-8889-notb0aafb2c1", + "c74ddef8-b260-44f9-8889-notb0aafb2c1")}); + std::shared_ptr runtimeStats3 = + std::make_shared(); + assertReadWithFilters( + fileName, + schema, + std::move(stringFilters), + makeRowVector({}), + readerOpts, + runtimeStats3); + EXPECT_EQ(runtimeStats3->skippedStrides, 1); +} + +TEST_F(ParquetReaderTest, bloomFilterInteger) { + std::string fileName = "sample_int64_string_int32_bloom_1k.snappy.parquet"; + // Using the below row from the parquet file for testing + // 7824166607706395581 | c74ddef8-b260-44f9-8889-752b0aafb2c1 | 607 + auto schema = ROW( + {"id", "i64", "uuid", "i32"}, {BIGINT(), BIGINT(), VARCHAR(), INTEGER()}); + auto expected = makeRowVector({ + makeFlatVector(std::vector{958}), + makeFlatVector(std::vector{7824166607706395581}), + makeFlatVector({"c74ddef8-b260-44f9-8889-752b0aafb2c1"}), + makeFlatVector(std::vector{607}), + }); + + facebook::velox::dwio::common::ReaderOptions readerOpts{leafPool_.get()}; + readerOpts.setReadBloomFilter(true); + + FilterMap int32Filters; + // Test equality filter + int32Filters.insert({"i32", exec::equal(607)}); + assertReadWithFilters( + fileName, schema, std::move(int32Filters), expected, readerOpts); + + // Test IN filter with at least one value present in the column value. + // 607 is present in the column, 2000 and 4000 are not present. + int32Filters.insert({"i32", exec::in({607, 2000, 4000})}); + assertReadWithFilters( + fileName, schema, std::move(int32Filters), expected, readerOpts); + + readerOpts.setReadBloomFilter(false); + // Test IN filter with values not present in the column but are within the + // stats min/max range. With bloom filter disabled, no row groups should be + // skipped. + int32Filters.insert({"i32", exec::in({610, 4000})}); + std::shared_ptr runtimeStats = + std::make_shared(); + assertReadWithFilters( + fileName, + schema, + std::move(int32Filters), + makeRowVector({}), + readerOpts, + runtimeStats); + EXPECT_EQ(runtimeStats->skippedStrides, 0); + + readerOpts.setReadBloomFilter(true); + + // Test IN filter with values not present in the column but are within the + // stats min/max range. With bloom filter enabled, row groups should be + // skipped. + int32Filters.insert({"i32", exec::in({610, 4000})}); + std::shared_ptr runtimeStats1 = + std::make_shared(); + assertReadWithFilters( + fileName, + schema, + std::move(int32Filters), + makeRowVector({}), + readerOpts, + runtimeStats1); + EXPECT_EQ(runtimeStats1->skippedStrides, 1); + + // Test BETWEEN filter, at least one value in the range is present in the + // column. + int32Filters.insert({"i32", exec::between(607, 610)}); + assertReadWithFilters( + fileName, schema, std::move(int32Filters), expected, readerOpts); + + readerOpts.setReadBloomFilter(false); + // Test BETWEEN filter, None of the values in the range are present in the + // column but are within stats min/max range. With bloom filter disabled, no + // row groups should be skipped + int32Filters.insert({"i32", exec::between(1985, 1988)}); + std::shared_ptr runtimeStats2 = + std::make_shared(); + assertReadWithFilters( + fileName, + schema, + std::move(int32Filters), + makeRowVector({}), + readerOpts, + runtimeStats2); + EXPECT_EQ(runtimeStats2->skippedStrides, 0); + + readerOpts.setReadBloomFilter(true); + // Test BETWEEN filter, None of the values in the range are present in the + // column but are within stats min/max range. With bloom filter enabled, row + // groups should be skipped + int32Filters.insert({"i32", exec::between(1985, 1988)}); + std::shared_ptr runtimeStats3 = + std::make_shared(); + assertReadWithFilters( + fileName, + schema, + std::move(int32Filters), + makeRowVector({}), + readerOpts, + runtimeStats3); + EXPECT_EQ(runtimeStats3->skippedStrides, 1); +} + TEST_F(ParquetReaderTest, doubleFilters) { // Read sample.parquet with the double filter "b < 10.0". FilterMap filters; diff --git a/velox/type/Filter.cpp b/velox/type/Filter.cpp index d703946c6807..b556e096dc83 100644 --- a/velox/type/Filter.cpp +++ b/velox/type/Filter.cpp @@ -280,6 +280,35 @@ bool BigintRange::testingEquals(const Filter& other) const { (upper_ == otherBigintRange->upper_); } +bool BigintRange::testBloomFilter( + const AbstractBloomFilter& bloomFilter, + const velox::Type& type) const { + // Don't test bloom filter for a wider range + if ((upper_ - lower_) > kMaxBloomFilterChecks) + return true; + + switch (type.kind()) { + case TypeKind::INTEGER: + for (int32_t val = lower32_; val <= upper32_; ++val) { + if (bloomFilter.mightContainInt32(val)) { + return true; + } + } + break; + case TypeKind::BIGINT: + for (int64_t val = lower_; val <= upper_; ++val) { + if (bloomFilter.mightContainInt64(val)) { + return true; + } + } + break; + default: + return true; + } + + return false; +} + folly::dynamic NegatedBigintRange::serialize() const { auto obj = Filter::serializeBase("NegatedBigintRange"); obj["lower"] = nonNegated_->lower(); @@ -1846,6 +1875,7 @@ std::unique_ptr BigintRange::mergeWith(const Filter* other) const { case FilterKind::kNegatedBigintValuesUsingBitmask: case FilterKind::kNegatedBigintValuesUsingHashTable: { bool bothNullAllowed = nullAllowed_ && other->testNull(); + bool unused = false; if (!other->testInt64Range(lower_, upper_, false)) { return nullOrFalse(bothNullAllowed); } diff --git a/velox/type/Filter.h b/velox/type/Filter.h index f46b26c5f12a..1f95c8ff056c 100644 --- a/velox/type/Filter.h +++ b/velox/type/Filter.h @@ -60,6 +60,7 @@ enum class FilterKind { }; class Filter; +class AbstractBloomFilter; using FilterPtr = std::unique_ptr; using SubfieldFilters = std::unordered_map>; @@ -73,6 +74,8 @@ class Filter : public velox::ISerializable { Filter(bool deterministic, bool nullAllowed, FilterKind kind) : nullAllowed_(nullAllowed), deterministic_(deterministic), kind_(kind) {} + static constexpr int kMaxBloomFilterChecks = 10; + public: virtual ~Filter() = default; @@ -83,6 +86,12 @@ class Filter : public velox::ISerializable { // runtime. static constexpr bool deterministic = true; + virtual bool testBloomFilter( + const AbstractBloomFilter& bloomFilter, + const velox::Type& type) const { + return true; + } + FilterKind kind() const { return kind_; } @@ -293,6 +302,14 @@ class Filter : public velox::ISerializable { } }; +class AbstractBloomFilter { + public: + virtual bool mightContainInt32(int32_t value) const = 0; + virtual bool mightContainInt64(int64_t value) const = 0; + virtual bool mightContainString(const std::string& value) const = 0; + virtual ~AbstractBloomFilter() = default; +}; + /// TODO Check if this filter is needed. This should not be passed down. class AlwaysFalse final : public Filter { public: @@ -780,6 +797,10 @@ class BigintRange final : public Filter { return !(min > upper_ || max < lower_); } + bool testBloomFilter( + const AbstractBloomFilter& bloomFilter, + const velox::Type& type) const override; + int64_t lower() const { return lower_; } @@ -983,6 +1004,30 @@ class BigintValuesUsingHashTable final : public Filter { } } + bool testBloomFilter( + const AbstractBloomFilter& bloomFilter, + const velox::Type& type) const override { + // Limit checks to IN-list with upto 10 values. + if (values_.size() > kMaxBloomFilterChecks) { + return true; + } + // For IN-list, if any value matches, return true. + for (auto i = 0; i < values_.size() && i < 10; ++i) { + if (type.kind() == TypeKind::INTEGER) { + int32_t val = values_[i]; + if (bloomFilter.mightContainInt32(val)) { + return true; + } + } else if ( + type.kind() == TypeKind::BIGINT && + bloomFilter.mightContainInt64(values_[i])) { + return true; + } + } + + return false; + } + bool testInt64(int64_t value) const final; xsimd::batch_bool testValues(xsimd::batch) const final; xsimd::batch_bool testValues(xsimd::batch) const final; @@ -1123,6 +1168,31 @@ class BigintValuesUsingBitmask final : public Filter { bool testingEquals(const Filter& other) const final; + bool testBloomFilter( + const AbstractBloomFilter& bloomFilter, + const velox::Type& type) const override { + // Limit checks to only maxBloomFilterChecks values + if (bitmask_.size() > kMaxBloomFilterChecks) { + return true; + } + + for (int i = 0; i < bitmask_.size(); i++) { + if (bitmask_[i]) { + int64_t val = min_ + i; + // For IN-list, if any value matches, return true. + if (type.kind() == TypeKind::INTEGER && + bloomFilter.mightContainInt32((int32_t)val)) { + return true; + } else if ( + type.kind() == TypeKind::BIGINT && + bloomFilter.mightContainInt64(val)) { + return true; + } + } + } + return false; + } + private: std::unique_ptr mergeWith(int64_t min, int64_t max, const Filter* other) const; @@ -1660,6 +1730,15 @@ class BytesRange final : public AbstractRange { } } + bool testBloomFilter( + const AbstractBloomFilter& bloomFilter, + const velox::Type& type) const override { + if (!singleValue_) { + return true; + } + return bloomFilter.mightContainString(lower_); + } + std::string toString() const override { return fmt::format( "BytesRange: {}{}, {}{} {}", @@ -1944,6 +2023,23 @@ class BytesValues final : public Filter { } } + bool testBloomFilter( + const AbstractBloomFilter& bloomFilter, + const velox::Type& type) const override { + // For IN-list, if any value matches, return true. + // Limit checks to IN-list with only 10 values. + if (values_.size() > kMaxBloomFilterChecks) { + return true; + } + + for (auto it = values_.begin(); it != values_.end(); ++it) { + if (bloomFilter.mightContainString(*it)) { + return true; + } + } + return false; + } + bool testLength(int32_t length) const final { return lengths_.contains(length); } @@ -1994,6 +2090,29 @@ class BigintMultiRange final : public Filter { std::unique_ptr clone( std::optional nullAllowed = std::nullopt) const final; + bool testBloomFilter( + const AbstractBloomFilter& bloomFilter, + const velox::Type& type) const override { + int numRange = 0; + // Limit number of values to check to maxBloomFilterChecks. + std::for_each( + ranges_.begin(), + ranges_.end(), + [this, &numRange](const std::unique_ptr& range) { + numRange = numRange + range->upper() - range->lower() + 1; + }); + if (numRange > kMaxBloomFilterChecks) { + return true; + } + + for (auto& range : ranges_) { + if (range->testBloomFilter(bloomFilter, type)) { + return true; + } + } + return false; + } + bool testInt64(int64_t value) const final; bool testInt64Range(int64_t min, int64_t max, bool hasNull) const final;