77
88package org .elasticsearch .xpack .esql .expression .function .fulltext ;
99
10+ import org .apache .lucene .util .BytesRef ;
1011import org .elasticsearch .common .io .stream .NamedWriteableRegistry ;
1112import org .elasticsearch .common .io .stream .StreamInput ;
1213import org .elasticsearch .common .io .stream .StreamOutput ;
1819import org .elasticsearch .xpack .esql .core .InvalidArgumentException ;
1920import org .elasticsearch .xpack .esql .core .expression .Expression ;
2021import org .elasticsearch .xpack .esql .core .expression .FieldAttribute ;
22+ import org .elasticsearch .xpack .esql .core .expression .FoldContext ;
2123import org .elasticsearch .xpack .esql .core .expression .MapExpression ;
2224import org .elasticsearch .xpack .esql .core .querydsl .query .Query ;
2325import org .elasticsearch .xpack .esql .core .tree .NodeInfo ;
2426import org .elasticsearch .xpack .esql .core .tree .Source ;
2527import org .elasticsearch .xpack .esql .core .type .DataType ;
2628import org .elasticsearch .xpack .esql .core .type .MultiTypeEsField ;
2729import org .elasticsearch .xpack .esql .core .util .Check ;
30+ import org .elasticsearch .xpack .esql .core .util .NumericUtils ;
2831import org .elasticsearch .xpack .esql .expression .function .Example ;
2932import org .elasticsearch .xpack .esql .expression .function .FunctionAppliesTo ;
3033import org .elasticsearch .xpack .esql .expression .function .FunctionAppliesToLifecycle ;
3740import org .elasticsearch .xpack .esql .plan .logical .LogicalPlan ;
3841import org .elasticsearch .xpack .esql .planner .TranslatorHandler ;
3942import org .elasticsearch .xpack .esql .querydsl .query .MultiMatchQuery ;
43+ import org .elasticsearch .xpack .esql .type .EsqlDataTypeConverter ;
4044
4145import java .io .IOException ;
4246import java .util .HashMap ;
8286import static org .elasticsearch .xpack .esql .core .type .DataType .TEXT ;
8387import static org .elasticsearch .xpack .esql .core .type .DataType .UNSIGNED_LONG ;
8488import 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 */
8994public 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
0 commit comments