Skip to content

Commit 7dce974

Browse files
Add ExpressionQueryBuilder with serialization
1 parent 6060269 commit 7dce974

34 files changed

+383
-89
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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.index.query;
8+
9+
import org.apache.lucene.util.automaton.Automaton;
10+
11+
/**
12+
* Query that matches documents based on a Lucene Automaton.
13+
*/
14+
public interface AutomatonTranslatable {
15+
Automaton getAutomaton();
16+
17+
String getAutomatonDescription();
18+
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616
/**
1717
* Query that matches documents based on a Lucene Automaton.
1818
*/
19-
public class AutomatonQuery extends Query {
19+
public class EsqlAutomatonQuery extends Query {
2020

2121
private final String field;
2222
private final Automaton automaton;
2323
private final String automatonDescription;
2424

25-
public AutomatonQuery(Source source, String field, Automaton automaton, String automatonDescription) {
25+
public EsqlAutomatonQuery(Source source, String field, Automaton automaton, String automatonDescription) {
2626
super(source);
2727
this.field = field;
2828
this.automaton = automaton;
@@ -53,7 +53,7 @@ public boolean equals(Object obj) {
5353
return false;
5454
}
5555

56-
AutomatonQuery other = (AutomatonQuery) obj;
56+
EsqlAutomatonQuery other = (EsqlAutomatonQuery) obj;
5757
return Objects.equals(field, other.field)
5858
&& Objects.equals(automaton, other.automaton)
5959
&& Objects.equals(automatonDescription, other.automatonDescription);

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

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,55 +7,76 @@
77

88
package org.elasticsearch.xpack.esql.expression.function.scalar.string.regex;
99

10+
import org.apache.lucene.util.automaton.Automaton;
11+
import org.elasticsearch.common.breaker.CircuitBreaker;
12+
import org.elasticsearch.common.breaker.NoopCircuitBreaker;
1013
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
1114
import org.elasticsearch.common.io.stream.StreamInput;
1215
import org.elasticsearch.common.io.stream.StreamOutput;
16+
import org.elasticsearch.common.util.BigArrays;
17+
import org.elasticsearch.compute.data.BlockFactory;
18+
import org.elasticsearch.compute.data.BlockStreamInput;
19+
import org.elasticsearch.index.query.AutomatonTranslatable;
1320
import org.elasticsearch.xpack.esql.core.expression.Expression;
1421
import org.elasticsearch.xpack.esql.core.expression.FieldAttribute;
1522
import org.elasticsearch.xpack.esql.core.expression.predicate.regex.WildcardPattern;
1623
import org.elasticsearch.xpack.esql.core.expression.predicate.regex.WildcardPatternList;
17-
import org.elasticsearch.xpack.esql.core.querydsl.query.AutomatonQuery;
1824
import org.elasticsearch.xpack.esql.core.querydsl.query.Query;
1925
import org.elasticsearch.xpack.esql.core.querydsl.query.WildcardQuery;
2026
import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
2127
import org.elasticsearch.xpack.esql.core.tree.Source;
2228
import org.elasticsearch.xpack.esql.expression.function.Param;
29+
import org.elasticsearch.xpack.esql.io.stream.ExpressionQuery;
2330
import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput;
2431
import org.elasticsearch.xpack.esql.optimizer.rules.physical.local.LucenePushdownPredicates;
2532
import org.elasticsearch.xpack.esql.planner.TranslatorHandler;
33+
import org.elasticsearch.xpack.esql.session.Configuration;
2634

2735
import java.io.IOException;
2836
import java.util.stream.Collectors;
2937

30-
public class WildcardLikeList extends RegexMatch<WildcardPatternList> {
38+
public class WildcardLikeList extends RegexMatch<WildcardPatternList> implements AutomatonTranslatable {
3139
public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(
3240
Expression.class,
3341
"WildcardLikeList",
3442
WildcardLikeList::new
3543
);
44+
private Configuration configuration;
3645

3746
/**
3847
* The documentation for this function is in WildcardLike, and shown to the users `LIKE` in the docs.
3948
*/
4049
public WildcardLikeList(
4150
Source source,
4251
@Param(name = "str", type = { "keyword", "text" }, description = "A literal expression.") Expression left,
43-
@Param(name = "pattern", type = { "keyword", "text" }, description = "Pattern.") WildcardPatternList patterns
52+
@Param(name = "pattern", type = { "keyword", "text" }, description = "Pattern.") WildcardPatternList patterns,
53+
Configuration configuration
4454
) {
45-
this(source, left, patterns, false);
55+
this(source, left, patterns, false, configuration);
4656
}
4757

48-
public WildcardLikeList(Source source, Expression left, WildcardPatternList patterns, boolean caseInsensitive) {
58+
public WildcardLikeList(
59+
Source source,
60+
Expression left,
61+
WildcardPatternList patterns,
62+
boolean caseInsensitive,
63+
Configuration configuration
64+
) {
4965
super(source, left, patterns, caseInsensitive);
66+
this.configuration = configuration;
5067
}
5168

5269
public WildcardLikeList(StreamInput in) throws IOException {
5370
this(
5471
Source.readFrom((PlanStreamInput) in),
5572
in.readNamedWriteable(Expression.class),
5673
new WildcardPatternList(in),
57-
deserializeCaseInsensitivity(in)
74+
deserializeCaseInsensitivity(in),
75+
null
5876
);
77+
BlockFactory blockFactory = new BlockFactory(new NoopCircuitBreaker(CircuitBreaker.REQUEST), BigArrays.NON_RECYCLING_INSTANCE);
78+
BlockStreamInput blockStreamInput = new BlockStreamInput(in, blockFactory);
79+
this.configuration = new Configuration(blockStreamInput);
5980
}
6081

6182
@Override
@@ -64,6 +85,7 @@ public void writeTo(StreamOutput out) throws IOException {
6485
out.writeNamedWriteable(field());
6586
pattern().writeTo(out);
6687
serializeCaseInsensitivity(out);
88+
configuration.writeTo(out);
6789
}
6890

6991
@Override
@@ -78,12 +100,12 @@ public String getWriteableName() {
78100

79101
@Override
80102
protected NodeInfo<WildcardLikeList> info() {
81-
return NodeInfo.create(this, WildcardLikeList::new, field(), pattern(), caseInsensitive());
103+
return NodeInfo.create(this, WildcardLikeList::new, field(), pattern(), caseInsensitive(), configuration);
82104
}
83105

84106
@Override
85107
protected WildcardLikeList replaceChild(Expression newLeft) {
86-
return new WildcardLikeList(source(), newLeft, pattern(), caseInsensitive());
108+
return new WildcardLikeList(source(), newLeft, pattern(), caseInsensitive(), configuration);
87109
}
88110

89111
/**
@@ -104,20 +126,27 @@ public Translatable translatable(LucenePushdownPredicates pushdownPredicates) {
104126
public Query asQuery(LucenePushdownPredicates pushdownPredicates, TranslatorHandler handler) {
105127
var field = field();
106128
LucenePushdownPredicates.checkIsPushableAttribute(field);
107-
return translateField(handler.nameOf(field instanceof FieldAttribute fa ? fa.exactAttribute() : field));
129+
String targetFieldName = handler.nameOf(field instanceof FieldAttribute fa ? fa.exactAttribute() : field);
130+
return translateField(targetFieldName);
108131
}
109132

110133
/**
111134
* Translates the field to a {@link WildcardQuery} using the first pattern in the list.
112135
* Throws an {@link IllegalArgumentException} if the pattern list contains more than one pattern.
113136
*/
114137
private Query translateField(String targetFieldName) {
115-
return new AutomatonQuery(source(), targetFieldName, pattern().createAutomaton(caseInsensitive()), getAutomatonDescription());
138+
return new ExpressionQuery(source(), targetFieldName, this, configuration);
116139
}
117140

118-
private String getAutomatonDescription() {
141+
@Override
142+
public String getAutomatonDescription() {
119143
// we use the information used to create the automaton to describe the query here
120144
String patternDesc = pattern().patternList().stream().map(WildcardPattern::pattern).collect(Collectors.joining("\", \""));
121145
return "LIKE(\"" + patternDesc + "\"), caseInsensitive=" + caseInsensitive();
122146
}
147+
148+
@Override
149+
public Automaton getAutomaton() {
150+
return pattern().createAutomaton(caseInsensitive());
151+
}
123152
}
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.io.stream;
8+
9+
import org.elasticsearch.index.query.QueryBuilder;
10+
import org.elasticsearch.xpack.esql.core.expression.Expression;
11+
import org.elasticsearch.xpack.esql.core.querydsl.query.Query;
12+
import org.elasticsearch.xpack.esql.core.tree.Source;
13+
import org.elasticsearch.xpack.esql.session.Configuration;
14+
15+
import java.util.Objects;
16+
17+
/**
18+
* Query that matches documents based on a Lucene Automaton.
19+
*/
20+
public class ExpressionQuery extends Query {
21+
22+
private final String targetFieldName;
23+
private final Expression expression;
24+
private final Configuration config;
25+
26+
public ExpressionQuery(Source source, String targetFieldName, Expression expression, Configuration config) {
27+
super(source);
28+
this.targetFieldName = targetFieldName;
29+
this.expression = expression;
30+
this.config = config;
31+
}
32+
33+
public String field() {
34+
return targetFieldName;
35+
}
36+
37+
@Override
38+
protected QueryBuilder asBuilder() {
39+
return new ExpressionQueryBuilder(targetFieldName, expression, config);
40+
}
41+
42+
@Override
43+
public int hashCode() {
44+
return Objects.hash(targetFieldName, expression);
45+
}
46+
47+
@Override
48+
public boolean equals(Object obj) {
49+
if (this == obj) {
50+
return true;
51+
}
52+
53+
if (obj == null || getClass() != obj.getClass()) {
54+
return false;
55+
}
56+
57+
ExpressionQuery other = (ExpressionQuery) obj;
58+
return Objects.equals(targetFieldName, other.targetFieldName)
59+
&& Objects.equals(expression, other.expression);
60+
}
61+
62+
@Override
63+
protected String innerToString() {
64+
return "AutomatonQuery{" + "field='" + targetFieldName + '\'' + '}';
65+
}
66+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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+
8+
package org.elasticsearch.xpack.esql.io.stream;
9+
10+
import org.apache.lucene.index.Term;
11+
import org.apache.lucene.search.AutomatonQuery;
12+
import org.apache.lucene.search.Query;
13+
import org.apache.lucene.util.automaton.Automaton;
14+
import org.elasticsearch.TransportVersion;
15+
import org.elasticsearch.common.Strings;
16+
import org.elasticsearch.common.breaker.CircuitBreaker;
17+
import org.elasticsearch.common.breaker.NoopCircuitBreaker;
18+
import org.elasticsearch.common.io.stream.NamedWriteable;
19+
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
20+
import org.elasticsearch.common.io.stream.StreamInput;
21+
import org.elasticsearch.common.io.stream.StreamOutput;
22+
import org.elasticsearch.common.util.BigArrays;
23+
import org.elasticsearch.compute.data.BlockFactory;
24+
import org.elasticsearch.compute.data.BlockStreamInput;
25+
import org.elasticsearch.compute.operator.Operator;
26+
import org.elasticsearch.index.query.AbstractQueryBuilder;
27+
import org.elasticsearch.index.query.AutomatonTranslatable;
28+
import org.elasticsearch.index.query.MultiTermQueryBuilder;
29+
import org.elasticsearch.index.query.QueryBuilder;
30+
import org.elasticsearch.index.query.SearchExecutionContext;
31+
import org.elasticsearch.xcontent.XContentBuilder;
32+
import org.elasticsearch.xpack.esql.core.expression.Expression;
33+
import org.elasticsearch.xpack.esql.session.Configuration;
34+
35+
import java.io.IOException;
36+
import java.io.UnsupportedEncodingException;
37+
import java.util.Objects;
38+
39+
/**
40+
* Implements an Automaton query, which matches documents based on a Lucene Automaton.
41+
* It does not support serialization or XContent representation.
42+
*/
43+
public class ExpressionQueryBuilder extends AbstractQueryBuilder<ExpressionQueryBuilder> implements MultiTermQueryBuilder {
44+
public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(
45+
QueryBuilder.class,
46+
"expressionQueryBuilder",
47+
ExpressionQueryBuilder::new
48+
);
49+
private final String fieldName;
50+
private final Expression expression;
51+
private final Configuration config;
52+
53+
public ExpressionQueryBuilder(String fieldName, Expression expression, Configuration config) {
54+
if (Strings.isEmpty(fieldName)) {
55+
throw new IllegalArgumentException("field name is null or empty");
56+
}
57+
if (expression == null) {
58+
throw new IllegalArgumentException("expression cannot be null");
59+
}
60+
this.fieldName = fieldName;
61+
this.expression = expression;
62+
this.config = config;
63+
}
64+
65+
/**
66+
* Read from a stream.
67+
*/
68+
public ExpressionQueryBuilder(StreamInput in) throws IOException {
69+
super(in);
70+
fieldName = in.readString();
71+
BlockFactory blockFactory = new BlockFactory(new NoopCircuitBreaker(CircuitBreaker.REQUEST), BigArrays.NON_RECYCLING_INSTANCE);
72+
BlockStreamInput blockStreamInput = new BlockStreamInput(in, blockFactory);
73+
this.config = new Configuration(blockStreamInput);
74+
PlanStreamInput planStreamInput = new PlanStreamInput(blockStreamInput, in.namedWriteableRegistry(), config);
75+
this.expression = planStreamInput.readNamedWriteable(Expression.class);
76+
}
77+
78+
@Override
79+
protected void doWriteTo(StreamOutput out) throws IOException {
80+
out.writeString(this.fieldName);
81+
config.writeTo(out);
82+
PlanStreamOutput planStreamOutput = new PlanStreamOutput(out, config);
83+
planStreamOutput.writeNamedWriteable(expression);
84+
}
85+
86+
@Override
87+
public String fieldName() {
88+
return fieldName;
89+
}
90+
91+
@Override
92+
public String getWriteableName() {
93+
return ENTRY.name;
94+
}
95+
96+
@Override
97+
protected void doXContent(XContentBuilder builder, Params params) throws IOException {
98+
throw new UnsupportedEncodingException("AutomatonQueryBuilder does not support doXContent");
99+
}
100+
101+
@Override
102+
protected Query doToQuery(SearchExecutionContext context) throws IOException {
103+
if (expression instanceof AutomatonTranslatable automatonTranslatable) {
104+
Automaton automaton = automatonTranslatable.getAutomaton();
105+
String description = automatonTranslatable.getAutomatonDescription();
106+
return new AutomatonQueryWithDescription(new Term(fieldName), automaton, description);
107+
} else {
108+
throw new UnsupportedOperationException("ExpressionQueryBuilder does not support non-automaton expressions");
109+
}
110+
}
111+
112+
@Override
113+
protected int doHashCode() {
114+
return Objects.hash(fieldName, expression);
115+
}
116+
117+
@Override
118+
protected boolean doEquals(ExpressionQueryBuilder other) {
119+
return Objects.equals(fieldName, other.fieldName) && Objects.equals(expression, other.expression);
120+
}
121+
122+
@Override
123+
public TransportVersion getMinimalSupportedVersion() {
124+
throw new UnsupportedOperationException("AutomatonQueryBuilder does not support getMinimalSupportedVersion");
125+
}
126+
127+
static class AutomatonQueryWithDescription extends AutomatonQuery {
128+
private final String description;
129+
130+
AutomatonQueryWithDescription(Term term, Automaton automaton, String description) {
131+
super(term, automaton);
132+
this.description = description;
133+
}
134+
135+
@Override
136+
public String toString(String field) {
137+
if (this.field.equals(field)) {
138+
return description;
139+
}
140+
return this.field + ":" + description;
141+
}
142+
}
143+
}

0 commit comments

Comments
 (0)