Skip to content

Commit b5f4c5e

Browse files
authored
ESQL: Push CIDR_MATCH to Lucene if possible (#105061)
1 parent 0977ba9 commit b5f4c5e

File tree

5 files changed

+96
-1
lines changed

5 files changed

+96
-1
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
@@ -61,6 +61,14 @@ public CIDRMatch(
6161
this.matches = matches;
6262
}
6363

64+
public Expression ipField() {
65+
return ipField;
66+
}
67+
68+
public List<Expression> matches() {
69+
return matches;
70+
}
71+
6472
@Override
6573
public ExpressionEvaluator.Factory toEvaluator(Function<Expression, ExpressionEvaluator.Factory> toEvaluator) {
6674
var ipEvaluatorSupplier = toEvaluator.apply(ipField);

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.InsensitiveBinaryComparison;
1515
import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.NotEquals;
1616
import org.elasticsearch.xpack.esql.expression.function.aggregate.Count;
17+
import org.elasticsearch.xpack.esql.expression.function.scalar.ip.CIDRMatch;
1718
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.In;
1819
import org.elasticsearch.xpack.esql.optimizer.PhysicalOptimizerRules.OptimizerRule;
1920
import org.elasticsearch.xpack.esql.plan.physical.AggregateExec;
@@ -251,6 +252,9 @@ public static boolean canPushToSource(Expression exp, Predicate<FieldAttribute>
251252
if (usf instanceof RegexMatch<?> || usf instanceof IsNull || usf instanceof IsNotNull) {
252253
return isAttributePushable(usf.field(), usf, hasIdenticalDelegate);
253254
}
255+
} else if (exp instanceof CIDRMatch cidrMatch) {
256+
return isAttributePushable(cidrMatch.ipField(), cidrMatch, hasIdenticalDelegate)
257+
&& Expressions.foldable(cidrMatch.matches());
254258
}
255259
return false;
256260
}

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.elasticsearch.common.lucene.BytesRefs;
1212
import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException;
1313
import org.elasticsearch.xpack.esql.evaluator.predicate.operator.comparison.InsensitiveEquals;
14+
import org.elasticsearch.xpack.esql.expression.function.scalar.ip.CIDRMatch;
1415
import org.elasticsearch.xpack.esql.querydsl.query.SingleValueQuery;
1516
import org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter;
1617
import org.elasticsearch.xpack.ql.QlIllegalArgumentException;
@@ -37,14 +38,17 @@
3738
import org.elasticsearch.xpack.ql.querydsl.query.MatchAll;
3839
import org.elasticsearch.xpack.ql.querydsl.query.Query;
3940
import org.elasticsearch.xpack.ql.querydsl.query.TermQuery;
41+
import org.elasticsearch.xpack.ql.querydsl.query.TermsQuery;
4042
import org.elasticsearch.xpack.ql.tree.Source;
4143
import org.elasticsearch.xpack.ql.type.DataType;
4244
import org.elasticsearch.xpack.ql.type.DataTypes;
4345
import org.elasticsearch.xpack.ql.util.Check;
4446

4547
import java.math.BigDecimal;
4648
import java.math.BigInteger;
49+
import java.util.LinkedHashSet;
4750
import java.util.List;
51+
import java.util.Set;
4852
import java.util.function.Supplier;
4953

5054
import static org.elasticsearch.xpack.ql.type.DataTypes.UNSIGNED_LONG;
@@ -66,7 +70,7 @@ public final class EsqlTranslatorHandler extends QlTranslatorHandler {
6670
new ExpressionTranslators.StringQueries(),
6771
new ExpressionTranslators.Matches(),
6872
new ExpressionTranslators.MultiMatches(),
69-
new ExpressionTranslators.Scalars()
73+
new Scalars()
7074
);
7175

7276
@Override
@@ -245,4 +249,26 @@ private static boolean isInRange(DataType numericFieldDataType, DataType valueDa
245249
return minValue.compareTo(decimalValue) <= 0 && maxValue.compareTo(decimalValue) >= 0;
246250
}
247251
}
252+
253+
public static class Scalars extends ExpressionTranslator<ScalarFunction> {
254+
@Override
255+
protected Query asQuery(ScalarFunction f, TranslatorHandler handler) {
256+
return doTranslate(f, handler);
257+
}
258+
259+
public static Query doTranslate(ScalarFunction f, TranslatorHandler handler) {
260+
if (f instanceof CIDRMatch cm) {
261+
if (cm.ipField() instanceof FieldAttribute fa && Expressions.foldable(cm.matches())) {
262+
String targetFieldName = handler.nameOf(fa.exactAttribute());
263+
Set<Object> set = new LinkedHashSet<>(Expressions.fold(cm.matches()));
264+
265+
Query query = new TermsQuery(f.source(), targetFieldName, set);
266+
// CIDR_MATCH applies only to single values.
267+
return handler.wrapFunctionQuery(f, cm.ipField(), () -> query);
268+
}
269+
}
270+
271+
return ExpressionTranslators.Scalars.doTranslate(f, handler);
272+
}
273+
}
248274
}

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

Lines changed: 51 additions & 0 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;
@@ -54,10 +55,13 @@
5455
import org.elasticsearch.xpack.ql.type.EsField;
5556
import org.junit.Before;
5657

58+
import java.util.ArrayList;
5759
import java.util.List;
5860
import java.util.Map;
61+
import java.util.stream.Collectors;
5962

6063
import static java.util.Arrays.asList;
64+
import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
6165
import static org.elasticsearch.xpack.esql.EsqlTestUtils.as;
6266
import static org.elasticsearch.xpack.esql.EsqlTestUtils.configuration;
6367
import static org.elasticsearch.xpack.esql.EsqlTestUtils.loadMapping;
@@ -415,6 +419,44 @@ public void testIsNullPushdownFilter() {
415419
assertThat(query.query().toString(), is(expected.toString()));
416420
}
417421

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

420462
public void testOutOfRangeFilterPushdown() {
@@ -621,4 +663,13 @@ private PhysicalPlan physicalPlan(String query, Analyzer analyzer) {
621663
protected List<String> filteredWarnings() {
622664
return withDefaultLimitWarning(super.filteredWarnings());
623665
}
666+
667+
private String randomCidrBlock() {
668+
boolean ipv4 = randomBoolean();
669+
670+
String address = NetworkAddress.format(randomIp(ipv4));
671+
int cidrPrefixLength = ipv4 ? randomIntBetween(0, 32) : randomIntBetween(0, 128);
672+
673+
return format(null, "{}/{}", address, cidrPrefixLength);
674+
}
624675
}

0 commit comments

Comments
 (0)