Skip to content

Commit caae426

Browse files
Pushdown for LIKE (LIST) (#129557)
Improved performance of LIKE (LIST) by pushing an Automaton to do the evaluation down to Lucine.
1 parent e6347b8 commit caae426

File tree

5 files changed

+189
-12
lines changed

5 files changed

+189
-12
lines changed

docs/changelog/129557.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 129557
2+
summary: Pushdown for LIKE (LIST)
3+
area: ES|QL
4+
type: enhancement
5+
issues: []
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.index.query;
11+
12+
import org.apache.lucene.index.Term;
13+
import org.apache.lucene.search.AutomatonQuery;
14+
import org.apache.lucene.search.Query;
15+
import org.apache.lucene.util.automaton.Automaton;
16+
import org.elasticsearch.TransportVersion;
17+
import org.elasticsearch.common.Strings;
18+
import org.elasticsearch.common.io.stream.StreamOutput;
19+
import org.elasticsearch.xcontent.XContentBuilder;
20+
21+
import java.io.IOException;
22+
import java.io.UnsupportedEncodingException;
23+
import java.util.Objects;
24+
25+
/**
26+
* Implements an Automaton query, which matches documents based on a Lucene Automaton.
27+
* It does not support serialization or XContent representation.
28+
*/
29+
public class AutomatonQueryBuilder extends AbstractQueryBuilder<AutomatonQueryBuilder> implements MultiTermQueryBuilder {
30+
private final String fieldName;
31+
private final Automaton automaton;
32+
private final String description;
33+
34+
public AutomatonQueryBuilder(String fieldName, Automaton automaton, String description) {
35+
if (Strings.isEmpty(fieldName)) {
36+
throw new IllegalArgumentException("field name is null or empty");
37+
}
38+
if (automaton == null) {
39+
throw new IllegalArgumentException("automaton cannot be null");
40+
}
41+
this.fieldName = fieldName;
42+
this.automaton = automaton;
43+
this.description = description;
44+
}
45+
46+
@Override
47+
public String fieldName() {
48+
return fieldName;
49+
}
50+
51+
@Override
52+
public String getWriteableName() {
53+
throw new UnsupportedOperationException("AutomatonQueryBuilder does not support getWriteableName");
54+
}
55+
56+
@Override
57+
protected void doWriteTo(StreamOutput out) throws IOException {
58+
throw new UnsupportedEncodingException("AutomatonQueryBuilder does not support doWriteTo");
59+
}
60+
61+
@Override
62+
protected void doXContent(XContentBuilder builder, Params params) throws IOException {
63+
throw new UnsupportedEncodingException("AutomatonQueryBuilder does not support doXContent");
64+
}
65+
66+
@Override
67+
protected Query doToQuery(SearchExecutionContext context) throws IOException {
68+
return new AutomatonQueryWithDescription(new Term(fieldName), automaton, description);
69+
}
70+
71+
@Override
72+
protected int doHashCode() {
73+
return Objects.hash(fieldName, automaton, description);
74+
}
75+
76+
@Override
77+
protected boolean doEquals(AutomatonQueryBuilder other) {
78+
return Objects.equals(fieldName, other.fieldName)
79+
&& Objects.equals(automaton, other.automaton)
80+
&& Objects.equals(description, other.description);
81+
}
82+
83+
@Override
84+
public TransportVersion getMinimalSupportedVersion() {
85+
throw new UnsupportedOperationException("AutomatonQueryBuilder does not support getMinimalSupportedVersion");
86+
}
87+
88+
static class AutomatonQueryWithDescription extends AutomatonQuery {
89+
private final String description;
90+
91+
AutomatonQueryWithDescription(Term term, Automaton automaton, String description) {
92+
super(term, automaton);
93+
this.description = description;
94+
}
95+
96+
@Override
97+
public String toString(String field) {
98+
if (this.field.equals(field)) {
99+
return description;
100+
}
101+
return this.field + ":" + description;
102+
}
103+
}
104+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
package org.elasticsearch.xpack.esql.core.querydsl.query;
8+
9+
import org.apache.lucene.util.automaton.Automaton;
10+
import org.elasticsearch.index.query.AutomatonQueryBuilder;
11+
import org.elasticsearch.index.query.QueryBuilder;
12+
import org.elasticsearch.xpack.esql.core.tree.Source;
13+
14+
import java.util.Objects;
15+
16+
/**
17+
* Query that matches documents based on a Lucene Automaton.
18+
*/
19+
public class AutomatonQuery extends Query {
20+
21+
private final String field;
22+
private final Automaton automaton;
23+
private final String automatonDescription;
24+
25+
public AutomatonQuery(Source source, String field, Automaton automaton, String automatonDescription) {
26+
super(source);
27+
this.field = field;
28+
this.automaton = automaton;
29+
this.automatonDescription = automatonDescription;
30+
}
31+
32+
public String field() {
33+
return field;
34+
}
35+
36+
@Override
37+
protected QueryBuilder asBuilder() {
38+
return new AutomatonQueryBuilder(field, automaton, automatonDescription);
39+
}
40+
41+
@Override
42+
public int hashCode() {
43+
return Objects.hash(field, automaton, automatonDescription);
44+
}
45+
46+
@Override
47+
public boolean equals(Object obj) {
48+
if (this == obj) {
49+
return true;
50+
}
51+
52+
if (obj == null || getClass() != obj.getClass()) {
53+
return false;
54+
}
55+
56+
AutomatonQuery other = (AutomatonQuery) obj;
57+
return Objects.equals(field, other.field)
58+
&& Objects.equals(automaton, other.automaton)
59+
&& Objects.equals(automatonDescription, other.automatonDescription);
60+
}
61+
62+
@Override
63+
protected String innerToString() {
64+
return "AutomatonQuery{" + "field='" + field + '\'' + '}';
65+
}
66+
}

x-pack/plugin/esql/qa/server/single-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/single_node/PushQueriesIT.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,13 +264,13 @@ public void testLikeList() throws IOException {
264264
| WHERE test like ("%value*", "abc*")
265265
""";
266266
String luceneQuery = switch (type) {
267-
case KEYWORD, CONSTANT_KEYWORD, MATCH_ONLY_TEXT_WITH_KEYWORD, AUTO, TEXT_WITH_KEYWORD -> "*:*";
267+
case CONSTANT_KEYWORD, MATCH_ONLY_TEXT_WITH_KEYWORD, AUTO, TEXT_WITH_KEYWORD -> "*:*";
268268
case SEMANTIC_TEXT_WITH_KEYWORD -> "FieldExistsQuery [field=_primary_term]";
269+
case KEYWORD -> "test:LIKE(\"%value*\", \"abc*\"), caseInsensitive=false";
269270
};
270271
ComputeSignature dataNodeSignature = switch (type) {
271-
case CONSTANT_KEYWORD -> ComputeSignature.FILTER_IN_QUERY;
272-
case AUTO, KEYWORD, TEXT_WITH_KEYWORD, MATCH_ONLY_TEXT_WITH_KEYWORD, SEMANTIC_TEXT_WITH_KEYWORD ->
273-
ComputeSignature.FILTER_IN_COMPUTE;
272+
case CONSTANT_KEYWORD, KEYWORD -> ComputeSignature.FILTER_IN_QUERY;
273+
case AUTO, TEXT_WITH_KEYWORD, MATCH_ONLY_TEXT_WITH_KEYWORD, SEMANTIC_TEXT_WITH_KEYWORD -> ComputeSignature.FILTER_IN_COMPUTE;
274274
};
275275
testPushQuery(value, esqlQuery, List.of(luceneQuery), dataNodeSignature, true);
276276
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/regex/WildcardLikeList.java

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
import org.elasticsearch.common.io.stream.StreamOutput;
1313
import org.elasticsearch.xpack.esql.core.expression.Expression;
1414
import org.elasticsearch.xpack.esql.core.expression.FieldAttribute;
15+
import org.elasticsearch.xpack.esql.core.expression.predicate.regex.WildcardPattern;
1516
import org.elasticsearch.xpack.esql.core.expression.predicate.regex.WildcardPatternList;
17+
import org.elasticsearch.xpack.esql.core.querydsl.query.AutomatonQuery;
1618
import org.elasticsearch.xpack.esql.core.querydsl.query.Query;
1719
import org.elasticsearch.xpack.esql.core.querydsl.query.WildcardQuery;
1820
import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
@@ -23,6 +25,7 @@
2325
import org.elasticsearch.xpack.esql.planner.TranslatorHandler;
2426

2527
import java.io.IOException;
28+
import java.util.stream.Collectors;
2629

2730
public class WildcardLikeList extends RegexMatch<WildcardPatternList> {
2831
public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(
@@ -89,10 +92,6 @@ protected WildcardLikeList replaceChild(Expression newLeft) {
8992
*/
9093
@Override
9194
public Translatable translatable(LucenePushdownPredicates pushdownPredicates) {
92-
if (pattern().patternList().size() != 1) {
93-
// we only support a single pattern in the list for pushdown for now
94-
return Translatable.NO;
95-
}
9695
return pushdownPredicates.isPushableAttribute(field()) ? Translatable.YES : Translatable.NO;
9796

9897
}
@@ -113,9 +112,12 @@ public Query asQuery(LucenePushdownPredicates pushdownPredicates, TranslatorHand
113112
* Throws an {@link IllegalArgumentException} if the pattern list contains more than one pattern.
114113
*/
115114
private Query translateField(String targetFieldName) {
116-
if (pattern().patternList().size() != 1) {
117-
throw new IllegalArgumentException("WildcardLikeList can only be translated when it has a single pattern");
118-
}
119-
return new WildcardQuery(source(), targetFieldName, pattern().patternList().getFirst().asLuceneWildcard(), caseInsensitive());
115+
return new AutomatonQuery(source(), targetFieldName, pattern().createAutomaton(caseInsensitive()), getAutomatonDescription());
116+
}
117+
118+
private String getAutomatonDescription() {
119+
// we use the information used to create the automaton to describe the query here
120+
String patternDesc = pattern().patternList().stream().map(WildcardPattern::pattern).collect(Collectors.joining("\", \""));
121+
return "LIKE(\"" + patternDesc + "\"), caseInsensitive=" + caseInsensitive();
120122
}
121123
}

0 commit comments

Comments
 (0)