Skip to content

Commit 9a0636a

Browse files
Merge
1 parent ab1e21a commit 9a0636a

File tree

3 files changed

+75
-45
lines changed

3 files changed

+75
-45
lines changed

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

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

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

10+
import org.apache.lucene.util.BytesRef;
1011
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
1112
import org.elasticsearch.common.io.stream.StreamInput;
1213
import org.elasticsearch.common.io.stream.StreamOutput;
@@ -18,13 +19,15 @@
1819
import org.elasticsearch.xpack.esql.core.InvalidArgumentException;
1920
import org.elasticsearch.xpack.esql.core.expression.Expression;
2021
import org.elasticsearch.xpack.esql.core.expression.FieldAttribute;
22+
import org.elasticsearch.xpack.esql.core.expression.FoldContext;
2123
import org.elasticsearch.xpack.esql.core.expression.MapExpression;
2224
import org.elasticsearch.xpack.esql.core.querydsl.query.Query;
2325
import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
2426
import org.elasticsearch.xpack.esql.core.tree.Source;
2527
import org.elasticsearch.xpack.esql.core.type.DataType;
2628
import org.elasticsearch.xpack.esql.core.type.MultiTypeEsField;
2729
import org.elasticsearch.xpack.esql.core.util.Check;
30+
import org.elasticsearch.xpack.esql.core.util.NumericUtils;
2831
import org.elasticsearch.xpack.esql.expression.function.Example;
2932
import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesTo;
3033
import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesToLifecycle;
@@ -37,6 +40,7 @@
3740
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
3841
import org.elasticsearch.xpack.esql.planner.TranslatorHandler;
3942
import org.elasticsearch.xpack.esql.querydsl.query.MultiMatchQuery;
43+
import org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter;
4044

4145
import java.io.IOException;
4246
import java.util.HashMap;
@@ -82,9 +86,10 @@
8286
import static org.elasticsearch.xpack.esql.core.type.DataType.TEXT;
8387
import static org.elasticsearch.xpack.esql.core.type.DataType.UNSIGNED_LONG;
8488
import static org.elasticsearch.xpack.esql.core.type.DataType.VERSION;
89+
import static org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.EsqlBinaryComparison.formatIncompatibleTypesMessage;
8590

8691
/**
87-
* Full text function that performs a {@link org.elasticsearch.xpack.esql.querydsl.query.MultiMatchQuery} .
92+
* Full text function that performs a {@link org.elasticsearch.xpack.esql.querydsl.query.MatchQuery} .
8893
*/
8994
public class Match extends FullTextFunction implements OptionalArgument, PostAnalysisPlanVerificationAware {
9095

@@ -310,8 +315,9 @@ private static Match readFrom(StreamInput in) throws IOException {
310315
return new Match(source, fields, query, null, queryBuilder);
311316
}
312317

318+
// This is not meant to be overriden by MatchOperator - MatchOperator should be serialized to Match
313319
@Override
314-
public void writeTo(StreamOutput out) throws IOException {
320+
public final void writeTo(StreamOutput out) throws IOException {
315321
source().writeTo(out);
316322
out.writeNamedWriteable(query());
317323
out.writeNamedWriteableCollection(fields);
@@ -320,7 +326,7 @@ public void writeTo(StreamOutput out) throws IOException {
320326

321327
@Override
322328
protected TypeResolution resolveParams() {
323-
return resolveFields().and(resolveQuery()).and(resolveOptions());
329+
return resolveFields().and(resolveQuery()).and(resolveOptions()).and(checkParamCompatibility());
324330
}
325331

326332
private TypeResolution resolveFields() {
@@ -350,12 +356,26 @@ private TypeResolution resolveQuery() {
350356
).and(isNotNullAndFoldable(query(), sourceText(), SECOND));
351357
}
352358

353-
public List<Expression> fields() {
354-
return fields;
355-
}
359+
private TypeResolution checkParamCompatibility() {
360+
DataType queryType = query().dataType();
356361

357-
public Expression options() {
358-
return options;
362+
return fields.stream().map((Expression field) -> {
363+
DataType fieldType = field.dataType();
364+
365+
// Field and query types should match. If the query is a string, then it can match any field type.
366+
if ((fieldType == queryType) || (queryType == KEYWORD)) {
367+
return TypeResolution.TYPE_RESOLVED;
368+
}
369+
370+
if (fieldType.isNumeric() && queryType.isNumeric()) {
371+
// When doing an unsigned long query, field must be an unsigned long
372+
if ((queryType == UNSIGNED_LONG && fieldType != UNSIGNED_LONG) == false) {
373+
return TypeResolution.TYPE_RESOLVED;
374+
}
375+
}
376+
377+
return new TypeResolution(formatIncompatibleTypesMessage(fieldType, queryType, sourceText()));
378+
}).reduce(TypeResolution::and).orElse(null);
359379
}
360380

361381
@Override
@@ -395,6 +415,14 @@ private Map<String, Object> matchQueryOptions() throws InvalidArgumentException
395415
return options;
396416
}
397417

418+
public List<Expression> fields() {
419+
return fields;
420+
}
421+
422+
public Expression options() {
423+
return options;
424+
}
425+
398426
@Override
399427
protected NodeInfo<? extends Expression> info() {
400428
// Specifically create new instance with original arguments.
@@ -427,8 +455,9 @@ public Expression replaceQueryBuilder(QueryBuilder queryBuilder) {
427455
@Override
428456
public BiConsumer<LogicalPlan, Failures> postAnalysisPlanVerification() {
429457
return (plan, failures) -> {
458+
// TODO: fix this.
430459
super.postAnalysisPlanVerification().accept(plan, failures);
431-
plan.forEachExpression(Match.class, mm -> {
460+
plan.forEachExpression(Match.class, match -> {
432461
for (Expression field : fields) {
433462
if (fieldAsFieldAttribute(field) == null) {
434463
failures.add(
@@ -446,12 +475,38 @@ public BiConsumer<LogicalPlan, Failures> postAnalysisPlanVerification() {
446475
};
447476
}
448477

478+
@Override
479+
public Object queryAsObject() {
480+
Object queryAsObject = query().fold(FoldContext.small() /* TODO remove me */);
481+
482+
// Convert BytesRef to string for string-based values
483+
if (queryAsObject instanceof BytesRef bytesRef) {
484+
return switch (query().dataType()) {
485+
case IP -> EsqlDataTypeConverter.ipToString(bytesRef);
486+
case VERSION -> EsqlDataTypeConverter.versionToString(bytesRef);
487+
default -> bytesRef.utf8ToString();
488+
};
489+
}
490+
491+
// Converts specific types to the correct type for the query
492+
if (query().dataType() == DataType.UNSIGNED_LONG) {
493+
return NumericUtils.unsignedLongAsBigInteger((Long) queryAsObject);
494+
} else if (query().dataType() == DataType.DATETIME && queryAsObject instanceof Long) {
495+
// When casting to date and datetime, we get a long back. But Match query needs a date string
496+
return EsqlDataTypeConverter.dateTimeToString((Long) queryAsObject);
497+
} else if (query().dataType() == DATE_NANOS && queryAsObject instanceof Long) {
498+
return EsqlDataTypeConverter.nanoTimeToString((Long) queryAsObject);
499+
}
500+
501+
return queryAsObject;
502+
}
503+
449504
@Override
450505
protected Query translate(TranslatorHandler handler) {
451506
Map<String, Float> fieldsWithBoost = new HashMap<>();
452507
for (Expression field : fields) {
453508
var fieldAttribute = fieldAsFieldAttribute(field);
454-
Check.notNull(fieldAttribute, "MultiMatch must have field attributes as arguments #1 to #N-1.");
509+
Check.notNull(fieldAttribute, "Match must have field attributes as arguments #1 to #N-1.");
455510
String fieldName = getNameFromFieldAttribute(fieldAttribute);
456511
fieldsWithBoost.put(fieldName, 1.0f);
457512
}
@@ -478,13 +533,13 @@ public static FieldAttribute fieldAsFieldAttribute(Expression field) {
478533

479534
@Override
480535
public boolean equals(Object o) {
481-
// MultiMatch does not serialize options, as they get included in the query builder. We need to override equals and hashcode to
482-
// ignore options when comparing two MultiMatch functions
536+
// Match does not serialize options, as they get included in the query builder. We need to override equals and hashcode to
537+
// ignore options when comparing two Match functions
483538
if (o == null || getClass() != o.getClass()) return false;
484-
Match mm = (Match) o;
485-
return Objects.equals(fields(), mm.fields())
486-
&& Objects.equals(query(), mm.query())
487-
&& Objects.equals(queryBuilder(), mm.queryBuilder());
539+
Match match = (Match) o;
540+
return Objects.equals(fields(), match.fields())
541+
&& Objects.equals(query(), match.query())
542+
&& Objects.equals(queryBuilder(), match.queryBuilder());
488543
}
489544

490545
@Override

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,16 +1215,12 @@ public void testMatchInsideEval() throws Exception {
12151215
}
12161216

12171217
public void testFieldBasedFullTextFunctions() throws Exception {
1218-
checkFieldBasedWithNonIndexedColumn("MATCH", "match(text, \"cat\")", "function");
1219-
checkFieldBasedFunctionNotAllowedAfterCommands("MATCH", "function", "match(title, \"Meditation\")");
1218+
checkFieldBasedWithNonIndexedColumn("Match", "match(text, \"cat\")", "function");
1219+
checkFieldBasedFunctionNotAllowedAfterCommands("Match", "function", "match(title, \"Meditation\")");
12201220

12211221
checkFieldBasedWithNonIndexedColumn(":", "text : \"cat\"", "operator");
12221222
checkFieldBasedFunctionNotAllowedAfterCommands(":", "operator", "title : \"Meditation\"");
12231223

1224-
if (EsqlCapabilities.Cap.MULTI_MATCH_FUNCTION.isEnabled()) {
1225-
checkFieldBasedWithNonIndexedColumn("MultiMatch", "multi_match(\"cat\", text)", "function");
1226-
checkFieldBasedFunctionNotAllowedAfterCommands("MultiMatch", "function", "multi_match(\"Meditation\", title)");
1227-
}
12281224
if (EsqlCapabilities.Cap.TERM_FUNCTION.isEnabled()) {
12291225
checkFieldBasedWithNonIndexedColumn("Term", "term(text, \"cat\")", "function");
12301226
checkFieldBasedFunctionNotAllowedAfterCommands("Term", "function", "term(title, \"Meditation\")");
@@ -1348,16 +1344,13 @@ private void checkNonFieldBasedFullTextFunctionsNotAllowedAfterCommands(String f
13481344
}
13491345

13501346
public void testFullTextFunctionsOnlyAllowedInWhere() throws Exception {
1351-
checkFullTextFunctionsOnlyAllowedInWhere("MATCH", "match(title, \"Meditation\")", "function");
1347+
checkFullTextFunctionsOnlyAllowedInWhere("Match", "match(title, \"Meditation\")", "function");
13521348
checkFullTextFunctionsOnlyAllowedInWhere(":", "title:\"Meditation\"", "operator");
13531349
checkFullTextFunctionsOnlyAllowedInWhere("QSTR", "qstr(\"Meditation\")", "function");
13541350
checkFullTextFunctionsOnlyAllowedInWhere("KQL", "kql(\"Meditation\")", "function");
13551351
if (EsqlCapabilities.Cap.TERM_FUNCTION.isEnabled()) {
13561352
checkFullTextFunctionsOnlyAllowedInWhere("Term", "term(title, \"Meditation\")", "function");
13571353
}
1358-
if (EsqlCapabilities.Cap.MULTI_MATCH_FUNCTION.isEnabled()) {
1359-
checkFullTextFunctionsOnlyAllowedInWhere("MultiMatch", "multi_match(\"Meditation\", title, body)", "function");
1360-
}
13611354
}
13621355

13631356
public void testMatchFunctionOnlyAllowedInWhere() throws Exception {
@@ -1522,9 +1515,6 @@ private void checkFullTextFunctionsWithNonBooleanFunctions(String functionName,
15221515
public void testFullTextFunctionsTargetsExistingField() throws Exception {
15231516
testFullTextFunctionTargetsExistingField("match(title, \"Meditation\")");
15241517
testFullTextFunctionTargetsExistingField("title : \"Meditation\"");
1525-
if (EsqlCapabilities.Cap.MULTI_MATCH_FUNCTION.isEnabled()) {
1526-
testFullTextFunctionTargetsExistingField("multi_match(\"Meditation\", title)");
1527-
}
15281518
if (EsqlCapabilities.Cap.TERM_FUNCTION.isEnabled()) {
15291519
testFullTextFunctionTargetsExistingField("term(fist_name, \"Meditation\")");
15301520
}
@@ -2047,11 +2037,8 @@ public void testLookupJoinDataTypeMismatch() {
20472037
}
20482038

20492039
public void testFullTextFunctionOptions() {
2050-
checkOptionDataTypes(Match.ALLOWED_OPTIONS, "FROM test | WHERE match(title, \"Jean\", {\"%s\": %s})");
2040+
checkOptionDataTypes(Match.OPTIONS, "FROM test | WHERE match(title, \"Jean\", {\"%s\": %s})");
20512041
checkOptionDataTypes(QueryString.ALLOWED_OPTIONS, "FROM test | WHERE QSTR(\"title: Jean\", {\"%s\": %s})");
2052-
if (EsqlCapabilities.Cap.MULTI_MATCH_FUNCTION.isEnabled()) {
2053-
checkOptionDataTypes(MultiMatch.OPTIONS, "FROM test | WHERE MULTI_MATCH(\"Jean\", title, body, {\"%s\": %s})");
2054-
}
20552042
}
20562043

20572044
/**
@@ -2106,9 +2093,6 @@ private static String exampleValueForType(DataType currentType) {
21062093
public void testFullTextFunctionCurrentlyUnsupportedBehaviour() throws Exception {
21072094
testFullTextFunctionsCurrentlyUnsupportedBehaviour("match(title, \"Meditation\")");
21082095
testFullTextFunctionsCurrentlyUnsupportedBehaviour("title : \"Meditation\"");
2109-
if (EsqlCapabilities.Cap.MULTI_MATCH_FUNCTION.isEnabled()) {
2110-
testFullTextFunctionsCurrentlyUnsupportedBehaviour("multi_match(\"Meditation\", title)");
2111-
}
21122096
if (EsqlCapabilities.Cap.TERM_FUNCTION.isEnabled()) {
21132097
testFullTextFunctionsCurrentlyUnsupportedBehaviour("term(title, \"Meditation\")");
21142098
}
@@ -2126,10 +2110,6 @@ public void testFullTextFunctionsNullArgs() throws Exception {
21262110
checkFullTextFunctionNullArgs("match(title, null)", "second");
21272111
checkFullTextFunctionNullArgs("qstr(null)", "");
21282112
checkFullTextFunctionNullArgs("kql(null)", "");
2129-
if (EsqlCapabilities.Cap.MULTI_MATCH_FUNCTION.isEnabled()) {
2130-
checkFullTextFunctionNullArgs("multi_match(null, title)", "first");
2131-
checkFullTextFunctionNullArgs("multi_match(\"query\", null)", "second");
2132-
}
21332113
if (EsqlCapabilities.Cap.TERM_FUNCTION.isEnabled()) {
21342114
checkFullTextFunctionNullArgs("term(null, \"query\")", "first");
21352115
checkFullTextFunctionNullArgs("term(title, null)", "second");
@@ -2147,10 +2127,6 @@ public void testFullTextFunctionsConstantQuery() throws Exception {
21472127
checkFullTextFunctionsConstantQuery("match(title, category)", "second");
21482128
checkFullTextFunctionsConstantQuery("qstr(title)", "");
21492129
checkFullTextFunctionsConstantQuery("kql(title)", "");
2150-
if (EsqlCapabilities.Cap.MULTI_MATCH_FUNCTION.isEnabled()) {
2151-
checkFullTextFunctionsConstantQuery("multi_match(category, body)", "first");
2152-
checkFullTextFunctionsConstantQuery("multi_match(concat(title, \"world\"), title)", "first");
2153-
}
21542130
if (EsqlCapabilities.Cap.TERM_FUNCTION.isEnabled()) {
21552131
checkFullTextFunctionsConstantQuery("term(title, tags)", "second");
21562132
}

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1262,7 +1262,6 @@ public void testMatchOptionsPushDown() {
12621262
var fieldExtract = as(project.child(), FieldExtractExec.class);
12631263
var actualLuceneQuery = as(fieldExtract.child(), EsQueryExec.class).query();
12641264

1265-
Source filterSource = new Source(4, 8, "emp_no > 10000");
12661265
var expectedLuceneQuery = new MatchQueryBuilder("first_name", "Anna").fuzziness(Fuzziness.AUTO)
12671266
.prefixLength(3)
12681267
.maxExpansions(10)

0 commit comments

Comments
 (0)