Skip to content

Commit b13d768

Browse files
authored
[8.12] ESQL: Push CIDR_MATCH to Lucene if possible (#105061) (#105177)
(cherry picked from commit b5f4c5e) Backport of #105061
1 parent 7af3682 commit b13d768

File tree

5 files changed

+139
-27
lines changed

5 files changed

+139
-27
lines changed

docs/changelog/105061.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 105061
2+
summary: "ESQL: Push CIDR_MATCH to Lucene if possible"
3+
area: ES|QL
4+
type: bug
5+
issues:
6+
- 105042

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/ip/CIDRMatch.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ public CIDRMatch(Source source, Expression ipField, List<Expression> matches) {
5454
this.matches = matches;
5555
}
5656

57+
public Expression ipField() {
58+
return ipField;
59+
}
60+
61+
public List<Expression> matches() {
62+
return matches;
63+
}
64+
5765
@Override
5866
public ExpressionEvaluator.Factory toEvaluator(Function<Expression, ExpressionEvaluator.Factory> toEvaluator) {
5967
var ipEvaluatorSupplier = toEvaluator.apply(ipField);

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.Equals;
1414
import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.NotEquals;
1515
import org.elasticsearch.xpack.esql.expression.function.aggregate.Count;
16+
import org.elasticsearch.xpack.esql.expression.function.scalar.ip.CIDRMatch;
1617
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.In;
1718
import org.elasticsearch.xpack.esql.optimizer.PhysicalOptimizerRules.OptimizerRule;
1819
import org.elasticsearch.xpack.esql.plan.physical.AggregateExec;
@@ -240,6 +241,8 @@ public static boolean canPushToSource(Expression exp) {
240241
if (usf instanceof RegexMatch<?> || usf instanceof IsNull || usf instanceof IsNotNull) {
241242
return isAttributePushable(usf.field(), usf);
242243
}
244+
} else if (exp instanceof CIDRMatch cidrMatch) {
245+
return isAttributePushable(cidrMatch.ipField(), cidrMatch) && Expressions.foldable(cidrMatch.matches());
243246
}
244247
return false;
245248
}

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

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
package org.elasticsearch.xpack.esql.planner;
99

1010
import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException;
11+
import org.elasticsearch.xpack.esql.expression.function.scalar.ip.CIDRMatch;
1112
import org.elasticsearch.xpack.esql.querydsl.query.SingleValueQuery;
1213
import org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter;
14+
import org.elasticsearch.xpack.ql.QlIllegalArgumentException;
1315
import org.elasticsearch.xpack.ql.expression.Expression;
16+
import org.elasticsearch.xpack.ql.expression.Expressions;
1417
import org.elasticsearch.xpack.ql.expression.FieldAttribute;
1518
import org.elasticsearch.xpack.ql.expression.MetadataAttribute;
1619
import org.elasticsearch.xpack.ql.expression.function.scalar.ScalarFunction;
@@ -19,15 +22,44 @@
1922
import org.elasticsearch.xpack.ql.planner.ExpressionTranslator;
2023
import org.elasticsearch.xpack.ql.planner.ExpressionTranslators;
2124
import org.elasticsearch.xpack.ql.planner.QlTranslatorHandler;
25+
import org.elasticsearch.xpack.ql.planner.TranslatorHandler;
2226
import org.elasticsearch.xpack.ql.querydsl.query.Query;
27+
import org.elasticsearch.xpack.ql.querydsl.query.TermsQuery;
2328
import org.elasticsearch.xpack.ql.type.DataType;
2429

30+
import java.util.LinkedHashSet;
31+
import java.util.List;
32+
import java.util.Set;
2533
import java.util.function.Supplier;
2634

2735
public final class EsqlTranslatorHandler extends QlTranslatorHandler {
36+
37+
public static final List<ExpressionTranslator<?>> QUERY_TRANSLATORS = List.of(
38+
new ExpressionTranslators.BinaryComparisons(),
39+
new ExpressionTranslators.Ranges(),
40+
new ExpressionTranslators.BinaryLogic(),
41+
new ExpressionTranslators.IsNulls(),
42+
new ExpressionTranslators.IsNotNulls(),
43+
new ExpressionTranslators.Nots(),
44+
new ExpressionTranslators.Likes(),
45+
new ExpressionTranslators.InComparisons(),
46+
new ExpressionTranslators.StringQueries(),
47+
new ExpressionTranslators.Matches(),
48+
new ExpressionTranslators.MultiMatches(),
49+
new Scalars()
50+
);
51+
2852
@Override
2953
public Query asQuery(Expression e) {
30-
return ExpressionTranslators.toQuery(e, this);
54+
Query translation = null;
55+
for (ExpressionTranslator<?> translator : QUERY_TRANSLATORS) {
56+
translation = translator.translate(e, this);
57+
if (translation != null) {
58+
return translation;
59+
}
60+
}
61+
62+
throw new QlIllegalArgumentException("Don't know how to translate {} {}", e.nodeName(), e);
3163
}
3264

3365
@Override
@@ -56,4 +88,26 @@ public Query wrapFunctionQuery(ScalarFunction sf, Expression field, Supplier<Que
5688
}
5789
throw new EsqlIllegalArgumentException("Expected a FieldAttribute or MetadataAttribute but received [" + field + "]");
5890
}
91+
92+
public static class Scalars extends ExpressionTranslator<ScalarFunction> {
93+
@Override
94+
protected Query asQuery(ScalarFunction f, TranslatorHandler handler) {
95+
return doTranslate(f, handler);
96+
}
97+
98+
public static Query doTranslate(ScalarFunction f, TranslatorHandler handler) {
99+
if (f instanceof CIDRMatch cm) {
100+
if (cm.ipField() instanceof FieldAttribute fa && Expressions.foldable(cm.matches())) {
101+
String targetFieldName = handler.nameOf(fa.exactAttribute());
102+
Set<Object> set = new LinkedHashSet<>(Expressions.fold(cm.matches()));
103+
104+
Query query = new TermsQuery(f.source(), targetFieldName, set);
105+
// CIDR_MATCH applies only to single values.
106+
return handler.wrapFunctionQuery(f, cm.ipField(), () -> query);
107+
}
108+
}
109+
110+
return ExpressionTranslators.Scalars.doTranslate(f, handler);
111+
}
112+
}
59113
}

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java

Lines changed: 67 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
1111

12+
import org.elasticsearch.common.network.NetworkAddress;
1213
import org.elasticsearch.common.settings.Settings;
1314
import org.elasticsearch.core.Tuple;
1415
import org.elasticsearch.index.query.QueryBuilder;
@@ -43,24 +44,25 @@
4344
import org.elasticsearch.xpack.esql.session.EsqlConfiguration;
4445
import org.elasticsearch.xpack.esql.stats.Metrics;
4546
import org.elasticsearch.xpack.esql.stats.SearchStats;
46-
import org.elasticsearch.xpack.esql.type.EsqlDataTypes;
4747
import org.elasticsearch.xpack.ql.expression.Alias;
4848
import org.elasticsearch.xpack.ql.expression.Expression;
4949
import org.elasticsearch.xpack.ql.expression.Expressions;
5050
import org.elasticsearch.xpack.ql.expression.ReferenceAttribute;
51-
import org.elasticsearch.xpack.ql.expression.function.FunctionRegistry;
5251
import org.elasticsearch.xpack.ql.index.EsIndex;
5352
import org.elasticsearch.xpack.ql.index.IndexResolution;
5453
import org.elasticsearch.xpack.ql.tree.Source;
5554
import org.elasticsearch.xpack.ql.type.DataTypes;
5655
import org.elasticsearch.xpack.ql.type.EsField;
5756
import org.junit.Before;
5857

58+
import java.util.ArrayList;
5959
import java.util.List;
6060
import java.util.Map;
6161
import java.util.Set;
62+
import java.util.stream.Collectors;
6263

6364
import static java.util.Arrays.asList;
65+
import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
6466
import static org.elasticsearch.xpack.esql.EsqlTestUtils.as;
6567
import static org.elasticsearch.xpack.esql.EsqlTestUtils.configuration;
6668
import static org.elasticsearch.xpack.esql.EsqlTestUtils.loadMapping;
@@ -87,9 +89,8 @@ public class LocalPhysicalPlanOptimizerTests extends ESTestCase {
8789
private Analyzer analyzer;
8890
private LogicalPlanOptimizer logicalOptimizer;
8991
private PhysicalPlanOptimizer physicalPlanOptimizer;
92+
private EsqlFunctionRegistry functionRegistry;
9093
private Mapper mapper;
91-
private Map<String, EsField> mapping;
92-
private int allFieldRowSize;
9394

9495
private final EsqlConfiguration config;
9596
private final SearchStats IS_SV_STATS = new TestSearchStats() {
@@ -118,24 +119,9 @@ public LocalPhysicalPlanOptimizerTests(String name, EsqlConfiguration config) {
118119
@Before
119120
public void init() {
120121
parser = new EsqlParser();
121-
122-
mapping = loadMapping("mapping-basic.json");
123-
allFieldRowSize = mapping.values()
124-
.stream()
125-
.mapToInt(
126-
f -> (EstimatesRowSize.estimateSize(EsqlDataTypes.widenSmallNumericTypes(f.getDataType())) + f.getProperties()
127-
.values()
128-
.stream()
129-
// check one more level since the mapping contains TEXT fields with KEYWORD multi-fields
130-
.mapToInt(x -> EstimatesRowSize.estimateSize(EsqlDataTypes.widenSmallNumericTypes(x.getDataType())))
131-
.sum())
132-
)
133-
.sum();
134-
EsIndex test = new EsIndex("test", mapping);
135-
IndexResolution getIndexResult = IndexResolution.valid(test);
136122
logicalOptimizer = new LogicalPlanOptimizer(new LogicalOptimizerContext(EsqlTestUtils.TEST_CFG));
137123
physicalPlanOptimizer = new PhysicalPlanOptimizer(new PhysicalOptimizerContext(config));
138-
FunctionRegistry functionRegistry = new EsqlFunctionRegistry();
124+
functionRegistry = new EsqlFunctionRegistry();
139125
mapper = new Mapper(functionRegistry);
140126
var enrichResolution = new EnrichResolution(
141127
Set.of(
@@ -155,11 +141,15 @@ public void init() {
155141
),
156142
Set.of("foo")
157143
);
144+
analyzer = makeAnalyzer("mapping-basic.json", enrichResolution);
145+
}
158146

159-
analyzer = new Analyzer(
160-
new AnalyzerContext(config, functionRegistry, getIndexResult, enrichResolution),
161-
new Verifier(new Metrics())
162-
);
147+
private Analyzer makeAnalyzer(String mappingFileName, EnrichResolution enrichResolution) {
148+
var mapping = loadMapping(mappingFileName);
149+
EsIndex test = new EsIndex("test", mapping);
150+
IndexResolution getIndexResult = IndexResolution.valid(test);
151+
152+
return new Analyzer(new AnalyzerContext(config, functionRegistry, getIndexResult, enrichResolution), new Verifier(new Metrics()));
163153
}
164154

165155
/**
@@ -430,6 +420,44 @@ public void testIsNullPushdownFilter() {
430420
assertThat(query.query().toString(), is(expected.toString()));
431421
}
432422

423+
/**
424+
* Expects
425+
* LimitExec[500[INTEGER]]
426+
* \_ExchangeExec[[],false]
427+
* \_ProjectExec[[!alias_integer, boolean{f}#4, byte{f}#5, constant_keyword-foo{f}#6, date{f}#7, double{f}#8, float{f}#9,
428+
* half_float{f}#10, integer{f}#12, ip{f}#13, keyword{f}#14, long{f}#15, scaled_float{f}#11, short{f}#17, text{f}#18,
429+
* unsigned_long{f}#16, version{f}#19, wildcard{f}#20]]
430+
* \_FieldExtractExec[!alias_integer, boolean{f}#4, byte{f}#5, constant_k..][]
431+
* \_EsQueryExec[test], query[{"esql_single_value":{"field":"ip","next":{"terms":{"ip":["127.0.0.0/24"],"boost":1.0}},"source":
432+
* "cidr_match(ip, \"127.0.0.0/24\")@1:19"}}][_doc{f}#21], limit[500], sort[] estimatedRowSize[389]
433+
*/
434+
public void testCidrMatchPushdownFilter() {
435+
var allTypeMappingAnalyzer = makeAnalyzer("mapping-ip.json", new EnrichResolution(Set.of(), Set.of()));
436+
final String fieldName = "ip_addr";
437+
438+
int cidrBlockCount = randomIntBetween(1, 10);
439+
ArrayList<String> cidrBlocks = new ArrayList<>();
440+
for (int i = 0; i < cidrBlockCount; i++) {
441+
cidrBlocks.add(randomCidrBlock());
442+
}
443+
String cidrBlocksString = cidrBlocks.stream().map((s) -> "\"" + s + "\"").collect(Collectors.joining(","));
444+
String cidrMatch = format(null, "cidr_match({}, {})", fieldName, cidrBlocksString);
445+
446+
var query = "from test | where " + cidrMatch;
447+
var plan = plan(query, EsqlTestUtils.TEST_SEARCH_STATS, allTypeMappingAnalyzer);
448+
449+
var limit = as(plan, LimitExec.class);
450+
var exchange = as(limit.child(), ExchangeExec.class);
451+
var project = as(exchange.child(), ProjectExec.class);
452+
var field = as(project.child(), FieldExtractExec.class);
453+
var queryExec = as(field.child(), EsQueryExec.class);
454+
assertThat(queryExec.limit().fold(), is(500));
455+
456+
var expectedInnerQuery = QueryBuilders.termsQuery(fieldName, cidrBlocks);
457+
var expectedQuery = wrapWithSingleQuery(expectedInnerQuery, fieldName, new Source(1, 18, cidrMatch));
458+
assertThat(queryExec.query().toString(), is(expectedQuery.toString()));
459+
}
460+
433461
/**
434462
* Expects
435463
* LimitExec[500[INTEGER]]
@@ -493,7 +521,11 @@ private PhysicalPlan plan(String query) {
493521
}
494522

495523
private PhysicalPlan plan(String query, SearchStats stats) {
496-
var physical = optimizedPlan(physicalPlan(query), stats);
524+
return plan(query, stats, analyzer);
525+
}
526+
527+
private PhysicalPlan plan(String query, SearchStats stats, Analyzer analyzer) {
528+
var physical = optimizedPlan(physicalPlan(query, analyzer), stats);
497529
return physical;
498530
}
499531

@@ -516,7 +548,7 @@ private PhysicalPlan optimizedPlan(PhysicalPlan plan, SearchStats searchStats) {
516548
return l;
517549
}
518550

519-
private PhysicalPlan physicalPlan(String query) {
551+
private PhysicalPlan physicalPlan(String query, Analyzer analyzer) {
520552
var logical = logicalOptimizer.optimize(analyzer.analyze(parser.createStatement(query)));
521553
// System.out.println("Logical\n" + logical);
522554
var physical = mapper.map(logical);
@@ -527,4 +559,13 @@ private PhysicalPlan physicalPlan(String query) {
527559
protected List<String> filteredWarnings() {
528560
return withDefaultLimitWarning(super.filteredWarnings());
529561
}
562+
563+
private String randomCidrBlock() {
564+
boolean ipv4 = randomBoolean();
565+
566+
String address = NetworkAddress.format(randomIp(ipv4));
567+
int cidrPrefixLength = ipv4 ? randomIntBetween(0, 32) : randomIntBetween(0, 128);
568+
569+
return format(null, "{}/{}", address, cidrPrefixLength);
570+
}
530571
}

0 commit comments

Comments
 (0)