Skip to content

Commit 6b76e1a

Browse files
LikeListPushdown
1 parent 1edf77c commit 6b76e1a

File tree

4 files changed

+165
-18
lines changed

4 files changed

+165
-18
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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.TransportVersions;
18+
import org.elasticsearch.common.Strings;
19+
import org.elasticsearch.common.io.stream.StreamOutput;
20+
import org.elasticsearch.xcontent.XContentBuilder;
21+
22+
import java.io.IOException;
23+
import java.io.UnsupportedEncodingException;
24+
import java.util.Objects;
25+
26+
/**
27+
* Implements an Automaton query, which matches documents based on a Lucene Automaton.
28+
* It does not support serialization or XContent representation,
29+
*/
30+
public class AutomatonQueryBuilder extends AbstractQueryBuilder<AutomatonQueryBuilder> implements MultiTermQueryBuilder {
31+
private final String fieldName;
32+
private final Automaton automaton;
33+
34+
public AutomatonQueryBuilder(String fieldName, Automaton automaton) {
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+
}
44+
45+
@Override
46+
public String fieldName() {
47+
return fieldName;
48+
}
49+
50+
@Override
51+
public String getWriteableName() {
52+
throw new UnsupportedOperationException("AutomatonQueryBuilder does not support getWriteableName");
53+
}
54+
55+
@Override
56+
protected void doWriteTo(StreamOutput out) throws IOException {
57+
throw new UnsupportedEncodingException("AutomatonQueryBuilder does not support doWriteTo");
58+
}
59+
60+
@Override
61+
protected void doXContent(XContentBuilder builder, Params params) throws IOException {
62+
throw new UnsupportedEncodingException("AutomatonQueryBuilder does not support doXContent");
63+
}
64+
65+
@Override
66+
protected Query doToQuery(SearchExecutionContext context) throws IOException {
67+
return new AutomatonQuery(new Term(fieldName), automaton);
68+
}
69+
70+
@Override
71+
protected int doHashCode() {
72+
return Objects.hash(fieldName, automaton);
73+
}
74+
75+
@Override
76+
protected boolean doEquals(AutomatonQueryBuilder other) {
77+
return Objects.equals(fieldName, other.fieldName) && Objects.equals(automaton, other.automaton);
78+
}
79+
80+
// TO DO, what should be the minimal supported version?
81+
@Override
82+
public TransportVersion getMinimalSupportedVersion() {
83+
return TransportVersions.ZERO;
84+
}
85+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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+
24+
public AutomatonQuery(Source source, String field, Automaton automaton) {
25+
super(source);
26+
this.field = field;
27+
this.automaton = automaton;
28+
}
29+
30+
public String field() {
31+
return field;
32+
}
33+
34+
@Override
35+
protected QueryBuilder asBuilder() {
36+
return new AutomatonQueryBuilder(field, automaton);
37+
}
38+
39+
@Override
40+
public int hashCode() {
41+
return Objects.hash(field, automaton);
42+
}
43+
44+
@Override
45+
public boolean equals(Object obj) {
46+
if (this == obj) {
47+
return true;
48+
}
49+
50+
if (obj == null || getClass() != obj.getClass()) {
51+
return false;
52+
}
53+
54+
AutomatonQuery other = (AutomatonQuery) obj;
55+
return Objects.equals(field, other.field) && Objects.equals(automaton, other.automaton);
56+
}
57+
58+
@Override
59+
protected String innerToString() {
60+
return "AutomatonQuery{" + "field='" + field + '\'' + '}';
61+
}
62+
}

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

Lines changed: 16 additions & 10 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:AutomatonQuery";
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
}
@@ -324,12 +324,18 @@ private void testPushQuery(
324324
matchesList().item(matchesMap().entry("name", "test").entry("type", anyOf(equalTo("text"), equalTo("keyword")))),
325325
equalTo(found ? List.of(List.of(value)) : List.of())
326326
);
327-
Matcher<String> luceneQueryMatcher = anyOf(
328-
() -> Iterators.map(
329-
luceneQueryOptions.iterator(),
330-
(String s) -> equalTo(s.replaceAll("%value", value).replaceAll("%different_value", differentValue))
331-
)
332-
);
327+
328+
Matcher<String> luceneQueryMatcher;
329+
if (luceneQueryOptions.size() == 1 && luceneQueryOptions.get(0).equals("test:AutomatonQuery")) {
330+
luceneQueryMatcher = anyOf(startsWith("test:AutomatonQuery"));
331+
} else {
332+
luceneQueryMatcher = anyOf(
333+
() -> Iterators.map(
334+
luceneQueryOptions.iterator(),
335+
(String s) -> equalTo(s.replaceAll("%value", value).replaceAll("%different_value", differentValue))
336+
)
337+
);
338+
}
333339

334340
@SuppressWarnings("unchecked")
335341
List<Map<String, Object>> profiles = (List<Map<String, Object>>) ((Map<String, Object>) result.get("profile")).get("drivers");

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

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.elasticsearch.xpack.esql.core.expression.Expression;
1414
import org.elasticsearch.xpack.esql.core.expression.FieldAttribute;
1515
import org.elasticsearch.xpack.esql.core.expression.predicate.regex.WildcardPatternList;
16+
import org.elasticsearch.xpack.esql.core.querydsl.query.AutomatonQuery;
1617
import org.elasticsearch.xpack.esql.core.querydsl.query.Query;
1718
import org.elasticsearch.xpack.esql.core.querydsl.query.WildcardQuery;
1819
import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
@@ -89,10 +90,6 @@ protected WildcardLikeList replaceChild(Expression newLeft) {
8990
*/
9091
@Override
9192
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-
}
9693
return pushdownPredicates.isPushableAttribute(field()) ? Translatable.YES : Translatable.NO;
9794

9895
}
@@ -113,9 +110,6 @@ public Query asQuery(LucenePushdownPredicates pushdownPredicates, TranslatorHand
113110
* Throws an {@link IllegalArgumentException} if the pattern list contains more than one pattern.
114111
*/
115112
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());
113+
return new AutomatonQuery(source(), targetFieldName, pattern().createAutomaton(caseInsensitive()));
120114
}
121115
}

0 commit comments

Comments
 (0)