Skip to content

Commit 709c078

Browse files
fang-xing-esqlcbuescher
authored andcommitted
[ES|QL] Combine Disjunctive CIDRMatch (elastic#111501)
* support CIDRMatch in CombineDisjunctions
1 parent 59bbec7 commit 709c078

File tree

11 files changed

+605
-286
lines changed

11 files changed

+605
-286
lines changed

docs/changelog/111501.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 111501
2+
summary: "[ES|QL] Combine Disjunctive CIDRMatch"
3+
area: ES|QL
4+
type: enhancement
5+
issues:
6+
- 105143

x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/planner/ExpressionTranslators.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,8 +260,12 @@ private static Query boolQuery(Source source, Query left, Query right, boolean i
260260
}
261261
List<Query> queries;
262262
// check if either side is already a bool query to an extra bool query
263-
if (left instanceof BoolQuery bool && bool.isAnd() == isAnd) {
264-
queries = CollectionUtils.combine(bool.queries(), right);
263+
if (left instanceof BoolQuery leftBool && leftBool.isAnd() == isAnd) {
264+
if (right instanceof BoolQuery rightBool && rightBool.isAnd() == isAnd) {
265+
queries = CollectionUtils.combine(leftBool.queries(), rightBool.queries());
266+
} else {
267+
queries = CollectionUtils.combine(leftBool.queries(), right);
268+
}
265269
} else if (right instanceof BoolQuery bool && bool.isAnd() == isAnd) {
266270
queries = CollectionUtils.combine(bool.queries(), left);
267271
} else {

x-pack/plugin/esql/qa/testFixtures/src/main/resources/ip.csv-spec

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,78 @@ str1:keyword |str2:keyword |ip1:ip |ip2:ip
282282
// end::to_ip-result[]
283283
;
284284

285+
cdirMatchOrsIPs
286+
required_capability: combine_disjunctive_cidrmatches
287+
288+
FROM hosts
289+
| WHERE CIDR_MATCH(ip1, "127.0.0.2/32") or CIDR_MATCH(ip0, "127.0.0.1") or CIDR_MATCH(ip1, "127.0.0.3/32") or CIDR_MATCH(ip0, "fe80::cae2:65ff:fece:feb9")
290+
| KEEP card, host, ip0, ip1
291+
| sort host, card, ip0, ip1
292+
;
293+
warning:Line 2:20: evaluation of [ip1] failed, treating result as null. Only first 20 failures recorded.
294+
warning:Line 2:20: java.lang.IllegalArgumentException: single-value function encountered multi-value
295+
warning:Line 2:55: evaluation of [ip0] failed, treating result as null. Only first 20 failures recorded.
296+
warning:Line 2:55: java.lang.IllegalArgumentException: single-value function encountered multi-value
297+
298+
card:keyword |host:keyword |ip0:ip |ip1:ip
299+
eth0 |alpha |127.0.0.1 |127.0.0.1
300+
eth0 |beta |127.0.0.1 |::1
301+
eth1 |beta |127.0.0.1 |127.0.0.2
302+
eth1 |beta |127.0.0.1 |128.0.0.1
303+
eth0 |gamma |fe80::cae2:65ff:fece:feb9|127.0.0.3
304+
lo0 |gamma |fe80::cae2:65ff:fece:feb9|fe81::cae2:65ff:fece:feb9
305+
;
306+
307+
cdirMatchEqualsInsOrs
308+
required_capability: combine_disjunctive_cidrmatches
309+
310+
FROM hosts
311+
| WHERE host == "alpha" OR host == "gamma" OR CIDR_MATCH(ip1, "127.0.0.2/32") OR CIDR_MATCH(ip0, "127.0.0.1") OR card IN ("eth0", "eth1") OR CIDR_MATCH(ip1, "127.0.0.3/32") OR card == "lo0" OR CIDR_MATCH(ip0, "fe80::cae2:65ff:fece:feb9") OR host == "beta"
312+
| KEEP card, host, ip0, ip1
313+
| sort host, card, ip0, ip1
314+
;
315+
warning:Line 2:58: evaluation of [ip1] failed, treating result as null. Only first 20 failures recorded.
316+
warning:Line 2:58: java.lang.IllegalArgumentException: single-value function encountered multi-value
317+
warning:Line 2:93: evaluation of [ip0] failed, treating result as null. Only first 20 failures recorded.
318+
warning:Line 2:93: java.lang.IllegalArgumentException: single-value function encountered multi-value
319+
320+
card:keyword |host:keyword |ip0:ip |ip1:ip
321+
eth0 |alpha |127.0.0.1 |127.0.0.1
322+
eth1 |alpha |::1 |::1
323+
eth0 |beta |127.0.0.1 |::1
324+
eth1 |beta |127.0.0.1 |127.0.0.2
325+
eth1 |beta |127.0.0.1 |128.0.0.1
326+
eth0 |epsilon |[fe80::cae2:65ff:fece:feb9, fe80::cae2:65ff:fece:fec0, fe80::cae2:65ff:fece:fec1] |fe80::cae2:65ff:fece:fec1
327+
eth1 |epsilon |null |[127.0.0.1, 127.0.0.2, 127.0.0.3]
328+
eth0 |gamma |fe80::cae2:65ff:fece:feb9 |127.0.0.3
329+
lo0 |gamma |fe80::cae2:65ff:fece:feb9 |fe81::cae2:65ff:fece:feb9
330+
;
331+
332+
cdirMatchEqualsInsOrsIPs
333+
required_capability: combine_disjunctive_cidrmatches
334+
335+
FROM hosts
336+
| WHERE host == "alpha" OR host == "gamma" OR CIDR_MATCH(ip1, "127.0.0.2/32") OR ip0 == "127.0.0.1" OR card IN ("eth0", "eth1") OR ip1 IN ("127.0.0.3", "127.0.0.1") OR card == "lo0" OR CIDR_MATCH(ip0, "fe80::cae2:65ff:fece:feb9") OR host == "beta"
337+
| KEEP card, host, ip0, ip1
338+
| sort host, card, ip0, ip1
339+
;
340+
warning:Line 2:58: evaluation of [ip1] failed, treating result as null. Only first 20 failures recorded.
341+
warning:Line 2:58: java.lang.IllegalArgumentException: single-value function encountered multi-value
342+
warning:Line 2:82: evaluation of [ip0] failed, treating result as null. Only first 20 failures recorded.
343+
warning:Line 2:82: java.lang.IllegalArgumentException: single-value function encountered multi-value
344+
345+
card:keyword |host:keyword |ip0:ip |ip1:ip
346+
eth0 |alpha |127.0.0.1 |127.0.0.1
347+
eth1 |alpha |::1 |::1
348+
eth0 |beta |127.0.0.1 |::1
349+
eth1 |beta |127.0.0.1 |127.0.0.2
350+
eth1 |beta |127.0.0.1 |128.0.0.1
351+
eth0 |epsilon |[fe80::cae2:65ff:fece:feb9, fe80::cae2:65ff:fece:fec0, fe80::cae2:65ff:fece:fec1] |fe80::cae2:65ff:fece:fec1
352+
eth1 |epsilon |null |[127.0.0.1, 127.0.0.2, 127.0.0.3]
353+
eth0 |gamma |fe80::cae2:65ff:fece:feb9 |127.0.0.3
354+
lo0 |gamma |fe80::cae2:65ff:fece:feb9 |fe81::cae2:65ff:fece:feb9
355+
;
356+
285357
pushDownIP
286358
from hosts | where ip1 == to_ip("::1") | keep card, host, ip0, ip1;
287359
ignoreOrder:true

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,12 @@ public enum Cap {
213213
/**
214214
* Support for nanosecond dates as a data type
215215
*/
216-
DATE_NANOS_TYPE(EsqlCorePlugin.DATE_NANOS_FEATURE_FLAG);
216+
DATE_NANOS_TYPE(EsqlCorePlugin.DATE_NANOS_FEATURE_FLAG),
217+
218+
/**
219+
* Support CIDRMatch in CombineDisjunctions rule.
220+
*/
221+
COMBINE_DISJUNCTIVE_CIDRMATCHES;
217222

218223
private final boolean snapshotOnly;
219224
private final FeatureFlag featureFlag;

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
import org.elasticsearch.xpack.esql.optimizer.rules.BooleanFunctionEqualsElimination;
2929
import org.elasticsearch.xpack.esql.optimizer.rules.BooleanSimplification;
3030
import org.elasticsearch.xpack.esql.optimizer.rules.CombineBinaryComparisons;
31-
import org.elasticsearch.xpack.esql.optimizer.rules.CombineDisjunctionsToIn;
31+
import org.elasticsearch.xpack.esql.optimizer.rules.CombineDisjunctions;
3232
import org.elasticsearch.xpack.esql.optimizer.rules.CombineEvals;
3333
import org.elasticsearch.xpack.esql.optimizer.rules.CombineProjections;
3434
import org.elasticsearch.xpack.esql.optimizer.rules.ConstantFolding;
@@ -209,7 +209,7 @@ protected static Batch<LogicalPlan> operators() {
209209
new PropagateNullable(),
210210
new BooleanFunctionEqualsElimination(),
211211
new CombineBinaryComparisons(),
212-
new CombineDisjunctionsToIn(),
212+
new CombineDisjunctions(),
213213
new SimplifyComparisonsArithmetics(DataType::areCompatible),
214214
// prune/elimination
215215
new PruneFilters(),
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.esql.optimizer.rules;
9+
10+
import org.apache.lucene.util.BytesRef;
11+
import org.elasticsearch.xpack.esql.core.expression.Expression;
12+
import org.elasticsearch.xpack.esql.core.expression.Literal;
13+
import org.elasticsearch.xpack.esql.core.expression.predicate.logical.Or;
14+
import org.elasticsearch.xpack.esql.core.tree.Source;
15+
import org.elasticsearch.xpack.esql.core.type.DataType;
16+
import org.elasticsearch.xpack.esql.expression.function.scalar.ip.CIDRMatch;
17+
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.Equals;
18+
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.In;
19+
20+
import java.time.ZoneId;
21+
import java.util.ArrayList;
22+
import java.util.LinkedHashMap;
23+
import java.util.LinkedHashSet;
24+
import java.util.LinkedList;
25+
import java.util.List;
26+
import java.util.Map;
27+
import java.util.Set;
28+
29+
import static org.elasticsearch.xpack.esql.core.expression.predicate.Predicates.combineOr;
30+
import static org.elasticsearch.xpack.esql.core.expression.predicate.Predicates.splitOr;
31+
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.ipToString;
32+
33+
/**
34+
* Combine disjunctive Equals, In or CIDRMatch expressions on the same field into an In or CIDRMatch expression.
35+
* This rule looks for both simple equalities:
36+
* 1. a == 1 OR a == 2 becomes a IN (1, 2)
37+
* and combinations of In
38+
* 2. a == 1 OR a IN (2) becomes a IN (1, 2)
39+
* 3. a IN (1) OR a IN (2) becomes a IN (1, 2)
40+
* and combinations of CIDRMatch
41+
* 4. CIDRMatch(a, ip1) OR CIDRMatch(a, ip2) OR a == ip3 or a IN (ip4, ip5) becomes CIDRMatch(a, ip1, ip2, ip3, ip4, ip5)
42+
* <p>
43+
* This rule does NOT check for type compatibility as that phase has been
44+
* already be verified in the analyzer.
45+
*/
46+
public final class CombineDisjunctions extends OptimizerRules.OptimizerExpressionRule<Or> {
47+
public CombineDisjunctions() {
48+
super(OptimizerRules.TransformDirection.UP);
49+
}
50+
51+
protected static In createIn(Expression key, List<Expression> values, ZoneId zoneId) {
52+
return new In(key.source(), key, values);
53+
}
54+
55+
protected static Equals createEquals(Expression k, Set<Expression> v, ZoneId finalZoneId) {
56+
return new Equals(k.source(), k, v.iterator().next(), finalZoneId);
57+
}
58+
59+
protected static CIDRMatch createCIDRMatch(Expression k, List<Expression> v) {
60+
return new CIDRMatch(k.source(), k, v);
61+
}
62+
63+
@Override
64+
public Expression rule(Or or) {
65+
Expression e = or;
66+
// look only at equals, In and CIDRMatch
67+
List<Expression> exps = splitOr(e);
68+
69+
Map<Expression, Set<Expression>> ins = new LinkedHashMap<>();
70+
Map<Expression, Set<Expression>> cidrs = new LinkedHashMap<>();
71+
Map<Expression, Set<Expression>> ips = new LinkedHashMap<>();
72+
ZoneId zoneId = null;
73+
List<Expression> ors = new LinkedList<>();
74+
boolean changed = false;
75+
for (Expression exp : exps) {
76+
if (exp instanceof Equals eq) {
77+
// consider only equals against foldables
78+
if (eq.right().foldable()) {
79+
ins.computeIfAbsent(eq.left(), k -> new LinkedHashSet<>()).add(eq.right());
80+
if (eq.left().dataType() == DataType.IP) {
81+
Object value = eq.right().fold();
82+
// ImplicitCasting and ConstantFolding(includes explicit casting) are applied before CombineDisjunctions.
83+
// They fold the input IP string to an internal IP format. These happen to Equals and IN, but not for CIDRMatch,
84+
// as CIDRMatch takes strings as input, ImplicitCasting does not apply to it, and the first input to CIDRMatch is a
85+
// field, ConstantFolding does not apply to it either.
86+
// If the data type is IP, convert the internal IP format in Equals and IN to the format that is compatible with
87+
// CIDRMatch, and store them in a separate map, so that they can be combined into existing CIDRMatch later.
88+
if (value instanceof BytesRef bytesRef) {
89+
value = ipToString(bytesRef);
90+
}
91+
ips.computeIfAbsent(eq.left(), k -> new LinkedHashSet<>()).add(new Literal(Source.EMPTY, value, DataType.IP));
92+
}
93+
} else {
94+
ors.add(exp);
95+
}
96+
if (zoneId == null) {
97+
zoneId = eq.zoneId();
98+
}
99+
} else if (exp instanceof In in) {
100+
ins.computeIfAbsent(in.value(), k -> new LinkedHashSet<>()).addAll(in.list());
101+
if (in.value().dataType() == DataType.IP) {
102+
List<Expression> values = new ArrayList<>(in.list().size());
103+
for (Expression i : in.list()) {
104+
Object value = i.fold();
105+
// Same as Equals.
106+
if (value instanceof BytesRef bytesRef) {
107+
value = ipToString(bytesRef);
108+
}
109+
values.add(new Literal(Source.EMPTY, value, DataType.IP));
110+
}
111+
ips.computeIfAbsent(in.value(), k -> new LinkedHashSet<>()).addAll(values);
112+
}
113+
} else if (exp instanceof CIDRMatch cm) {
114+
cidrs.computeIfAbsent(cm.ipField(), k -> new LinkedHashSet<>()).addAll(cm.matches());
115+
} else {
116+
ors.add(exp);
117+
}
118+
}
119+
120+
if (cidrs.isEmpty() == false) {
121+
for (Expression f : ips.keySet()) {
122+
cidrs.computeIfAbsent(f, k -> new LinkedHashSet<>()).addAll(ips.get(f));
123+
ins.remove(f);
124+
}
125+
}
126+
127+
if (ins.isEmpty() == false) {
128+
// combine equals alongside the existing ors
129+
final ZoneId finalZoneId = zoneId;
130+
ins.forEach(
131+
(k, v) -> { ors.add(v.size() == 1 ? createEquals(k, v, finalZoneId) : createIn(k, new ArrayList<>(v), finalZoneId)); }
132+
);
133+
134+
changed = true;
135+
}
136+
137+
if (cidrs.isEmpty() == false) {
138+
cidrs.forEach((k, v) -> { ors.add(createCIDRMatch(k, new ArrayList<>(v))); });
139+
changed = true;
140+
}
141+
142+
if (changed) {
143+
// TODO: this makes a QL `or`, not an ESQL `or`
144+
Expression combineOr = combineOr(ors);
145+
// check the result semantically since the result might different in order
146+
// but be actually the same which can trigger a loop
147+
// e.g. a == 1 OR a == 2 OR null --> null OR a in (1,2) --> literalsOnTheRight --> cycle
148+
if (e.semanticEquals(combineOr) == false) {
149+
e = combineOr;
150+
}
151+
}
152+
153+
return e;
154+
}
155+
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/CombineDisjunctionsToIn.java

Lines changed: 0 additions & 98 deletions
This file was deleted.

0 commit comments

Comments
 (0)