sunchao opened a new issue, #5912:
URL: https://github.com/apache/datafusion-comet/issues/5912
### What is the problem the feature request solves?
Queries sometimes test a string column for any of several literal substrings:
```sql
SELECT id
FROM events
WHERE contains(message, 'timeout')
OR contains(message, 'connection reset')
OR contains(message, 'permission denied')
OR contains(message, 'rate limit');
```
Each individual `Contains` is already native. In the current implementation,
[Comet's scalar-needle
path](https://github.com/apache/datafusion-comet/blob/451c99963206fa6bf0387239aa12887a16255516/native/spark-expr/src/string_funcs/contains.rs#L75)
passes a scalar directly to Arrow's optimized `contains` kernel instead of
expanding the needle into a full array. However, the alternatives remain
separate searches of the same input.
[CometOr](https://github.com/apache/datafusion-comet/blob/451c99963206fa6bf0387239aa12887a16255516/spark/src/main/scala/org/apache/comet/serde/predicates.scala#L102)
flattens and balances the OR tree; it does not combine the searches.
For a large string and many absent or late-matching needles, repeated
searches can revisit the same bytes and produce intermediate Boolean arrays. A
prepared matcher for a fixed set of literal needles could answer “does any
needle occur?” with less repeated work. Log/event filtering and text
categorization are useful synthetic workload shapes for evaluating this
opportunity.
This is a performance hypothesis, not a demonstrated regression or a claim
that every pair of `Contains` expressions should be fused. The current
single-pattern kernels are optimized, and DataFusion's [OR
evaluation](https://github.com/apache/datafusion/blob/55.1.0/datafusion/physical-expr/src/expressions/binary.rs#L540)
can avoid work when earlier alternatives match. Preparation cost and
early-match behavior must be included in the comparison.
### Describe the potential solution
Add a narrowly scoped optimization for an OR subtree made entirely of
eligible literal `Contains` predicates on the same input attribute. Keep the
Spark SQL expression unchanged for users; lower the eligible subtree to one
native matcher during Comet planning.
#### Initial eligibility
- Every leaf in the selected OR subtree is `Contains(attribute,
nonNullStringLiteral)`.
- Every leaf refers to the **same bound input attribute**, not merely the
same column name. Restrict the input to an `AttributeReference`/its
bound-column representation initially. Do not generalize to repeated casts,
UDFs, nested-field expressions, or other computed children.
- All operands use Spark's `UTF8_BINARY` semantics. On Spark versions with
collations, retain the existing [CometContains collation
check](https://github.com/apache/datafusion-comet/blob/451c99963206fa6bf0387239aa12887a16255516/spark/src/main/scala/org/apache/comet/serde/strings.scala#L365).
- Ordinary expression enablement and native-support checks must already have
succeeded for `Or` and every `Contains`. In particular, fusion must not bypass
`spark.comet.expression.Contains.enabled=false` or turn a JVM-dispatched
collated predicate into a native byte comparison. The existing [expression
admission
path](https://github.com/apache/datafusion-comet/blob/451c99963206fa6bf0387239aa12887a16255516/spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala#L938)
enforces these checks.
- Pattern count and total literal bytes must fall within bounded limits, and
the pattern set must meet a benchmark-supported threshold for fusion. Two
alternatives are a correctness-eligible shape, not necessarily a profitable one.
Match a complete eligible subtree. Do not collect `Contains` leaves across
unrelated OR operands or move them across `AND`, `IF`, `CASE`, or other
evaluation boundaries. A surrounding expression must retain its existing
evaluation and short-circuit behavior.
#### Implementation approaches
Two reasonable locations are:
1. **Native planning after normal Scala admission:** recognize the
serialized/native OR tree whose leaves are the admitted Comet `contains`
function with a common bound column and literal needles. Build the prepared
matcher in place of that subtree. This avoids bypassing Scala's
individual-expression gates and may avoid a new protobuf expression kind.
2. **A dedicated serde lowering:** recognize the restricted shape in
`CometOr` and emit a private native expression/function representation. This
approach must explicitly preserve the normal admission checks for every
consumed leaf, along with explain/coverage attribution and the original
fallback path.
Either approach should preserve the original OR implementation when fusion
is declined. Avoid repeatedly flattening and compiling overlapping subtrees in
a deep OR chain; prepare one matcher for the selected group.
For the native implementation, evaluate a prepared multiple-literal
searcher, such as an Aho–Corasick matcher or another suitable byte-oriented
algorithm. The algorithm choice should follow benchmarks. Do not translate the
literals into user-facing regular-expression semantics.
The matcher should be prepared once per native planned expression and reused
across batches. Own the literal data and compiled search state for that
expression's lifetime; do not retain input batch buffers or add an unbounded
executor-global cache. Bound both accepted pattern data and compiled-state
growth, and release the state with its owning plan. An over-limit or
recoverably unsupported preparation attempt should use the ordinary OR path
rather than introduce a new query failure. This does not require recovering
from process-level allocation failure.
#### Required semantics
The result remains Spark's nullable Boolean result:
| Input / pattern set | Result |
| --- | --- |
| Null input with any eligible non-null literals | `NULL` |
| Non-null input with at least one matching literal | `TRUE` |
| Non-null input with no matching literals | `FALSE` |
| Non-null input with an empty-string literal among the alternatives |
`TRUE` |
| Null input with an empty-string literal among the alternatives | `NULL` |
Null literals are outside the initial eligibility rule; leave those
expressions on the existing path. Empty, duplicate, overlapping, and
prefix-related literals must preserve results. An empty literal must not
simplify the result to an unconditional `TRUE`, because the input can be null.
Match case-sensitively under `UTF8_BINARY`, including multibyte text and
embedded NUL bytes. Do not add Unicode normalization or case folding. Keep row
boundaries intact: searching Arrow's concatenated value buffer must not
manufacture a match spanning two different rows. Preserve slices, null offsets,
and every input representation admitted to the fused path; retain the existing
path for representations not supported initially.
#### Validation and acceptance criteria
- [ ] Compare fused, unfused Comet, and Spark results for eligible shapes in
both filters and projections. Include nullable input, empty strings/literals,
duplicate and overlapping needles, match at the start/end, longer-than-input
needles, multibyte text, embedded NULs, and multiple batches.
- [ ] Verify the plan contains one prepared native matcher for an eligible
group and that preparation is not repeated per row or batch. Exercise sliced
inputs and dictionary-backed inputs if they can reach the admitted path.
- [ ] Verify no fusion for different input attributes, computed/fallible or
nondeterministic children, null or nonliteral needles, non-default collations,
disabled constituent expressions, and over-limit pattern sets. Include a
surrounding conditional to guard against changing evaluation boundaries.
- [ ] Verify repeated execution/plan reuse does not use stale needles and
that compiled state is released when its owner is dropped. Test the recoverable
preparation-decline path.
- [ ] Benchmark the prepared matcher against the **actual current Comet OR
expression**, including its vectorized short-circuit/selection behavior, rather
than against an artificial loop that always searches every pattern.
- [ ] Cover 2/4/8/16/32 alternatives, short and long strings, small and
large batches, no matches, first-alternative-heavy matches, late-alternative
matches, different match positions, common pattern prefixes, and null-heavy
input. Measure preparation time, steady-state evaluation, retained matcher
memory, and allocation changes; include at least one end-to-end SQL benchmark.
- [ ] Use those results to choose a conservative admission
threshold/strategy and provide a way to disable fusion for comparison and
troubleshooting. Keep shapes that regress on the existing path. No speedup
target is asserted before this measurement.
#### Non-goals
The first implementation does not add a public SQL function, regex
alternation, `LIKE ANY`, case-insensitive/collation-aware multi-search,
arbitrary expression commoning, dynamic per-row pattern arrays, scan-index
integration, or a general predicate-reordering framework. It should fit one
focused optimization PR with tests and benchmarks.
### Additional context
- ClickHouse's documented
[multiSearchAny](https://clickhouse.com/docs/reference/functions/regular-functions/string-search-functions#multiSearchAny)
and
[multiSearchAnyUTF8](https://clickhouse.com/docs/reference/functions/regular-functions/string-search-functions#multiSearchAnyUTF8)
demonstrate the usefulness of evaluating several literal substring
alternatives together. They are an algorithm/API precedent; this proposal
retains Spark's Boolean, null, and binary-collation semantics rather than
importing another engine's contract.
- DataFusion 55.1.0's [Contains
implementation](https://github.com/apache/datafusion/blob/55.1.0/datafusion/functions/src/string/contains.rs#L102)
also delegates one needle to Arrow. This proposal targets combining several
calls, not replacing a missing single-pattern vector kernel.
- [PR #5322](https://github.com/apache/datafusion-comet/pull/5322) optimizes
the scalar-haystack/array-needle case of an individual `Contains`; it does not
fuse OR-connected literal needles on an input column. The changes can be
evaluated independently.
- The existing [Contains
benchmark](https://github.com/apache/datafusion-comet/blob/451c99963206fa6bf0387239aa12887a16255516/native/spark-expr/benches/contains.rs)
and [expression optimization
guide](https://github.com/apache/datafusion-comet/blob/451c99963206fa6bf0387239aa12887a16255516/docs/source/contributor-guide/optimizing_expressions.md)
provide starting points for the benchmark and no-regression work.
Source references above describe Comet main at
`451c99963206fa6bf0387239aa12887a16255516`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]