Skip to content

Commit d6813cb

Browse files
committed
EQL: Convert wildcards to LIKE in analyzer (#51901)
* EQL: Convert wildcard comparisons to Like * EQL: Simplify wildcard handling, update tests * EQL: Lint fixes for Optimizer.java
1 parent f96ad5c commit d6813cb

File tree

2 files changed

+168
-0
lines changed

2 files changed

+168
-0
lines changed

x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/optimizer/Optimizer.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,23 @@
66

77
package org.elasticsearch.xpack.eql.optimizer;
88

9+
import org.elasticsearch.xpack.ql.expression.Expression;
10+
import org.elasticsearch.xpack.ql.expression.predicate.logical.Not;
11+
import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.BinaryComparison;
12+
import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.Equals;
13+
import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.NotEquals;
14+
import org.elasticsearch.xpack.ql.expression.predicate.regex.Like;
15+
import org.elasticsearch.xpack.ql.expression.predicate.regex.LikePattern;
916
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.BooleanLiteralsOnTheRight;
1017
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.BooleanSimplification;
1118
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.CombineBinaryComparisons;
1219
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.ConstantFolding;
20+
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.OptimizerRule;
1321
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.PropagateEquals;
1422
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.PruneFilters;
1523
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.PruneLiteralsInOrderBy;
1624
import org.elasticsearch.xpack.ql.optimizer.OptimizerRules.SetAsOptimized;
25+
import org.elasticsearch.xpack.ql.plan.logical.Filter;
1726
import org.elasticsearch.xpack.ql.plan.logical.LogicalPlan;
1827
import org.elasticsearch.xpack.ql.rule.RuleExecutor;
1928

@@ -33,6 +42,7 @@ protected Iterable<RuleExecutor<LogicalPlan>.Batch> batches() {
3342
new BooleanSimplification(),
3443
new BooleanLiteralsOnTheRight(),
3544
// needs to occur before BinaryComparison combinations
45+
new ReplaceWildcards(),
3646
new PropagateEquals(),
3747
new CombineBinaryComparisons(),
3848
// prune/elimination
@@ -45,4 +55,51 @@ protected Iterable<RuleExecutor<LogicalPlan>.Batch> batches() {
4555

4656
return Arrays.asList(operators, label);
4757
}
58+
59+
60+
private static class ReplaceWildcards extends OptimizerRule<Filter> {
61+
62+
private static boolean isWildcard(Expression expr) {
63+
if (expr.foldable()) {
64+
Object value = expr.fold();
65+
return value instanceof String && value.toString().contains("*");
66+
}
67+
return false;
68+
}
69+
70+
private static LikePattern toLikePattern(String s) {
71+
// pick a character that is guaranteed not to be in the string, because it isn't allowed to escape itself
72+
char escape = 1;
73+
74+
// replace wildcards with % and escape special characters
75+
String likeString = s.replace("%", escape + "%")
76+
.replace("_", escape + "_")
77+
.replace("*", "%");
78+
79+
return new LikePattern(likeString, escape);
80+
}
81+
82+
@Override
83+
protected LogicalPlan rule(Filter filter) {
84+
return filter.transformExpressionsUp(e -> {
85+
// expr == "wildcard*phrase" || expr != "wildcard*phrase"
86+
if (e instanceof Equals || e instanceof NotEquals) {
87+
BinaryComparison cmp = (BinaryComparison) e;
88+
89+
if (isWildcard(cmp.right())) {
90+
String wcString = cmp.right().fold().toString();
91+
Expression like = new Like(e.source(), cmp.left(), toLikePattern(wcString));
92+
93+
if (e instanceof NotEquals) {
94+
like = new Not(e.source(), like);
95+
}
96+
97+
e = like;
98+
}
99+
}
100+
101+
return e;
102+
});
103+
}
104+
}
48105
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.eql.optimizer;
8+
9+
import org.elasticsearch.test.ESTestCase;
10+
import org.elasticsearch.xpack.eql.analysis.Analyzer;
11+
import org.elasticsearch.xpack.eql.analysis.PreAnalyzer;
12+
import org.elasticsearch.xpack.eql.analysis.Verifier;
13+
import org.elasticsearch.xpack.eql.expression.function.EqlFunctionRegistry;
14+
import org.elasticsearch.xpack.eql.parser.EqlParser;
15+
import org.elasticsearch.xpack.ql.expression.FieldAttribute;
16+
import org.elasticsearch.xpack.ql.expression.predicate.logical.And;
17+
import org.elasticsearch.xpack.ql.expression.predicate.logical.Not;
18+
import org.elasticsearch.xpack.ql.expression.predicate.regex.Like;
19+
import org.elasticsearch.xpack.ql.index.EsIndex;
20+
import org.elasticsearch.xpack.ql.index.IndexResolution;
21+
import org.elasticsearch.xpack.ql.plan.logical.Filter;
22+
import org.elasticsearch.xpack.ql.plan.logical.LogicalPlan;
23+
import org.elasticsearch.xpack.ql.plan.logical.OrderBy;
24+
import org.elasticsearch.xpack.ql.type.EsField;
25+
import org.elasticsearch.xpack.ql.type.TypesTests;
26+
27+
import java.util.Map;
28+
29+
public class OptimizerTests extends ESTestCase {
30+
31+
32+
private static final String INDEX_NAME = "test";
33+
private EqlParser parser = new EqlParser();
34+
private IndexResolution index = loadIndexResolution("mapping-default.json");
35+
private static Map<String, EsField> loadEqlMapping(String name) {
36+
return TypesTests.loadMapping(name);
37+
}
38+
39+
private IndexResolution loadIndexResolution(String name) {
40+
return IndexResolution.valid(new EsIndex(INDEX_NAME, loadEqlMapping(name)));
41+
}
42+
43+
private LogicalPlan accept(IndexResolution resolution, String eql) {
44+
PreAnalyzer preAnalyzer = new PreAnalyzer();
45+
Analyzer analyzer = new Analyzer(new EqlFunctionRegistry(), new Verifier());
46+
Optimizer optimizer = new Optimizer();
47+
return optimizer.optimize(analyzer.analyze(preAnalyzer.preAnalyze(parser.createStatement(eql), resolution)));
48+
}
49+
50+
private LogicalPlan accept(String eql) {
51+
return accept(index, eql);
52+
}
53+
54+
55+
public void testEqualsWildcard() {
56+
for (String q : new String[]{"foo where command_line == '* bar *'", "foo where '* bar *' == command_line"}) {
57+
LogicalPlan plan = accept(q);
58+
assertTrue(plan instanceof OrderBy);
59+
plan = ((OrderBy) plan).child();
60+
assertTrue(plan instanceof Filter);
61+
62+
Filter filter = (Filter) plan;
63+
And condition = (And) filter.condition();
64+
assertTrue(condition.right() instanceof Like);
65+
66+
Like like = (Like) condition.right();
67+
assertEquals(((FieldAttribute) like.field()).name(), "command_line");
68+
assertEquals(like.pattern().asJavaRegex(), "^.* bar .*$");
69+
assertEquals(like.pattern().asLuceneWildcard(), "* bar *");
70+
assertEquals(like.pattern().asIndexNameWildcard(), "* bar *");
71+
}
72+
}
73+
74+
public void testNotEqualsWildcard() {
75+
for (String q : new String[]{"foo where command_line != '* baz *'", "foo where '* baz *' != command_line"}) {
76+
77+
LogicalPlan plan = accept(q);
78+
assertTrue(plan instanceof OrderBy);
79+
plan = ((OrderBy) plan).child();
80+
assertTrue(plan instanceof Filter);
81+
82+
Filter filter = (Filter) plan;
83+
And condition = (And) filter.condition();
84+
assertTrue(condition.right() instanceof Not);
85+
86+
Not not = (Not) condition.right();
87+
Like like = (Like) not.field();
88+
assertEquals(((FieldAttribute) like.field()).name(), "command_line");
89+
assertEquals(like.pattern().asJavaRegex(), "^.* baz .*$");
90+
assertEquals(like.pattern().asLuceneWildcard(), "* baz *");
91+
assertEquals(like.pattern().asIndexNameWildcard(), "* baz *");
92+
}
93+
}
94+
95+
public void testWildcardEscapes() {
96+
LogicalPlan plan = accept("foo where command_line == '* %bar_ * \\\\ \\n \\r \\t'");
97+
assertTrue(plan instanceof OrderBy);
98+
plan = ((OrderBy) plan).child();
99+
assertTrue(plan instanceof Filter);
100+
101+
Filter filter = (Filter) plan;
102+
And condition = (And) filter.condition();
103+
assertTrue(condition.right() instanceof Like);
104+
105+
Like like = (Like) condition.right();
106+
assertEquals(((FieldAttribute) like.field()).name(), "command_line");
107+
assertEquals(like.pattern().asJavaRegex(), "^.* %bar_ .* \\\\ \n \r \t$");
108+
assertEquals(like.pattern().asLuceneWildcard(), "* %bar_ * \\\\ \n \r \t");
109+
assertEquals(like.pattern().asIndexNameWildcard(), "* %bar_ * \\ \n \r \t");
110+
}
111+
}

0 commit comments

Comments
 (0)