Skip to content

Commit 322fd1b

Browse files
committed
subfield bloom filter
1 parent 6853c22 commit 322fd1b

4 files changed

Lines changed: 205 additions & 1 deletion

File tree

cpp/velox/operators/functions/SparkExprToSubfieldFilterParser.cc

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,104 @@
1616
*/
1717
#include "operators/functions/SparkExprToSubfieldFilterParser.h"
1818

19+
#include "utils/Exception.h"
20+
#include "velox/common/base/BloomFilter.h"
21+
#include "velox/functions/sparksql/XxHash64.h"
22+
#include "velox/expression/Expr.h"
23+
#include "velox/vector/ComplexVector.h"
24+
1925
namespace gluten {
2026

2127
using namespace facebook::velox;
2228

2329
namespace {
30+
31+
// Evaluates an expression as a constant. Returns nullptr if the expression is
32+
// not constant or evaluation fails. Errors are intentionally swallowed because
33+
// a non-evaluable expression simply means the filter cannot be pushed down.
34+
VectorPtr toConstant(const core::TypedExprPtr& expr, core::ExpressionEvaluator* evaluator) {
35+
auto exprSet = evaluator->compile(expr);
36+
if (!exprSet->exprs()[0]->isConstantExpr()) {
37+
return nullptr;
38+
}
39+
RowVector input(evaluator->pool(), ROW({}, {}), nullptr, 1, std::vector<VectorPtr>{});
40+
SelectivityVector rows(1);
41+
VectorPtr result;
42+
try {
43+
evaluator->evaluate(exprSet.get(), rows, input, result);
44+
} catch (const VeloxUserError&) {
45+
return nullptr;
46+
}
47+
return result;
48+
}
49+
50+
/// Subfield filter backed by Velox's BloomFilter from bloom_filter_agg / might_contain.
51+
/// Values are hashed with Spark-compatible XXH64 using the seed extracted from
52+
/// xxhash64_with_seed, then re-hashed with folly hasher for bloom filter bucket
53+
/// selection, matching bloom_filter_agg's insertion path.
54+
class SparkMightContain final : public common::BigintValuesUsingBloomFilter {
55+
public:
56+
SparkMightContain(VectorPtr constantVector, bool nullAllowed, int64_t seed)
57+
: common::BigintValuesUsingBloomFilter(0, nullAllowed),
58+
constantVector_(std::move(constantVector)),
59+
seed_(seed) {
60+
auto sv = constantVector_->as<SimpleVector<StringView>>()->valueAt(0);
61+
view_ = std::make_unique<BloomFilterView>(sv.data());
62+
}
63+
64+
bool testInt64(int64_t value) const override {
65+
return view_->mayContain(folly::hasher<int64_t>()(
66+
facebook::velox::functions::sparksql::XxHash64::hashInt64(value, seed_)));
67+
}
68+
69+
bool testDouble(double value) const override {
70+
return view_->mayContain(folly::hasher<int64_t>()(
71+
facebook::velox::functions::sparksql::XxHash64::hashDouble(value, seed_)));
72+
}
73+
74+
bool testFloat(float value) const override {
75+
return view_->mayContain(folly::hasher<int64_t>()(
76+
facebook::velox::functions::sparksql::XxHash64::hashFloat(value, seed_)));
77+
}
78+
79+
bool testBytes(const char* data, int32_t len) const override {
80+
return view_->mayContain(folly::hasher<int64_t>()(
81+
facebook::velox::functions::sparksql::XxHash64::hashBytes(StringView(data, len), seed_)));
82+
}
83+
84+
bool testTimestamp(const Timestamp& value) const override {
85+
return view_->mayContain(folly::hasher<int64_t>()(
86+
facebook::velox::functions::sparksql::XxHash64::hashTimestamp(value, seed_)));
87+
}
88+
89+
bool testInt128(const int128_t& value) const override {
90+
return view_->mayContain(folly::hasher<int64_t>()(
91+
facebook::velox::functions::sparksql::XxHash64::hashLongDecimal(value, seed_)));
92+
}
93+
94+
bool testInt64Range(int64_t /*min*/, int64_t /*max*/, bool /*hasNull*/) const override {
95+
return true;
96+
}
97+
98+
std::unique_ptr<Filter> clone(std::optional<bool> nullAllowed) const override {
99+
return std::make_unique<SparkMightContain>(
100+
constantVector_, nullAllowed.value_or(nullAllowed_), seed_);
101+
}
102+
103+
bool testingEquals(const Filter& other) const override {
104+
return dynamic_cast<const SparkMightContain*>(&other) != nullptr;
105+
}
106+
107+
folly::dynamic serialize() const override {
108+
VELOX_UNSUPPORTED("Serialization is not supported for SparkMightContain");
109+
}
110+
111+
private:
112+
VectorPtr constantVector_;
113+
std::unique_ptr<BloomFilterView> view_;
114+
int64_t seed_;
115+
};
116+
24117
std::optional<std::pair<facebook::velox::common::Subfield, std::unique_ptr<facebook::velox::common::Filter>>> combine(
25118
facebook::velox::common::Subfield& subfield,
26119
std::unique_ptr<facebook::velox::common::Filter>& filter) {
@@ -30,6 +123,7 @@ std::optional<std::pair<facebook::velox::common::Subfield, std::unique_ptr<faceb
30123

31124
return std::nullopt;
32125
}
126+
33127
} // namespace
34128

35129
std::optional<std::pair<facebook::velox::common::Subfield, std::unique_ptr<facebook::velox::common::Filter>>>
@@ -93,6 +187,34 @@ SparkExprToSubfieldFilterParser::leafCallToSubfieldFilter(
93187
}
94188
return std::make_pair(std::move(subfield), facebook::velox::exec::isNotNull());
95189
}
190+
} else if (call.name() == "might_contain" && !negated) {
191+
// Matches: might_contain(bloomFilter, xxhash64_with_seed(seed, field)).
192+
GLUTEN_CHECK(call.inputs().size() == 2,
193+
"might_contain expects 2 arguments: bloomFilter and xxhash64_with_seed(seed, field)");
194+
const auto* hashCall =
195+
dynamic_cast<const core::CallTypedExpr*>(call.inputs()[1].get());
196+
if (hashCall && hashCall->name() == "xxhash64_with_seed") {
197+
GLUTEN_CHECK(hashCall->inputs().size() == 2, "xxhash64_with_seed expects 2 arguments");
198+
auto seedValue = toConstant(hashCall->inputs()[0], evaluator);
199+
if (!seedValue || seedValue->isNullAt(0)) {
200+
LOG(WARNING) << "might_contain: seed value is null or not constant, "
201+
<< "cannot push down to subfield filter";
202+
return std::nullopt;
203+
}
204+
auto seed = seedValue->as<SimpleVector<int64_t>>()->valueAt(0);
205+
if (!toSubfield(hashCall->inputs()[1].get(), subfield)) {
206+
LOG(WARNING) << "might_contain: second argument to xxhash64_with_seed "
207+
<< "is not a subfield, cannot push down to subfield filter";
208+
return std::nullopt;
209+
}
210+
auto bloomFilterValue = toConstant(call.inputs()[0], evaluator);
211+
if (bloomFilterValue && !bloomFilterValue->isNullAt(0)) {
212+
std::unique_ptr<common::Filter> filter =
213+
std::make_unique<SparkMightContain>(bloomFilterValue, false /*nullAllowed*/, seed);
214+
return combine(subfield, filter);
215+
}
216+
}
217+
LOG(WARNING) << "might_contain could not be converted to a subfield filter";
96218
}
97219
return std::nullopt;
98220
}

ep/build-velox/src/get-velox.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,17 @@ function apply_compilation_fixes {
166166
git add ${VELOX_HOME}/CMake/resolve_dependency_modules/arrow/modify_arrow.patch # to avoid the file from being deleted by git clean -dffx :/
167167
}
168168

169+
function apply_bloomfilter_final_patch {
170+
echo "Applying bloom filter final-removal patch..."
171+
pushd $VELOX_HOME
172+
git apply --check ${CURRENT_DIR}/remove-bloomfilter-final.patch && \
173+
git apply ${CURRENT_DIR}/remove-bloomfilter-final.patch || {
174+
echo "Failed to apply bloom filter final-removal patch"
175+
exit 1
176+
}
177+
popd
178+
}
179+
169180
function setup_linux {
170181
local LINUX_DISTRIBUTION=$(. /etc/os-release && echo ${ID})
171182
local LINUX_VERSION_ID=$(. /etc/os-release && echo ${VERSION_ID})
@@ -248,4 +259,6 @@ apply_provided_velox_patch
248259

249260
apply_compilation_fixes
250261

262+
apply_bloomfilter_final_patch
263+
251264
echo "Finished getting Velox code"
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
diff --git a/velox/type/Filter.h b/velox/type/Filter.h
2+
index 82ecd8eb2..cc8e6b31b 100644
3+
--- a/velox/type/Filter.h
4+
+++ b/velox/type/Filter.h
5+
@@ -1290,7 +1290,7 @@ class BigintValuesUsingBitmask final : public Filter {
6+
const int64_t max_;
7+
};
8+
9+
-class BigintValuesUsingBloomFilter final : public Filter {
10+
+class BigintValuesUsingBloomFilter : public Filter {
11+
public:
12+
static int64_t numBlocks(int64_t capacity) {
13+
return SplitBlockBloomFilter::numBlocks(capacity, 0.01);
14+
@@ -1301,7 +1301,7 @@ class BigintValuesUsingBloomFilter final : public Filter {
15+
blocks_(numBlocks(capacity)),
16+
filter_(blocks_) {}
17+
18+
- bool testInt64(int64_t value) const final {
19+
+ bool testInt64(int64_t value) const override {
20+
return filter_.mayContain(hash(value));
21+
}
22+
23+
@@ -1318,7 +1318,7 @@ class BigintValuesUsingBloomFilter final : public Filter {
24+
}
25+
26+
bool testInt64Range(int64_t /*min*/, int64_t /*max*/, bool /*hasNull*/)
27+
- const final {
28+
+ const override {
29+
return true;
30+
}
31+

gluten-ut/spark35/src/test/scala/org/apache/spark/sql/GlutenBloomFilterAggregateQuerySuite.scala

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ package org.apache.spark.sql
1818

1919
import org.apache.gluten.backendsapi.BackendsApiManager
2020
import org.apache.gluten.config.GlutenConfig
21-
import org.apache.gluten.execution.HashAggregateExecBaseTransformer
21+
import org.apache.gluten.execution.{FileSourceScanExecTransformer, HashAggregateExecBaseTransformer}
2222

2323
import org.apache.spark.SparkConf
2424
import org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanHelper
@@ -112,6 +112,44 @@ class GlutenBloomFilterAggregateQuerySuite
112112
}
113113
}
114114

115+
testGluten("Test might_contain constant bloom filter pushdown to scan") {
116+
val table = "bloom_filter_test"
117+
withTable(table) {
118+
(1L to 200L).toDF("col").write.format("parquet").saveAsTable(table)
119+
120+
// Build a bloom filter over xxhash64(col) so it matches the probe side.
121+
val hexBf = spark.sql(
122+
s"SELECT hex(bloom_filter_agg(xxhash64(col), " +
123+
s"cast(200 as long), cast($veloxBloomFilterMaxNumBits as long))) " +
124+
s"FROM $table")
125+
.collect()(0).getString(0)
126+
127+
val sqlString =
128+
s"SELECT * FROM $table WHERE might_contain(X'$hexBf', xxhash64(col))"
129+
130+
val df = spark.sql(sqlString)
131+
val result = df.collect()
132+
// All 200 rows should match since the bloom filter covers them.
133+
assert(result.length == 200)
134+
135+
// Verify the filter was pushed to the scan.
136+
val scans = collect(df.queryExecution.executedPlan) {
137+
case s: FileSourceScanExecTransformer => s
138+
}
139+
assert(scans.nonEmpty)
140+
scans.foreach { scan =>
141+
val metrics = scan.metrics
142+
assert(metrics.contains("rawInputRows"))
143+
assert(metrics.contains("numOutputRows"))
144+
assert(metrics.contains("remainingFilterTime"))
145+
assert(metrics("rawInputRows").value > 0)
146+
assert(metrics("numOutputRows").value > 0)
147+
assert(metrics("numOutputRows").value < metrics("rawInputRows").value)
148+
assert(metrics("remainingFilterTime").value == 0)
149+
}
150+
}
151+
}
152+
115153
testGluten("Test bloom_filter_agg agg fallback") {
116154
val table = "bloom_filter_test"
117155
val numEstimatedItems = 5000000L

0 commit comments

Comments
 (0)