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

zhztheplayer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git


The following commit(s) were added to refs/heads/main by this push:
     new 533fe85c0a [GLUTEN-11708][VL] Enable Bloom filter might_contain to 
subfield filter translation (#12793)
533fe85c0a is described below

commit 533fe85c0a9fdf70226b18400ce382c28eb8bced
Author: Hongze Zhang <[email protected]>
AuthorDate: Mon Aug 17 19:25:09 2026 +0100

    [GLUTEN-11708][VL] Enable Bloom filter might_contain to subfield filter 
translation (#12793)
---
 .../org/apache/gluten/config/VeloxConfig.scala     |   8 ++
 cpp/velox/compute/VeloxBackend.cc                  |   3 +-
 cpp/velox/config/VeloxConfig.h                     |   3 +
 .../functions/SparkExprToSubfieldFilterParser.cc   | 109 +++++++++++++++++++++
 .../functions/SparkExprToSubfieldFilterParser.h    |   6 ++
 docs/velox-configuration.md                        |   1 +
 6 files changed, 129 insertions(+), 1 deletion(-)

diff --git 
a/backends-velox/src/main/scala/org/apache/gluten/config/VeloxConfig.scala 
b/backends-velox/src/main/scala/org/apache/gluten/config/VeloxConfig.scala
index d0e55c2238..802a47cadd 100644
--- a/backends-velox/src/main/scala/org/apache/gluten/config/VeloxConfig.scala
+++ b/backends-velox/src/main/scala/org/apache/gluten/config/VeloxConfig.scala
@@ -113,6 +113,8 @@ class VeloxConfig(conf: SQLConf) extends GlutenConfig(conf) 
{
 
   def hashProbeBloomFilterBypassMinPct: Int = 
getConf(HASH_PROBE_BLOOM_FILTER_BYPASS_MIN_PCT)
 
+  def scanBloomFilterPushdownEnabled: Boolean = 
getConf(SCAN_BLOOM_FILTER_PUSHDOWN_ENABLED)
+
   def enableTimestampNtzValidation: Boolean = 
getConf(ENABLE_TIMESTAMP_NTZ_VALIDATION)
 
   def enableDriverSideBroadcastHashTableBuild: Boolean =
@@ -550,6 +552,12 @@ object VeloxConfig extends ConfigRegistry {
       .checkValue(value => value >= 0 && value <= 100, "The percentage must be 
in [0, 100]")
       .createWithDefault(85)
 
+  val SCAN_BLOOM_FILTER_PUSHDOWN_ENABLED =
+    
buildStaticConf("spark.gluten.sql.columnar.backend.velox.scan.bloomFilterPushdown.enabled")
+      .doc("Whether to push Bloom filters into Velox scans.")
+      .booleanConf
+      .createWithDefault(false)
+
   val COLUMNAR_VELOX_FILE_HANDLE_CACHE_ENABLED =
     
buildStaticConf("spark.gluten.sql.columnar.backend.velox.fileHandleCacheEnabled")
       .doc(
diff --git a/cpp/velox/compute/VeloxBackend.cc 
b/cpp/velox/compute/VeloxBackend.cc
index 3b735601a1..074aec9df4 100644
--- a/cpp/velox/compute/VeloxBackend.cc
+++ b/cpp/velox/compute/VeloxBackend.cc
@@ -260,7 +260,8 @@ void VeloxBackend::init(
   velox::parquet::registerParquetReaderFactory();
   velox::parquet::registerParquetWriterFactory();
   velox::orc::registerOrcReaderFactory();
-  
velox::exec::ExprToSubfieldFilterParser::registerParser(std::make_unique<SparkExprToSubfieldFilterParser>());
+  
velox::exec::ExprToSubfieldFilterParser::registerParser(std::make_unique<SparkExprToSubfieldFilterParser>(
+      backendConf_->get<bool>(kScanBloomFilterPushdownEnabled, 
kScanBloomFilterPushdownEnabledDefault)));
   
velox::connector::hive::BufferedInputBuilder::registerBuilder(std::make_shared<GlutenBufferedInputBuilder>());
 
   // Register Velox functions
diff --git a/cpp/velox/config/VeloxConfig.h b/cpp/velox/config/VeloxConfig.h
index 7dde032d43..c0ae4b3ff8 100644
--- a/cpp/velox/config/VeloxConfig.h
+++ b/cpp/velox/config/VeloxConfig.h
@@ -84,6 +84,9 @@ const std::string kSparkBloomFilterExpectedNumItems = 
"spark.sql.optimizer.runti
 const std::string kSparkBloomFilterNumBits = 
"spark.sql.optimizer.runtime.bloomFilter.numBits";
 const std::string kSparkBloomFilterMaxNumBits = 
"spark.sql.optimizer.runtime.bloomFilter.maxNumBits";
 const std::string kSparkBloomFilterMaxNumItems = 
"spark.sql.optimizer.runtime.bloomFilter.maxNumItems";
+const std::string kScanBloomFilterPushdownEnabled =
+    "spark.gluten.sql.columnar.backend.velox.scan.bloomFilterPushdown.enabled";
+const bool kScanBloomFilterPushdownEnabledDefault = false;
 const std::string kVeloxSplitPreloadPerDriver = 
"spark.gluten.sql.columnar.backend.velox.SplitPreloadPerDriver";
 
 const std::string kHashProbeDynamicFilterPushdownEnabled =
diff --git a/cpp/velox/operators/functions/SparkExprToSubfieldFilterParser.cc 
b/cpp/velox/operators/functions/SparkExprToSubfieldFilterParser.cc
index 20baaa413e..31d44211cd 100644
--- a/cpp/velox/operators/functions/SparkExprToSubfieldFilterParser.cc
+++ b/cpp/velox/operators/functions/SparkExprToSubfieldFilterParser.cc
@@ -16,11 +16,83 @@
  */
 #include "operators/functions/SparkExprToSubfieldFilterParser.h"
 
+#include "utils/Exception.h"
+#include "velox/common/base/BloomFilter.h"
+#include "velox/expression/Expr.h"
+#include "velox/functions/sparksql/XxHash64.h"
+#include "velox/vector/ComplexVector.h"
+
 namespace gluten {
 
 using namespace facebook::velox;
 
 namespace {
+
+// Evaluates an expression as a constant. Returns nullptr if the expression is
+// not constant or evaluation fails. Errors are intentionally swallowed because
+// a non-evaluable expression simply means the filter cannot be pushed down.
+VectorPtr toConstant(const core::TypedExprPtr& expr, 
core::ExpressionEvaluator* evaluator) {
+  auto exprSet = evaluator->compile(expr);
+  if (!exprSet->exprs()[0]->isConstantExpr()) {
+    return nullptr;
+  }
+  RowVector input(evaluator->pool(), ROW({}, {}), nullptr, 1, 
std::vector<VectorPtr>{});
+  SelectivityVector rows(1);
+  VectorPtr result;
+  try {
+    evaluator->evaluate(exprSet.get(), rows, input, result);
+  } catch (const VeloxUserError& error) {
+    VLOG(1) << "Failed to evaluate constant expression for scan filter 
pushdown: " << error.what();
+    return nullptr;
+  }
+  return result;
+}
+
+/// Subfield filter backed by Velox's BloomFilter from bloom_filter_agg / 
might_contain.
+/// Values are hashed with Spark-compatible XXH64 using the seed extracted from
+/// xxhash64_with_seed, then re-hashed with folly hasher for bloom filter 
bucket
+/// selection, matching bloom_filter_agg's insertion path.
+template <bool kIsInt32>
+class SparkMightContain final : public common::BigintValuesUsingBloomFilter {
+ public:
+  SparkMightContain(VectorPtr constantVector, bool nullAllowed, int64_t seed)
+      : common::BigintValuesUsingBloomFilter(0, nullAllowed), 
constantVector_(std::move(constantVector)), seed_(seed) {
+    auto sv = constantVector_->as<SimpleVector<StringView>>()->valueAt(0);
+    view_ = std::make_unique<BloomFilterView>(sv.data());
+  }
+
+  bool testInt64(int64_t value) const override {
+    uint64_t hash;
+    if constexpr (kIsInt32) {
+      hash = 
functions::sparksql::XxHash64::hashInt32(static_cast<int32_t>(value), seed_);
+    } else {
+      hash = functions::sparksql::XxHash64::hashInt64(value, seed_);
+    }
+    return view_->mayContain(folly::hasher<int64_t>()(hash));
+  }
+
+  bool testInt64Range(int64_t /*min*/, int64_t /*max*/, bool /*hasNull*/) 
const override {
+    return true;
+  }
+
+  std::unique_ptr<Filter> clone(std::optional<bool> nullAllowed) const 
override {
+    return std::make_unique<SparkMightContain<kIsInt32>>(constantVector_, 
nullAllowed.value_or(nullAllowed_), seed_);
+  }
+
+  bool testingEquals(const Filter& other) const override {
+    return dynamic_cast<const SparkMightContain<kIsInt32>*>(&other) != nullptr;
+  }
+
+  folly::dynamic serialize() const override {
+    VELOX_UNSUPPORTED("Serialization is not supported for SparkMightContain");
+  }
+
+ private:
+  VectorPtr constantVector_;
+  std::unique_ptr<BloomFilterView> view_;
+  int64_t seed_;
+};
+
 std::optional<std::pair<facebook::velox::common::Subfield, 
std::unique_ptr<facebook::velox::common::Filter>>> combine(
     facebook::velox::common::Subfield& subfield,
     std::unique_ptr<facebook::velox::common::Filter>& filter) {
@@ -30,6 +102,7 @@ std::optional<std::pair<facebook::velox::common::Subfield, 
std::unique_ptr<faceb
 
   return std::nullopt;
 }
+
 } // namespace
 
 std::optional<std::pair<facebook::velox::common::Subfield, 
std::unique_ptr<facebook::velox::common::Filter>>>
@@ -93,6 +166,42 @@ SparkExprToSubfieldFilterParser::leafCallToSubfieldFilter(
       }
       return std::make_pair(std::move(subfield), 
facebook::velox::exec::isNotNull());
     }
+  } else if (scanBloomFilterPushdownEnabled_ && call.name() == "might_contain" 
&& !negated) {
+    // Matches: might_contain(bloomFilter, xxhash64_with_seed(seed, field)).
+    GLUTEN_CHECK(
+        call.inputs().size() == 2,
+        "might_contain expects 2 arguments: bloomFilter and 
xxhash64_with_seed(seed, field)");
+    const auto* hashCall = dynamic_cast<const 
core::CallTypedExpr*>(call.inputs()[1].get());
+    if (hashCall && hashCall->name() == "xxhash64_with_seed") {
+      GLUTEN_CHECK(hashCall->inputs().size() == 2, "xxhash64_with_seed expects 
2 arguments");
+      const auto inputTypeKind = hashCall->inputs()[1]->type()->kind();
+      if (inputTypeKind != TypeKind::INTEGER && inputTypeKind != 
TypeKind::BIGINT) {
+        return std::nullopt;
+      }
+      auto seedValue = toConstant(hashCall->inputs()[0], evaluator);
+      if (!seedValue || seedValue->isNullAt(0)) {
+        LOG(WARNING) << "might_contain: seed value is null or not constant, "
+                     << "cannot push down to subfield filter";
+        return std::nullopt;
+      }
+      auto seed = seedValue->as<SimpleVector<int64_t>>()->valueAt(0);
+      if (!toSubfield(hashCall->inputs()[1].get(), subfield)) {
+        LOG(WARNING) << "might_contain: second argument to xxhash64_with_seed "
+                     << "is not a subfield, cannot push down to subfield 
filter";
+        return std::nullopt;
+      }
+      auto bloomFilterValue = toConstant(call.inputs()[0], evaluator);
+      if (bloomFilterValue && !bloomFilterValue->isNullAt(0)) {
+        std::unique_ptr<common::Filter> filter;
+        if (inputTypeKind == TypeKind::INTEGER) {
+          filter = std::make_unique<SparkMightContain<true>>(bloomFilterValue, 
false /*nullAllowed*/, seed);
+        } else {
+          filter = 
std::make_unique<SparkMightContain<false>>(bloomFilterValue, false 
/*nullAllowed*/, seed);
+        }
+        return combine(subfield, filter);
+      }
+    }
+    LOG(WARNING) << "might_contain could not be converted to a subfield 
filter";
   }
   return std::nullopt;
 }
diff --git a/cpp/velox/operators/functions/SparkExprToSubfieldFilterParser.h 
b/cpp/velox/operators/functions/SparkExprToSubfieldFilterParser.h
index 0f7d826446..0f28d30f6a 100644
--- a/cpp/velox/operators/functions/SparkExprToSubfieldFilterParser.h
+++ b/cpp/velox/operators/functions/SparkExprToSubfieldFilterParser.h
@@ -23,11 +23,17 @@ namespace gluten {
 /// 2) The supported functions vary.
 class SparkExprToSubfieldFilterParser : public 
facebook::velox::exec::ExprToSubfieldFilterParser {
  public:
+  explicit SparkExprToSubfieldFilterParser(bool scanBloomFilterPushdownEnabled)
+      : scanBloomFilterPushdownEnabled_(scanBloomFilterPushdownEnabled) {}
+
   std::optional<std::pair<facebook::velox::common::Subfield, 
std::unique_ptr<facebook::velox::common::Filter>>>
   leafCallToSubfieldFilter(
       const facebook::velox::core::CallTypedExpr& call,
       facebook::velox::core::ExpressionEvaluator* evaluator,
       bool negated) override;
+
+ private:
+  const bool scanBloomFilterPushdownEnabled_;
 };
 
 } // namespace gluten
diff --git a/docs/velox-configuration.md b/docs/velox-configuration.md
index 5a0a9bf498..712260f984 100644
--- a/docs/velox-configuration.md
+++ b/docs/velox-configuration.md
@@ -75,6 +75,7 @@ nav_order: 16
 | spark.gluten.sql.columnar.backend.velox.resizeBatches.shuffleInput.minSize   
    | 🔄 Dynamic    | &lt;undefined&gt; | The minimum batch size for shuffle. If 
size of an input batch is smaller than the value, it will be combined with 
other batches before sending to shuffle. Only functions when 
spark.gluten.sql.columnar.backend.velox.resizeBatches.shuffleInput is set to 
true. Default value: 0.25 * <max batch size>                                    
                                         [...]
 | 
spark.gluten.sql.columnar.backend.velox.resizeBatches.shuffleInputOutput.minSize
 | 🔄 Dynamic    | &lt;undefined&gt; | The minimum batch size for shuffle input 
and output. If size of an input batch is smaller than the value, it will be 
combined with other batches before sending to shuffle. The same applies for 
batches output by shuffle read. Only functions when 
spark.gluten.sql.columnar.backend.velox.resizeBatches.shuffleInput or 
spark.gluten.sql.columnar.backend.velox.resizeBatches.shu [...]
 | spark.gluten.sql.columnar.backend.velox.resizeBatches.shuffleOutput          
    | 🔄 Dynamic    | false             | If true, combine small columnar 
batches together right after shuffle read. The default minimum output batch 
size is equal to 0.25 * spark.gluten.sql.columnar.maxBatchSize                  
                                                                                
                                                                                
                         [...]
+| spark.gluten.sql.columnar.backend.velox.scan.bloomFilterPushdown.enabled     
    | âš“ Static      | false             | Whether to push Bloom filters into 
Velox scans.                                                                    
                                                                                
                                                                                
                                                                                
                 [...]
 | spark.gluten.sql.columnar.backend.velox.showTaskMetricsWhenFinished          
    | 🔄 Dynamic    | false             | Show velox full task metrics when 
finished.                                                                       
                                                                                
                                                                                
                                                                                
                   [...]
 | spark.gluten.sql.columnar.backend.velox.spillFileSystem                      
    | 🔄 Dynamic    | local             | The filesystem used to store spill 
data. local: The local file system. heap-over-local: Write file to JVM heap if 
having extra heap space. Otherwise write to local file system.                  
                                                                                
                                                                                
                   [...]
 | spark.gluten.sql.columnar.backend.velox.spillStrategy                        
    | 🔄 Dynamic    | auto              | none: Disable spill on Velox backend; 
auto: Let Spark memory manager manage Velox's spilling                          
                                                                                
                                                                                
                                                                                
               [...]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to