Skip to content

Commit 9b2a84d

Browse files
ioanatiaelasticmachine
authored andcommitted
Make search functions translation aware (elastic#118355) (elastic#118677)
* Introduce TranslationAware interface * Serialize query builder * Fix EsqlNodeSubclassTests * Add javadoc * Address review comments * Revert changes on making constructors private Co-authored-by: Elastic Machine <[email protected]>
1 parent a341a79 commit 9b2a84d

File tree

10 files changed

+234
-25
lines changed

10 files changed

+234
-25
lines changed

server/src/main/java/org/elasticsearch/TransportVersions.java

+1
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ static TransportVersion def(int id) {
145145
public static final TransportVersion ADD_DATA_STREAM_OPTIONS_TO_TEMPLATES = def(8_805_00_0);
146146
public static final TransportVersion KNN_QUERY_RESCORE_OVERSAMPLE = def(8_806_00_0);
147147
public static final TransportVersion SEMANTIC_QUERY_LENIENT = def(8_807_00_0);
148+
public static final TransportVersion ESQL_QUERY_BUILDER_IN_SEARCH_FUNCTIONS = def(8_808_00_0);
148149

149150
/*
150151
* STOP! READ THIS FIRST! No, really,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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.core.expression;
9+
10+
import org.elasticsearch.xpack.esql.core.planner.TranslatorHandler;
11+
import org.elasticsearch.xpack.esql.core.querydsl.query.Query;
12+
13+
/**
14+
* Expressions can implement this interface to control how they would be translated and pushed down as Lucene queries.
15+
* When an expression implements {@link TranslationAware}, we call {@link #asQuery(TranslatorHandler)} to get the
16+
* {@link Query} translation, instead of relying on the registered translators from EsqlExpressionTranslators.
17+
*/
18+
public interface TranslationAware {
19+
Query asQuery(TranslatorHandler translatorHandler);
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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.core.querydsl.query;
9+
10+
import org.elasticsearch.index.query.QueryBuilder;
11+
import org.elasticsearch.xpack.esql.core.tree.Source;
12+
13+
/**
14+
* Expressions that store their own {@link QueryBuilder} and implement
15+
* {@link org.elasticsearch.xpack.esql.core.expression.TranslationAware} can use {@link TranslationAwareExpressionQuery}
16+
* to wrap their {@link QueryBuilder}, instead of using the other existing {@link Query} implementations.
17+
*/
18+
public class TranslationAwareExpressionQuery extends Query {
19+
private final QueryBuilder queryBuilder;
20+
21+
public TranslationAwareExpressionQuery(Source source, QueryBuilder queryBuilder) {
22+
super(source);
23+
this.queryBuilder = queryBuilder;
24+
}
25+
26+
@Override
27+
public QueryBuilder asBuilder() {
28+
return queryBuilder;
29+
}
30+
31+
@Override
32+
protected String innerToString() {
33+
return queryBuilder.toString();
34+
}
35+
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ private static FunctionDefinition[][] functions() {
404404
def(MvSum.class, MvSum::new, "mv_sum"),
405405
def(Split.class, Split::new, "split") },
406406
// fulltext functions
407-
new FunctionDefinition[] { def(Match.class, Match::new, "match"), def(QueryString.class, QueryString::new, "qstr") } };
407+
new FunctionDefinition[] { def(Match.class, bi(Match::new), "match"), def(QueryString.class, uni(QueryString::new), "qstr") } };
408408

409409
}
410410

@@ -414,9 +414,9 @@ private static FunctionDefinition[][] snapshotFunctions() {
414414
// The delay() function is for debug/snapshot environments only and should never be enabled in a non-snapshot build.
415415
// This is an experimental function and can be removed without notice.
416416
def(Delay.class, Delay::new, "delay"),
417-
def(Kql.class, Kql::new, "kql"),
417+
def(Kql.class, uni(Kql::new), "kql"),
418418
def(Rate.class, Rate::withUnresolvedTimestamp, "rate"),
419-
def(Term.class, Term::new, "term") } };
419+
def(Term.class, bi(Term::new), "term") } };
420420
}
421421

422422
public EsqlFunctionRegistry snapshotRegistry() {

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextFunction.java

+44-2
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,21 @@
88
package org.elasticsearch.xpack.esql.expression.function.fulltext;
99

1010
import org.apache.lucene.util.BytesRef;
11+
import org.elasticsearch.index.query.QueryBuilder;
1112
import org.elasticsearch.xpack.esql.core.expression.Expression;
1213
import org.elasticsearch.xpack.esql.core.expression.Nullability;
14+
import org.elasticsearch.xpack.esql.core.expression.TranslationAware;
1315
import org.elasticsearch.xpack.esql.core.expression.TypeResolutions;
1416
import org.elasticsearch.xpack.esql.core.expression.function.Function;
17+
import org.elasticsearch.xpack.esql.core.planner.ExpressionTranslator;
18+
import org.elasticsearch.xpack.esql.core.planner.TranslatorHandler;
19+
import org.elasticsearch.xpack.esql.core.querydsl.query.Query;
20+
import org.elasticsearch.xpack.esql.core.querydsl.query.TranslationAwareExpressionQuery;
1521
import org.elasticsearch.xpack.esql.core.tree.Source;
1622
import org.elasticsearch.xpack.esql.core.type.DataType;
1723

1824
import java.util.List;
25+
import java.util.Objects;
1926

2027
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.DEFAULT;
2128
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNullAndFoldable;
@@ -26,13 +33,15 @@
2633
* These functions needs to be pushed down to Lucene queries to be executed - there's no Evaluator for them, but depend on
2734
* {@link org.elasticsearch.xpack.esql.optimizer.LocalPhysicalPlanOptimizer} to rewrite them into Lucene queries.
2835
*/
29-
public abstract class FullTextFunction extends Function {
36+
public abstract class FullTextFunction extends Function implements TranslationAware {
3037

3138
private final Expression query;
39+
private final QueryBuilder queryBuilder;
3240

33-
protected FullTextFunction(Source source, Expression query, List<Expression> children) {
41+
protected FullTextFunction(Source source, Expression query, List<Expression> children, QueryBuilder queryBuilder) {
3442
super(source, children);
3543
this.query = query;
44+
this.queryBuilder = queryBuilder;
3645
}
3746

3847
@Override
@@ -116,4 +125,37 @@ public Nullability nullable() {
116125
public String functionType() {
117126
return "function";
118127
}
128+
129+
@Override
130+
public int hashCode() {
131+
return Objects.hash(super.hashCode(), queryBuilder);
132+
}
133+
134+
@Override
135+
public boolean equals(Object obj) {
136+
if (false == super.equals(obj)) {
137+
return false;
138+
}
139+
140+
return Objects.equals(queryBuilder, ((FullTextFunction) obj).queryBuilder);
141+
}
142+
143+
@Override
144+
public Query asQuery(TranslatorHandler translatorHandler) {
145+
if (queryBuilder != null) {
146+
return new TranslationAwareExpressionQuery(source(), queryBuilder);
147+
}
148+
149+
ExpressionTranslator<? extends FullTextFunction> translator = translator();
150+
return translator.translate(this, translatorHandler);
151+
}
152+
153+
public QueryBuilder queryBuilder() {
154+
return queryBuilder;
155+
}
156+
157+
@SuppressWarnings("rawtypes")
158+
protected abstract ExpressionTranslator<? extends FullTextFunction> translator();
159+
160+
public abstract Expression replaceQueryBuilder(QueryBuilder queryBuilder);
119161
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Kql.java

+32-6
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,20 @@
77

88
package org.elasticsearch.xpack.esql.expression.function.fulltext;
99

10+
import org.elasticsearch.TransportVersions;
1011
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
1112
import org.elasticsearch.common.io.stream.StreamInput;
1213
import org.elasticsearch.common.io.stream.StreamOutput;
14+
import org.elasticsearch.index.query.QueryBuilder;
1315
import org.elasticsearch.xpack.esql.core.expression.Expression;
16+
import org.elasticsearch.xpack.esql.core.planner.ExpressionTranslator;
1417
import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
1518
import org.elasticsearch.xpack.esql.core.tree.Source;
1619
import org.elasticsearch.xpack.esql.expression.function.Example;
1720
import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
1821
import org.elasticsearch.xpack.esql.expression.function.Param;
1922
import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput;
23+
import org.elasticsearch.xpack.esql.planner.EsqlExpressionTranslators;
2024
import org.elasticsearch.xpack.esql.querydsl.query.KqlQuery;
2125

2226
import java.io.IOException;
@@ -26,7 +30,7 @@
2630
* Full text function that performs a {@link KqlQuery} .
2731
*/
2832
public class Kql extends FullTextFunction {
29-
public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Kql", Kql::new);
33+
public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Kql", Kql::readFrom);
3034

3135
@FunctionInfo(
3236
returnType = "boolean",
@@ -42,17 +46,30 @@ public Kql(
4246
description = "Query string in KQL query string format."
4347
) Expression queryString
4448
) {
45-
super(source, queryString, List.of(queryString));
49+
super(source, queryString, List.of(queryString), null);
4650
}
4751

48-
private Kql(StreamInput in) throws IOException {
49-
this(Source.readFrom((PlanStreamInput) in), in.readNamedWriteable(Expression.class));
52+
public Kql(Source source, Expression queryString, QueryBuilder queryBuilder) {
53+
super(source, queryString, List.of(queryString), queryBuilder);
54+
}
55+
56+
private static Kql readFrom(StreamInput in) throws IOException {
57+
Source source = Source.readFrom((PlanStreamInput) in);
58+
Expression query = in.readNamedWriteable(Expression.class);
59+
QueryBuilder queryBuilder = null;
60+
if (in.getTransportVersion().onOrAfter(TransportVersions.ESQL_QUERY_BUILDER_IN_SEARCH_FUNCTIONS)) {
61+
queryBuilder = in.readOptionalNamedWriteable(QueryBuilder.class);
62+
}
63+
return new Kql(source, query, queryBuilder);
5064
}
5165

5266
@Override
5367
public void writeTo(StreamOutput out) throws IOException {
5468
source().writeTo(out);
5569
out.writeNamedWriteable(query());
70+
if (out.getTransportVersion().onOrAfter(TransportVersions.ESQL_QUERY_BUILDER_IN_SEARCH_FUNCTIONS)) {
71+
out.writeOptionalNamedWriteable(queryBuilder());
72+
}
5673
}
5774

5875
@Override
@@ -62,12 +79,21 @@ public String getWriteableName() {
6279

6380
@Override
6481
public Expression replaceChildren(List<Expression> newChildren) {
65-
return new Kql(source(), newChildren.get(0));
82+
return new Kql(source(), newChildren.get(0), queryBuilder());
6683
}
6784

6885
@Override
6986
protected NodeInfo<? extends Expression> info() {
70-
return NodeInfo.create(this, Kql::new, query());
87+
return NodeInfo.create(this, Kql::new, query(), queryBuilder());
7188
}
7289

90+
@Override
91+
protected ExpressionTranslator<Kql> translator() {
92+
return new EsqlExpressionTranslators.KqlFunctionTranslator();
93+
}
94+
95+
@Override
96+
public Expression replaceQueryBuilder(QueryBuilder queryBuilder) {
97+
return new Kql(source(), query(), queryBuilder);
98+
}
7399
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java

+29-4
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@
88
package org.elasticsearch.xpack.esql.expression.function.fulltext;
99

1010
import org.apache.lucene.util.BytesRef;
11+
import org.elasticsearch.TransportVersions;
1112
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
1213
import org.elasticsearch.common.io.stream.StreamInput;
1314
import org.elasticsearch.common.io.stream.StreamOutput;
15+
import org.elasticsearch.index.query.QueryBuilder;
1416
import org.elasticsearch.xpack.esql.capabilities.Validatable;
1517
import org.elasticsearch.xpack.esql.common.Failure;
1618
import org.elasticsearch.xpack.esql.common.Failures;
1719
import org.elasticsearch.xpack.esql.core.expression.Expression;
1820
import org.elasticsearch.xpack.esql.core.expression.FieldAttribute;
1921
import org.elasticsearch.xpack.esql.core.expression.TypeResolutions;
22+
import org.elasticsearch.xpack.esql.core.planner.ExpressionTranslator;
2023
import org.elasticsearch.xpack.esql.core.querydsl.query.QueryStringQuery;
2124
import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
2225
import org.elasticsearch.xpack.esql.core.tree.Source;
@@ -27,6 +30,7 @@
2730
import org.elasticsearch.xpack.esql.expression.function.Param;
2831
import org.elasticsearch.xpack.esql.expression.function.scalar.convert.AbstractConvertFunction;
2932
import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput;
33+
import org.elasticsearch.xpack.esql.planner.EsqlExpressionTranslators;
3034
import org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter;
3135

3236
import java.io.IOException;
@@ -109,22 +113,33 @@ public Match(
109113
description = "Value to find in the provided field."
110114
) Expression matchQuery
111115
) {
112-
super(source, matchQuery, List.of(field, matchQuery));
116+
this(source, field, matchQuery, null);
117+
}
118+
119+
public Match(Source source, Expression field, Expression matchQuery, QueryBuilder queryBuilder) {
120+
super(source, matchQuery, List.of(field, matchQuery), queryBuilder);
113121
this.field = field;
114122
}
115123

116124
private static Match readFrom(StreamInput in) throws IOException {
117125
Source source = Source.readFrom((PlanStreamInput) in);
118126
Expression field = in.readNamedWriteable(Expression.class);
119127
Expression query = in.readNamedWriteable(Expression.class);
120-
return new Match(source, field, query);
128+
QueryBuilder queryBuilder = null;
129+
if (in.getTransportVersion().onOrAfter(TransportVersions.ESQL_QUERY_BUILDER_IN_SEARCH_FUNCTIONS)) {
130+
queryBuilder = in.readOptionalNamedWriteable(QueryBuilder.class);
131+
}
132+
return new Match(source, field, query, queryBuilder);
121133
}
122134

123135
@Override
124136
public void writeTo(StreamOutput out) throws IOException {
125137
source().writeTo(out);
126138
out.writeNamedWriteable(field());
127139
out.writeNamedWriteable(query());
140+
if (out.getTransportVersion().onOrAfter(TransportVersions.ESQL_QUERY_BUILDER_IN_SEARCH_FUNCTIONS)) {
141+
out.writeOptionalNamedWriteable(queryBuilder());
142+
}
128143
}
129144

130145
@Override
@@ -224,12 +239,12 @@ public Object queryAsObject() {
224239

225240
@Override
226241
public Expression replaceChildren(List<Expression> newChildren) {
227-
return new Match(source(), newChildren.get(0), newChildren.get(1));
242+
return new Match(source(), newChildren.get(0), newChildren.get(1), queryBuilder());
228243
}
229244

230245
@Override
231246
protected NodeInfo<? extends Expression> info() {
232-
return NodeInfo.create(this, Match::new, field, query());
247+
return NodeInfo.create(this, Match::new, field, query(), queryBuilder());
233248
}
234249

235250
protected TypeResolutions.ParamOrdinal queryParamOrdinal() {
@@ -245,6 +260,16 @@ public String functionType() {
245260
return isOperator() ? "operator" : super.functionType();
246261
}
247262

263+
@Override
264+
protected ExpressionTranslator<Match> translator() {
265+
return new EsqlExpressionTranslators.MatchFunctionTranslator();
266+
}
267+
268+
@Override
269+
public Expression replaceQueryBuilder(QueryBuilder queryBuilder) {
270+
return new Match(source(), field, query(), queryBuilder);
271+
}
272+
248273
@Override
249274
public String functionName() {
250275
return isOperator() ? ":" : super.functionName();

0 commit comments

Comments
 (0)