From 32ba25aa1ed64ea4a9bf992debf5f652a86bbf4a Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Tue, 27 May 2025 10:07:25 -0400 Subject: [PATCH 01/33] Initial --- .../functions/parameters/multi_match.md | 6 +- .../_snippets/functions/types/multi_match.md | 2 +- .../esql/images/functions/multi_match.svg | 2 +- .../definition/functions/multi_match.json | 232 +++++++++--------- .../resources/multi-match-function.csv-spec | 14 +- .../src/main/resources/scoring.csv-spec | 6 +- .../xpack/esql/plugin/ScoringIT.java | 2 +- .../function/EsqlFunctionRegistry.java | 6 +- .../function/fulltext/MultiMatch.java | 35 +-- .../xpack/esql/analysis/AnalyzerTests.java | 2 +- .../xpack/esql/analysis/VerifierTests.java | 78 +++--- .../function/fulltext/MultiMatchTests.java | 3 +- .../LocalPhysicalPlanOptimizerTests.java | 2 +- .../optimizer/LogicalPlanOptimizerTests.java | 2 +- 14 files changed, 196 insertions(+), 196 deletions(-) diff --git a/docs/reference/query-languages/esql/_snippets/functions/parameters/multi_match.md b/docs/reference/query-languages/esql/_snippets/functions/parameters/multi_match.md index f42b89118e349..ed09c5ad51b37 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/parameters/multi_match.md +++ b/docs/reference/query-languages/esql/_snippets/functions/parameters/multi_match.md @@ -2,12 +2,12 @@ **Parameters** -`query` -: Value to find in the provided fields. - `fields` : Fields to use for matching +`query` +: Value to find in the provided fields. + `options` : (Optional) Additional options for MultiMatch, passed as [function named parameters](/reference/query-languages/esql/esql-syntax.md#esql-function-named-params)." See [multi-match query](/reference/query-languages/query-dsl/query-dsl-match-query.md#query-dsl-multi-match-query) for more information. diff --git a/docs/reference/query-languages/esql/_snippets/functions/types/multi_match.md b/docs/reference/query-languages/esql/_snippets/functions/types/multi_match.md index 6b0a9d9cbb5b5..9db8a91d42d8a 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/types/multi_match.md +++ b/docs/reference/query-languages/esql/_snippets/functions/types/multi_match.md @@ -2,7 +2,7 @@ **Supported types** -| query | fields | options | result | +| fields | query | options | result | | --- | --- | --- | --- | | boolean | boolean | named parameters | boolean | | boolean | keyword | named parameters | boolean | diff --git a/docs/reference/query-languages/esql/images/functions/multi_match.svg b/docs/reference/query-languages/esql/images/functions/multi_match.svg index 93e27774b2c63..423522a0b0397 100644 --- a/docs/reference/query-languages/esql/images/functions/multi_match.svg +++ b/docs/reference/query-languages/esql/images/functions/multi_match.svg @@ -1 +1 @@ -MULTI_MATCH(query,fields,options) \ No newline at end of file +MULTI_MATCH(,fields,query,options) \ No newline at end of file diff --git a/docs/reference/query-languages/esql/kibana/definition/functions/multi_match.json b/docs/reference/query-languages/esql/kibana/definition/functions/multi_match.json index f3729892883aa..9200203094d92 100644 --- a/docs/reference/query-languages/esql/kibana/definition/functions/multi_match.json +++ b/docs/reference/query-languages/esql/kibana/definition/functions/multi_match.json @@ -7,16 +7,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "boolean", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "boolean", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -32,16 +32,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "boolean", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -57,16 +57,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "date", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "date", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -82,16 +82,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "date", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -107,16 +107,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "date_nanos", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "date_nanos", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -132,16 +132,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "date_nanos", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -157,16 +157,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "double", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "double", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -182,16 +182,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "double", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "integer", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -207,16 +207,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "double", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -232,16 +232,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "double", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "long", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -257,16 +257,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "integer", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "double", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -282,16 +282,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "integer", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "integer", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -307,16 +307,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "integer", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -332,16 +332,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "integer", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "long", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -357,16 +357,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "ip", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "ip", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -382,16 +382,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "ip", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -407,16 +407,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "keyword", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -432,16 +432,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "long", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "double", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -457,16 +457,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "long", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "integer", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -482,16 +482,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "long", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -507,16 +507,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "long", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "long", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -532,16 +532,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "text", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -557,16 +557,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "unsigned_long", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "double", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -582,16 +582,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "unsigned_long", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "integer", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -607,16 +607,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "unsigned_long", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -632,16 +632,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "unsigned_long", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "long", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -657,16 +657,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "unsigned_long", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "unsigned_long", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -682,16 +682,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "version", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", @@ -707,16 +707,16 @@ { "params" : [ { - "name" : "query", + "name" : "fields", "type" : "version", "optional" : false, - "description" : "Value to find in the provided fields." + "description" : "Fields to use for matching" }, { - "name" : "fields", + "name" : "query", "type" : "version", "optional" : false, - "description" : "Fields to use for matching" + "description" : "Value to find in the provided fields." }, { "name" : "options", diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multi-match-function.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multi-match-function.csv-spec index a821cfca93585..4503205543aa8 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multi-match-function.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multi-match-function.csv-spec @@ -7,7 +7,7 @@ required_capability: multi_match_function // tag::multi-match-with-field[] FROM books -| WHERE MULTI_MATCH("Faulkner", author, description) +| WHERE MULTI_MATCH(author, description, "Faulkner") | KEEP book_no, author | SORT book_no | LIMIT 5 @@ -28,7 +28,7 @@ testMultiMatchWithOptionsFuzziness required_capability: multi_match_function from books -| where multi_match("Pings", title, description, {"fuzziness": 1}) +| where multi_match(title, description, "Pings", {"fuzziness": 1}) | keep book_no; ignoreOrder:true @@ -50,7 +50,7 @@ required_capability: multi_match_function // tag::multi-match-with-named-function-params[] FROM books -| WHERE MULTI_MATCH("Hobbit Back Again", title, description, {"operator": "AND"}) +| WHERE MULTI_MATCH(title, description, "Hobbit Back Again", {"operator": "AND"}) | KEEP title; // end::multi-match-with-named-function-params[] @@ -64,7 +64,7 @@ testMultiMatchWithOptionsMinimumShouldMatch required_capability: multi_match_function from books -| where multi_match("here back again", title, description, {"minimum_should_match": 2, "operator": "OR"}) +| where multi_match(title, description, "here back again", {"minimum_should_match": 2, "operator": "OR"}) | sort book_no | keep title; @@ -78,7 +78,7 @@ required_capability: multi_match_function required_capability: full_text_functions_disjunctions_compute_engine from books -| where multi_match("lord", title, description) or length(title) > 130 +| where multi_match(title, description, "lord") or length(title) > 130 | keep book_no ; ignoreOrder: true @@ -100,7 +100,7 @@ testMultiMatchPhraseQuery required_capability: multi_match_function from books -| where multi_match("Lord of the rings", title, description, { "type": "phrase" } ) +| where multi_match(title, description, "Lord of the rings", { "type": "phrase" } ) | keep title ; ignoreOrder: true @@ -120,7 +120,7 @@ testMultiMatchPhrasePrefixQuery required_capability: multi_match_function from books -| where multi_match("Lord of the ri", title, { "type": "phrase_prefix" } ) +| where multi_match(title, "Lord of the ri", { "type": "phrase_prefix" } ) | keep title ; ignoreOrder: true diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec index 401d946334c45..608feadd65fa9 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec @@ -119,7 +119,7 @@ required_capability: multi_match_function required_capability: metadata_score from books metadata _score -| where multi_match("Mark", author, title, {"fuzziness": 1}) +| where multi_match(author, title, "Mark", {"fuzziness": 1}) | keep book_no, title, author, _score; ignoreOrder:true @@ -132,7 +132,7 @@ required_capability: multi_match_function required_capability: metadata_score from books metadata _score -| where multi_match("Hobbit", description, title, {"type": "best_fields"}) +| where multi_match(description, title, "Hobbit", {"type": "best_fields"}) | sort book_no | eval _score = round(_score) | keep book_no, _score; @@ -159,7 +159,7 @@ required_capability: multi_match_function required_capability: metadata_score from books metadata _score -| where multi_match("Hobbit", description, title, {"type": "most_fields"}) +| where multi_match(description, title, "Hobbit", {"type": "most_fields"}) | sort book_no | eval _score = round(_score) | keep book_no, _score; diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/ScoringIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/ScoringIT.java index a9dada8e2fe31..66d3f12840cdd 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/ScoringIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/ScoringIT.java @@ -46,7 +46,7 @@ protected Collection> nodePlugins() { public static List params() { List params = new ArrayList<>(); params.add(new Object[] { "match(content, \"fox\")" }); - params.add(new Object[] { "multi_match(\"fox\", content, {\"operator\": \"AND\"})" }); + params.add(new Object[] { "multi_match(content, \"fox\", {\"operator\": \"AND\"})" }); params.add(new Object[] { "content:\"fox\"" }); params.add(new Object[] { "qstr(\"content: fox\")" }); params.add(new Object[] { "kql(\"content*: fox\")" }); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java index b2d85d809c058..c2044a5256bbf 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java @@ -1037,7 +1037,7 @@ protected interface UnaryVariadicBuilder { } protected interface BinaryVariadicWithOptionsBuilder { - T build(Source source, Expression exp, List variadic, Expression options); + T build(Source source, List variadic, Expression exp, Expression options); }; protected static FunctionDefinition def( @@ -1054,10 +1054,10 @@ protected static FunctionDefinition def( } Expression options = children.getLast(); if (options instanceof MapExpression) { - return ctorRef.build(source, children.get(0), children.subList(1, children.size() - 1), options); + return ctorRef.build(source, children.subList(0, children.size() - 2), children.get(children.size() - 2), options); } - return ctorRef.build(source, children.get(0), children.subList(1, children.size()), null); + return ctorRef.build(source, children.subList(0, children.size() - 1), children.getLast(), null); }; return def(function, builder, names); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatch.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatch.java index 5014263ba755b..89fbc038082bf 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatch.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatch.java @@ -152,16 +152,16 @@ public class MultiMatch extends FullTextFunction implements OptionalArgument, Po ) public MultiMatch( Source source, - @Param( - name = "query", - type = { "keyword", "boolean", "date", "date_nanos", "double", "integer", "ip", "long", "text", "unsigned_long", "version" }, - description = "Value to find in the provided fields." - ) Expression query, @Param( name = "fields", - type = { "keyword", "boolean", "date", "date_nanos", "double", "integer", "ip", "long", "unsigned_long", "version" }, + type = { "keyword", "boolean", "date", "date_nanos", "double", "integer", "ip", "long", "text", "unsigned_long", "version" }, description = "Fields to use for matching" ) List fields, + @Param( + name = "query", + type = { "keyword", "boolean", "date", "date_nanos", "double", "integer", "ip", "long", "unsigned_long", "version" }, + description = "Value to find in the provided fields." + ) Expression query, @MapParam( name = "options", params = { @@ -262,7 +262,7 @@ public MultiMatch( optional = true ) Expression options ) { - this(source, query, fields, options, null); + this(source, fields, query, options, null); } // Due to current limitations, the options field may contain a field, in which case treat it as a field, and use "null" for actual @@ -277,7 +277,7 @@ private static List initChildren(Expression query, List return (options == null ? fieldsAndQuery : Stream.concat(fieldsAndQuery, Stream.of(options))).toList(); } - private MultiMatch(Source source, Expression query, List fields, Expression options, QueryBuilder queryBuilder) { + private MultiMatch(Source source, List fields, Expression query, Expression options, QueryBuilder queryBuilder) { super(source, query, initChildren(query, fields, options), queryBuilder); this.fieldsOriginal = fields; this.optionsOriginal = options; @@ -296,7 +296,7 @@ private static MultiMatch readFrom(StreamInput in) throws IOException { Expression query = in.readNamedWriteable(Expression.class); List fields = in.readNamedWriteableCollectionAsList(Expression.class); QueryBuilder queryBuilder = in.readOptionalNamedWriteable(QueryBuilder.class); - return new MultiMatch(source, query, fields, null, queryBuilder); + return new MultiMatch(source, fields, query, null, queryBuilder); } @Override @@ -314,24 +314,25 @@ public String getWriteableName() { @Override public Expression replaceChildren(List newChildren) { + // "query" is the first child. if (newChildren.getLast() instanceof MapExpression || newChildren.size() == children().size()) { // if the last child is a MapExpression, it is the options map return new MultiMatch( source(), - newChildren.getFirst(), newChildren.subList(1, newChildren.size() - 1), + newChildren.getFirst(), newChildren.getLast(), queryBuilder() ); } - return new MultiMatch(source(), newChildren.getFirst(), newChildren.subList(1, newChildren.size()), null, queryBuilder()); + return new MultiMatch(source(), newChildren.subList(1, newChildren.size()), newChildren.getFirst(), null, queryBuilder()); } @Override protected NodeInfo info() { // Specifically create new instance with original arguments. - return NodeInfo.create(this, MultiMatch::new, query(), fieldsOriginal, optionsOriginal); + return NodeInfo.create(this, MultiMatch::new, fieldsOriginal, query(), optionsOriginal); } @Override @@ -349,7 +350,7 @@ protected Query translate(TranslatorHandler handler) { @Override public Expression replaceQueryBuilder(QueryBuilder queryBuilder) { // Specifically create new instance with original arguments. - return new MultiMatch(source(), query(), fieldsOriginal, optionsOriginal, queryBuilder); + return new MultiMatch(source(), fieldsOriginal, query(), optionsOriginal, queryBuilder); } public List fields() { @@ -384,7 +385,7 @@ private TypeResolution resolveFields() { field, FIELD_DATA_TYPES::contains, sourceText(), - SECOND, + FIRST, "keyword, text, boolean, date, date_nanos, double, integer, ip, long, unsigned_long, version" ) ) @@ -419,14 +420,14 @@ private TypeResolution resolveQuery() { query(), QUERY_DATA_TYPES::contains, sourceText(), - FIRST, + SECOND, "keyword, boolean, date, date_nanos, double, integer, ip, long, unsigned_long, version" - ).and(isNotNullAndFoldable(query(), sourceText(), FIRST)); + ).and(isNotNullAndFoldable(query(), sourceText(), SECOND)); } @Override protected TypeResolution resolveParams() { - return resolveQuery().and(resolveFields()).and(resolveOptions()); + return resolveFields().and(resolveQuery()).and(resolveOptions()); } @Override diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java index f3afa24969f33..ab46a9e5ecbc2 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java @@ -2863,7 +2863,7 @@ public void testFunctionNamedParamsAsFunctionArgument1() { public void testFunctionNamedParamsAsFunctionArgument2() { LogicalPlan plan = analyze(""" from test - | WHERE MULTI_MATCH("Anna Smith", first_name, last_name, {"minimum_should_match": 3.0}) + | WHERE MULTI_MATCH(first_name, last_name, "Anna Smith", {"minimum_should_match": 3.0}) """); Limit limit = as(plan, Limit.class); Filter filter = as(limit.child(), Filter.class); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java index d76a355a6c9a9..51feb72b6c434 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java @@ -1458,7 +1458,7 @@ private void checkWithDisjunctions(String functionName, String functionInvocatio public void testFullTextFunctionsDisjunctions() { checkWithFullTextFunctionsDisjunctions("match(last_name, \"Smith\")"); - checkWithFullTextFunctionsDisjunctions("multi_match(\"Smith\", first_name, last_name)"); + checkWithFullTextFunctionsDisjunctions("multi_match(first_name, last_name, \"Smith\")"); checkWithFullTextFunctionsDisjunctions("last_name : \"Smith\""); checkWithFullTextFunctionsDisjunctions("qstr(\"last_name: Smith\")"); checkWithFullTextFunctionsDisjunctions("kql(\"last_name: Smith\")"); @@ -2321,20 +2321,20 @@ public void testQueryStringOptions() { public void testMultiMatchOptions() { // Check positive cases - query("FROM test | WHERE MULTI_MATCH(\"Jean\", first_name)"); - query("FROM test | WHERE MULTI_MATCH(\"Jean\", first_name, {\"analyzer\": \"standard\"})"); - query("FROM test | WHERE MULTI_MATCH(\"Jean\", first_name, last_name, {\"analyzer\": \"standard\"})"); - query("FROM test | WHERE MULTI_MATCH(\"Jean\", first_name, last_name, {\"slop\": 10})"); - query("FROM test | WHERE MULTI_MATCH(\"Jean\", first_name, last_name, {\"auto_generate_synonyms_phrase_query\": true})"); - query("FROM test | WHERE MULTI_MATCH(\"Jean\", first_name, last_name, {\"fuzziness\": 2})"); - query("FROM test | WHERE MULTI_MATCH(\"Jean\", first_name, last_name, {\"fuzzy_transpositions\": false})"); - query("FROM test | WHERE MULTI_MATCH(\"Jean\", first_name, last_name, {\"lenient\": false})"); - query("FROM test | WHERE MULTI_MATCH(\"Jean\", first_name, last_name, {\"max_expansions\": 10})"); - query("FROM test | WHERE MULTI_MATCH(\"Jean\", first_name, last_name, {\"minimum_should_match\": \"2\"})"); - query("FROM test | WHERE MULTI_MATCH(\"Jean\", first_name, last_name, {\"operator\": \"AND\"})"); - query("FROM test | WHERE MULTI_MATCH(\"Jean\", first_name, last_name, {\"prefix_length\": 2})"); - query("FROM test | WHERE MULTI_MATCH(\"Jean\", first_name, last_name, {\"tie_breaker\": 1.0})"); - query("FROM test | WHERE MULTI_MATCH(\"Jean\", first_name, last_name, {\"type\": \"best_fields\"})"); + query("FROM test | WHERE MULTI_MATCH(first_name, \"Jean\")"); + query("FROM test | WHERE MULTI_MATCH(first_name, \"Jean\", {\"analyzer\": \"standard\"})"); + query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"analyzer\": \"standard\"})"); + query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"slop\": 10})"); + query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"auto_generate_synonyms_phrase_query\": true})"); + query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"fuzziness\": 2})"); + query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"fuzzy_transpositions\": false})"); + query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"lenient\": false})"); + query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"max_expansions\": 10})"); + query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"minimum_should_match\": \"2\"})"); + query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"operator\": \"AND\"})"); + query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"prefix_length\": 2})"); + query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"tie_breaker\": 1.0})"); + query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"type\": \"best_fields\"})"); // Check all data types for available options DataType[] optionTypes = new DataType[] { INTEGER, LONG, FLOAT, DOUBLE, KEYWORD, BOOLEAN }; @@ -2357,7 +2357,7 @@ public void testMultiMatchOptions() { queryOptionValue = "\"" + optionValue + "\""; } - String query = "FROM test | WHERE MULTI_MATCH(\"Jean\", first_name, last_name, {\"" + String query = "FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"" + optionName + "\": " + queryOptionValue @@ -2372,7 +2372,7 @@ public void testMultiMatchOptions() { assertEquals( "1:19: Invalid option [" + optionName - + "] in [MULTI_MATCH(\"Jean\", first_name, last_name, {\"" + + "] in [MULTI_MATCH(first_name, last_name, \"Jean\", {\"" + optionName + "\": " + queryOptionValue @@ -2392,9 +2392,9 @@ public void testMultiMatchOptions() { } assertThat( - error("FROM test | WHERE MULTI_MATCH(\"Jean\", first_name, last_name, {\"unknown_option\": true})"), + error("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"unknown_option\": true})"), containsString( - "1:19: Invalid option [unknown_option] in [MULTI_MATCH(\"Jean\", first_name, last_name, " + "1:19: Invalid option [unknown_option] in [MULTI_MATCH(first_name, last_name, \"Jean\", " + "{\"unknown_option\": true})], expected one of " ) ); @@ -2402,39 +2402,39 @@ public void testMultiMatchOptions() { public void testMultiMatchFunctionIsNotNullable() { assertEquals( - "1:62: [MultiMatch] function cannot operate on [text::keyword], which is not a field from an index mapping", - error("row n = null | eval text = n + 5 | where multi_match(\"Anna\", text::keyword)") + "1:54: [MultiMatch] function cannot operate on [text::keyword], which is not a field from an index mapping", + error("row n = null | eval text = n + 5 | where multi_match(text::keyword, \"Anna\")") ); } public void testMultiMatchWithNonIndexedColumnCurrentlyUnsupported() { assertEquals( - "1:78: [MultiMatch] function cannot operate on [initial], which is not a field from an index mapping", - error("from test | eval initial = substring(first_name, 1) | where multi_match(\"A\", initial)") + "1:73: [MultiMatch] function cannot operate on [initial], which is not a field from an index mapping", + error("from test | eval initial = substring(first_name, 1) | where multi_match(initial, \"A\")") ); assertEquals( - "1:80: [MultiMatch] function cannot operate on [text], which is not a field from an index mapping", - error("from test | eval text=concat(first_name, last_name) | where multi_match(\"cat\", text)") + "1:73: [MultiMatch] function cannot operate on [text], which is not a field from an index mapping", + error("from test | eval text=concat(first_name, last_name) | where multi_match(text, \"cat\")") ); } public void testMultiMatchFunctionNotAllowedAfterCommands() throws Exception { assertEquals( "1:24: [MultiMatch] function cannot be used after LIMIT", - error("from test | limit 10 | where multi_match(\"Anna\", first_name)") + error("from test | limit 10 | where multi_match(first_name, \"Anna\")") ); assertEquals( "1:47: [MultiMatch] function cannot be used after STATS", - error("from test | STATS c = AVG(salary) BY gender | where multi_match(\"F\", gender)") + error("from test | STATS c = AVG(salary) BY gender | where multi_match(gender, \"F\")") ); } public void testMultiMatchFunctionWithDisjunctions() { - checkWithDisjunctions("MultiMatch", "multi_match(\"Anna\", first_name, last_name)", "function"); + checkWithDisjunctions("MultiMatch", "multi_match(first_name, last_name, \"Anna\")", "function"); } public void testMultiMatchFunctionWithNonBooleanFunctions() { - checkFullTextFunctionsWithNonBooleanFunctions("MultiMatch", "multi_match(\"Anna\", first_name, last_name)", "function"); + checkFullTextFunctionsWithNonBooleanFunctions("MultiMatch", "multi_match(first_name, last_name, \"Anna\")", "function"); } public void testMultiMatchFunctionArgNotConstant() throws Exception { @@ -2452,26 +2452,26 @@ public void testMultiMatchFunctionArgNotConstant() throws Exception { // Should pass eventually once we lift some restrictions on the multi-match function. public void testMultiMatchFunctionCurrentlyUnsupportedBehaviour() throws Exception { assertEquals( - "1:82: Unknown column [first_name]\nline 1:94: Unknown column [last_name]", - error("from test | stats max_salary = max(salary) by emp_no | where multi_match(\"Anna\", first_name, last_name)") + "1:74: Unknown column [first_name]\nline 1:86: Unknown column [last_name]", + error("from test | stats max_salary = max(salary) by emp_no | where multi_match(first_name, last_name, \"Anna\")") ); } public void testMultiMatchFunctionNullArgs() throws Exception { assertEquals( - "1:19: first argument of [multi_match(\"query\", null)] cannot be null, received [null]", + "1:19: second argument of [multi_match(\"query\", null)] cannot be null, received [null]", error("from test | where multi_match(\"query\", null)") ); assertEquals( - "1:19: first argument of [multi_match(null, first_name)] cannot be null, received [null]", - error("from test | where multi_match(null, first_name)") + "1:19: second argument of [multi_match(first_name, null)] cannot be null, received [null]", + error("from test | where multi_match(first_name, null)") ); } public void testMultiMatchTargetsExistingField() throws Exception { assertEquals( - "1:53: Unknown column [first_name]\nline 1:65: Unknown column [last_name]", - error("from test | keep emp_no | where multi_match(\"Anna\", first_name, last_name)") + "1:45: Unknown column [first_name]\nline 1:57: Unknown column [last_name]", + error("from test | keep emp_no | where multi_match(first_name, last_name, \"Anna\")") ); } @@ -2479,8 +2479,8 @@ public void testMultiMatchInsideEval() throws Exception { assumeTrue("MultiMatch operator is available just for snapshots", Build.current().isSnapshot()); assertEquals( "1:36: [MultiMatch] function is only supported in WHERE and STATS commands\n" - + "line 1:55: [MultiMatch] function cannot operate on [title], which is not a field from an index mapping", - error("row title = \"brown fox\" | eval x = multi_match(\"fox\", title)") + + "line 1:48: [MultiMatch] function cannot operate on [title], which is not a field from an index mapping", + error("row title = \"brown fox\" | eval x = multi_match(title, \"fox\")") ); } @@ -2495,7 +2495,7 @@ public void testInsistNotOnTopOfFrom() { public void testFullTextFunctionsInStats() { checkFullTextFunctionsInStats("match(last_name, \"Smith\")"); - checkFullTextFunctionsInStats("multi_match(\"Smith\", first_name, last_name)"); + checkFullTextFunctionsInStats("multi_match(first_name, last_name, \"Smith\")"); checkFullTextFunctionsInStats("last_name : \"Smith\""); checkFullTextFunctionsInStats("qstr(\"last_name: Smith\")"); checkFullTextFunctionsInStats("kql(\"last_name: Smith\")"); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatchTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatchTests.java index e14abda61ca63..9113e872d89cd 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatchTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatchTests.java @@ -34,8 +34,7 @@ public MultiMatchTests(@Name("TestCase") Supplier tes @Override protected Expression build(Source source, List args) { - // Note we are reversing the order of arguments here. - MultiMatch mm = new MultiMatch(source, args.get(1), List.of(args.get(0)), args.get(2)); + MultiMatch mm = new MultiMatch(source, List.of(args.get(0)), args.get(1), args.get(2)); // We need to add the QueryBuilder to the multi_match expression, as it is used to implement equals() and hashCode() and // thus test the serialization methods. But we can only do this if the parameters make sense . if (mm.query().foldable() && mm.fields().stream().allMatch(field -> field instanceof FieldAttribute)) { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java index 0cc8c670895e9..1d0208dd839d6 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java @@ -1682,7 +1682,7 @@ public void testQStrOptionsPushDown() { public void testMultiMatchOptionsPushDown() { String query = """ from test - | where MULTI_MATCH("Anna", first_name, last_name, {"fuzzy_rewrite": "constant_score", "slop": 10, "analyzer": "auto", + | where MULTI_MATCH(first_name, last_name, "Anna", {"fuzzy_rewrite": "constant_score", "slop": 10, "analyzer": "auto", "auto_generate_synonyms_phrase_query": "false", "fuzziness": "auto", "fuzzy_transpositions": false, "lenient": "false", "max_expansions": 10, "minimum_should_match": 3, "operator": "AND", "prefix_length": 20, "tie_breaker": 1.0, "type": "best_fields", "boost": 2.0}) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java index de56f0676b4a7..b7e2c3c0ebeb6 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java @@ -7447,7 +7447,7 @@ public void testFunctionNamedParamsAsFunctionArgument() { public void testFunctionNamedParamsAsFunctionArgument1() { var query = """ from test - | WHERE MULTI_MATCH("Anna Smith", first_name, last_name, {"minimum_should_match": 2.0}) + | WHERE MULTI_MATCH(first_name, last_name, "Anna Smith", {"minimum_should_match": 2.0}) """; var plan = optimizedPlan(query); Limit limit = as(plan, Limit.class); From 602d6f7ff3bf6180799b1c8e038120265595afb1 Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Thu, 29 May 2025 10:12:31 -0400 Subject: [PATCH 02/33] New capability --- .../main/resources/multi-match-function.csv-spec | 14 +++++++------- .../src/main/resources/scoring.csv-spec | 6 +++--- .../xpack/esql/action/EsqlCapabilities.java | 5 +++++ .../org/elasticsearch/xpack/esql/CsvTests.java | 2 +- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multi-match-function.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multi-match-function.csv-spec index 4503205543aa8..b908012d73340 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multi-match-function.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multi-match-function.csv-spec @@ -3,7 +3,7 @@ # multiMatchWithField -required_capability: multi_match_function +required_capability: multi_match_unified_function // tag::multi-match-with-field[] FROM books @@ -25,7 +25,7 @@ book_no:keyword | author:text ; testMultiMatchWithOptionsFuzziness -required_capability: multi_match_function +required_capability: multi_match_unified_function from books | where multi_match(title, description, "Pings", {"fuzziness": 1}) @@ -46,7 +46,7 @@ book_no:keyword ; testMultiMatchWithOptionsOperator -required_capability: multi_match_function +required_capability: multi_match_unified_function // tag::multi-match-with-named-function-params[] FROM books @@ -61,7 +61,7 @@ The Hobbit or There and Back Again ; testMultiMatchWithOptionsMinimumShouldMatch -required_capability: multi_match_function +required_capability: multi_match_unified_function from books | where multi_match(title, description, "here back again", {"minimum_should_match": 2, "operator": "OR"}) @@ -74,7 +74,7 @@ The Hobbit or There and Back Again ; testMultiMatchWithNonPushableDisjunctions -required_capability: multi_match_function +required_capability: multi_match_unified_function required_capability: full_text_functions_disjunctions_compute_engine from books @@ -97,7 +97,7 @@ book_no:keyword ; testMultiMatchPhraseQuery -required_capability: multi_match_function +required_capability: multi_match_unified_function from books | where multi_match(title, description, "Lord of the rings", { "type": "phrase" } ) @@ -117,7 +117,7 @@ Return of the King Being the Third Part of The Lord of the Rings ; testMultiMatchPhrasePrefixQuery -required_capability: multi_match_function +required_capability: multi_match_unified_function from books | where multi_match(title, "Lord of the ri", { "type": "phrase_prefix" } ) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec index 608feadd65fa9..5b9288416a1c7 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec @@ -115,7 +115,7 @@ book_no:keyword | title:text ; testMultiMatchWithScore -required_capability: multi_match_function +required_capability: multi_match_unified_function required_capability: metadata_score from books metadata _score @@ -128,7 +128,7 @@ book_no:keyword | title:text | author ; testMultiMatchWithScore1 -required_capability: multi_match_function +required_capability: multi_match_unified_function required_capability: metadata_score from books metadata _score @@ -155,7 +155,7 @@ book_no:keyword | _score:double ; testMultiMatchWithScore2 -required_capability: multi_match_function +required_capability: multi_match_unified_function required_capability: metadata_score from books metadata _score diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index d628b0a601c13..5ce1ba7118e55 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -937,6 +937,11 @@ public enum Cap { */ MULTI_MATCH_FUNCTION(Build.current().isSnapshot()), + /** + * Support for unified match and multi-match functions. + */ + MULTI_MATCH_UNIFIED_FUNCTION(Build.current().isSnapshot()), + /** * Do {@code TO_LOWER} and {@code TO_UPPER} process all field values? */ diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java index bf25feb9db553..2a25281ea0fcf 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java @@ -314,7 +314,7 @@ public final void test() throws Throwable { ); assumeFalse( "CSV tests cannot currently handle multi_match function that depends on Lucene", - testCase.requiredCapabilities.contains(EsqlCapabilities.Cap.MULTI_MATCH_FUNCTION.capabilityName()) + testCase.requiredCapabilities.contains(EsqlCapabilities.Cap.MULTI_MATCH_UNIFIED_FUNCTION.capabilityName()) ); if (Build.current().isSnapshot()) { From 75dd50b005ac63f152e49be758d2cf160123ebae Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Mon, 2 Jun 2025 09:29:52 -0400 Subject: [PATCH 03/33] Update match and remove multi_match --- .../_snippets/functions/description/match.md | 2 +- .../_snippets/functions/examples/match.md | 7 +- .../functions/functionNamedParams/match.md | 34 +- .../esql/_snippets/functions/layout/match.md | 4 +- .../_snippets/functions/parameters/match.md | 8 +- .../esql/_snippets/functions/types/match.md | 2 +- .../esql/images/functions/match.svg | 2 +- .../kibana/definition/functions/match.json | 354 ++++++------- .../esql/kibana/docs/functions/match.md | 19 +- .../function/EsqlFunctionRegistry.java | 4 +- .../function/fulltext/FullTextFunction.java | 2 +- .../function/fulltext/FullTextWritables.java | 1 - .../expression/function/fulltext/Match.java | 331 ++++++------ .../function/fulltext/MatchOperator.java | 8 +- .../function/fulltext/MultiMatch.java | 470 ------------------ .../xpack/esql/analysis/AnalyzerTests.java | 3 +- .../xpack/esql/analysis/VerifierTests.java | 111 ++--- .../function/fulltext/MatchErrorTests.java | 2 +- .../function/fulltext/MatchTests.java | 2 +- .../function/fulltext/MultiMatchTests.java | 2 +- .../function/fulltext/TermTests.java | 2 +- .../optimizer/LogicalPlanOptimizerTests.java | 5 +- .../optimizer/PhysicalPlanOptimizerTests.java | 14 +- .../PushDownAndCombineFiltersTests.java | 6 +- .../esql/parser/StatementParserTests.java | 171 ++++--- 25 files changed, 555 insertions(+), 1011 deletions(-) delete mode 100644 x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatch.java diff --git a/docs/reference/query-languages/esql/_snippets/functions/description/match.md b/docs/reference/query-languages/esql/_snippets/functions/description/match.md index 8dd64d9671501..6745c7412c3ba 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/description/match.md +++ b/docs/reference/query-languages/esql/_snippets/functions/description/match.md @@ -2,5 +2,5 @@ **Description** -Use `MATCH` to perform a [match query](/reference/query-languages/query-dsl/query-dsl-match-query.md) on the specified field. Using `MATCH` is equivalent to using the `match` query in the Elasticsearch Query DSL. Match can be used on fields from the text family like [text](/reference/elasticsearch/mapping-reference/text.md) and [semantic_text](/reference/elasticsearch/mapping-reference/semantic-text.md), as well as other field types like keyword, boolean, dates, and numeric types. Match can use [function named parameters](/reference/query-languages/esql/esql-syntax.md#esql-function-named-params) to specify additional options for the match query. All [match query parameters](/reference/query-languages/query-dsl/query-dsl-match-query.md#match-field-params) are supported. For a simplified syntax, you can use the [match operator](/reference/query-languages/esql/functions-operators/operators.md#esql-match-operator) `:` operator instead of `MATCH`. `MATCH` returns true if the provided query matches the row. +Use `MULTI_MATCH` to perform a [multi-match query](/reference/query-languages/query-dsl/query-dsl-match-query.md#query-dsl-multi-match-query) on the specified field. The multi_match query builds on the match query to allow multi-field queries. diff --git a/docs/reference/query-languages/esql/_snippets/functions/examples/match.md b/docs/reference/query-languages/esql/_snippets/functions/examples/match.md index 21694aee454bc..87cb5760b6a4e 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/examples/match.md +++ b/docs/reference/query-languages/esql/_snippets/functions/examples/match.md @@ -4,7 +4,10 @@ ```esql FROM books -| WHERE MATCH(author, "Faulkner") +| WHERE MULTI_MATCH(author, description, "Faulkner") +| KEEP book_no, author +| SORT book_no +| LIMIT 5 ``` | book_no:keyword | author:text | @@ -17,7 +20,7 @@ FROM books ```esql FROM books -| WHERE MATCH(title, "Hobbit Back Again", {"operator": "AND"}) +| WHERE MULTI_MATCH(title, description, "Hobbit Back Again", {"operator": "AND"}) | KEEP title; ``` diff --git a/docs/reference/query-languages/esql/_snippets/functions/functionNamedParams/match.md b/docs/reference/query-languages/esql/_snippets/functions/functionNamedParams/match.md index 434b0fc4fd98e..0424db60c21b3 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/functionNamedParams/match.md +++ b/docs/reference/query-languages/esql/_snippets/functions/functionNamedParams/match.md @@ -8,29 +8,20 @@ `auto_generate_synonyms_phrase_query` : (boolean) If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true. -`analyzer` -: (keyword) Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used. - `minimum_should_match` : (integer) Minimum number of clauses that must match for a document to be returned. -`zero_terms_query` -: (keyword) Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none. - -`boost` -: (float) Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0. - `fuzzy_transpositions` : (boolean) If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true. -`fuzzy_rewrite` -: (keyword) Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default. +`tie_breaker` +: (float) Controls how score is blended together between field groups. Defaults to 0 (best score from each group). -`prefix_length` -: (integer) Number of beginning characters left unchanged for fuzzy matching. Defaults to 0. +`type` +: (object) Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>. `lenient` -: (boolean) If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false. +: (boolean) If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true. `operator` : (keyword) Boolean logic used to interpret text in the query value. Defaults to OR. @@ -38,3 +29,18 @@ `max_expansions` : (integer) Maximum number of terms to which the query will expand. Defaults to 50. +`analyzer` +: (keyword) Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used. + +`zero_terms_query` +: (keyword) Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none. + +`boost` +: (float) Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0. + +`fuzzy_rewrite` +: (keyword) Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default. + +`prefix_length` +: (integer) Number of beginning characters left unchanged for fuzzy matching. Defaults to 0. + diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/match.md b/docs/reference/query-languages/esql/_snippets/functions/layout/match.md index b5368c12e2b4a..50076798a106d 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/layout/match.md +++ b/docs/reference/query-languages/esql/_snippets/functions/layout/match.md @@ -8,8 +8,8 @@ are not subject to the support SLA of official GA features. ::: :::{note} -###### Serverless: GA, Elastic Stack: COMING -Support for optional named parameters is only available in serverless, or in a future {{es}} release +###### Serverless: GA, Elastic Stack: COMING 9.1.0 +Support for optional named parameters is only available from 9.1.0 ::: **Syntax** diff --git a/docs/reference/query-languages/esql/_snippets/functions/parameters/match.md b/docs/reference/query-languages/esql/_snippets/functions/parameters/match.md index ed5fa4ddb9f9b..ed09c5ad51b37 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/parameters/match.md +++ b/docs/reference/query-languages/esql/_snippets/functions/parameters/match.md @@ -2,12 +2,12 @@ **Parameters** -`field` -: Field that the query will target. +`fields` +: Fields to use for matching `query` -: Value to find in the provided field. +: Value to find in the provided fields. `options` -: (Optional) Match additional options as [function named parameters](/reference/query-languages/esql/esql-syntax.md#esql-function-named-params). See [match query](/reference/query-languages/query-dsl/query-dsl-match-query.md) for more information. +: (Optional) Additional options for MultiMatch, passed as [function named parameters](/reference/query-languages/esql/esql-syntax.md#esql-function-named-params)." See [multi-match query](/reference/query-languages/query-dsl/query-dsl-match-query.md#query-dsl-multi-match-query) for more information. diff --git a/docs/reference/query-languages/esql/_snippets/functions/types/match.md b/docs/reference/query-languages/esql/_snippets/functions/types/match.md index 8c3024914a15a..9db8a91d42d8a 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/types/match.md +++ b/docs/reference/query-languages/esql/_snippets/functions/types/match.md @@ -2,7 +2,7 @@ **Supported types** -| field | query | options | result | +| fields | query | options | result | | --- | --- | --- | --- | | boolean | boolean | named parameters | boolean | | boolean | keyword | named parameters | boolean | diff --git a/docs/reference/query-languages/esql/images/functions/match.svg b/docs/reference/query-languages/esql/images/functions/match.svg index d3e5c362a1a74..a902905ffe003 100644 --- a/docs/reference/query-languages/esql/images/functions/match.svg +++ b/docs/reference/query-languages/esql/images/functions/match.svg @@ -1 +1 @@ -MATCH(field,query,options) \ No newline at end of file +MATCH(,fields,query,options) \ No newline at end of file diff --git a/docs/reference/query-languages/esql/kibana/definition/functions/match.json b/docs/reference/query-languages/esql/kibana/definition/functions/match.json index 18048a9091550..ceb38942ed29d 100644 --- a/docs/reference/query-languages/esql/kibana/definition/functions/match.json +++ b/docs/reference/query-languages/esql/kibana/definition/functions/match.json @@ -2,737 +2,737 @@ "comment" : "This is generated by ESQL’s AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.", "type" : "scalar", "name" : "match", - "description" : "Use `MATCH` to perform a match query on the specified field.\nUsing `MATCH` is equivalent to using the `match` query in the Elasticsearch Query DSL.\n\nMatch can be used on fields from the text family like text and semantic_text,\nas well as other field types like keyword, boolean, dates, and numeric types.\n\nMatch can use function named parameters to specify additional options for the match query.\nAll match query parameters are supported.\n\nFor a simplified syntax, you can use the match operator `:` operator instead of `MATCH`.\n\n`MATCH` returns true if the provided query matches the row.", + "description" : "Use `MULTI_MATCH` to perform a multi-match query on the specified field.\nThe multi_match query builds on the match query to allow multi-field queries.", "signatures" : [ { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "boolean", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "boolean", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "boolean", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "date", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "date", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "date", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "date_nanos", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "date_nanos", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "date_nanos", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "double", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "double", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "double", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "integer", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "double", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "double", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "long", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "integer", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "double", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "integer", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "integer", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "integer", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "integer", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "long", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "ip", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "ip", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "ip", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "keyword", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "long", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "double", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "long", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "integer", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "long", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "long", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "long", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "text", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "unsigned_long", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "double", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "unsigned_long", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "integer", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "unsigned_long", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "unsigned_long", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "long", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "unsigned_long", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "unsigned_long", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "version", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "keyword", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" }, { "params" : [ { - "name" : "field", + "name" : "fields", "type" : "version", "optional" : false, - "description" : "Field that the query will target." + "description" : "Fields to use for matching" }, { "name" : "query", "type" : "version", "optional" : false, - "description" : "Value to find in the provided field." + "description" : "Value to find in the provided fields." }, { "name" : "options", "type" : "function_named_parameters", - "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to false.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}", + "mapParams" : "{name='fuzziness', values=[AUTO, 1, 2], description='Maximum edit distance allowed for matching.'}, {name='auto_generate_synonyms_phrase_query', values=[true, false], description='If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true.'}, {name='minimum_should_match', values=[2], description='Minimum number of clauses that must match for a document to be returned.'}, {name='fuzzy_transpositions', values=[true, false], description='If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). Defaults to true.'}, {name='tie_breaker', values=[0], description='Controls how score is blended together between field groups. Defaults to 0 (best score from each group).'}, {name='type', values=['best_fields'], description='Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, `cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. See <>.'}, {name='lenient', values=[true, false], description='If false, format-based errors, such as providing a text query value for a numeric field, are returned. Defaults to true.'}, {name='operator', values=[AND, OR], description='Boolean logic used to interpret text in the query value. Defaults to OR.'}, {name='max_expansions', values=[50], description='Maximum number of terms to which the query will expand. Defaults to 50.'}, {name='analyzer', values=[standard], description='Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer mapped for the field. If no analyzer is mapped, the index’s default analyzer is used.'}, {name='zero_terms_query', values=[none, all], description='Indicates whether all documents or none are returned if the analyzer removes all tokens, such as when using a stop filter. Defaults to none.'}, {name='boost', values=[2.5], description='Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0.'}, {name='fuzzy_rewrite', values=[constant_score_blended, constant_score, constant_score_boolean, top_terms_blended_freqs_N, top_terms_boost_N, top_terms_N], description='Method used to rewrite the query. See the rewrite parameter for valid values and more information. If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of top_terms_blended_freqs_${max_expansions} by default.'}, {name='prefix_length', values=[1], description='Number of beginning characters left unchanged for fuzzy matching. Defaults to 0.'}", "optional" : true, - "description" : "(Optional) Match additional options as <>. See <> for more information." + "description" : "(Optional) Additional options for MultiMatch, passed as <>.\" See <> for more information." } ], - "variadic" : false, + "variadic" : true, "returnType" : "boolean" } ], "examples" : [ - "FROM books\n| WHERE MATCH(author, \"Faulkner\")", - "FROM books\n| WHERE MATCH(title, \"Hobbit Back Again\", {\"operator\": \"AND\"})\n| KEEP title;" + "FROM books\n| WHERE MULTI_MATCH(author, description, \"Faulkner\")\n| KEEP book_no, author\n| SORT book_no\n| LIMIT 5", + "FROM books\n| WHERE MULTI_MATCH(title, description, \"Hobbit Back Again\", {\"operator\": \"AND\"})\n| KEEP title;" ], "preview" : true, "snapshot_only" : false diff --git a/docs/reference/query-languages/esql/kibana/docs/functions/match.md b/docs/reference/query-languages/esql/kibana/docs/functions/match.md index 9e63e998afd7b..c77b660aa1ece 100644 --- a/docs/reference/query-languages/esql/kibana/docs/functions/match.md +++ b/docs/reference/query-languages/esql/kibana/docs/functions/match.md @@ -1,20 +1,13 @@ % This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it. ### MATCH -Use `MATCH` to perform a [match query](https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-match-query) on the specified field. -Using `MATCH` is equivalent to using the `match` query in the Elasticsearch Query DSL. - -Match can be used on fields from the text family like [text](https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/text) and [semantic_text](https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text), -as well as other field types like keyword, boolean, dates, and numeric types. - -Match can use [function named parameters](https://www.elastic.co/docs/reference/query-languages/esql/esql-syntax#esql-function-named-params) to specify additional options for the match query. -All [match query parameters](https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-match-query#match-field-params) are supported. - -For a simplified syntax, you can use the [match operator](https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/operators#esql-match-operator) `:` operator instead of `MATCH`. - -`MATCH` returns true if the provided query matches the row. +Use `MULTI_MATCH` to perform a [multi-match query](https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-match-query#query-dsl-multi-match-query) on the specified field. +The multi_match query builds on the match query to allow multi-field queries. ```esql FROM books -| WHERE MATCH(author, "Faulkner") +| WHERE MULTI_MATCH(author, description, "Faulkner") +| KEEP book_no, author +| SORT book_no +| LIMIT 5 ``` diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java index cfd7613d643e6..69edc68b7539e 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java @@ -43,7 +43,6 @@ import org.elasticsearch.xpack.esql.expression.function.aggregate.WeightedAvg; import org.elasticsearch.xpack.esql.expression.function.fulltext.Kql; import org.elasticsearch.xpack.esql.expression.function.fulltext.Match; -import org.elasticsearch.xpack.esql.expression.function.fulltext.MultiMatch; import org.elasticsearch.xpack.esql.expression.function.fulltext.QueryString; import org.elasticsearch.xpack.esql.expression.function.fulltext.Term; import org.elasticsearch.xpack.esql.expression.function.grouping.Bucket; @@ -439,8 +438,7 @@ private static FunctionDefinition[][] functions() { // fulltext functions new FunctionDefinition[] { def(Kql.class, uni(Kql::new), "kql"), - def(Match.class, tri(Match::new), "match"), - def(MultiMatch.class, MultiMatch::new, "multi_match"), + def(Match.class, Match::new, "match"), def(QueryString.class, bi(QueryString::new), "qstr") } }; } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextFunction.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextFunction.java index 3abdebf63c1fc..db087aaadc91e 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextFunction.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextFunction.java @@ -212,7 +212,7 @@ private static void checkFullTextQueryFunctions(LogicalPlan plan, Failures failu checkCommandsBeforeExpression( plan, condition, - MultiMatch.class, + Match.class, lp -> (lp instanceof Limit == false) && (lp instanceof Aggregate == false), m -> "[" + m.functionName() + "] " + m.functionType(), failures diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextWritables.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextWritables.java index 7ef632c66d44b..536332c10f448 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextWritables.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextWritables.java @@ -21,7 +21,6 @@ public static List getNamedWriteables() { entries.add(QueryString.ENTRY); entries.add(Match.ENTRY); - entries.add(MultiMatch.ENTRY); entries.add(Kql.ENTRY); if (EsqlCapabilities.Cap.TERM_FUNCTION.isEnabled()) { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java index bd5efb7279a99..7cd74fadab9cd 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java @@ -7,12 +7,10 @@ package org.elasticsearch.xpack.esql.expression.function.fulltext; -import org.apache.lucene.util.BytesRef; -import org.elasticsearch.TransportVersions; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.unit.Fuzziness; +import org.elasticsearch.index.query.MatchQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.xpack.esql.capabilities.PostAnalysisPlanVerificationAware; import org.elasticsearch.xpack.esql.common.Failure; @@ -20,7 +18,6 @@ import org.elasticsearch.xpack.esql.core.InvalidArgumentException; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; -import org.elasticsearch.xpack.esql.core.expression.FoldContext; import org.elasticsearch.xpack.esql.core.expression.MapExpression; import org.elasticsearch.xpack.esql.core.querydsl.query.Query; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; @@ -28,7 +25,6 @@ import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.core.type.MultiTypeEsField; import org.elasticsearch.xpack.esql.core.util.Check; -import org.elasticsearch.xpack.esql.core.util.NumericUtils; import org.elasticsearch.xpack.esql.expression.function.Example; import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesTo; import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesToLifecycle; @@ -40,8 +36,7 @@ import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; import org.elasticsearch.xpack.esql.planner.TranslatorHandler; -import org.elasticsearch.xpack.esql.querydsl.query.MatchQuery; -import org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter; +import org.elasticsearch.xpack.esql.querydsl.query.MultiMatchQuery; import java.io.IOException; import java.util.HashMap; @@ -50,19 +45,24 @@ import java.util.Objects; import java.util.Set; import java.util.function.BiConsumer; +import java.util.stream.Stream; import static java.util.Map.entry; -import static org.elasticsearch.index.query.AbstractQueryBuilder.BOOST_FIELD; -import static org.elasticsearch.index.query.MatchQueryBuilder.ANALYZER_FIELD; -import static org.elasticsearch.index.query.MatchQueryBuilder.FUZZY_REWRITE_FIELD; -import static org.elasticsearch.index.query.MatchQueryBuilder.FUZZY_TRANSPOSITIONS_FIELD; -import static org.elasticsearch.index.query.MatchQueryBuilder.GENERATE_SYNONYMS_PHRASE_QUERY; -import static org.elasticsearch.index.query.MatchQueryBuilder.LENIENT_FIELD; -import static org.elasticsearch.index.query.MatchQueryBuilder.MAX_EXPANSIONS_FIELD; -import static org.elasticsearch.index.query.MatchQueryBuilder.MINIMUM_SHOULD_MATCH_FIELD; -import static org.elasticsearch.index.query.MatchQueryBuilder.OPERATOR_FIELD; -import static org.elasticsearch.index.query.MatchQueryBuilder.PREFIX_LENGTH_FIELD; import static org.elasticsearch.index.query.MatchQueryBuilder.ZERO_TERMS_QUERY_FIELD; +import static org.elasticsearch.index.query.MultiMatchQueryBuilder.ANALYZER_FIELD; +import static org.elasticsearch.index.query.MultiMatchQueryBuilder.BOOST_FIELD; +import static org.elasticsearch.index.query.MultiMatchQueryBuilder.FUZZINESS_FIELD; +import static org.elasticsearch.index.query.MultiMatchQueryBuilder.FUZZY_REWRITE_FIELD; +import static org.elasticsearch.index.query.MultiMatchQueryBuilder.FUZZY_TRANSPOSITIONS_FIELD; +import static org.elasticsearch.index.query.MultiMatchQueryBuilder.GENERATE_SYNONYMS_PHRASE_QUERY; +import static org.elasticsearch.index.query.MultiMatchQueryBuilder.LENIENT_FIELD; +import static org.elasticsearch.index.query.MultiMatchQueryBuilder.MAX_EXPANSIONS_FIELD; +import static org.elasticsearch.index.query.MultiMatchQueryBuilder.MINIMUM_SHOULD_MATCH_FIELD; +import static org.elasticsearch.index.query.MultiMatchQueryBuilder.OPERATOR_FIELD; +import static org.elasticsearch.index.query.MultiMatchQueryBuilder.PREFIX_LENGTH_FIELD; +import static org.elasticsearch.index.query.MultiMatchQueryBuilder.SLOP_FIELD; +import static org.elasticsearch.index.query.MultiMatchQueryBuilder.TIE_BREAKER_FIELD; +import static org.elasticsearch.index.query.MultiMatchQueryBuilder.TYPE_FIELD; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.THIRD; @@ -82,10 +82,9 @@ import static org.elasticsearch.xpack.esql.core.type.DataType.TEXT; import static org.elasticsearch.xpack.esql.core.type.DataType.UNSIGNED_LONG; import static org.elasticsearch.xpack.esql.core.type.DataType.VERSION; -import static org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.EsqlBinaryComparison.formatIncompatibleTypesMessage; /** - * Full text function that performs a {@link org.elasticsearch.xpack.esql.querydsl.query.MatchQuery} . + * Full text function that performs a {@link org.elasticsearch.xpack.esql.querydsl.query.MultiMatchQuery} . */ public class Match extends FullTextFunction implements OptionalArgument, PostAnalysisPlanVerificationAware { @@ -115,64 +114,53 @@ public class Match extends FullTextFunction implements OptionalArgument, PostAna UNSIGNED_LONG, VERSION ); - - protected final Expression field; - - // Options for match function. They don’t need to be serialized as the data nodes will retrieve them from the query builder - private final transient Expression options; - - public static final Map ALLOWED_OPTIONS = Map.ofEntries( + public static final Map OPTIONS = Map.ofEntries( + entry(BOOST_FIELD.getPreferredName(), FLOAT), + entry(SLOP_FIELD.getPreferredName(), INTEGER), entry(ANALYZER_FIELD.getPreferredName(), KEYWORD), entry(GENERATE_SYNONYMS_PHRASE_QUERY.getPreferredName(), BOOLEAN), - entry(Fuzziness.FIELD.getPreferredName(), KEYWORD), - entry(BOOST_FIELD.getPreferredName(), FLOAT), - entry(FUZZY_TRANSPOSITIONS_FIELD.getPreferredName(), BOOLEAN), + entry(FUZZINESS_FIELD.getPreferredName(), KEYWORD), entry(FUZZY_REWRITE_FIELD.getPreferredName(), KEYWORD), + entry(FUZZY_TRANSPOSITIONS_FIELD.getPreferredName(), BOOLEAN), entry(LENIENT_FIELD.getPreferredName(), BOOLEAN), entry(MAX_EXPANSIONS_FIELD.getPreferredName(), INTEGER), entry(MINIMUM_SHOULD_MATCH_FIELD.getPreferredName(), KEYWORD), entry(OPERATOR_FIELD.getPreferredName(), KEYWORD), entry(PREFIX_LENGTH_FIELD.getPreferredName(), INTEGER), + entry(TIE_BREAKER_FIELD.getPreferredName(), FLOAT), + entry(TYPE_FIELD.getPreferredName(), KEYWORD), entry(ZERO_TERMS_QUERY_FIELD.getPreferredName(), KEYWORD) ); + // TODO: update descriptions and comments. @FunctionInfo( returnType = "boolean", preview = true, description = """ - Use `MATCH` to perform a <> on the specified field. - Using `MATCH` is equivalent to using the `match` query in the Elasticsearch Query DSL. - - Match can be used on fields from the text family like <> and <>, - as well as other field types like keyword, boolean, dates, and numeric types. - - Match can use <> to specify additional options for the match query. - All <> are supported. - - For a simplified syntax, you can use the <> `:` operator instead of `MATCH`. - - `MATCH` returns true if the provided query matches the row.""", + Use `MULTI_MATCH` to perform a <> on the specified field. + The multi_match query builds on the match query to allow multi-field queries.""", examples = { - @Example(file = "match-function", tag = "match-with-field"), - @Example(file = "match-function", tag = "match-with-named-function-params") }, + @Example(file = "multi-match-function", tag = "multi-match-with-field"), + @Example(file = "multi-match-function", tag = "multi-match-with-named-function-params") }, appliesTo = { @FunctionAppliesTo( lifeCycle = FunctionAppliesToLifecycle.COMING, - description = "Support for optional named parameters is only available in serverless, or in a future {{es}} release" + version = "9.1.0", + description = "Support for optional named parameters is only available from 9.1.0" ) } ) public Match( Source source, @Param( - name = "field", - type = { "keyword", "text", "boolean", "date", "date_nanos", "double", "integer", "ip", "long", "unsigned_long", "version" }, - description = "Field that the query will target." - ) Expression field, + name = "fields", + type = { "keyword", "boolean", "date", "date_nanos", "double", "integer", "ip", "long", "text", "unsigned_long", "version" }, + description = "Fields to use for matching" + ) List fields, @Param( name = "query", type = { "keyword", "boolean", "date", "date_nanos", "double", "integer", "ip", "long", "unsigned_long", "version" }, - description = "Value to find in the provided field." - ) Expression matchQuery, + description = "Value to find in the provided fields." + ) Expression query, @MapParam( name = "options", params = { @@ -227,7 +215,7 @@ public Match( type = "boolean", valueHint = { "true", "false" }, description = "If false, format-based errors, such as providing a text query value for a numeric field, are returned. " - + "Defaults to false." + + "Defaults to true." ), @MapParam.MapParamEntry( name = "max_expansions", @@ -253,6 +241,20 @@ public Match( valueHint = { "1" }, description = "Number of beginning characters left unchanged for fuzzy matching. Defaults to 0." ), + @MapParam.MapParamEntry( + name = "tie_breaker", + type = "float", + valueHint = { "0" }, + description = "Controls how score is blended together between field groups. Defaults to 0 (best score from each group)." + ), + @MapParam.MapParamEntry( + name = "type", + type = "object", + valueHint = { "'best_fields'" }, + description = "Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, " + + "`cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. " + + "See <>." + ), @MapParam.MapParamEntry( name = "zero_terms_query", type = "keyword", @@ -260,18 +262,39 @@ public Match( description = "Indicates whether all documents or none are returned if the analyzer removes all tokens, such as " + "when using a stop filter. Defaults to none." ) }, - description = "(Optional) Match additional options as <>." - + " See <> for more information.", + description = "(Optional) Additional options for MultiMatch, " + + "passed as <>.\"\n" + + " See <> for more information.", optional = true ) Expression options ) { - this(source, field, matchQuery, options, null); + this(source, fields, query, options, null); } - public Match(Source source, Expression field, Expression matchQuery, Expression options, QueryBuilder queryBuilder) { - super(source, matchQuery, options == null ? List.of(field, matchQuery) : List.of(field, matchQuery, options), queryBuilder); - this.field = field; - this.options = options; + // Due to current limitations, the options field may contain a field, in which case treat it as a field, and use "null" for actual + // options. We also remember the originally supplied arguments in order to make tests happy. + private final transient List fields; + private final transient List fieldsOriginal; + private final transient Expression options; + private final transient Expression optionsOriginal; + + private static List initChildren(Expression query, List fields, Expression options) { + Stream fieldsAndQuery = Stream.concat(Stream.of(query), fields.stream()); + return (options == null ? fieldsAndQuery : Stream.concat(fieldsAndQuery, Stream.of(options))).toList(); + } + + protected Match(Source source, List fields, Expression query, Expression options, QueryBuilder queryBuilder) { + super(source, query, initChildren(query, fields, options), queryBuilder); + this.fieldsOriginal = fields; + this.optionsOriginal = options; + + if (options == null || options instanceof MapExpression) { + this.fields = fields; + this.options = options; + } else { + this.fields = Stream.concat(fields.stream(), Stream.of(options)).toList(); + this.options = null; + } } @Override @@ -281,41 +304,40 @@ public String getWriteableName() { private static Match readFrom(StreamInput in) throws IOException { Source source = Source.readFrom((PlanStreamInput) in); - Expression field = in.readNamedWriteable(Expression.class); Expression query = in.readNamedWriteable(Expression.class); - QueryBuilder queryBuilder = null; - if (in.getTransportVersion().onOrAfter(TransportVersions.ESQL_QUERY_BUILDER_IN_SEARCH_FUNCTIONS)) { - queryBuilder = in.readOptionalNamedWriteable(QueryBuilder.class); - } - return new Match(source, field, query, null, queryBuilder); + List fields = in.readNamedWriteableCollectionAsList(Expression.class); + QueryBuilder queryBuilder = in.readOptionalNamedWriteable(QueryBuilder.class); + return new Match(source, fields, query, null, queryBuilder); } - // This is not meant to be overriden by MatchOperator - MatchOperator should be serialized to Match @Override - public final void writeTo(StreamOutput out) throws IOException { + public void writeTo(StreamOutput out) throws IOException { source().writeTo(out); - out.writeNamedWriteable(field()); out.writeNamedWriteable(query()); - if (out.getTransportVersion().onOrAfter(TransportVersions.ESQL_QUERY_BUILDER_IN_SEARCH_FUNCTIONS)) { - out.writeOptionalNamedWriteable(queryBuilder()); - } + out.writeNamedWriteableCollection(fields); + out.writeOptionalNamedWriteable(queryBuilder()); } @Override protected TypeResolution resolveParams() { - return resolveField().and(resolveQuery()).and(resolveOptions()).and(checkParamCompatibility()); + return resolveFields().and(resolveQuery()).and(resolveOptions()); } - private TypeResolution resolveField() { - return isNotNull(field, sourceText(), FIRST).and( - isType( - field, - FIELD_DATA_TYPES::contains, - sourceText(), - FIRST, - "keyword, text, boolean, date, date_nanos, double, integer, ip, long, unsigned_long, version" + private TypeResolution resolveFields() { + return fields.stream() + .map( + (Expression field) -> isNotNull(field, sourceText(), FIRST).and( + isType( + field, + FIELD_DATA_TYPES::contains, + sourceText(), + FIRST, + "keyword, text, boolean, date, date_nanos, double, integer, ip, long, unsigned_long, version" + ) + ) ) - ); + .reduce(TypeResolution::and) + .orElse(null); } private TypeResolution resolveQuery() { @@ -328,23 +350,17 @@ private TypeResolution resolveQuery() { ).and(isNotNullAndFoldable(query(), sourceText(), SECOND)); } - private TypeResolution checkParamCompatibility() { - DataType fieldType = field().dataType(); - DataType queryType = query().dataType(); - - // Field and query types should match. If the query is a string, then it can match any field type. - if ((fieldType == queryType) || (queryType == KEYWORD)) { - return TypeResolution.TYPE_RESOLVED; - } + public List fields() { + return fields; + } - if (fieldType.isNumeric() && queryType.isNumeric()) { - // When doing an unsigned long query, field must be an unsigned long - if ((queryType == UNSIGNED_LONG && fieldType != UNSIGNED_LONG) == false) { - return TypeResolution.TYPE_RESOLVED; - } - } + public Expression options() { + return options; + } - return new TypeResolution(formatIncompatibleTypesMessage(fieldType, queryType, sourceText())); + @Override + public String functionName() { + return ENTRY.name; } private TypeResolution resolveOptions() { @@ -369,100 +385,77 @@ private TypeResolution resolveOptions() { } private Map matchQueryOptions() throws InvalidArgumentException { + Map options = new HashMap<>(); + options.put(MatchQueryBuilder.LENIENT_FIELD.getPreferredName(), true); if (options() == null) { - return Map.of(LENIENT_FIELD.getPreferredName(), true); + return options; } - Map matchOptions = new HashMap<>(); - // Match is lenient by default to avoid failing on incompatible types - matchOptions.put(LENIENT_FIELD.getPreferredName(), true); - - populateOptionsMap((MapExpression) options(), matchOptions, SECOND, sourceText(), ALLOWED_OPTIONS); - return matchOptions; - } - - public Expression field() { - return field; - } - - public Expression options() { + Match.populateOptionsMap((MapExpression) options(), options, THIRD, sourceText(), OPTIONS); return options; } @Override protected NodeInfo info() { - return NodeInfo.create(this, Match::new, field(), query(), options(), queryBuilder()); + // Specifically create new instance with original arguments. + return NodeInfo.create(this, Match::new, fieldsOriginal, query(), optionsOriginal); } @Override public Expression replaceChildren(List newChildren) { - return new Match( - source(), - newChildren.get(0), - newChildren.get(1), - newChildren.size() > 2 ? newChildren.get(2) : null, - queryBuilder() - ); + // "query" is the first child. + if (newChildren.getLast() instanceof MapExpression || newChildren.size() == children().size()) { + // if the last child is a MapExpression, it is the options map + return new Match( + source(), + newChildren.subList(1, newChildren.size() - 1), + newChildren.getFirst(), + newChildren.getLast(), + queryBuilder() + ); + } + + return new Match(source(), newChildren.subList(1, newChildren.size()), newChildren.getFirst(), null, queryBuilder()); } @Override public Expression replaceQueryBuilder(QueryBuilder queryBuilder) { - return new Match(source(), field, query(), options(), queryBuilder); + // Specifically create new instance with original arguments. + return new Match(source(), fieldsOriginal, query(), optionsOriginal, queryBuilder); } @Override public BiConsumer postAnalysisPlanVerification() { return (plan, failures) -> { super.postAnalysisPlanVerification().accept(plan, failures); - plan.forEachExpression(Match.class, m -> { - if (m.fieldAsFieldAttribute() == null) { - failures.add( - Failure.fail( - m.field(), - "[{}] {} cannot operate on [{}], which is not a field from an index mapping", - functionName(), - functionType(), - m.field().sourceText() - ) - ); + plan.forEachExpression(Match.class, mm -> { + for (Expression field : fields) { + if (fieldAsFieldAttribute(field) == null) { + failures.add( + Failure.fail( + field, + "[{}] {} cannot operate on [{}], which is not a field from an index mapping", + functionName(), + functionType(), + field.sourceText() + ) + ); + } } }); }; } - @Override - public Object queryAsObject() { - Object queryAsObject = query().fold(FoldContext.small() /* TODO remove me */); - - // Convert BytesRef to string for string-based values - if (queryAsObject instanceof BytesRef bytesRef) { - return switch (query().dataType()) { - case IP -> EsqlDataTypeConverter.ipToString(bytesRef); - case VERSION -> EsqlDataTypeConverter.versionToString(bytesRef); - default -> bytesRef.utf8ToString(); - }; - } - - // Converts specific types to the correct type for the query - if (query().dataType() == DataType.UNSIGNED_LONG) { - return NumericUtils.unsignedLongAsBigInteger((Long) queryAsObject); - } else if (query().dataType() == DataType.DATETIME && queryAsObject instanceof Long) { - // When casting to date and datetime, we get a long back. But Match query needs a date string - return EsqlDataTypeConverter.dateTimeToString((Long) queryAsObject); - } else if (query().dataType() == DATE_NANOS && queryAsObject instanceof Long) { - return EsqlDataTypeConverter.nanoTimeToString((Long) queryAsObject); - } - - return queryAsObject; - } - @Override protected Query translate(TranslatorHandler handler) { - var fieldAttribute = fieldAsFieldAttribute(); - Check.notNull(fieldAttribute, "Match must have a field attribute as the first argument"); - String fieldName = getNameFromFieldAttribute(fieldAttribute); - // Make query lenient so mixed field types can be queried when a field type is incompatible with the value provided - return new MatchQuery(source(), fieldName, queryAsObject(), matchQueryOptions()); + Map fieldsWithBoost = new HashMap<>(); + for (Expression field : fields) { + var fieldAttribute = fieldAsFieldAttribute(field); + Check.notNull(fieldAttribute, "MultiMatch must have field attributes as arguments #1 to #N-1."); + String fieldName = getNameFromFieldAttribute(fieldAttribute); + fieldsWithBoost.put(fieldName, 1.0f); + } + return new MultiMatchQuery(source(), Objects.toString(queryAsObject()), fieldsWithBoost, matchQueryOptions()); } public static String getNameFromFieldAttribute(FieldAttribute fieldAttribute) { @@ -483,23 +476,19 @@ public static FieldAttribute fieldAsFieldAttribute(Expression field) { return fieldExpression instanceof FieldAttribute fieldAttribute ? fieldAttribute : null; } - private FieldAttribute fieldAsFieldAttribute() { - return fieldAsFieldAttribute(field); - } - @Override public boolean equals(Object o) { - // Match does not serialize options, as they get included in the query builder. We need to override equals and hashcode to - // ignore options when comparing two Match functions + // MultiMatch does not serialize options, as they get included in the query builder. We need to override equals and hashcode to + // ignore options when comparing two MultiMatch functions if (o == null || getClass() != o.getClass()) return false; - Match match = (Match) o; - return Objects.equals(field(), match.field()) - && Objects.equals(query(), match.query()) - && Objects.equals(queryBuilder(), match.queryBuilder()); + Match mm = (Match) o; + return Objects.equals(fields(), mm.fields()) + && Objects.equals(query(), mm.query()) + && Objects.equals(queryBuilder(), mm.queryBuilder()); } @Override public int hashCode() { - return Objects.hash(field(), query(), queryBuilder()); + return Objects.hash(fields(), query(), queryBuilder()); } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchOperator.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchOperator.java index 535eb15d29038..1da9ce7b167b4 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchOperator.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchOperator.java @@ -54,11 +54,11 @@ public MatchOperator( description = "Value to find in the provided field." ) Expression matchQuery ) { - super(source, field, matchQuery, null, null); + super(source, List.of(field), matchQuery, null, null); } private MatchOperator(Source source, Expression field, Expression matchQuery, QueryBuilder queryBuilder) { - super(source, field, matchQuery, null, queryBuilder); + super(source, List.of(field), matchQuery, null, queryBuilder); } @Override @@ -73,7 +73,7 @@ public String functionName() { @Override protected NodeInfo info() { - return NodeInfo.create(this, MatchOperator::new, field(), query()); + return NodeInfo.create(this, MatchOperator::new, fields().getFirst(), query()); } @Override @@ -83,6 +83,6 @@ public Expression replaceChildren(List newChildren) { @Override public Expression replaceQueryBuilder(QueryBuilder queryBuilder) { - return new MatchOperator(source(), field, query(), queryBuilder); + return new MatchOperator(source(), fields().getFirst(), query(), queryBuilder); } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatch.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatch.java deleted file mode 100644 index 89fbc038082bf..0000000000000 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatch.java +++ /dev/null @@ -1,470 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.esql.expression.function.fulltext; - -import org.elasticsearch.common.io.stream.NamedWriteableRegistry; -import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.index.query.MatchQueryBuilder; -import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.xpack.esql.capabilities.PostAnalysisPlanVerificationAware; -import org.elasticsearch.xpack.esql.common.Failure; -import org.elasticsearch.xpack.esql.common.Failures; -import org.elasticsearch.xpack.esql.core.InvalidArgumentException; -import org.elasticsearch.xpack.esql.core.expression.Expression; -import org.elasticsearch.xpack.esql.core.expression.MapExpression; -import org.elasticsearch.xpack.esql.core.querydsl.query.Query; -import org.elasticsearch.xpack.esql.core.tree.NodeInfo; -import org.elasticsearch.xpack.esql.core.tree.Source; -import org.elasticsearch.xpack.esql.core.type.DataType; -import org.elasticsearch.xpack.esql.core.util.Check; -import org.elasticsearch.xpack.esql.expression.function.Example; -import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesTo; -import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesToLifecycle; -import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; -import org.elasticsearch.xpack.esql.expression.function.MapParam; -import org.elasticsearch.xpack.esql.expression.function.OptionalArgument; -import org.elasticsearch.xpack.esql.expression.function.Param; -import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; -import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; -import org.elasticsearch.xpack.esql.planner.TranslatorHandler; -import org.elasticsearch.xpack.esql.querydsl.query.MultiMatchQuery; - -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.stream.Stream; - -import static java.util.Map.entry; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.ANALYZER_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.BOOST_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.FUZZINESS_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.FUZZY_REWRITE_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.FUZZY_TRANSPOSITIONS_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.GENERATE_SYNONYMS_PHRASE_QUERY; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.LENIENT_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.MAX_EXPANSIONS_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.MINIMUM_SHOULD_MATCH_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.OPERATOR_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.PREFIX_LENGTH_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.SLOP_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.TIE_BREAKER_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.TYPE_FIELD; -import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; -import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND; -import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.THIRD; -import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isMapExpression; -import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNull; -import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNullAndFoldable; -import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType; -import static org.elasticsearch.xpack.esql.core.type.DataType.BOOLEAN; -import static org.elasticsearch.xpack.esql.core.type.DataType.DATETIME; -import static org.elasticsearch.xpack.esql.core.type.DataType.DATE_NANOS; -import static org.elasticsearch.xpack.esql.core.type.DataType.DOUBLE; -import static org.elasticsearch.xpack.esql.core.type.DataType.FLOAT; -import static org.elasticsearch.xpack.esql.core.type.DataType.INTEGER; -import static org.elasticsearch.xpack.esql.core.type.DataType.IP; -import static org.elasticsearch.xpack.esql.core.type.DataType.KEYWORD; -import static org.elasticsearch.xpack.esql.core.type.DataType.LONG; -import static org.elasticsearch.xpack.esql.core.type.DataType.TEXT; -import static org.elasticsearch.xpack.esql.core.type.DataType.UNSIGNED_LONG; -import static org.elasticsearch.xpack.esql.core.type.DataType.VERSION; - -/** - * Full text function that performs a {@link org.elasticsearch.xpack.esql.querydsl.query.MultiMatchQuery} . - */ -public class MultiMatch extends FullTextFunction implements OptionalArgument, PostAnalysisPlanVerificationAware { - public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry( - Expression.class, - "MultiMatch", - MultiMatch::readFrom - ); - - public static final Set QUERY_DATA_TYPES = Set.of( - KEYWORD, - BOOLEAN, - DATETIME, - DATE_NANOS, - DOUBLE, - INTEGER, - IP, - LONG, - UNSIGNED_LONG, - VERSION - ); - - public static final Set FIELD_DATA_TYPES = Set.of( - KEYWORD, - TEXT, - BOOLEAN, - DATETIME, - DATE_NANOS, - DOUBLE, - INTEGER, - IP, - LONG, - UNSIGNED_LONG, - VERSION - ); - - public static final Map OPTIONS = Map.ofEntries( - entry(BOOST_FIELD.getPreferredName(), FLOAT), - entry(SLOP_FIELD.getPreferredName(), INTEGER), - // TODO: add "zero_terms_query" - entry(ANALYZER_FIELD.getPreferredName(), KEYWORD), - entry(GENERATE_SYNONYMS_PHRASE_QUERY.getPreferredName(), BOOLEAN), - entry(FUZZINESS_FIELD.getPreferredName(), KEYWORD), - entry(FUZZY_REWRITE_FIELD.getPreferredName(), KEYWORD), - entry(FUZZY_TRANSPOSITIONS_FIELD.getPreferredName(), BOOLEAN), - entry(LENIENT_FIELD.getPreferredName(), BOOLEAN), - entry(MAX_EXPANSIONS_FIELD.getPreferredName(), INTEGER), - entry(MINIMUM_SHOULD_MATCH_FIELD.getPreferredName(), KEYWORD), - entry(OPERATOR_FIELD.getPreferredName(), KEYWORD), - entry(PREFIX_LENGTH_FIELD.getPreferredName(), INTEGER), - entry(TIE_BREAKER_FIELD.getPreferredName(), FLOAT), - entry(TYPE_FIELD.getPreferredName(), KEYWORD) - ); - - @FunctionInfo( - returnType = "boolean", - preview = true, - description = """ - Use `MULTI_MATCH` to perform a <> on the specified field. - The multi_match query builds on the match query to allow multi-field queries.""", - examples = { - @Example(file = "multi-match-function", tag = "multi-match-with-field"), - @Example(file = "multi-match-function", tag = "multi-match-with-named-function-params") }, - appliesTo = { - @FunctionAppliesTo( - lifeCycle = FunctionAppliesToLifecycle.COMING, - version = "9.1.0", - description = "Support for optional named parameters is only available from 9.1.0" - ) } - ) - public MultiMatch( - Source source, - @Param( - name = "fields", - type = { "keyword", "boolean", "date", "date_nanos", "double", "integer", "ip", "long", "text", "unsigned_long", "version" }, - description = "Fields to use for matching" - ) List fields, - @Param( - name = "query", - type = { "keyword", "boolean", "date", "date_nanos", "double", "integer", "ip", "long", "unsigned_long", "version" }, - description = "Value to find in the provided fields." - ) Expression query, - @MapParam( - name = "options", - params = { - @MapParam.MapParamEntry( - name = "boost", - type = "float", - valueHint = { "2.5" }, - description = "Floating point number used to decrease or increase the relevance scores of the query." - ), - @MapParam.MapParamEntry( - name = "analyzer", - type = "keyword", - valueHint = { "standard" }, - description = "Analyzer used to convert the text in the query value into token. Defaults to the index-time analyzer" - + " mapped for the field. If no analyzer is mapped, the index’s default analyzer is used." - ), - @MapParam.MapParamEntry( - name = "auto_generate_synonyms_phrase_query", - type = "boolean", - valueHint = { "true", "false" }, - description = "If true, match phrase queries are automatically created for multi-term synonyms. Defaults to true." - ), - @MapParam.MapParamEntry( - name = "fuzziness", - type = "keyword", - valueHint = { "AUTO", "1", "2" }, - description = "Maximum edit distance allowed for matching." - ), - @MapParam.MapParamEntry( - name = "fuzzy_rewrite", - type = "keyword", - valueHint = { - "constant_score_blended", - "constant_score", - "constant_score_boolean", - "top_terms_blended_freqs_N", - "top_terms_boost_N", - "top_terms_N" }, - description = "Method used to rewrite the query. See the rewrite parameter for valid values and more information. " - + "If the fuzziness parameter is not 0, the match query uses a fuzzy_rewrite method of " - + "top_terms_blended_freqs_${max_expansions} by default." - ), - @MapParam.MapParamEntry( - name = "fuzzy_transpositions", - type = "boolean", - valueHint = { "true", "false" }, - description = "If true, edits for fuzzy matching include transpositions of two adjacent characters (ab → ba). " - + "Defaults to true." - ), - @MapParam.MapParamEntry( - name = "lenient", - type = "boolean", - valueHint = { "true", "false" }, - description = "If false, format-based errors, such as providing a text query value for a numeric field, are returned. " - + "Defaults to true." - ), - @MapParam.MapParamEntry( - name = "max_expansions", - type = "integer", - valueHint = { "50" }, - description = "Maximum number of terms to which the query will expand. Defaults to 50." - ), - @MapParam.MapParamEntry( - name = "minimum_should_match", - type = "integer", - valueHint = { "2" }, - description = "Minimum number of clauses that must match for a document to be returned." - ), - @MapParam.MapParamEntry( - name = "operator", - type = "keyword", - valueHint = { "AND", "OR" }, - description = "Boolean logic used to interpret text in the query value. Defaults to OR." - ), - @MapParam.MapParamEntry( - name = "prefix_length", - type = "integer", - valueHint = { "1" }, - description = "Number of beginning characters left unchanged for fuzzy matching. Defaults to 0." - ), - @MapParam.MapParamEntry( - name = "tie_breaker", - type = "float", - valueHint = { "0" }, - description = "Controls how score is blended together between field groups. Defaults to 0 (best score from each group)." - ), - @MapParam.MapParamEntry( - name = "type", - type = "object", - valueHint = { "'best_fields'" }, - description = "Controls the way multi_match is executed internally. Can be one of `best_fields`, `most_fields`, " - + "`cross_fields`, `phrase`, `phrase_prefix` or `bool_prefix`. Defaults to 'best_fields'. " - + "See <>." - ), }, - description = "(Optional) Additional options for MultiMatch, " - + "passed as <>.\"\n" - + " See <> for more information.", - optional = true - ) Expression options - ) { - this(source, fields, query, options, null); - } - - // Due to current limitations, the options field may contain a field, in which case treat it as a field, and use "null" for actual - // options. We also remember the originally supplied arguments in order to make tests happy. - private final transient List fields; - private final transient List fieldsOriginal; - private final transient Expression options; - private final transient Expression optionsOriginal; - - private static List initChildren(Expression query, List fields, Expression options) { - Stream fieldsAndQuery = Stream.concat(Stream.of(query), fields.stream()); - return (options == null ? fieldsAndQuery : Stream.concat(fieldsAndQuery, Stream.of(options))).toList(); - } - - private MultiMatch(Source source, List fields, Expression query, Expression options, QueryBuilder queryBuilder) { - super(source, query, initChildren(query, fields, options), queryBuilder); - this.fieldsOriginal = fields; - this.optionsOriginal = options; - - if (options == null || options instanceof MapExpression) { - this.fields = fields; - this.options = options; - } else { - this.fields = Stream.concat(fields.stream(), Stream.of(options)).toList(); - this.options = null; - } - } - - private static MultiMatch readFrom(StreamInput in) throws IOException { - Source source = Source.readFrom((PlanStreamInput) in); - Expression query = in.readNamedWriteable(Expression.class); - List fields = in.readNamedWriteableCollectionAsList(Expression.class); - QueryBuilder queryBuilder = in.readOptionalNamedWriteable(QueryBuilder.class); - return new MultiMatch(source, fields, query, null, queryBuilder); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - source().writeTo(out); - out.writeNamedWriteable(query()); - out.writeNamedWriteableCollection(fields); - out.writeOptionalNamedWriteable(queryBuilder()); - } - - @Override - public String getWriteableName() { - return ENTRY.name; - } - - @Override - public Expression replaceChildren(List newChildren) { - // "query" is the first child. - if (newChildren.getLast() instanceof MapExpression || newChildren.size() == children().size()) { - // if the last child is a MapExpression, it is the options map - return new MultiMatch( - source(), - newChildren.subList(1, newChildren.size() - 1), - newChildren.getFirst(), - newChildren.getLast(), - queryBuilder() - ); - } - - return new MultiMatch(source(), newChildren.subList(1, newChildren.size()), newChildren.getFirst(), null, queryBuilder()); - } - - @Override - protected NodeInfo info() { - // Specifically create new instance with original arguments. - return NodeInfo.create(this, MultiMatch::new, fieldsOriginal, query(), optionsOriginal); - } - - @Override - protected Query translate(TranslatorHandler handler) { - Map fieldsWithBoost = new HashMap<>(); - for (Expression field : fields) { - var fieldAttribute = Match.fieldAsFieldAttribute(field); - Check.notNull(fieldAttribute, "MultiMatch must have field attributes as arguments #2 to #N-1."); - String fieldName = Match.getNameFromFieldAttribute(fieldAttribute); - fieldsWithBoost.put(fieldName, 1.0f); - } - return new MultiMatchQuery(source(), Objects.toString(queryAsObject()), fieldsWithBoost, getOptions()); - } - - @Override - public Expression replaceQueryBuilder(QueryBuilder queryBuilder) { - // Specifically create new instance with original arguments. - return new MultiMatch(source(), fieldsOriginal, query(), optionsOriginal, queryBuilder); - } - - public List fields() { - return fields; - } - - public Expression options() { - return options; - } - - private Map getOptions() throws InvalidArgumentException { - Map options = new HashMap<>(); - options.put(MatchQueryBuilder.LENIENT_FIELD.getPreferredName(), true); - if (options() == null) { - return options; - } - - Match.populateOptionsMap((MapExpression) options(), options, THIRD, sourceText(), OPTIONS); - return options; - } - - @Override - public String functionName() { - return ENTRY.name; - } - - private TypeResolution resolveFields() { - return fields.stream() - .map( - (Expression field) -> isNotNull(field, sourceText(), FIRST).and( - isType( - field, - FIELD_DATA_TYPES::contains, - sourceText(), - FIRST, - "keyword, text, boolean, date, date_nanos, double, integer, ip, long, unsigned_long, version" - ) - ) - ) - .reduce(TypeResolution::and) - .orElse(null); - } - - private TypeResolution resolveOptions() { - if (options() != null) { - TypeResolution resolution = isNotNull(options(), sourceText(), THIRD); - if (resolution.unresolved()) { - return resolution; - } - // MapExpression does not have a DataType associated with it - resolution = isMapExpression(options(), sourceText(), THIRD); - if (resolution.unresolved()) { - return resolution; - } - - try { - getOptions(); - } catch (InvalidArgumentException e) { - return new TypeResolution(e.getMessage()); - } - } - return TypeResolution.TYPE_RESOLVED; - } - - private TypeResolution resolveQuery() { - return isType( - query(), - QUERY_DATA_TYPES::contains, - sourceText(), - SECOND, - "keyword, boolean, date, date_nanos, double, integer, ip, long, unsigned_long, version" - ).and(isNotNullAndFoldable(query(), sourceText(), SECOND)); - } - - @Override - protected TypeResolution resolveParams() { - return resolveFields().and(resolveQuery()).and(resolveOptions()); - } - - @Override - public boolean equals(Object o) { - // MultiMatch does not serialize options, as they get included in the query builder. We need to override equals and hashcode to - // ignore options when comparing two MultiMatch functions - if (o == null || getClass() != o.getClass()) return false; - MultiMatch mm = (MultiMatch) o; - return Objects.equals(fields(), mm.fields()) - && Objects.equals(query(), mm.query()) - && Objects.equals(queryBuilder(), mm.queryBuilder()); - } - - @Override - public int hashCode() { - return Objects.hash(fields(), query(), queryBuilder()); - } - - @Override - public BiConsumer postAnalysisPlanVerification() { - return (plan, failures) -> { - super.postAnalysisPlanVerification().accept(plan, failures); - plan.forEachExpression(MultiMatch.class, mm -> { - for (Expression field : fields) { - if (Match.fieldAsFieldAttribute(field) == null) { - failures.add( - Failure.fail( - field, - "[{}] {} cannot operate on [{}], which is not a field from an index mapping", - functionName(), - functionType(), - field.sourceText() - ) - ); - } - } - }); - }; - } -} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java index 8d77c33bd5f3b..becea15bd5ecf 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java @@ -45,7 +45,6 @@ import org.elasticsearch.xpack.esql.expression.function.aggregate.Min; import org.elasticsearch.xpack.esql.expression.function.fulltext.Match; import org.elasticsearch.xpack.esql.expression.function.fulltext.MatchOperator; -import org.elasticsearch.xpack.esql.expression.function.fulltext.MultiMatch; import org.elasticsearch.xpack.esql.expression.function.fulltext.QueryString; import org.elasticsearch.xpack.esql.expression.function.grouping.Bucket; import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToInteger; @@ -2867,7 +2866,7 @@ public void testFunctionNamedParamsAsFunctionArgument2() { """); Limit limit = as(plan, Limit.class); Filter filter = as(limit.child(), Filter.class); - MultiMatch mm = as(filter.condition(), MultiMatch.class); + Match mm = as(filter.condition(), Match.class); MapExpression me = as(mm.options(), MapExpression.class); assertEquals(1, me.entryExpressions().size()); EntryExpression ee = as(me.entryExpressions().get(0), EntryExpression.class); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java index 51feb72b6c434..fafc8f7612c72 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java @@ -19,7 +19,6 @@ import org.elasticsearch.xpack.esql.core.type.InvalidMappedField; import org.elasticsearch.xpack.esql.core.type.UnsupportedEsField; import org.elasticsearch.xpack.esql.expression.function.fulltext.Match; -import org.elasticsearch.xpack.esql.expression.function.fulltext.MultiMatch; import org.elasticsearch.xpack.esql.expression.function.fulltext.QueryString; import org.elasticsearch.xpack.esql.index.EsIndex; import org.elasticsearch.xpack.esql.index.IndexResolution; @@ -1217,22 +1216,22 @@ public void testMatchInsideEval() throws Exception { public void testMatchFunctionNotAllowedAfterCommands() throws Exception { assertEquals( - "1:24: [MATCH] function cannot be used after LIMIT", + "1:24: [Match] function cannot be used after LIMIT", error("from test | limit 10 | where match(first_name, \"Anna\")") ); assertEquals( - "1:47: [MATCH] function cannot be used after STATS", + "1:47: [Match] function cannot be used after STATS", error("from test | STATS c = AVG(salary) BY gender | where match(gender, \"F\")") ); } public void testMatchFunctionAndOperatorHaveCorrectErrorMessages() throws Exception { assertEquals( - "1:24: [MATCH] function cannot be used after LIMIT", + "1:24: [Match] function cannot be used after LIMIT", error("from test | limit 10 | where match(first_name, \"Anna\")") ); assertEquals( - "1:24: [MATCH] function cannot be used after LIMIT", + "1:24: [Match] function cannot be used after LIMIT", error("from test | limit 10 | where match ( first_name, \"Anna\" ) ") ); assertEquals("1:24: [:] operator cannot be used after LIMIT", error("from test | limit 10 | where first_name:\"Anna\"")); @@ -1242,18 +1241,18 @@ public void testMatchFunctionAndOperatorHaveCorrectErrorMessages() throws Except // These should pass eventually once we lift some restrictions on match function public void testMatchWithNonIndexedColumnCurrentlyUnsupported() { assertEquals( - "1:67: [MATCH] function cannot operate on [initial], which is not a field from an index mapping", + "1:67: [Match] function cannot operate on [initial], which is not a field from an index mapping", error("from test | eval initial = substring(first_name, 1) | where match(initial, \"A\")") ); assertEquals( - "1:67: [MATCH] function cannot operate on [text], which is not a field from an index mapping", + "1:67: [Match] function cannot operate on [text], which is not a field from an index mapping", error("from test | eval text=concat(first_name, last_name) | where match(text, \"cat\")") ); } public void testMatchFunctionIsNotNullable() { assertEquals( - "1:48: [MATCH] function cannot operate on [text::keyword], which is not a field from an index mapping", + "1:48: [Match] function cannot operate on [text::keyword], which is not a field from an index mapping", error("row n = null | eval text = n + 5 | where match(text::keyword, \"Anna\")") ); } @@ -1379,7 +1378,7 @@ public void testKqlFunctionOnlyAllowedInWhere() throws Exception { } public void testMatchFunctionOnlyAllowedInWhere() throws Exception { - checkFullTextFunctionsOnlyAllowedInWhere("MATCH", "match(first_name, \"Anna\")", "function"); + checkFullTextFunctionsOnlyAllowedInWhere("Match", "match(first_name, \"Anna\")", "function"); } public void testTermFunctionOnlyAllowedInWhere() throws Exception { @@ -1458,7 +1457,7 @@ private void checkWithDisjunctions(String functionName, String functionInvocatio public void testFullTextFunctionsDisjunctions() { checkWithFullTextFunctionsDisjunctions("match(last_name, \"Smith\")"); - checkWithFullTextFunctionsDisjunctions("multi_match(first_name, last_name, \"Smith\")"); + checkWithFullTextFunctionsDisjunctions("match(first_name, last_name, \"Smith\")"); checkWithFullTextFunctionsDisjunctions("last_name : \"Smith\""); checkWithFullTextFunctionsDisjunctions("qstr(\"last_name: Smith\")"); checkWithFullTextFunctionsDisjunctions("kql(\"last_name: Smith\")"); @@ -1513,7 +1512,7 @@ public void testKqlFunctionWithNonBooleanFunctions() { } public void testMatchFunctionWithNonBooleanFunctions() { - checkFullTextFunctionsWithNonBooleanFunctions("MATCH", "match(first_name, \"Anna\")", "function"); + checkFullTextFunctionsWithNonBooleanFunctions("Match", "match(first_name, \"Anna\")", "function"); } public void testTermFunctionWithNonBooleanFunctions() { @@ -2188,7 +2187,7 @@ public void testMatchOptions() { // Check all data types for available options DataType[] optionTypes = new DataType[] { INTEGER, LONG, FLOAT, DOUBLE, KEYWORD, BOOLEAN }; - for (Map.Entry allowedOptions : Match.ALLOWED_OPTIONS.entrySet()) { + for (Map.Entry allowedOptions : Match.OPTIONS.entrySet()) { String optionName = allowedOptions.getKey(); DataType optionType = allowedOptions.getValue(); // Check every possible type for the option - we'll try to convert it to the expected type @@ -2321,24 +2320,24 @@ public void testQueryStringOptions() { public void testMultiMatchOptions() { // Check positive cases - query("FROM test | WHERE MULTI_MATCH(first_name, \"Jean\")"); - query("FROM test | WHERE MULTI_MATCH(first_name, \"Jean\", {\"analyzer\": \"standard\"})"); - query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"analyzer\": \"standard\"})"); - query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"slop\": 10})"); - query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"auto_generate_synonyms_phrase_query\": true})"); - query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"fuzziness\": 2})"); - query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"fuzzy_transpositions\": false})"); - query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"lenient\": false})"); - query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"max_expansions\": 10})"); - query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"minimum_should_match\": \"2\"})"); - query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"operator\": \"AND\"})"); - query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"prefix_length\": 2})"); - query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"tie_breaker\": 1.0})"); - query("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"type\": \"best_fields\"})"); + query("FROM test | WHERE MATCH(first_name, \"Jean\")"); + query("FROM test | WHERE MATCH(first_name, \"Jean\", {\"analyzer\": \"standard\"})"); + query("FROM test | WHERE MATCH(first_name, last_name, \"Jean\", {\"analyzer\": \"standard\"})"); + query("FROM test | WHERE MATCH(first_name, last_name, \"Jean\", {\"slop\": 10})"); + query("FROM test | WHERE MATCH(first_name, last_name, \"Jean\", {\"auto_generate_synonyms_phrase_query\": true})"); + query("FROM test | WHERE MATCH(first_name, last_name, \"Jean\", {\"fuzziness\": 2})"); + query("FROM test | WHERE MATCH(first_name, last_name, \"Jean\", {\"fuzzy_transpositions\": false})"); + query("FROM test | WHERE MATCH(first_name, last_name, \"Jean\", {\"lenient\": false})"); + query("FROM test | WHERE MATCH(first_name, last_name, \"Jean\", {\"max_expansions\": 10})"); + query("FROM test | WHERE MATCH(first_name, last_name, \"Jean\", {\"minimum_should_match\": \"2\"})"); + query("FROM test | WHERE MATCH(first_name, last_name, \"Jean\", {\"operator\": \"AND\"})"); + query("FROM test | WHERE MATCH(first_name, last_name, \"Jean\", {\"prefix_length\": 2})"); + query("FROM test | WHERE MATCH(first_name, last_name, \"Jean\", {\"tie_breaker\": 1.0})"); + query("FROM test | WHERE MATCH(first_name, last_name, \"Jean\", {\"type\": \"best_fields\"})"); // Check all data types for available options DataType[] optionTypes = new DataType[] { INTEGER, LONG, FLOAT, DOUBLE, KEYWORD, BOOLEAN }; - for (Map.Entry allowedOptions : MultiMatch.OPTIONS.entrySet()) { + for (Map.Entry allowedOptions : Match.OPTIONS.entrySet()) { String optionName = allowedOptions.getKey(); DataType optionType = allowedOptions.getValue(); // Check every possible type for the option - we'll try to convert it to the expected type @@ -2357,7 +2356,7 @@ public void testMultiMatchOptions() { queryOptionValue = "\"" + optionValue + "\""; } - String query = "FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"" + String query = "FROM test | WHERE MATCH(first_name, last_name, \"Jean\", {\"" + optionName + "\": " + queryOptionValue @@ -2372,7 +2371,7 @@ public void testMultiMatchOptions() { assertEquals( "1:19: Invalid option [" + optionName - + "] in [MULTI_MATCH(first_name, last_name, \"Jean\", {\"" + + "] in [MATCH(first_name, last_name, \"Jean\", {\"" + optionName + "\": " + queryOptionValue @@ -2392,9 +2391,9 @@ public void testMultiMatchOptions() { } assertThat( - error("FROM test | WHERE MULTI_MATCH(first_name, last_name, \"Jean\", {\"unknown_option\": true})"), + error("FROM test | WHERE MATCH(first_name, last_name, \"Jean\", {\"unknown_option\": true})"), containsString( - "1:19: Invalid option [unknown_option] in [MULTI_MATCH(first_name, last_name, \"Jean\", " + "1:19: Invalid option [unknown_option] in [MATCH(first_name, last_name, \"Jean\", " + "{\"unknown_option\": true})], expected one of " ) ); @@ -2402,39 +2401,39 @@ public void testMultiMatchOptions() { public void testMultiMatchFunctionIsNotNullable() { assertEquals( - "1:54: [MultiMatch] function cannot operate on [text::keyword], which is not a field from an index mapping", - error("row n = null | eval text = n + 5 | where multi_match(text::keyword, \"Anna\")") + "1:48: [Match] function cannot operate on [text::keyword], which is not a field from an index mapping", + error("row n = null | eval text = n + 5 | where match(text::keyword, \"Anna\")") ); } public void testMultiMatchWithNonIndexedColumnCurrentlyUnsupported() { assertEquals( - "1:73: [MultiMatch] function cannot operate on [initial], which is not a field from an index mapping", - error("from test | eval initial = substring(first_name, 1) | where multi_match(initial, \"A\")") + "1:67: [Match] function cannot operate on [initial], which is not a field from an index mapping", + error("from test | eval initial = substring(first_name, 1) | where match(initial, \"A\")") ); assertEquals( - "1:73: [MultiMatch] function cannot operate on [text], which is not a field from an index mapping", - error("from test | eval text=concat(first_name, last_name) | where multi_match(text, \"cat\")") + "1:67: [Match] function cannot operate on [text], which is not a field from an index mapping", + error("from test | eval text=concat(first_name, last_name) | where match(text, \"cat\")") ); } public void testMultiMatchFunctionNotAllowedAfterCommands() throws Exception { assertEquals( - "1:24: [MultiMatch] function cannot be used after LIMIT", - error("from test | limit 10 | where multi_match(first_name, \"Anna\")") + "1:24: [Match] function cannot be used after LIMIT", + error("from test | limit 10 | where match(first_name, \"Anna\")") ); assertEquals( - "1:47: [MultiMatch] function cannot be used after STATS", - error("from test | STATS c = AVG(salary) BY gender | where multi_match(gender, \"F\")") + "1:47: [Match] function cannot be used after STATS", + error("from test | STATS c = AVG(salary) BY gender | where match(gender, \"F\")") ); } public void testMultiMatchFunctionWithDisjunctions() { - checkWithDisjunctions("MultiMatch", "multi_match(first_name, last_name, \"Anna\")", "function"); + checkWithDisjunctions("MultiMatch", "match(first_name, last_name, \"Anna\")", "function"); } public void testMultiMatchFunctionWithNonBooleanFunctions() { - checkFullTextFunctionsWithNonBooleanFunctions("MultiMatch", "multi_match(first_name, last_name, \"Anna\")", "function"); + checkFullTextFunctionsWithNonBooleanFunctions("Match", "match(first_name, last_name, \"Anna\")", "function"); } public void testMultiMatchFunctionArgNotConstant() throws Exception { @@ -2452,35 +2451,35 @@ public void testMultiMatchFunctionArgNotConstant() throws Exception { // Should pass eventually once we lift some restrictions on the multi-match function. public void testMultiMatchFunctionCurrentlyUnsupportedBehaviour() throws Exception { assertEquals( - "1:74: Unknown column [first_name]\nline 1:86: Unknown column [last_name]", - error("from test | stats max_salary = max(salary) by emp_no | where multi_match(first_name, last_name, \"Anna\")") + "1:68: Unknown column [first_name]\nline 1:80: Unknown column [last_name]", + error("from test | stats max_salary = max(salary) by emp_no | where match(first_name, last_name, \"Anna\")") ); } public void testMultiMatchFunctionNullArgs() throws Exception { assertEquals( - "1:19: second argument of [multi_match(\"query\", null)] cannot be null, received [null]", - error("from test | where multi_match(\"query\", null)") + "1:19: second argument of [match(\"query\", null)] cannot be null, received [null]", + error("from test | where match(\"query\", null)") ); assertEquals( - "1:19: second argument of [multi_match(first_name, null)] cannot be null, received [null]", - error("from test | where multi_match(first_name, null)") + "1:19: second argument of [match(first_name, null)] cannot be null, received [null]", + error("from test | where match(first_name, null)") ); } public void testMultiMatchTargetsExistingField() throws Exception { assertEquals( - "1:45: Unknown column [first_name]\nline 1:57: Unknown column [last_name]", - error("from test | keep emp_no | where multi_match(first_name, last_name, \"Anna\")") + "1:39: Unknown column [first_name]\nline 1:51: Unknown column [last_name]", + error("from test | keep emp_no | where match(first_name, last_name, \"Anna\")") ); } public void testMultiMatchInsideEval() throws Exception { - assumeTrue("MultiMatch operator is available just for snapshots", Build.current().isSnapshot()); + assumeTrue("Match operator is available just for snapshots", Build.current().isSnapshot()); assertEquals( - "1:36: [MultiMatch] function is only supported in WHERE and STATS commands\n" - + "line 1:48: [MultiMatch] function cannot operate on [title], which is not a field from an index mapping", - error("row title = \"brown fox\" | eval x = multi_match(title, \"fox\")") + "1:36: [Match] function is only supported in WHERE and STATS commands\n" + + "line 1:48: [Match] function cannot operate on [title], which is not a field from an index mapping", + error("row title = \"brown fox\" | eval x = match(title, \"fox\")") ); } @@ -2495,7 +2494,7 @@ public void testInsistNotOnTopOfFrom() { public void testFullTextFunctionsInStats() { checkFullTextFunctionsInStats("match(last_name, \"Smith\")"); - checkFullTextFunctionsInStats("multi_match(first_name, last_name, \"Smith\")"); + checkFullTextFunctionsInStats("match(first_name, last_name, \"Smith\")"); checkFullTextFunctionsInStats("last_name : \"Smith\""); checkFullTextFunctionsInStats("qstr(\"last_name: Smith\")"); checkFullTextFunctionsInStats("kql(\"last_name: Smith\")"); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchErrorTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchErrorTests.java index a60a522b86aa4..2277ad01e0c95 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchErrorTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchErrorTests.java @@ -37,7 +37,7 @@ protected List cases() { @Override protected Expression build(Source source, List args) { - Match match = new Match(source, args.get(0), args.get(1), args.size() > 2 ? args.get(2) : null); + Match match = new Match(source, List.of(args.get(0)), args.get(1), args.size() > 2 ? args.get(2) : null); // We need to add the QueryBuilder to the match expression, as it is used to implement equals() and hashCode() and // thus test the serialization methods. But we can only do this if the parameters make sense . if (args.get(0) instanceof FieldAttribute && args.get(1).foldable()) { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchTests.java index 6993f7583dd02..8c112fcc8317b 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchTests.java @@ -77,7 +77,7 @@ private static List addFunctionNamedParams(List args) { - Match match = new Match(source, args.get(0), args.get(1), args.size() > 2 ? args.get(2) : null); + Match match = new Match(source, List.of(args.get(0)), args.get(1), args.size() > 2 ? args.get(2) : null); // We need to add the QueryBuilder to the match expression, as it is used to implement equals() and hashCode() and // thus test the serialization methods. But we can only do this if the parameters make sense . if (args.get(0) instanceof FieldAttribute && args.get(1).foldable()) { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatchTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatchTests.java index 9113e872d89cd..67629613d9ba8 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatchTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatchTests.java @@ -34,7 +34,7 @@ public MultiMatchTests(@Name("TestCase") Supplier tes @Override protected Expression build(Source source, List args) { - MultiMatch mm = new MultiMatch(source, List.of(args.get(0)), args.get(1), args.get(2)); + Match mm = new Match(source, List.of(args.get(0)), args.get(1), args.get(2)); // We need to add the QueryBuilder to the multi_match expression, as it is used to implement equals() and hashCode() and // thus test the serialization methods. But we can only do this if the parameters make sense . if (mm.query().foldable() && mm.fields().stream().allMatch(field -> field instanceof FieldAttribute)) { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/TermTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/TermTests.java index eca246e20452c..c675661f5c6ba 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/TermTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/TermTests.java @@ -122,6 +122,6 @@ private static String matchTypeErrorSupplier(boolean includeOrdinal, List args) { - return new Match(source, args.get(0), args.get(1), args.size() > 2 ? args.get(2) : null); + return new Match(source, List.of(args.get(0)), args.get(1), args.size() > 2 ? args.get(2) : null); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java index a9f910e60a558..99f553dca1213 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java @@ -61,7 +61,6 @@ import org.elasticsearch.xpack.esql.expression.function.aggregate.ToPartial; import org.elasticsearch.xpack.esql.expression.function.aggregate.Values; import org.elasticsearch.xpack.esql.expression.function.fulltext.Match; -import org.elasticsearch.xpack.esql.expression.function.fulltext.MultiMatch; import org.elasticsearch.xpack.esql.expression.function.grouping.Bucket; import org.elasticsearch.xpack.esql.expression.function.grouping.Categorize; import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToDouble; @@ -7491,12 +7490,12 @@ public void testFunctionNamedParamsAsFunctionArgument() { public void testFunctionNamedParamsAsFunctionArgument1() { var query = """ from test - | WHERE MULTI_MATCH(first_name, last_name, "Anna Smith", {"minimum_should_match": 2.0}) + | WHERE MATCH(first_name, last_name, "Anna Smith", {"minimum_should_match": 2.0}) """; var plan = optimizedPlan(query); Limit limit = as(plan, Limit.class); Filter filter = as(limit.child(), Filter.class); - MultiMatch match = as(filter.condition(), MultiMatch.class); + Match match = as(filter.condition(), Match.class); MapExpression me = as(match.options(), MapExpression.class); assertEquals(1, me.entryExpressions().size()); EntryExpression ee = as(me.entryExpressions().get(0), EntryExpression.class); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java index 62cb19609834a..9dda21a1fb54e 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java @@ -7729,8 +7729,11 @@ public void testScore() { Filter filter = as(limit.child(), Filter.class); Match match = as(filter.condition(), Match.class); - assertTrue(match.field() instanceof FieldAttribute); - assertEquals("first_name", ((FieldAttribute) match.field()).field().getName()); + var fields = match.fields(); + assertEquals(1, fields.size()); + var field = fields.getFirst(); + assertTrue(field instanceof FieldAttribute); + assertEquals("first_name", ((FieldAttribute) field).field().getName()); EsRelation esRelation = as(filter.child(), EsRelation.class); assertTrue(esRelation.optimized()); @@ -7761,8 +7764,11 @@ public void testScoreTopN() { Filter filter = as(topN.child(), Filter.class); Match match = as(filter.condition(), Match.class); - assertTrue(match.field() instanceof FieldAttribute); - assertEquals("first_name", ((FieldAttribute) match.field()).field().getName()); + var fields = match.fields(); + assertEquals(1, fields.size()); + var field = fields.getFirst(); + assertTrue(field instanceof FieldAttribute); + assertEquals("first_name", ((FieldAttribute) field).field().getName()); EsRelation esRelation = as(filter.child(), EsRelation.class); assertTrue(esRelation.optimized()); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineFiltersTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineFiltersTests.java index d8cae21257201..5f6b8d81a5cec 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineFiltersTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineFiltersTests.java @@ -260,11 +260,11 @@ public void testPushDownFilterPastCompletion() { LessThan conditionB = lessThanOf(getFieldAttribute("b"), TWO); Match conditionCompletion = new Match( EMPTY, - completion.targetField(), + List.of(completion.targetField()), randomLiteral(DataType.TEXT), - mock(Expression.class), - mock(QueryBuilder.class) + mock(Expression.class) ); + conditionCompletion.replaceQueryBuilder(mock(QueryBuilder.class)); Filter filterB = new Filter(EMPTY, completion, new And(EMPTY, conditionB, conditionCompletion)); LogicalPlan expectedOptimizedPlan = new Filter( diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java index 4f73b2b99d628..57606953c04c1 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java @@ -2473,7 +2473,9 @@ public void testMatchOperatorConstantQueryString() { var plan = statement("FROM test | WHERE field:\"value\""); var filter = as(plan, Filter.class); var match = (MatchOperator) filter.condition(); - var matchField = (UnresolvedAttribute) match.field(); + var fields = match.fields(); + assertEquals(1, fields.size()); + var matchField = (UnresolvedAttribute) fields.getFirst(); assertThat(matchField.name(), equalTo("field")); assertThat(match.query().fold(FoldContext.small()), equalTo("value")); } @@ -2517,7 +2519,10 @@ public void testMatchOperatorFieldCasting() { var plan = statement("FROM test | WHERE field::int : \"value\""); var filter = as(plan, Filter.class); var match = (MatchOperator) filter.condition(); - var toInteger = (ToInteger) match.field(); + var fields = match.fields(); + assertEquals(1, fields.size()); + ; + var toInteger = (ToInteger) fields.getFirst(); var matchField = (UnresolvedAttribute) toInteger.field(); assertThat(matchField.name(), equalTo("field")); assertThat(match.query().fold(FoldContext.small()), equalTo("value")); @@ -3216,80 +3221,98 @@ public void testValidFork() { var fork = as(plan, Fork.class); var subPlans = fork.children(); - // first subplan - var eval = as(subPlans.get(0), Eval.class); - assertThat(as(eval.fields().get(0), Alias.class), equalTo(alias("_fork", literalString("fork1")))); - var limit = as(eval.child(), Limit.class); - assertThat(limit.limit(), instanceOf(Literal.class)); - assertThat(((Literal) limit.limit()).value(), equalTo(11)); - var filter = as(limit.child(), Filter.class); - var match = (MatchOperator) filter.condition(); - var matchField = (UnresolvedAttribute) match.field(); - assertThat(matchField.name(), equalTo("a")); - assertThat(match.query().fold(FoldContext.small()), equalTo("baz")); - - // second subplan - eval = as(subPlans.get(1), Eval.class); - assertThat(as(eval.fields().get(0), Alias.class), equalTo(alias("_fork", literalString("fork2")))); - var orderBy = as(eval.child(), OrderBy.class); - assertThat(orderBy.order().size(), equalTo(1)); - Order order = orderBy.order().get(0); - assertThat(order.child(), instanceOf(UnresolvedAttribute.class)); - assertThat(((UnresolvedAttribute) order.child()).name(), equalTo("b")); - filter = as(orderBy.child(), Filter.class); - match = (MatchOperator) filter.condition(); - matchField = (UnresolvedAttribute) match.field(); - assertThat(matchField.name(), equalTo("b")); - assertThat(match.query().fold(FoldContext.small()), equalTo("bar")); - - // third subplan - eval = as(subPlans.get(2), Eval.class); - assertThat(as(eval.fields().get(0), Alias.class), equalTo(alias("_fork", literalString("fork3")))); - filter = as(eval.child(), Filter.class); - match = (MatchOperator) filter.condition(); - matchField = (UnresolvedAttribute) match.field(); - assertThat(matchField.name(), equalTo("c")); - assertThat(match.query().fold(FoldContext.small()), equalTo("bat")); - - // fourth subplan - eval = as(subPlans.get(3), Eval.class); - assertThat(as(eval.fields().get(0), Alias.class), equalTo(alias("_fork", literalString("fork4")))); - orderBy = as(eval.child(), OrderBy.class); - assertThat(orderBy.order().size(), equalTo(1)); - order = orderBy.order().get(0); - assertThat(order.child(), instanceOf(UnresolvedAttribute.class)); - assertThat(((UnresolvedAttribute) order.child()).name(), equalTo("c")); + { + // first subplan + var eval = as(subPlans.get(0), Eval.class); + assertThat(as(eval.fields().get(0), Alias.class), equalTo(alias("_fork", literalString("fork1")))); + var limit = as(eval.child(), Limit.class); + assertThat(limit.limit(), instanceOf(Literal.class)); + assertThat(((Literal) limit.limit()).value(), equalTo(11)); + var filter = as(limit.child(), Filter.class); + var match = (MatchOperator) filter.condition(); + var fields = match.fields(); + assertEquals(1, fields.size()); + var matchField = (UnresolvedAttribute) fields.getFirst(); + assertThat(matchField.name(), equalTo("a")); + assertThat(match.query().fold(FoldContext.small()), equalTo("baz")); + } - // fifth subplan - eval = as(subPlans.get(4), Eval.class); - assertThat(as(eval.fields().get(0), Alias.class), equalTo(alias("_fork", literalString("fork5")))); - limit = as(eval.child(), Limit.class); - assertThat(limit.limit(), instanceOf(Literal.class)); - assertThat(((Literal) limit.limit()).value(), equalTo(5)); + { + // second subplan + var eval = as(subPlans.get(1), Eval.class); + assertThat(as(eval.fields().get(0), Alias.class), equalTo(alias("_fork", literalString("fork2")))); + var orderBy = as(eval.child(), OrderBy.class); + assertThat(orderBy.order().size(), equalTo(1)); + Order order = orderBy.order().get(0); + assertThat(order.child(), instanceOf(UnresolvedAttribute.class)); + assertThat(((UnresolvedAttribute) order.child()).name(), equalTo("b")); + var filter = as(orderBy.child(), Filter.class); + var match = (MatchOperator) filter.condition(); + var fields = match.fields(); + assertEquals(1, fields.size()); + var matchField = (UnresolvedAttribute) fields.getFirst(); + assertThat(matchField.name(), equalTo("b")); + assertThat(match.query().fold(FoldContext.small()), equalTo("bar")); + } + + { + // third subplan + var eval = as(subPlans.get(2), Eval.class); + assertThat(as(eval.fields().get(0), Alias.class), equalTo(alias("_fork", literalString("fork3")))); + var filter = as(eval.child(), Filter.class); + var match = (MatchOperator) filter.condition(); + var fields = match.fields(); + assertEquals(1, fields.size()); + var matchField = (UnresolvedAttribute) fields.getFirst(); + assertThat(matchField.name(), equalTo("c")); + assertThat(match.query().fold(FoldContext.small()), equalTo("bat")); + } + + { + // fourth subplan + var eval = as(subPlans.get(3), Eval.class); + assertThat(as(eval.fields().get(0), Alias.class), equalTo(alias("_fork", literalString("fork4")))); + var orderBy = as(eval.child(), OrderBy.class); + assertThat(orderBy.order().size(), equalTo(1)); + var order = orderBy.order().get(0); + assertThat(order.child(), instanceOf(UnresolvedAttribute.class)); + assertThat(((UnresolvedAttribute) order.child()).name(), equalTo("c")); + } + + { + // fifth subplan + var eval = as(subPlans.get(4), Eval.class); + assertThat(as(eval.fields().get(0), Alias.class), equalTo(alias("_fork", literalString("fork5")))); + var limit = as(eval.child(), Limit.class); + assertThat(limit.limit(), instanceOf(Literal.class)); + assertThat(((Literal) limit.limit()).value(), equalTo(5)); + } - // sixth subplan - eval = as(subPlans.get(5), Eval.class); - assertThat(as(eval.fields().get(0), Alias.class), equalTo(alias("_fork", literalString("fork6")))); - eval = as(eval.child(), Eval.class); - assertThat(as(eval.fields().get(0), Alias.class), equalTo(alias("xyz", literalString("abc")))); - - Aggregate aggregate = as(eval.child(), Aggregate.class); - assertThat(aggregate.aggregates().size(), equalTo(2)); - var alias = as(aggregate.aggregates().get(0), Alias.class); - assertThat(alias.name(), equalTo("x")); - assertThat(as(alias.child(), UnresolvedFunction.class).name(), equalTo("MIN")); - - alias = as(aggregate.aggregates().get(1), Alias.class); - assertThat(alias.name(), equalTo("y")); - var filteredExp = as(alias.child(), FilteredExpression.class); - assertThat(as(filteredExp.delegate(), UnresolvedFunction.class).name(), equalTo("MAX")); - var greaterThan = as(filteredExp.filter(), GreaterThan.class); - assertThat(as(greaterThan.left(), UnresolvedAttribute.class).name(), equalTo("d")); - assertThat(as(greaterThan.right(), Literal.class).value(), equalTo(1000)); - - var dissect = as(aggregate.child(), Dissect.class); - assertThat(as(dissect.input(), UnresolvedAttribute.class).name(), equalTo("a")); - assertThat(dissect.parser().pattern(), equalTo("%{d} %{e} %{f}")); + { + // sixth subplan + var eval = as(subPlans.get(5), Eval.class); + assertThat(as(eval.fields().get(0), Alias.class), equalTo(alias("_fork", literalString("fork6")))); + eval = as(eval.child(), Eval.class); + assertThat(as(eval.fields().get(0), Alias.class), equalTo(alias("xyz", literalString("abc")))); + + Aggregate aggregate = as(eval.child(), Aggregate.class); + assertThat(aggregate.aggregates().size(), equalTo(2)); + var alias = as(aggregate.aggregates().get(0), Alias.class); + assertThat(alias.name(), equalTo("x")); + assertThat(as(alias.child(), UnresolvedFunction.class).name(), equalTo("MIN")); + + alias = as(aggregate.aggregates().get(1), Alias.class); + assertThat(alias.name(), equalTo("y")); + var filteredExp = as(alias.child(), FilteredExpression.class); + assertThat(as(filteredExp.delegate(), UnresolvedFunction.class).name(), equalTo("MAX")); + var greaterThan = as(filteredExp.filter(), GreaterThan.class); + assertThat(as(greaterThan.left(), UnresolvedAttribute.class).name(), equalTo("d")); + assertThat(as(greaterThan.right(), Literal.class).value(), equalTo(1000)); + + var dissect = as(aggregate.child(), Dissect.class); + assertThat(as(dissect.input(), UnresolvedAttribute.class).name(), equalTo("a")); + assertThat(dissect.parser().pattern(), equalTo("%{d} %{e} %{f}")); + } } public void testInvalidFork() { From 1e54c343bd2fc3e3adbdfb6da4fda96b6a369fb4 Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Mon, 2 Jun 2025 15:28:26 -0400 Subject: [PATCH 04/33] Fix order of args --- .../xpack/esql/expression/function/fulltext/MatchOperator.java | 3 ++- .../org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchOperator.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchOperator.java index 1da9ce7b167b4..dd80056895b6a 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchOperator.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchOperator.java @@ -78,7 +78,8 @@ protected NodeInfo info() { @Override public Expression replaceChildren(List newChildren) { - return new MatchOperator(source(), newChildren.get(0), newChildren.get(1), queryBuilder()); + // Query first, then field. + return new MatchOperator(source(), newChildren.get(1), newChildren.get(0), queryBuilder()); } @Override diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java index 7ff3671fd4255..0ec4778db4a82 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java @@ -2862,7 +2862,7 @@ public void testFunctionNamedParamsAsFunctionArgument1() { public void testFunctionNamedParamsAsFunctionArgument2() { LogicalPlan plan = analyze(""" from test - | WHERE MULTI_MATCH(first_name, last_name, "Anna Smith", {"minimum_should_match": 3.0}) + | WHERE MATCH(first_name, last_name, "Anna Smith", {"minimum_should_match": 3.0}) """); Limit limit = as(plan, Limit.class); Filter filter = as(limit.child(), Filter.class); From 94ed47dbb70f8da8c1cfdc66e3824be9628113d7 Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Mon, 2 Jun 2025 19:48:46 -0400 Subject: [PATCH 05/33] Fix one test --- .../org/elasticsearch/xpack/esql/analysis/VerifierTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java index fafc8f7612c72..6f7ad73a508d9 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java @@ -2478,7 +2478,7 @@ public void testMultiMatchInsideEval() throws Exception { assumeTrue("Match operator is available just for snapshots", Build.current().isSnapshot()); assertEquals( "1:36: [Match] function is only supported in WHERE and STATS commands\n" - + "line 1:48: [Match] function cannot operate on [title], which is not a field from an index mapping", + + "line 1:42: [Match] function cannot operate on [title], which is not a field from an index mapping", error("row title = \"brown fox\" | eval x = match(title, \"fox\")") ); } From 67133a6e362b92682aedceababf898831294093b Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Mon, 2 Jun 2025 19:53:51 -0400 Subject: [PATCH 06/33] Fix more tests --- .../main/resources/multi-match-function.csv-spec | 14 +++++++------- .../src/main/resources/scoring.csv-spec | 6 +++--- .../elasticsearch/xpack/esql/plugin/ScoringIT.java | 1 - .../optimizer/LocalPhysicalPlanOptimizerTests.java | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multi-match-function.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multi-match-function.csv-spec index b908012d73340..eaab4b1c3d315 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multi-match-function.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multi-match-function.csv-spec @@ -7,7 +7,7 @@ required_capability: multi_match_unified_function // tag::multi-match-with-field[] FROM books -| WHERE MULTI_MATCH(author, description, "Faulkner") +| WHERE MATCH(author, description, "Faulkner") | KEEP book_no, author | SORT book_no | LIMIT 5 @@ -28,7 +28,7 @@ testMultiMatchWithOptionsFuzziness required_capability: multi_match_unified_function from books -| where multi_match(title, description, "Pings", {"fuzziness": 1}) +| where match(title, description, "Pings", {"fuzziness": 1}) | keep book_no; ignoreOrder:true @@ -50,7 +50,7 @@ required_capability: multi_match_unified_function // tag::multi-match-with-named-function-params[] FROM books -| WHERE MULTI_MATCH(title, description, "Hobbit Back Again", {"operator": "AND"}) +| WHERE MATCH(title, description, "Hobbit Back Again", {"operator": "AND"}) | KEEP title; // end::multi-match-with-named-function-params[] @@ -64,7 +64,7 @@ testMultiMatchWithOptionsMinimumShouldMatch required_capability: multi_match_unified_function from books -| where multi_match(title, description, "here back again", {"minimum_should_match": 2, "operator": "OR"}) +| where match(title, description, "here back again", {"minimum_should_match": 2, "operator": "OR"}) | sort book_no | keep title; @@ -78,7 +78,7 @@ required_capability: multi_match_unified_function required_capability: full_text_functions_disjunctions_compute_engine from books -| where multi_match(title, description, "lord") or length(title) > 130 +| where match(title, description, "lord") or length(title) > 130 | keep book_no ; ignoreOrder: true @@ -100,7 +100,7 @@ testMultiMatchPhraseQuery required_capability: multi_match_unified_function from books -| where multi_match(title, description, "Lord of the rings", { "type": "phrase" } ) +| where match(title, description, "Lord of the rings", { "type": "phrase" } ) | keep title ; ignoreOrder: true @@ -120,7 +120,7 @@ testMultiMatchPhrasePrefixQuery required_capability: multi_match_unified_function from books -| where multi_match(title, "Lord of the ri", { "type": "phrase_prefix" } ) +| where match(title, "Lord of the ri", { "type": "phrase_prefix" } ) | keep title ; ignoreOrder: true diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec index 5b9288416a1c7..5e9233898cecd 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec @@ -119,7 +119,7 @@ required_capability: multi_match_unified_function required_capability: metadata_score from books metadata _score -| where multi_match(author, title, "Mark", {"fuzziness": 1}) +| where match(author, title, "Mark", {"fuzziness": 1}) | keep book_no, title, author, _score; ignoreOrder:true @@ -132,7 +132,7 @@ required_capability: multi_match_unified_function required_capability: metadata_score from books metadata _score -| where multi_match(description, title, "Hobbit", {"type": "best_fields"}) +| where match(description, title, "Hobbit", {"type": "best_fields"}) | sort book_no | eval _score = round(_score) | keep book_no, _score; @@ -159,7 +159,7 @@ required_capability: multi_match_unified_function required_capability: metadata_score from books metadata _score -| where multi_match(description, title, "Hobbit", {"type": "most_fields"}) +| where match(description, title, "Hobbit", {"type": "most_fields"}) | sort book_no | eval _score = round(_score) | keep book_no, _score; diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/ScoringIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/ScoringIT.java index 66d3f12840cdd..4a76c7de81200 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/ScoringIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/ScoringIT.java @@ -46,7 +46,6 @@ protected Collection> nodePlugins() { public static List params() { List params = new ArrayList<>(); params.add(new Object[] { "match(content, \"fox\")" }); - params.add(new Object[] { "multi_match(content, \"fox\", {\"operator\": \"AND\"})" }); params.add(new Object[] { "content:\"fox\"" }); params.add(new Object[] { "qstr(\"content: fox\")" }); params.add(new Object[] { "kql(\"content*: fox\")" }); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java index bbfaa941a9342..38ae1f313bf28 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java @@ -1317,7 +1317,7 @@ public void testQStrOptionsPushDown() { public void testMultiMatchOptionsPushDown() { String query = """ from test - | where MULTI_MATCH(first_name, last_name, "Anna", {"fuzzy_rewrite": "constant_score", "slop": 10, "analyzer": "auto", + | where MATCH(first_name, last_name, "Anna", {"fuzzy_rewrite": "constant_score", "slop": 10, "analyzer": "auto", "auto_generate_synonyms_phrase_query": "false", "fuzziness": "auto", "fuzzy_transpositions": false, "lenient": "false", "max_expansions": 10, "minimum_should_match": 3, "operator": "AND", "prefix_length": 20, "tie_breaker": 1.0, "type": "best_fields", "boost": 2.0}) From 9a0636a2dc6184288f7720aa95b4b004a72b6a84 Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Tue, 3 Jun 2025 11:50:19 -0400 Subject: [PATCH 07/33] Merge --- .../expression/function/fulltext/Match.java | 87 +++++++++++++++---- .../xpack/esql/analysis/VerifierTests.java | 32 +------ .../LocalPhysicalPlanOptimizerTests.java | 1 - 3 files changed, 75 insertions(+), 45 deletions(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java index 7cd74fadab9cd..e19a684d6fc79 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.esql.expression.function.fulltext; +import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -18,6 +19,7 @@ import org.elasticsearch.xpack.esql.core.InvalidArgumentException; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; +import org.elasticsearch.xpack.esql.core.expression.FoldContext; import org.elasticsearch.xpack.esql.core.expression.MapExpression; import org.elasticsearch.xpack.esql.core.querydsl.query.Query; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; @@ -25,6 +27,7 @@ import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.core.type.MultiTypeEsField; import org.elasticsearch.xpack.esql.core.util.Check; +import org.elasticsearch.xpack.esql.core.util.NumericUtils; import org.elasticsearch.xpack.esql.expression.function.Example; import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesTo; import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesToLifecycle; @@ -37,6 +40,7 @@ import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; import org.elasticsearch.xpack.esql.planner.TranslatorHandler; import org.elasticsearch.xpack.esql.querydsl.query.MultiMatchQuery; +import org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter; import java.io.IOException; import java.util.HashMap; @@ -82,9 +86,10 @@ import static org.elasticsearch.xpack.esql.core.type.DataType.TEXT; import static org.elasticsearch.xpack.esql.core.type.DataType.UNSIGNED_LONG; import static org.elasticsearch.xpack.esql.core.type.DataType.VERSION; +import static org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.EsqlBinaryComparison.formatIncompatibleTypesMessage; /** - * Full text function that performs a {@link org.elasticsearch.xpack.esql.querydsl.query.MultiMatchQuery} . + * Full text function that performs a {@link org.elasticsearch.xpack.esql.querydsl.query.MatchQuery} . */ public class Match extends FullTextFunction implements OptionalArgument, PostAnalysisPlanVerificationAware { @@ -310,8 +315,9 @@ private static Match readFrom(StreamInput in) throws IOException { return new Match(source, fields, query, null, queryBuilder); } + // This is not meant to be overriden by MatchOperator - MatchOperator should be serialized to Match @Override - public void writeTo(StreamOutput out) throws IOException { + public final void writeTo(StreamOutput out) throws IOException { source().writeTo(out); out.writeNamedWriteable(query()); out.writeNamedWriteableCollection(fields); @@ -320,7 +326,7 @@ public void writeTo(StreamOutput out) throws IOException { @Override protected TypeResolution resolveParams() { - return resolveFields().and(resolveQuery()).and(resolveOptions()); + return resolveFields().and(resolveQuery()).and(resolveOptions()).and(checkParamCompatibility()); } private TypeResolution resolveFields() { @@ -350,12 +356,26 @@ private TypeResolution resolveQuery() { ).and(isNotNullAndFoldable(query(), sourceText(), SECOND)); } - public List fields() { - return fields; - } + private TypeResolution checkParamCompatibility() { + DataType queryType = query().dataType(); - public Expression options() { - return options; + return fields.stream().map((Expression field) -> { + DataType fieldType = field.dataType(); + + // Field and query types should match. If the query is a string, then it can match any field type. + if ((fieldType == queryType) || (queryType == KEYWORD)) { + return TypeResolution.TYPE_RESOLVED; + } + + if (fieldType.isNumeric() && queryType.isNumeric()) { + // When doing an unsigned long query, field must be an unsigned long + if ((queryType == UNSIGNED_LONG && fieldType != UNSIGNED_LONG) == false) { + return TypeResolution.TYPE_RESOLVED; + } + } + + return new TypeResolution(formatIncompatibleTypesMessage(fieldType, queryType, sourceText())); + }).reduce(TypeResolution::and).orElse(null); } @Override @@ -395,6 +415,14 @@ private Map matchQueryOptions() throws InvalidArgumentException return options; } + public List fields() { + return fields; + } + + public Expression options() { + return options; + } + @Override protected NodeInfo info() { // Specifically create new instance with original arguments. @@ -427,8 +455,9 @@ public Expression replaceQueryBuilder(QueryBuilder queryBuilder) { @Override public BiConsumer postAnalysisPlanVerification() { return (plan, failures) -> { + // TODO: fix this. super.postAnalysisPlanVerification().accept(plan, failures); - plan.forEachExpression(Match.class, mm -> { + plan.forEachExpression(Match.class, match -> { for (Expression field : fields) { if (fieldAsFieldAttribute(field) == null) { failures.add( @@ -446,12 +475,38 @@ public BiConsumer postAnalysisPlanVerification() { }; } + @Override + public Object queryAsObject() { + Object queryAsObject = query().fold(FoldContext.small() /* TODO remove me */); + + // Convert BytesRef to string for string-based values + if (queryAsObject instanceof BytesRef bytesRef) { + return switch (query().dataType()) { + case IP -> EsqlDataTypeConverter.ipToString(bytesRef); + case VERSION -> EsqlDataTypeConverter.versionToString(bytesRef); + default -> bytesRef.utf8ToString(); + }; + } + + // Converts specific types to the correct type for the query + if (query().dataType() == DataType.UNSIGNED_LONG) { + return NumericUtils.unsignedLongAsBigInteger((Long) queryAsObject); + } else if (query().dataType() == DataType.DATETIME && queryAsObject instanceof Long) { + // When casting to date and datetime, we get a long back. But Match query needs a date string + return EsqlDataTypeConverter.dateTimeToString((Long) queryAsObject); + } else if (query().dataType() == DATE_NANOS && queryAsObject instanceof Long) { + return EsqlDataTypeConverter.nanoTimeToString((Long) queryAsObject); + } + + return queryAsObject; + } + @Override protected Query translate(TranslatorHandler handler) { Map fieldsWithBoost = new HashMap<>(); for (Expression field : fields) { var fieldAttribute = fieldAsFieldAttribute(field); - Check.notNull(fieldAttribute, "MultiMatch must have field attributes as arguments #1 to #N-1."); + Check.notNull(fieldAttribute, "Match must have field attributes as arguments #1 to #N-1."); String fieldName = getNameFromFieldAttribute(fieldAttribute); fieldsWithBoost.put(fieldName, 1.0f); } @@ -478,13 +533,13 @@ public static FieldAttribute fieldAsFieldAttribute(Expression field) { @Override public boolean equals(Object o) { - // MultiMatch does not serialize options, as they get included in the query builder. We need to override equals and hashcode to - // ignore options when comparing two MultiMatch functions + // Match does not serialize options, as they get included in the query builder. We need to override equals and hashcode to + // ignore options when comparing two Match functions if (o == null || getClass() != o.getClass()) return false; - Match mm = (Match) o; - return Objects.equals(fields(), mm.fields()) - && Objects.equals(query(), mm.query()) - && Objects.equals(queryBuilder(), mm.queryBuilder()); + Match match = (Match) o; + return Objects.equals(fields(), match.fields()) + && Objects.equals(query(), match.query()) + && Objects.equals(queryBuilder(), match.queryBuilder()); } @Override diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java index 05a337cd7923b..1ce300e9ca022 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java @@ -1215,16 +1215,12 @@ public void testMatchInsideEval() throws Exception { } public void testFieldBasedFullTextFunctions() throws Exception { - checkFieldBasedWithNonIndexedColumn("MATCH", "match(text, \"cat\")", "function"); - checkFieldBasedFunctionNotAllowedAfterCommands("MATCH", "function", "match(title, \"Meditation\")"); + checkFieldBasedWithNonIndexedColumn("Match", "match(text, \"cat\")", "function"); + checkFieldBasedFunctionNotAllowedAfterCommands("Match", "function", "match(title, \"Meditation\")"); checkFieldBasedWithNonIndexedColumn(":", "text : \"cat\"", "operator"); checkFieldBasedFunctionNotAllowedAfterCommands(":", "operator", "title : \"Meditation\""); - if (EsqlCapabilities.Cap.MULTI_MATCH_FUNCTION.isEnabled()) { - checkFieldBasedWithNonIndexedColumn("MultiMatch", "multi_match(\"cat\", text)", "function"); - checkFieldBasedFunctionNotAllowedAfterCommands("MultiMatch", "function", "multi_match(\"Meditation\", title)"); - } if (EsqlCapabilities.Cap.TERM_FUNCTION.isEnabled()) { checkFieldBasedWithNonIndexedColumn("Term", "term(text, \"cat\")", "function"); checkFieldBasedFunctionNotAllowedAfterCommands("Term", "function", "term(title, \"Meditation\")"); @@ -1348,16 +1344,13 @@ private void checkNonFieldBasedFullTextFunctionsNotAllowedAfterCommands(String f } public void testFullTextFunctionsOnlyAllowedInWhere() throws Exception { - checkFullTextFunctionsOnlyAllowedInWhere("MATCH", "match(title, \"Meditation\")", "function"); + checkFullTextFunctionsOnlyAllowedInWhere("Match", "match(title, \"Meditation\")", "function"); checkFullTextFunctionsOnlyAllowedInWhere(":", "title:\"Meditation\"", "operator"); checkFullTextFunctionsOnlyAllowedInWhere("QSTR", "qstr(\"Meditation\")", "function"); checkFullTextFunctionsOnlyAllowedInWhere("KQL", "kql(\"Meditation\")", "function"); if (EsqlCapabilities.Cap.TERM_FUNCTION.isEnabled()) { checkFullTextFunctionsOnlyAllowedInWhere("Term", "term(title, \"Meditation\")", "function"); } - if (EsqlCapabilities.Cap.MULTI_MATCH_FUNCTION.isEnabled()) { - checkFullTextFunctionsOnlyAllowedInWhere("MultiMatch", "multi_match(\"Meditation\", title, body)", "function"); - } } public void testMatchFunctionOnlyAllowedInWhere() throws Exception { @@ -1522,9 +1515,6 @@ private void checkFullTextFunctionsWithNonBooleanFunctions(String functionName, public void testFullTextFunctionsTargetsExistingField() throws Exception { testFullTextFunctionTargetsExistingField("match(title, \"Meditation\")"); testFullTextFunctionTargetsExistingField("title : \"Meditation\""); - if (EsqlCapabilities.Cap.MULTI_MATCH_FUNCTION.isEnabled()) { - testFullTextFunctionTargetsExistingField("multi_match(\"Meditation\", title)"); - } if (EsqlCapabilities.Cap.TERM_FUNCTION.isEnabled()) { testFullTextFunctionTargetsExistingField("term(fist_name, \"Meditation\")"); } @@ -2047,11 +2037,8 @@ public void testLookupJoinDataTypeMismatch() { } public void testFullTextFunctionOptions() { - checkOptionDataTypes(Match.ALLOWED_OPTIONS, "FROM test | WHERE match(title, \"Jean\", {\"%s\": %s})"); + checkOptionDataTypes(Match.OPTIONS, "FROM test | WHERE match(title, \"Jean\", {\"%s\": %s})"); checkOptionDataTypes(QueryString.ALLOWED_OPTIONS, "FROM test | WHERE QSTR(\"title: Jean\", {\"%s\": %s})"); - if (EsqlCapabilities.Cap.MULTI_MATCH_FUNCTION.isEnabled()) { - checkOptionDataTypes(MultiMatch.OPTIONS, "FROM test | WHERE MULTI_MATCH(\"Jean\", title, body, {\"%s\": %s})"); - } } /** @@ -2106,9 +2093,6 @@ private static String exampleValueForType(DataType currentType) { public void testFullTextFunctionCurrentlyUnsupportedBehaviour() throws Exception { testFullTextFunctionsCurrentlyUnsupportedBehaviour("match(title, \"Meditation\")"); testFullTextFunctionsCurrentlyUnsupportedBehaviour("title : \"Meditation\""); - if (EsqlCapabilities.Cap.MULTI_MATCH_FUNCTION.isEnabled()) { - testFullTextFunctionsCurrentlyUnsupportedBehaviour("multi_match(\"Meditation\", title)"); - } if (EsqlCapabilities.Cap.TERM_FUNCTION.isEnabled()) { testFullTextFunctionsCurrentlyUnsupportedBehaviour("term(title, \"Meditation\")"); } @@ -2126,10 +2110,6 @@ public void testFullTextFunctionsNullArgs() throws Exception { checkFullTextFunctionNullArgs("match(title, null)", "second"); checkFullTextFunctionNullArgs("qstr(null)", ""); checkFullTextFunctionNullArgs("kql(null)", ""); - if (EsqlCapabilities.Cap.MULTI_MATCH_FUNCTION.isEnabled()) { - checkFullTextFunctionNullArgs("multi_match(null, title)", "first"); - checkFullTextFunctionNullArgs("multi_match(\"query\", null)", "second"); - } if (EsqlCapabilities.Cap.TERM_FUNCTION.isEnabled()) { checkFullTextFunctionNullArgs("term(null, \"query\")", "first"); checkFullTextFunctionNullArgs("term(title, null)", "second"); @@ -2147,10 +2127,6 @@ public void testFullTextFunctionsConstantQuery() throws Exception { checkFullTextFunctionsConstantQuery("match(title, category)", "second"); checkFullTextFunctionsConstantQuery("qstr(title)", ""); checkFullTextFunctionsConstantQuery("kql(title)", ""); - if (EsqlCapabilities.Cap.MULTI_MATCH_FUNCTION.isEnabled()) { - checkFullTextFunctionsConstantQuery("multi_match(category, body)", "first"); - checkFullTextFunctionsConstantQuery("multi_match(concat(title, \"world\"), title)", "first"); - } if (EsqlCapabilities.Cap.TERM_FUNCTION.isEnabled()) { checkFullTextFunctionsConstantQuery("term(title, tags)", "second"); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java index 38ae1f313bf28..324fe9020138b 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java @@ -1262,7 +1262,6 @@ public void testMatchOptionsPushDown() { var fieldExtract = as(project.child(), FieldExtractExec.class); var actualLuceneQuery = as(fieldExtract.child(), EsQueryExec.class).query(); - Source filterSource = new Source(4, 8, "emp_no > 10000"); var expectedLuceneQuery = new MatchQueryBuilder("first_name", "Anna").fuzziness(Fuzziness.AUTO) .prefixLength(3) .maxExpansions(10) From 23a2474487a4129ca552268239e728eb5cf572a0 Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Tue, 3 Jun 2025 11:51:56 -0400 Subject: [PATCH 08/33] delete --- .../xpack/esql/expression/function/fulltext/MultiMatch.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatch.java diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatch.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MultiMatch.java deleted file mode 100644 index e69de29bb2d1d..0000000000000 From bd93341b9728f395f59c78643e6c707f71eeb692 Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Tue, 3 Jun 2025 15:26:06 -0400 Subject: [PATCH 09/33] Make private --- .../xpack/esql/expression/function/fulltext/Match.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java index e19a684d6fc79..502f3379a1791 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java @@ -455,7 +455,6 @@ public Expression replaceQueryBuilder(QueryBuilder queryBuilder) { @Override public BiConsumer postAnalysisPlanVerification() { return (plan, failures) -> { - // TODO: fix this. super.postAnalysisPlanVerification().accept(plan, failures); plan.forEachExpression(Match.class, match -> { for (Expression field : fields) { @@ -513,7 +512,7 @@ protected Query translate(TranslatorHandler handler) { return new MultiMatchQuery(source(), Objects.toString(queryAsObject()), fieldsWithBoost, matchQueryOptions()); } - public static String getNameFromFieldAttribute(FieldAttribute fieldAttribute) { + private static String getNameFromFieldAttribute(FieldAttribute fieldAttribute) { String fieldName = fieldAttribute.name(); if (fieldAttribute.field() instanceof MultiTypeEsField multiTypeEsField) { // If we have multiple field types, we allow the query to be done, but getting the underlying field name @@ -522,7 +521,7 @@ public static String getNameFromFieldAttribute(FieldAttribute fieldAttribute) { return fieldName; } - public static FieldAttribute fieldAsFieldAttribute(Expression field) { + private static FieldAttribute fieldAsFieldAttribute(Expression field) { Expression fieldExpression = field; // Field may be converted to other data type (field_name :: data_type), so we need to check the original field if (fieldExpression instanceof AbstractConvertFunction convertFunction) { From 90530e7e1a37985194fee6e7113860e3bb501846 Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Tue, 3 Jun 2025 15:31:50 -0400 Subject: [PATCH 10/33] Make sure we verify type on a present column to get expected result --- .../elasticsearch/xpack/esql/analysis/VerifierTests.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java index 1ce300e9ca022..fa1df5833f1e9 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java @@ -1354,16 +1354,16 @@ public void testFullTextFunctionsOnlyAllowedInWhere() throws Exception { } public void testMatchFunctionOnlyAllowedInWhere() throws Exception { - checkFullTextFunctionsOnlyAllowedInWhere("Match", "match(first_name, \"Anna\")", "function"); + checkFullTextFunctionsOnlyAllowedInWhere("Match", "match(title, \"Anna\")", "function"); } public void testTermFunctionOnlyAllowedInWhere() throws Exception { assumeTrue("term function capability not available", EsqlCapabilities.Cap.TERM_FUNCTION.isEnabled()); - checkFullTextFunctionsOnlyAllowedInWhere("Term", "term(first_name, \"Anna\")", "function"); + checkFullTextFunctionsOnlyAllowedInWhere("Term", "term(title, \"Anna\")", "function"); } public void testMatchOperatornOnlyAllowedInWhere() throws Exception { - checkFullTextFunctionsOnlyAllowedInWhere(":", "first_name:\"Anna\"", "operator"); + checkFullTextFunctionsOnlyAllowedInWhere(":", "title:\"Anna\"", "operator"); } private void checkFullTextFunctionsOnlyAllowedInWhere(String functionName, String functionInvocation, String functionType) From 68d732a93b73e914a741c8ffa22be5ae537d6bd3 Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Thu, 5 Jun 2025 15:36:15 -0400 Subject: [PATCH 11/33] fix translation --- .../esql/expression/function/fulltext/Match.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java index 502f3379a1791..51dfdfc8387fc 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java @@ -39,6 +39,7 @@ import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; import org.elasticsearch.xpack.esql.planner.TranslatorHandler; +import org.elasticsearch.xpack.esql.querydsl.query.MatchQuery; import org.elasticsearch.xpack.esql.querydsl.query.MultiMatchQuery; import org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter; @@ -509,7 +510,15 @@ protected Query translate(TranslatorHandler handler) { String fieldName = getNameFromFieldAttribute(fieldAttribute); fieldsWithBoost.put(fieldName, 1.0f); } - return new MultiMatchQuery(source(), Objects.toString(queryAsObject()), fieldsWithBoost, matchQueryOptions()); + + // TODO: check if we have multi_match specific options, like "type". + if (fieldsWithBoost.size() == 1) { + // Translate to Match when having exactly one field. + return new MatchQuery(source(), fieldsWithBoost.keySet().stream().findFirst().get(), queryAsObject(), matchQueryOptions()); + } else { + // For 0 or 2+ fields, translate to multi_match. + return new MultiMatchQuery(source(), Objects.toString(queryAsObject()), fieldsWithBoost, matchQueryOptions()); + } } private static String getNameFromFieldAttribute(FieldAttribute fieldAttribute) { From 68e9b89be2a4ebbebf8dc1353c7d8d0c4dfdf792 Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Thu, 5 Jun 2025 18:38:53 -0400 Subject: [PATCH 12/33] more work on translation --- .../expression/function/fulltext/Match.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java index 51dfdfc8387fc..b9875229cff7e 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java @@ -138,6 +138,12 @@ public class Match extends FullTextFunction implements OptionalArgument, PostAna entry(ZERO_TERMS_QUERY_FIELD.getPreferredName(), KEYWORD) ); + private static final Set MULTIMATCH_SPECIFIC_OPTIONS = Set.of( + SLOP_FIELD.getPreferredName(), + TIE_BREAKER_FIELD.getPreferredName(), + TYPE_FIELD.getPreferredName() + ); + // TODO: update descriptions and comments. @FunctionInfo( returnType = "boolean", @@ -511,13 +517,13 @@ protected Query translate(TranslatorHandler handler) { fieldsWithBoost.put(fieldName, 1.0f); } - // TODO: check if we have multi_match specific options, like "type". - if (fieldsWithBoost.size() == 1) { - // Translate to Match when having exactly one field. - return new MatchQuery(source(), fieldsWithBoost.keySet().stream().findFirst().get(), queryAsObject(), matchQueryOptions()); + var options = matchQueryOptions(); + if (fieldsWithBoost.size() != 1 || options.keySet().stream().anyMatch(MULTIMATCH_SPECIFIC_OPTIONS::contains)) { + // For 0 or 2+ fields, or with multimatch-specific options, translate to multi_match. + return new MultiMatchQuery(source(), Objects.toString(queryAsObject()), fieldsWithBoost, options); } else { - // For 0 or 2+ fields, translate to multi_match. - return new MultiMatchQuery(source(), Objects.toString(queryAsObject()), fieldsWithBoost, matchQueryOptions()); + // Translate to Match when having exactly one field. + return new MatchQuery(source(), fieldsWithBoost.keySet().stream().findFirst().get(), queryAsObject(), options); } } From d488f5865632c2fca60367a4d8647690bdf0c0b6 Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Mon, 9 Jun 2025 14:06:26 -0400 Subject: [PATCH 13/33] remove MatchErrorTests --- .../function/fulltext/MatchErrorTests.java | 93 ------------------- 1 file changed, 93 deletions(-) delete mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchErrorTests.java diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchErrorTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchErrorTests.java deleted file mode 100644 index 2277ad01e0c95..0000000000000 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/MatchErrorTests.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.esql.expression.function.fulltext; - -import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.xpack.esql.core.expression.Expression; -import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; -import org.elasticsearch.xpack.esql.core.expression.TypeResolutions; -import org.elasticsearch.xpack.esql.core.tree.Source; -import org.elasticsearch.xpack.esql.core.type.DataType; -import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase; -import org.elasticsearch.xpack.esql.expression.function.ErrorsForCasesWithoutExamplesTestCase; -import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; -import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.EsqlBinaryComparison; -import org.elasticsearch.xpack.esql.optimizer.rules.physical.local.LucenePushdownPredicates; -import org.hamcrest.Matcher; - -import java.util.List; -import java.util.Locale; -import java.util.Set; - -import static org.elasticsearch.common.logging.LoggerMessageFormat.format; -import static org.elasticsearch.xpack.esql.planner.TranslatorHandler.TRANSLATOR_HANDLER; -import static org.hamcrest.Matchers.equalTo; - -public class MatchErrorTests extends ErrorsForCasesWithoutExamplesTestCase { - - @Override - protected List cases() { - return paramsToSuppliers(MatchTests.parameters()); - } - - @Override - protected Expression build(Source source, List args) { - Match match = new Match(source, List.of(args.get(0)), args.get(1), args.size() > 2 ? args.get(2) : null); - // We need to add the QueryBuilder to the match expression, as it is used to implement equals() and hashCode() and - // thus test the serialization methods. But we can only do this if the parameters make sense . - if (args.get(0) instanceof FieldAttribute && args.get(1).foldable()) { - QueryBuilder queryBuilder = TRANSLATOR_HANDLER.asQuery(LucenePushdownPredicates.DEFAULT, match).toQueryBuilder(); - match.replaceQueryBuilder(queryBuilder); - } - return match; - } - - @Override - protected Matcher expectedTypeErrorMatcher(List> validPerPosition, List signature) { - return equalTo( - errorMessageStringForMatch(validPerPosition, signature, (l, p) -> p == 0 ? FIELD_TYPE_ERROR_STRING : QUERY_TYPE_ERROR_STRING) - ); - } - - private static String errorMessageStringForMatch( - List> validPerPosition, - List signature, - AbstractFunctionTestCase.PositionalErrorMessageSupplier positionalErrorMessageSupplier - ) { - boolean invalid = false; - for (int i = 0; i < signature.size() && invalid == false; i++) { - // Need to check for nulls and bad parameters in order - if (signature.get(i) == DataType.NULL) { - return TypeResolutions.ParamOrdinal.fromIndex(i).name().toLowerCase(Locale.ROOT) - + " argument of [" - + sourceForSignature(signature) - + "] cannot be null, received []"; - } - if (validPerPosition.get(i).contains(signature.get(i)) == false) { - // Map expressions have different error messages - if (i == 2) { - return format(null, "third argument of [{}] must be a map expression, received []", sourceForSignature(signature)); - } - break; - } - } - - try { - return typeErrorMessage(true, validPerPosition, signature, positionalErrorMessageSupplier); - } catch (IllegalStateException e) { - // This means all the positional args were okay, so the expected error is for nulls or from the combination - return EsqlBinaryComparison.formatIncompatibleTypesMessage(signature.get(0), signature.get(1), sourceForSignature(signature)); - } - } - - private static final String FIELD_TYPE_ERROR_STRING = - "keyword, text, boolean, date, date_nanos, double, integer, ip, long, unsigned_long, version"; - - private static final String QUERY_TYPE_ERROR_STRING = - "keyword, boolean, date, date_nanos, double, integer, ip, long, unsigned_long, version"; -} From b922fa938e7b3777e1f28fa77a6d56c76a4f4eaf Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Mon, 9 Jun 2025 15:22:09 -0400 Subject: [PATCH 14/33] Update test --- .../xpack/esql/plugin/MatchFunctionIT.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchFunctionIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchFunctionIT.java index 23958fcd35f30..b77b2b5358b87 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchFunctionIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchFunctionIT.java @@ -87,7 +87,7 @@ public void testMultipleWhereMatch() { """; var error = expectThrows(ElasticsearchException.class, () -> run(query)); - assertThat(error.getMessage(), containsString("[MATCH] function cannot be used after LIMIT")); + assertThat(error.getMessage(), containsString("[Match] function cannot be used after LIMIT")); } public void testNotWhereMatch() { @@ -190,7 +190,7 @@ public void testWhereMatchEvalColumn() { var error = expectThrows(VerificationException.class, () -> run(query)); assertThat( error.getMessage(), - containsString("[MATCH] function cannot operate on [upper_content], which is not a field from an index mapping") + containsString("[Match] function cannot operate on [upper_content], which is not a field from an index mapping") ); } @@ -205,7 +205,7 @@ public void testWhereMatchOverWrittenColumn() { var error = expectThrows(VerificationException.class, () -> run(query)); assertThat( error.getMessage(), - containsString("[MATCH] function cannot operate on [content], which is not a field from an index mapping") + containsString("[Match] function cannot operate on [content], which is not a field from an index mapping") ); } @@ -244,7 +244,7 @@ public void testWhereMatchWithRow() { var error = expectThrows(ElasticsearchException.class, () -> run(query)); assertThat( error.getMessage(), - containsString("line 2:15: [MATCH] function cannot operate on [content], which is not a field from an index mapping") + containsString("line 2:15: [Match] function cannot operate on [content], which is not a field from an index mapping") ); } @@ -255,7 +255,7 @@ public void testMatchWithStats() { """; var error = expectThrows(ElasticsearchException.class, () -> run(errorQuery)); - assertThat(error.getMessage(), containsString("[MATCH] function is only supported in WHERE and STATS commands")); + assertThat(error.getMessage(), containsString("[Match] function is only supported in WHERE and STATS commands")); var query = """ FROM test @@ -291,7 +291,7 @@ public void testMatchWithinEval() { """; var error = expectThrows(VerificationException.class, () -> run(query)); - assertThat(error.getMessage(), containsString("[MATCH] function is only supported in WHERE and STATS commands")); + assertThat(error.getMessage(), containsString("[Match] function is only supported in WHERE and STATS commands")); } private void createAndPopulateIndex() { From 3a6ec183925dd392e2866f4f1b715ad3ce9a7948 Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Mon, 9 Jun 2025 17:11:49 -0400 Subject: [PATCH 15/33] Fix merge --- .../expression/function/fulltext/Match.java | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java index 6e01de2b5e6f5..188468f337f22 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java @@ -341,7 +341,7 @@ public final void writeTo(StreamOutput out) throws IOException { @Override protected TypeResolution resolveParams() { - return resolveFields().and(resolveQuery()).and(resolveOptions()).and(checkParamCompatibility()); + return resolveFields().and(resolveQuery()).and(resolveOptions(options(), THIRD)).and(checkParamCompatibility()); } private TypeResolution resolveFields() { @@ -519,24 +519,6 @@ protected Query translate(TranslatorHandler handler) { } } - private static String getNameFromFieldAttribute(FieldAttribute fieldAttribute) { - String fieldName = fieldAttribute.name(); - if (fieldAttribute.field() instanceof MultiTypeEsField multiTypeEsField) { - // If we have multiple field types, we allow the query to be done, but getting the underlying field name - fieldName = multiTypeEsField.getName(); - } - return fieldName; - } - - private static FieldAttribute fieldAsFieldAttribute(Expression field) { - Expression fieldExpression = field; - // Field may be converted to other data type (field_name :: data_type), so we need to check the original field - if (fieldExpression instanceof AbstractConvertFunction convertFunction) { - fieldExpression = convertFunction.field(); - } - return fieldExpression instanceof FieldAttribute fieldAttribute ? fieldAttribute : null; - } - @Override public boolean equals(Object o) { // Match does not serialize options, as they get included in the query builder. We need to override equals and hashcode to From 6934dbdb15b8c2288baa18a5ebcc29566a244c5f Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Mon, 9 Jun 2025 21:25:31 +0000 Subject: [PATCH 16/33] [CI] Auto commit changes from spotless --- .../xpack/esql/expression/function/fulltext/Match.java | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java index 188468f337f22..43ba15ffc9c34 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java @@ -18,7 +18,6 @@ import org.elasticsearch.xpack.esql.common.Failures; import org.elasticsearch.xpack.esql.core.InvalidArgumentException; import org.elasticsearch.xpack.esql.core.expression.Expression; -import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; import org.elasticsearch.xpack.esql.core.expression.FoldContext; import org.elasticsearch.xpack.esql.core.expression.MapExpression; import org.elasticsearch.xpack.esql.core.querydsl.query.Query; From 7c34deae1e34235265b4a3bce842fe3037fd6a36 Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Tue, 10 Jun 2025 09:54:06 -0400 Subject: [PATCH 17/33] Add comment --- .../xpack/esql/querydsl/query/MultiMatchQuery.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQuery.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQuery.java index 5526283dfe5a0..2218536a9db5f 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQuery.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQuery.java @@ -73,6 +73,9 @@ public MultiMatchQuery(Source source, String query, Map fields, M @Override protected QueryBuilder asBuilder() { + // TODO: create a new builder, group fields by analyzer (combined_fields query), separate groups are combined with dis_max query. + // TODO: needs to happen on shard level. + final MultiMatchQueryBuilder queryBuilder = QueryBuilders.multiMatchQuery(query); queryBuilder.fields(fields); options.forEach((k, v) -> { From 959582a2d20222c56cfae135a19acf16503f120d Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Thu, 12 Jun 2025 10:20:09 -0400 Subject: [PATCH 18/33] Initial version of multi-field match query builder --- .../query/MultiFieldMatchQueryBuilder.java | 455 ++++++++++++++++++ .../index/query/QueryBuilders.java | 8 + .../esql/querydsl/query/MultiMatchQuery.java | 45 +- 3 files changed, 477 insertions(+), 31 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java diff --git a/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java new file mode 100644 index 0000000000000..7e6699b252e40 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java @@ -0,0 +1,455 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.index.query; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.index.Term; +import org.apache.lucene.sandbox.search.CombinedFieldQuery; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.BoostAttribute; +import org.apache.lucene.search.BoostQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.similarities.BM25Similarity; +import org.apache.lucene.search.similarities.Similarity; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.QueryBuilder; +import org.elasticsearch.TransportVersion; +import org.elasticsearch.TransportVersions; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.lucene.search.Queries; +import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.mapper.TextFieldMapper; +import org.elasticsearch.index.mapper.TextSearchInfo; +import org.elasticsearch.index.search.QueryParserHelper; +import org.elasticsearch.lucene.analysis.miscellaneous.DisableGraphAttribute; +import org.elasticsearch.lucene.similarity.LegacyBM25Similarity; +import org.elasticsearch.xcontent.ConstructingObjectParser; +import org.elasticsearch.xcontent.ObjectParser; +import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; + +/** + * A query builder similar to MultiMatchQueryBuilder, but not exposed for external use, and translating to a combined query. + */ +public class MultiFieldMatchQueryBuilder extends AbstractQueryBuilder { + + public static final String NAME = "multi_field_match"; + + public static final ParseField QUERY_FIELD = new ParseField("query"); + public static final ParseField FIELDS_FIELD = new ParseField("fields"); + public static final ParseField OPERATOR_FIELD = new ParseField("operator"); + public static final ParseField MINIMUM_SHOULD_MATCH_FIELD = new ParseField("minimum_should_match"); + public static final ParseField GENERATE_SYNONYMS_PHRASE_QUERY = new ParseField("auto_generate_synonyms_phrase_query"); + public static final ParseField ZERO_TERMS_QUERY_FIELD = new ParseField("zero_terms_query"); + + private static final Operator DEFAULT_OPERATOR = Operator.OR; + private static final ZeroTermsQueryOption DEFAULT_ZERO_TERMS_QUERY = ZeroTermsQueryOption.NONE; + private static final boolean DEFAULT_GENERATE_SYNONYMS_PHRASE = true; + + private final Object value; + private final Map fieldsAndBoosts; + private Operator operator = DEFAULT_OPERATOR; + private String minimumShouldMatch; + private ZeroTermsQueryOption zeroTermsQuery = DEFAULT_ZERO_TERMS_QUERY; + private boolean autoGenerateSynonymsPhraseQuery = DEFAULT_GENERATE_SYNONYMS_PHRASE; + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + NAME, + a -> new MultiFieldMatchQueryBuilder(a[0]) + ); + + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), QUERY_FIELD); + PARSER.declareStringArray((builder, values) -> { + Map fieldsAndBoosts = QueryParserHelper.parseFieldsAndWeights(values); + builder.fields(fieldsAndBoosts); + }, FIELDS_FIELD); + + PARSER.declareString(MultiFieldMatchQueryBuilder::operator, Operator::fromString, OPERATOR_FIELD); + PARSER.declareField( + MultiFieldMatchQueryBuilder::minimumShouldMatch, + XContentParser::textOrNull, + MINIMUM_SHOULD_MATCH_FIELD, + // using INT_OR_NULL (which includes VALUE_NUMBER, VALUE_STRING, VALUE_NULL) to also allow for numeric values and null + ObjectParser.ValueType.INT_OR_NULL + ); + PARSER.declareBoolean(MultiFieldMatchQueryBuilder::autoGenerateSynonymsPhraseQuery, GENERATE_SYNONYMS_PHRASE_QUERY); + PARSER.declareString(MultiFieldMatchQueryBuilder::zeroTermsQuery, value -> { + if ("none".equalsIgnoreCase(value)) { + return ZeroTermsQueryOption.NONE; + } else if ("all".equalsIgnoreCase(value)) { + return ZeroTermsQueryOption.ALL; + } else { + throw new IllegalArgumentException("Unsupported [" + ZERO_TERMS_QUERY_FIELD.getPreferredName() + "] value [" + value + "]"); + } + }, ZERO_TERMS_QUERY_FIELD); + + PARSER.declareFloat(MultiFieldMatchQueryBuilder::boost, BOOST_FIELD); + PARSER.declareString(MultiFieldMatchQueryBuilder::queryName, NAME_FIELD); + } + + /** + * Constructs a new text query. + */ + public MultiFieldMatchQueryBuilder(Object value, String... fields) { + if (value == null) { + throw new IllegalArgumentException("[" + NAME + "] requires query value"); + } + if (fields == null) { + throw new IllegalArgumentException("[" + NAME + "] requires field list"); + } + this.value = value; + this.fieldsAndBoosts = new TreeMap<>(); + for (String field : fields) { + field(field); + } + } + + /** + * Read from a stream. + */ + public MultiFieldMatchQueryBuilder(StreamInput in) throws IOException { + super(in); + value = in.readGenericValue(); + int size = in.readVInt(); + fieldsAndBoosts = new TreeMap<>(); + for (int i = 0; i < size; i++) { + String field = in.readString(); + float boost = in.readFloat(); + fieldsAndBoosts.put(field, boost); + } + operator = Operator.readFromStream(in); + minimumShouldMatch = in.readOptionalString(); + zeroTermsQuery = ZeroTermsQueryOption.readFromStream(in); + autoGenerateSynonymsPhraseQuery = in.readBoolean(); + } + + @Override + protected void doWriteTo(StreamOutput out) throws IOException { + out.writeGenericValue(value); + out.writeVInt(fieldsAndBoosts.size()); + for (Map.Entry fieldsEntry : fieldsAndBoosts.entrySet()) { + out.writeString(fieldsEntry.getKey()); + out.writeFloat(fieldsEntry.getValue()); + } + operator.writeTo(out); + out.writeOptionalString(minimumShouldMatch); + zeroTermsQuery.writeTo(out); + out.writeBoolean(autoGenerateSynonymsPhraseQuery); + } + + /** + * Adds a field to run the query against. + */ + public MultiFieldMatchQueryBuilder field(String field) { + if (Strings.isEmpty(field)) { + throw new IllegalArgumentException("supplied field is null or empty."); + } + this.fieldsAndBoosts.put(field, AbstractQueryBuilder.DEFAULT_BOOST); + return this; + } + + /** + * Adds a field to run the query against with a specific boost. + */ + public MultiFieldMatchQueryBuilder field(String field, float boost) { + if (Strings.isEmpty(field)) { + throw new IllegalArgumentException("supplied field is null or empty."); + } + validateFieldBoost(boost); + this.fieldsAndBoosts.put(field, boost); + return this; + } + + /** + * Add several fields to run the query against with a specific boost. + */ + public MultiFieldMatchQueryBuilder fields(Map fields) { + for (float fieldBoost : fields.values()) { + validateFieldBoost(fieldBoost); + } + this.fieldsAndBoosts.putAll(fields); + return this; + } + + public Map fields() { + return fieldsAndBoosts; + } + + /** + * Sets the operator to use for the top-level boolean query. Defaults to {@code OR}. + */ + public MultiFieldMatchQueryBuilder operator(Operator operator) { + if (operator == null) { + throw new IllegalArgumentException("[" + NAME + "] requires operator to be non-null"); + } + this.operator = operator; + return this; + } + + public Operator operator() { + return operator; + } + + public MultiFieldMatchQueryBuilder minimumShouldMatch(String minimumShouldMatch) { + this.minimumShouldMatch = minimumShouldMatch; + return this; + } + + public String minimumShouldMatch() { + return minimumShouldMatch; + } + + public MultiFieldMatchQueryBuilder zeroTermsQuery(ZeroTermsQueryOption zeroTermsQuery) { + if (zeroTermsQuery == null) { + throw new IllegalArgumentException("[" + NAME + "] requires zero terms query to be non-null"); + } + this.zeroTermsQuery = zeroTermsQuery; + return this; + } + + public MultiFieldMatchQueryBuilder autoGenerateSynonymsPhraseQuery(boolean enable) { + this.autoGenerateSynonymsPhraseQuery = enable; + return this; + } + + private static void validateFieldBoost(float boost) { + if (boost < 1.0f) { + throw new IllegalArgumentException("[" + NAME + "] requires field boosts to be >= 1.0"); + } + } + + @Override + public void doXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(NAME); + builder.field(QUERY_FIELD.getPreferredName(), value); + builder.startArray(FIELDS_FIELD.getPreferredName()); + for (Map.Entry fieldEntry : this.fieldsAndBoosts.entrySet()) { + builder.value(fieldEntry.getKey() + "^" + fieldEntry.getValue()); + } + builder.endArray(); + if (operator != DEFAULT_OPERATOR) { + builder.field(OPERATOR_FIELD.getPreferredName(), operator.toString()); + } + if (minimumShouldMatch != null) { + builder.field(MINIMUM_SHOULD_MATCH_FIELD.getPreferredName(), minimumShouldMatch); + } + if (zeroTermsQuery != DEFAULT_ZERO_TERMS_QUERY) { + builder.field(ZERO_TERMS_QUERY_FIELD.getPreferredName(), zeroTermsQuery.toString()); + } + if (autoGenerateSynonymsPhraseQuery != DEFAULT_GENERATE_SYNONYMS_PHRASE) { + builder.field(GENERATE_SYNONYMS_PHRASE_QUERY.getPreferredName(), autoGenerateSynonymsPhraseQuery); + } + boostAndQueryNameToXContent(builder); + builder.endObject(); + } + + public static MultiFieldMatchQueryBuilder fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + public String getWriteableName() { + return NAME; + } + + @Override + protected Query doToQuery(SearchExecutionContext context) throws IOException { + if (fieldsAndBoosts.isEmpty()) { + throw new IllegalArgumentException("In [" + NAME + "] query, at least one field must be provided"); + } + + Map fields = QueryParserHelper.resolveMappingFields(context, fieldsAndBoosts); + // If all fields are unmapped, then return an 'unmapped field query'. + boolean hasMappedField = fields.keySet().stream().anyMatch(k -> context.getFieldType(k) != null); + if (hasMappedField == false) { + return Queries.newUnmappedFieldsQuery(fields.keySet()); + } + + validateSimilarity(context, fields); + + Analyzer sharedAnalyzer = null; + List fieldsAndBoosts = new ArrayList<>(); + for (Map.Entry entry : fields.entrySet()) { + String name = entry.getKey(); + MappedFieldType fieldType = context.getFieldType(name); + if (fieldType == null) { + continue; + } + + if (fieldType.familyTypeName().equals(TextFieldMapper.CONTENT_TYPE) == false) { + throw new IllegalArgumentException( + "Field [" + fieldType.name() + "] of type [" + fieldType.typeName() + "] does not support [" + NAME + "] queries" + ); + } + + float boost = entry.getValue() == null ? 1.0f : entry.getValue(); + fieldsAndBoosts.add(new MultiFieldMatchQueryBuilder.FieldAndBoost(fieldType, boost)); + + Analyzer analyzer = fieldType.getTextSearchInfo().searchAnalyzer(); + if (sharedAnalyzer != null && analyzer.equals(sharedAnalyzer) == false) { + throw new IllegalArgumentException("All fields in [" + NAME + "] query must have the same search analyzer"); + } + sharedAnalyzer = analyzer; + } + + assert fieldsAndBoosts.isEmpty() == false; + String placeholderFieldName = fieldsAndBoosts.get(0).fieldType.name(); + boolean canGenerateSynonymsPhraseQuery = autoGenerateSynonymsPhraseQuery; + for (MultiFieldMatchQueryBuilder.FieldAndBoost fieldAndBoost : fieldsAndBoosts) { + TextSearchInfo textSearchInfo = fieldAndBoost.fieldType.getTextSearchInfo(); + canGenerateSynonymsPhraseQuery &= textSearchInfo.hasPositions(); + } + + MultiFieldMatchQueryBuilder.CombinedFieldsBuilder builder = new MultiFieldMatchQueryBuilder.CombinedFieldsBuilder( + fieldsAndBoosts, + sharedAnalyzer, + canGenerateSynonymsPhraseQuery, + context + ); + Query query = builder.createBooleanQuery(placeholderFieldName, value.toString(), operator.toBooleanClauseOccur()); + + query = Queries.maybeApplyMinimumShouldMatch(query, minimumShouldMatch); + if (query == null) { + query = zeroTermsQuery.asQuery(); + } + return query; + } + + private static void validateSimilarity(SearchExecutionContext context, Map fields) { + for (Map.Entry entry : fields.entrySet()) { + String name = entry.getKey(); + MappedFieldType fieldType = context.getFieldType(name); + if (fieldType != null && fieldType.getTextSearchInfo().similarity() != null) { + throw new IllegalArgumentException("[" + NAME + "] queries cannot be used with per-field similarities"); + } + } + + Similarity defaultSimilarity = context.getDefaultSimilarity(); + if ((defaultSimilarity instanceof LegacyBM25Similarity || defaultSimilarity instanceof BM25Similarity) == false) { + throw new IllegalArgumentException("[" + NAME + "] queries can only be used with the [BM25] similarity"); + } + } + + private static final class FieldAndBoost { + final MappedFieldType fieldType; + final float boost; + + FieldAndBoost(MappedFieldType fieldType, float boost) { + this.fieldType = Objects.requireNonNull(fieldType); + this.boost = boost; + } + } + + private static class CombinedFieldsBuilder extends QueryBuilder { + private final List fields; + private final SearchExecutionContext context; + + CombinedFieldsBuilder( + List fields, + Analyzer analyzer, + boolean autoGenerateSynonymsPhraseQuery, + SearchExecutionContext context + ) { + super(analyzer); + this.fields = fields; + setAutoGenerateMultiTermSynonymsPhraseQuery(autoGenerateSynonymsPhraseQuery); + this.context = context; + } + + @Override + protected Query createFieldQuery(TokenStream source, BooleanClause.Occur operator, String field, boolean quoted, int phraseSlop) { + if (source.hasAttribute(DisableGraphAttribute.class)) { + /* + * A {@link TokenFilter} in this {@link TokenStream} disabled the graph analysis to avoid + * paths explosion. See {@link org.elasticsearch.index.analysis.ShingleTokenFilterFactory} for details. + */ + setEnableGraphQueries(false); + } + try { + return super.createFieldQuery(source, operator, field, quoted, phraseSlop); + } finally { + setEnableGraphQueries(true); + } + } + + @Override + public Query createPhraseQuery(String field, String queryText, int phraseSlop) { + throw new IllegalArgumentException("[combined_fields] queries don't support phrases"); + } + + @Override + protected Query newSynonymQuery(String field, TermAndBoost[] terms) { + CombinedFieldQuery.Builder query = new CombinedFieldQuery.Builder(); + for (TermAndBoost termAndBoost : terms) { + assert termAndBoost.boost() == BoostAttribute.DEFAULT_BOOST; + BytesRef bytes = termAndBoost.term(); + query.addTerm(bytes); + } + for (MultiFieldMatchQueryBuilder.FieldAndBoost fieldAndBoost : fields) { + MappedFieldType fieldType = fieldAndBoost.fieldType; + float fieldBoost = fieldAndBoost.boost; + query.addField(fieldType.name(), fieldBoost); + } + return query.build(); + } + + @Override + protected Query newTermQuery(Term term, float boost) { + TermAndBoost termAndBoost = new TermAndBoost(term.bytes(), boost); + return newSynonymQuery(term.field(), new TermAndBoost[] { termAndBoost }); + } + + @Override + protected Query analyzePhrase(String field, TokenStream stream, int slop) throws IOException { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + for (MultiFieldMatchQueryBuilder.FieldAndBoost fieldAndBoost : fields) { + Query query = fieldAndBoost.fieldType.phraseQuery(stream, slop, enablePositionIncrements, context); + if (fieldAndBoost.boost != 1f) { + query = new BoostQuery(query, fieldAndBoost.boost); + } + builder.add(query, BooleanClause.Occur.SHOULD); + } + return builder.build(); + } + } + + @Override + protected int doHashCode() { + return Objects.hash(value, fieldsAndBoosts, operator, minimumShouldMatch, zeroTermsQuery, autoGenerateSynonymsPhraseQuery); + } + + @Override + protected boolean doEquals(MultiFieldMatchQueryBuilder other) { + return Objects.equals(value, other.value) + && Objects.equals(fieldsAndBoosts, other.fieldsAndBoosts) + && Objects.equals(operator, other.operator) + && Objects.equals(minimumShouldMatch, other.minimumShouldMatch) + && Objects.equals(zeroTermsQuery, other.zeroTermsQuery) + && Objects.equals(autoGenerateSynonymsPhraseQuery, other.autoGenerateSynonymsPhraseQuery); + } + + @Override + public TransportVersion getMinimalSupportedVersion() { + return TransportVersions.ZERO; + } +} diff --git a/server/src/main/java/org/elasticsearch/index/query/QueryBuilders.java b/server/src/main/java/org/elasticsearch/index/query/QueryBuilders.java index 66f1c9a74d4c2..93ea5cf68af64 100644 --- a/server/src/main/java/org/elasticsearch/index/query/QueryBuilders.java +++ b/server/src/main/java/org/elasticsearch/index/query/QueryBuilders.java @@ -60,6 +60,14 @@ public static MultiMatchQueryBuilder multiMatchQuery(Object text, String... fiel return new MultiMatchQueryBuilder(text, fieldNames); // BOOLEAN is the default } + /** + * Creates a query builder for match with multiple fields (distinct from multi-match). + * TODO: better comment. + */ + public static MultiFieldMatchQueryBuilder multiFieldMatchQuery(Object text, String... fieldNames) { + return new MultiFieldMatchQueryBuilder(text, fieldNames); + } + /** * Creates a text query with type "BOOL_PREFIX" for the provided field name and text. * diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQuery.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQuery.java index 2218536a9db5f..8702b4877b4b0 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQuery.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQuery.java @@ -6,11 +6,11 @@ */ package org.elasticsearch.xpack.esql.querydsl.query; -import org.elasticsearch.common.unit.Fuzziness; -import org.elasticsearch.index.query.MultiMatchQueryBuilder; +import org.elasticsearch.index.query.MultiFieldMatchQueryBuilder; import org.elasticsearch.index.query.Operator; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.index.query.ZeroTermsQueryOption; import org.elasticsearch.xpack.esql.core.querydsl.query.Query; import org.elasticsearch.xpack.esql.core.tree.Source; @@ -20,43 +20,26 @@ import java.util.function.BiConsumer; import static java.util.Map.entry; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.ANALYZER_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.BOOST_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.FUZZINESS_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.FUZZY_REWRITE_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.FUZZY_TRANSPOSITIONS_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.GENERATE_SYNONYMS_PHRASE_QUERY; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.LENIENT_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.MAX_EXPANSIONS_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.MINIMUM_SHOULD_MATCH_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.OPERATOR_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.PREFIX_LENGTH_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.SLOP_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.TIE_BREAKER_FIELD; -import static org.elasticsearch.index.query.MultiMatchQueryBuilder.TYPE_FIELD; +import static org.elasticsearch.index.query.MultiFieldMatchQueryBuilder.BOOST_FIELD; +import static org.elasticsearch.index.query.MultiFieldMatchQueryBuilder.GENERATE_SYNONYMS_PHRASE_QUERY; +import static org.elasticsearch.index.query.MultiFieldMatchQueryBuilder.MINIMUM_SHOULD_MATCH_FIELD; +import static org.elasticsearch.index.query.MultiFieldMatchQueryBuilder.OPERATOR_FIELD; +import static org.elasticsearch.index.query.MultiFieldMatchQueryBuilder.ZERO_TERMS_QUERY_FIELD; public class MultiMatchQuery extends Query { - private static final Map> BUILDER_APPLIERS; + private static final Map> BUILDER_APPLIERS; static { BUILDER_APPLIERS = Map.ofEntries( entry(BOOST_FIELD.getPreferredName(), (qb, obj) -> qb.boost((Float) obj)), - entry(SLOP_FIELD.getPreferredName(), (qb, obj) -> qb.slop((Integer) obj)), - // TODO: add zero terms query support, I'm not sure the best way to parse it yet... - // appliers.put("zero_terms_query", (qb, s) -> qb.zeroTermsQuery(s)); - entry(ANALYZER_FIELD.getPreferredName(), (qb, obj) -> qb.analyzer((String) obj)), + entry( + ZERO_TERMS_QUERY_FIELD.getPreferredName(), + (qb, obj) -> qb.zeroTermsQuery(ZeroTermsQueryOption.readFromString((String) obj)) + ), entry(GENERATE_SYNONYMS_PHRASE_QUERY.getPreferredName(), (qb, obj) -> qb.autoGenerateSynonymsPhraseQuery((Boolean) obj)), - entry(FUZZINESS_FIELD.getPreferredName(), (qb, obj) -> qb.fuzziness(Fuzziness.fromString((String) obj))), - entry(FUZZY_REWRITE_FIELD.getPreferredName(), (qb, obj) -> qb.fuzzyRewrite((String) obj)), - entry(FUZZY_TRANSPOSITIONS_FIELD.getPreferredName(), (qb, obj) -> qb.fuzzyTranspositions((Boolean) obj)), - entry(LENIENT_FIELD.getPreferredName(), (qb, obj) -> qb.lenient((Boolean) obj)), - entry(MAX_EXPANSIONS_FIELD.getPreferredName(), (qb, obj) -> qb.maxExpansions((Integer) obj)), entry(MINIMUM_SHOULD_MATCH_FIELD.getPreferredName(), (qb, obj) -> qb.minimumShouldMatch((String) obj)), - entry(OPERATOR_FIELD.getPreferredName(), (qb, obj) -> qb.operator(Operator.fromString((String) obj))), - entry(PREFIX_LENGTH_FIELD.getPreferredName(), (qb, obj) -> qb.prefixLength((Integer) obj)), - entry(TIE_BREAKER_FIELD.getPreferredName(), (qb, obj) -> qb.tieBreaker((Float) obj)), - entry(TYPE_FIELD.getPreferredName(), (qb, obj) -> qb.type((String) obj)) + entry(OPERATOR_FIELD.getPreferredName(), (qb, obj) -> qb.operator(Operator.fromString((String) obj))) ); } @@ -76,7 +59,7 @@ protected QueryBuilder asBuilder() { // TODO: create a new builder, group fields by analyzer (combined_fields query), separate groups are combined with dis_max query. // TODO: needs to happen on shard level. - final MultiMatchQueryBuilder queryBuilder = QueryBuilders.multiMatchQuery(query); + final MultiFieldMatchQueryBuilder queryBuilder = QueryBuilders.multiFieldMatchQuery(query); queryBuilder.fields(fields); options.forEach((k, v) -> { if (BUILDER_APPLIERS.containsKey(k)) { From 34752c1c1aa6d16946df20395aafa2c73012e699 Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Thu, 12 Jun 2025 11:27:56 -0400 Subject: [PATCH 19/33] Fix constructor --- .../index/query/MultiFieldMatchQueryBuilder.java | 13 +++++++++---- .../elasticsearch/index/query/QueryBuilders.java | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java index 7e6699b252e40..d823e5d4959bc 100644 --- a/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java @@ -110,18 +110,23 @@ public class MultiFieldMatchQueryBuilder extends AbstractQueryBuilder(); + } + + public static MultiFieldMatchQueryBuilder create(Object value, String... fields) { if (fields == null) { throw new IllegalArgumentException("[" + NAME + "] requires field list"); } - this.value = value; - this.fieldsAndBoosts = new TreeMap<>(); + var result = new MultiFieldMatchQueryBuilder(value); for (String field : fields) { - field(field); + result = result.field(field); } + return result; } /** diff --git a/server/src/main/java/org/elasticsearch/index/query/QueryBuilders.java b/server/src/main/java/org/elasticsearch/index/query/QueryBuilders.java index 93ea5cf68af64..e47b25a1b524c 100644 --- a/server/src/main/java/org/elasticsearch/index/query/QueryBuilders.java +++ b/server/src/main/java/org/elasticsearch/index/query/QueryBuilders.java @@ -65,7 +65,7 @@ public static MultiMatchQueryBuilder multiMatchQuery(Object text, String... fiel * TODO: better comment. */ public static MultiFieldMatchQueryBuilder multiFieldMatchQuery(Object text, String... fieldNames) { - return new MultiFieldMatchQueryBuilder(text, fieldNames); + return MultiFieldMatchQueryBuilder.create(text, fieldNames); } /** From 7006718fe5684aa04acc87516c19c9dcdcb3242f Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Thu, 12 Jun 2025 14:00:53 -0400 Subject: [PATCH 20/33] Fix tests --- .../resources/multi-match-function.csv-spec | 57 ------------------- .../src/main/resources/scoring.csv-spec | 8 +-- .../expression/function/fulltext/Match.java | 2 - 3 files changed, 4 insertions(+), 63 deletions(-) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multi-match-function.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multi-match-function.csv-spec index eaab4b1c3d315..d80ba8233c563 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multi-match-function.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multi-match-function.csv-spec @@ -24,27 +24,6 @@ book_no:keyword | author:text // end::multi-match-with-field-result[] ; -testMultiMatchWithOptionsFuzziness -required_capability: multi_match_unified_function - -from books -| where match(title, description, "Pings", {"fuzziness": 1}) -| keep book_no; -ignoreOrder:true - -book_no:keyword -1463 -2675 -2714 -2936 -4023 -4917 -5335 -7140 -7350 -8875 -; - testMultiMatchWithOptionsOperator required_capability: multi_match_unified_function @@ -95,39 +74,3 @@ book_no:keyword 8678 8875 ; - -testMultiMatchPhraseQuery -required_capability: multi_match_unified_function - -from books -| where match(title, description, "Lord of the rings", { "type": "phrase" } ) -| keep title -; -ignoreOrder: true - -title:text -A Tolkien Compass: Including J. R. R. Tolkien's Guide to the Names in The Lord of the Rings -Return of the Shadow -The Lord of the Rings Poster Collection: Six Paintings by Alan Lee (No. 1) -Letters of J R R Tolkien -The Lord of the Rings - Boxed Set -The Two Towers -Realms of Tolkien: Images of Middle-earth -Return of the King Being the Third Part of The Lord of the Rings -; - -testMultiMatchPhrasePrefixQuery -required_capability: multi_match_unified_function - -from books -| where match(title, "Lord of the ri", { "type": "phrase_prefix" } ) -| keep title -; -ignoreOrder: true - -title:text -The Lord of the Rings - Boxed Set -Return of the King Being the Third Part of The Lord of the Rings -A Tolkien Compass: Including J. R. R. Tolkien's Guide to the Names in The Lord of the Rings -The Lord of the Rings Poster Collection: Six Paintings by Alan Lee (No. 1) -; diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec index 2e29d20a5f51f..09a187b1cc438 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec @@ -119,7 +119,7 @@ required_capability: multi_match_unified_function required_capability: metadata_score from books metadata _score -| where match(author, title, "Mark", {"fuzziness": 1}) +| where match(author, title, "Mark", {"minimum_should_match": 1}) | keep book_no, title, author, _score; ignoreOrder:true @@ -132,7 +132,7 @@ required_capability: multi_match_unified_function required_capability: metadata_score from books metadata _score -| where match(description, title, "Hobbit", {"type": "best_fields"}) +| where match(description, title, "Hobbit", {"operator": "AND"}) | sort book_no | eval _score = round(_score) | keep book_no, _score; @@ -159,7 +159,7 @@ required_capability: multi_match_unified_function required_capability: metadata_score from books metadata _score -| where match(description, title, "Hobbit", {"type": "most_fields"}) +| where match(description, title, "Hobbit", {"operator": "AND"}) | sort book_no | eval _score = round(_score) | keep book_no, _score; @@ -172,7 +172,7 @@ book_no:keyword | _score:double 2714 | 2.0 2936 | 1.0 4023 | 2.0 -4289 | 6.0 +4289 | 3.0 5335 | 2.0 5996 | 2.0 6405 | 2.0 diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java index 43ba15ffc9c34..595ee00a2e3a4 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java @@ -11,7 +11,6 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.index.query.MatchQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.xpack.esql.capabilities.PostAnalysisPlanVerificationAware; import org.elasticsearch.xpack.esql.common.Failure; @@ -404,7 +403,6 @@ protected Map resolvedOptions() { private Map matchQueryOptions() throws InvalidArgumentException { Map options = new HashMap<>(); - options.put(MatchQueryBuilder.LENIENT_FIELD.getPreferredName(), true); if (options() == null) { return options; } From f3bd8a96577c85f0770e341fa9df2c4cb28e16d3 Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Fri, 13 Jun 2025 09:44:28 -0400 Subject: [PATCH 21/33] Fix merge --- .../xpack/esql/expression/function/EsqlFunctionRegistry.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java index e925bfaa7fc4a..853990bc59bca 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java @@ -464,7 +464,7 @@ private static FunctionDefinition[][] functions() { new FunctionDefinition[] { def(Kql.class, uni(Kql::new), "kql"), def(Match.class, Match::new, "match"), - def(QueryString.class, bi(QueryString::new), "qstr") } }; + def(QueryString.class, bi(QueryString::new), "qstr"), def(MatchPhrase.class, tri(MatchPhrase::new), "match_phrase") } }; } From e20a1a0089835328bacb8110e40891f1cdef142d Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Fri, 13 Jun 2025 12:39:07 -0400 Subject: [PATCH 22/33] Fix test --- .../LocalPhysicalPlanOptimizerTests.java | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java index 5399d8769b360..54c18c791a793 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java @@ -21,12 +21,13 @@ import org.elasticsearch.index.mapper.ParsedDocument; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.MatchQueryBuilder; -import org.elasticsearch.index.query.MultiMatchQueryBuilder; +import org.elasticsearch.index.query.MultiFieldMatchQueryBuilder; import org.elasticsearch.index.query.Operator; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryStringQueryBuilder; import org.elasticsearch.index.query.RangeQueryBuilder; import org.elasticsearch.index.query.SearchExecutionContext; +import org.elasticsearch.index.query.ZeroTermsQueryOption; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.search.vectors.KnnVectorQueryBuilder; import org.elasticsearch.search.vectors.RescoreVectorBuilder; @@ -1221,7 +1222,7 @@ private void checkMatchFunctionPushDown( var fieldExtract = as(project.child(), FieldExtractExec.class); var actualLuceneQuery = as(fieldExtract.child(), EsQueryExec.class).query(); - var expectedLuceneQuery = new MatchQueryBuilder(fieldName, expectedValueProvider.apply(queryValue)).lenient(true); + var expectedLuceneQuery = new MatchQueryBuilder(fieldName, expectedValueProvider.apply(queryValue)); assertThat("Unexpected match query for data type " + fieldDataType, actualLuceneQuery, equalTo(expectedLuceneQuery)); } catch (ParsingException e) { fail("Error parsing ESQL query: " + esqlQuery + "\n" + e.getMessage()); @@ -1287,8 +1288,7 @@ public void testMatchOptionsPushDown() { .boost(2.1f) .minimumShouldMatch("2") .operator(Operator.AND) - .prefixLength(3) - .lenient(true); + .prefixLength(3); assertThat(actualLuceneQuery.toString(), is(expectedLuceneQuery.toString())); } @@ -1332,31 +1332,20 @@ public void testQStrOptionsPushDown() { public void testMultiMatchOptionsPushDown() { String query = """ from test - | where MATCH(first_name, last_name, "Anna", {"fuzzy_rewrite": "constant_score", "slop": 10, "analyzer": "auto", - "auto_generate_synonyms_phrase_query": "false", "fuzziness": "auto", "fuzzy_transpositions": false, "lenient": "false", - "max_expansions": 10, "minimum_should_match": 3, "operator": "AND", "prefix_length": 20, "tie_breaker": 1.0, - "type": "best_fields", "boost": 2.0}) + | where MATCH(first_name, last_name, "Anna", {"auto_generate_synonyms_phrase_query": "false", + "minimum_should_match": 3, "operator": "AND", "boost": 2.0, "zero_terms_query": "none"}) """; var plan = plannerOptimizer.plan(query); AtomicReference planStr = new AtomicReference<>(); plan.forEachDown(EsQueryExec.class, result -> planStr.set(result.query().toString())); - var expectedQuery = new MultiMatchQueryBuilder("Anna").fields(Map.of("first_name", 1.0f, "last_name", 1.0f)) - .slop(10) + var expectedQuery = MultiFieldMatchQueryBuilder.create("Anna", "first_name", "last_name") .boost(2.0f) - .analyzer("auto") .autoGenerateSynonymsPhraseQuery(false) .operator(Operator.fromString("AND")) - .fuzziness(Fuzziness.fromString("auto")) - .fuzzyRewrite("constant_score") - .fuzzyTranspositions(false) - .lenient(false) - .type("best_fields") - .maxExpansions(10) - .minimumShouldMatch("3") - .prefixLength(20) - .tieBreaker(1.0f); + .zeroTermsQuery(ZeroTermsQueryOption.readFromString("none")) + .minimumShouldMatch("3"); assertThat(expectedQuery.toString(), is(planStr.get())); } @@ -1960,7 +1949,7 @@ public void testMatchFunctionWithStatsWherePushable() { var agg = as(limit.child(), AggregateExec.class); var exchange = as(agg.child(), ExchangeExec.class); var stats = as(exchange.child(), EsStatsQueryExec.class); - QueryBuilder expected = new MatchQueryBuilder("last_name", "Smith").lenient(true); + QueryBuilder expected = new MatchQueryBuilder("last_name", "Smith"); assertThat(stats.query().toString(), equalTo(expected.toString())); } @@ -1980,7 +1969,7 @@ public void testMatchFunctionWithStatsPushableAndNonPushableCondition() { assertTrue(filter.condition() instanceof GreaterThan); var fieldExtract = as(filter.child(), FieldExtractExec.class); var esQuery = as(fieldExtract.child(), EsQueryExec.class); - QueryBuilder expected = new MatchQueryBuilder("last_name", "Smith").lenient(true); + QueryBuilder expected = new MatchQueryBuilder("last_name", "Smith"); assertThat(esQuery.query().toString(), equalTo(expected.toString())); } @@ -2130,7 +2119,7 @@ private class MatchFunctionTestCase extends FullTextFunctionTestCase { @Override public QueryBuilder queryBuilder() { - return new MatchQueryBuilder(fieldName(), queryString()).lenient(true); + return new MatchQueryBuilder(fieldName(), queryString()); } @Override @@ -2146,7 +2135,7 @@ private class MatchOperatorTestCase extends FullTextFunctionTestCase { @Override public QueryBuilder queryBuilder() { - return new MatchQueryBuilder(fieldName(), queryString()).lenient(true); + return new MatchQueryBuilder(fieldName(), queryString()); } @Override From 73f546829e110c052a855d05ce17f2c7030caab6 Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Fri, 13 Jun 2025 14:14:40 -0400 Subject: [PATCH 23/33] Fix tests --- .../src/main/resources/scoring.csv-spec | 4 +- .../querydsl/query/MultiMatchQueryTests.java | 46 ------------------- 2 files changed, 2 insertions(+), 48 deletions(-) delete mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQueryTests.java diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec index 09a187b1cc438..57be18212f57c 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec @@ -119,12 +119,12 @@ required_capability: multi_match_unified_function required_capability: metadata_score from books metadata _score -| where match(author, title, "Mark", {"minimum_should_match": 1}) +| where match(author, title, "Stranger", {"operator": "OR"}) | keep book_no, title, author, _score; ignoreOrder:true book_no:keyword | title:text | author:text | _score:double -2847 | To Love A Dark Stranger (Lovegram Historical Romance) | Colleen Faulkner | 1.9662091732025146 +2847 | To Love A Dark Stranger (Lovegram Historical Romance) | Colleen Faulkner | 2.8462226390838623 ; testMultiMatchWithScore1 diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQueryTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQueryTests.java deleted file mode 100644 index 81308a7538796..0000000000000 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQueryTests.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -package org.elasticsearch.xpack.esql.querydsl.query; - -import org.elasticsearch.ElasticsearchParseException; -import org.elasticsearch.index.query.MultiMatchQueryBuilder; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.esql.core.tree.Source; -import org.elasticsearch.xpack.esql.core.util.StringUtils; - -import java.util.Map; - -import static org.hamcrest.Matchers.equalTo; - -public class MultiMatchQueryTests extends ESTestCase { - - public void testQueryBuilding() { - MultiMatchQueryBuilder qb = getBuilder(Map.of("lenient", true)); - assertThat(qb.lenient(), equalTo(true)); - - qb = getBuilder(Map.of("type", "best_fields")); - assertThat(qb.getType(), equalTo(MultiMatchQueryBuilder.Type.BEST_FIELDS)); - - Exception e = expectThrows(IllegalArgumentException.class, () -> getBuilder(Map.of("pizza", "yummy"))); - assertThat(e.getMessage(), equalTo("illegal multi_match option [pizza]")); - - e = expectThrows(ElasticsearchParseException.class, () -> getBuilder(Map.of("type", "aoeu"))); - assertThat(e.getMessage(), equalTo("failed to parse [multi_match] query type [aoeu]. unknown type.")); - } - - private static MultiMatchQueryBuilder getBuilder(Map options) { - final Source source = new Source(1, 1, StringUtils.EMPTY); - final MultiMatchQuery mmq = new MultiMatchQuery(source, "eggplant", Map.of("bar", 1.0f, "foo", 1.0f), options); - return (MultiMatchQueryBuilder) mmq.asBuilder(); - } - - public void testToString() { - final Source source = new Source(1, 1, StringUtils.EMPTY); - final MultiMatchQuery mmq = new MultiMatchQuery(source, "eggplant", Map.of("bar", 1.0f, "foo", 1.0f), null); - assertEquals("MultiMatchQuery@1:2[{bar=1.0, foo=1.0}:eggplant]", mmq.toString()); - } -} From 1070ed882292dae079d90e7a842fb03c33abb27f Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Mon, 16 Jun 2025 09:00:09 -0400 Subject: [PATCH 24/33] Comments --- .../elasticsearch/index/query/MultiFieldMatchQueryBuilder.java | 3 ++- .../xpack/esql/querydsl/query/MultiMatchQuery.java | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java index d823e5d4959bc..89785efd10567 100644 --- a/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java @@ -310,6 +310,7 @@ protected Query doToQuery(SearchExecutionContext context) throws IOException { float boost = entry.getValue() == null ? 1.0f : entry.getValue(); fieldsAndBoosts.add(new MultiFieldMatchQueryBuilder.FieldAndBoost(fieldType, boost)); + // TODO: group by analyzer. Analyzer analyzer = fieldType.getTextSearchInfo().searchAnalyzer(); if (sharedAnalyzer != null && analyzer.equals(sharedAnalyzer) == false) { throw new IllegalArgumentException("All fields in [" + NAME + "] query must have the same search analyzer"); @@ -399,7 +400,7 @@ protected Query createFieldQuery(TokenStream source, BooleanClause.Occur operato @Override public Query createPhraseQuery(String field, String queryText, int phraseSlop) { - throw new IllegalArgumentException("[combined_fields] queries don't support phrases"); + throw new IllegalArgumentException("[multi_field_match] queries don't support phrases"); } @Override diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQuery.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQuery.java index 8702b4877b4b0..5c4abfe80ddd2 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQuery.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/querydsl/query/MultiMatchQuery.java @@ -56,9 +56,6 @@ public MultiMatchQuery(Source source, String query, Map fields, M @Override protected QueryBuilder asBuilder() { - // TODO: create a new builder, group fields by analyzer (combined_fields query), separate groups are combined with dis_max query. - // TODO: needs to happen on shard level. - final MultiFieldMatchQueryBuilder queryBuilder = QueryBuilders.multiFieldMatchQuery(query); queryBuilder.fields(fields); options.forEach((k, v) -> { From 5d29f25d65fe344effb86c117632f974b0996a5d Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Mon, 16 Jun 2025 18:51:22 -0400 Subject: [PATCH 25/33] Group by analyzer --- .../query/MultiFieldMatchQueryBuilder.java | 60 ++++++++++--------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java index 89785efd10567..754581683ad38 100644 --- a/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java @@ -42,6 +42,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -292,8 +293,7 @@ protected Query doToQuery(SearchExecutionContext context) throws IOException { validateSimilarity(context, fields); - Analyzer sharedAnalyzer = null; - List fieldsAndBoosts = new ArrayList<>(); + Map> groups = new HashMap<>(); for (Map.Entry entry : fields.entrySet()) { String name = entry.getKey(); MappedFieldType fieldType = context.getFieldType(name); @@ -308,37 +308,39 @@ protected Query doToQuery(SearchExecutionContext context) throws IOException { } float boost = entry.getValue() == null ? 1.0f : entry.getValue(); - fieldsAndBoosts.add(new MultiFieldMatchQueryBuilder.FieldAndBoost(fieldType, boost)); - // TODO: group by analyzer. Analyzer analyzer = fieldType.getTextSearchInfo().searchAnalyzer(); - if (sharedAnalyzer != null && analyzer.equals(sharedAnalyzer) == false) { - throw new IllegalArgumentException("All fields in [" + NAME + "] query must have the same search analyzer"); + if (groups.containsKey(analyzer) == false) { + groups.put(analyzer, new ArrayList<>()); } - sharedAnalyzer = analyzer; + groups.get(analyzer).add(new FieldAndBoost(fieldType, boost)); } - assert fieldsAndBoosts.isEmpty() == false; - String placeholderFieldName = fieldsAndBoosts.get(0).fieldType.name(); - boolean canGenerateSynonymsPhraseQuery = autoGenerateSynonymsPhraseQuery; - for (MultiFieldMatchQueryBuilder.FieldAndBoost fieldAndBoost : fieldsAndBoosts) { - TextSearchInfo textSearchInfo = fieldAndBoost.fieldType.getTextSearchInfo(); - canGenerateSynonymsPhraseQuery &= textSearchInfo.hasPositions(); - } + // TODO: For now assume we have one group. + assert groups.size() == 1; - MultiFieldMatchQueryBuilder.CombinedFieldsBuilder builder = new MultiFieldMatchQueryBuilder.CombinedFieldsBuilder( - fieldsAndBoosts, - sharedAnalyzer, - canGenerateSynonymsPhraseQuery, - context - ); - Query query = builder.createBooleanQuery(placeholderFieldName, value.toString(), operator.toBooleanClauseOccur()); + List queries = new ArrayList<>(); + for (Map.Entry> group : groups.entrySet()) { + var fieldsAndBoosts = group.getValue(); + + String placeholderFieldName = fieldsAndBoosts.get(0).fieldType.name(); + boolean canGenerateSynonymsPhraseQuery = autoGenerateSynonymsPhraseQuery; + for (FieldAndBoost fieldAndBoost : fieldsAndBoosts) { + TextSearchInfo textSearchInfo = fieldAndBoost.fieldType.getTextSearchInfo(); + canGenerateSynonymsPhraseQuery &= textSearchInfo.hasPositions(); + } - query = Queries.maybeApplyMinimumShouldMatch(query, minimumShouldMatch); - if (query == null) { - query = zeroTermsQuery.asQuery(); + var builder = new CombinedFieldsBuilder(fieldsAndBoosts, group.getKey(), canGenerateSynonymsPhraseQuery, context); + Query query = builder.createBooleanQuery(placeholderFieldName, value.toString(), operator.toBooleanClauseOccur()); + + query = Queries.maybeApplyMinimumShouldMatch(query, minimumShouldMatch); + if (query == null) { + query = zeroTermsQuery.asQuery(); + } } - return query; + + // TODO: combine queries. + return queries.getFirst(); } private static void validateSimilarity(SearchExecutionContext context, Map fields) { @@ -367,11 +369,11 @@ private static final class FieldAndBoost { } private static class CombinedFieldsBuilder extends QueryBuilder { - private final List fields; + private final List fields; private final SearchExecutionContext context; CombinedFieldsBuilder( - List fields, + List fields, Analyzer analyzer, boolean autoGenerateSynonymsPhraseQuery, SearchExecutionContext context @@ -411,7 +413,7 @@ protected Query newSynonymQuery(String field, TermAndBoost[] terms) { BytesRef bytes = termAndBoost.term(); query.addTerm(bytes); } - for (MultiFieldMatchQueryBuilder.FieldAndBoost fieldAndBoost : fields) { + for (FieldAndBoost fieldAndBoost : fields) { MappedFieldType fieldType = fieldAndBoost.fieldType; float fieldBoost = fieldAndBoost.boost; query.addField(fieldType.name(), fieldBoost); @@ -428,7 +430,7 @@ protected Query newTermQuery(Term term, float boost) { @Override protected Query analyzePhrase(String field, TokenStream stream, int slop) throws IOException { BooleanQuery.Builder builder = new BooleanQuery.Builder(); - for (MultiFieldMatchQueryBuilder.FieldAndBoost fieldAndBoost : fields) { + for (FieldAndBoost fieldAndBoost : fields) { Query query = fieldAndBoost.fieldType.phraseQuery(stream, slop, enablePositionIncrements, context); if (fieldAndBoost.boost != 1f) { query = new BoostQuery(query, fieldAndBoost.boost); From 44a18cd47173288a7b44652766d40bf2020647cb Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Tue, 17 Jun 2025 09:14:42 -0400 Subject: [PATCH 26/33] Fix --- .../elasticsearch/index/query/MultiFieldMatchQueryBuilder.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java index 754581683ad38..63c6867cfdea8 100644 --- a/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java @@ -337,6 +337,7 @@ protected Query doToQuery(SearchExecutionContext context) throws IOException { if (query == null) { query = zeroTermsQuery.asQuery(); } + queries.add(query); } // TODO: combine queries. From 9cead372ea8e03c205d88de84db9c87be508b9fc Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Wed, 18 Jun 2025 12:25:35 -0400 Subject: [PATCH 27/33] Implement dis_max and combined_fields --- .../query/MultiFieldMatchQueryBuilder.java | 128 +++--------------- 1 file changed, 18 insertions(+), 110 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java index 63c6867cfdea8..86731acf97eb5 100644 --- a/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java @@ -10,18 +10,9 @@ package org.elasticsearch.index.query; import org.apache.lucene.analysis.Analyzer; -import org.apache.lucene.analysis.TokenStream; -import org.apache.lucene.index.Term; -import org.apache.lucene.sandbox.search.CombinedFieldQuery; -import org.apache.lucene.search.BooleanClause; -import org.apache.lucene.search.BooleanQuery; -import org.apache.lucene.search.BoostAttribute; -import org.apache.lucene.search.BoostQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.similarities.BM25Similarity; import org.apache.lucene.search.similarities.Similarity; -import org.apache.lucene.util.BytesRef; -import org.apache.lucene.util.QueryBuilder; import org.elasticsearch.TransportVersion; import org.elasticsearch.TransportVersions; import org.elasticsearch.common.Strings; @@ -30,9 +21,7 @@ import org.elasticsearch.common.lucene.search.Queries; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.TextFieldMapper; -import org.elasticsearch.index.mapper.TextSearchInfo; import org.elasticsearch.index.search.QueryParserHelper; -import org.elasticsearch.lucene.analysis.miscellaneous.DisableGraphAttribute; import org.elasticsearch.lucene.similarity.LegacyBM25Similarity; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ObjectParser; @@ -293,7 +282,7 @@ protected Query doToQuery(SearchExecutionContext context) throws IOException { validateSimilarity(context, fields); - Map> groups = new HashMap<>(); + Map> groups = new HashMap<>(); for (Map.Entry entry : fields.entrySet()) { String name = entry.getKey(); MappedFieldType fieldType = context.getFieldType(name); @@ -307,41 +296,43 @@ protected Query doToQuery(SearchExecutionContext context) throws IOException { ); } - float boost = entry.getValue() == null ? 1.0f : entry.getValue(); + // TODO: handle per-field boosts. Analyzer analyzer = fieldType.getTextSearchInfo().searchAnalyzer(); if (groups.containsKey(analyzer) == false) { groups.put(analyzer, new ArrayList<>()); } - groups.get(analyzer).add(new FieldAndBoost(fieldType, boost)); + groups.get(analyzer).add(name); } // TODO: For now assume we have one group. assert groups.size() == 1; - List queries = new ArrayList<>(); - for (Map.Entry> group : groups.entrySet()) { - var fieldsAndBoosts = group.getValue(); - + var disMax = new DisMaxQueryBuilder(); + for (Map.Entry> group : groups.entrySet()) { + /* + TODO String placeholderFieldName = fieldsAndBoosts.get(0).fieldType.name(); boolean canGenerateSynonymsPhraseQuery = autoGenerateSynonymsPhraseQuery; for (FieldAndBoost fieldAndBoost : fieldsAndBoosts) { TextSearchInfo textSearchInfo = fieldAndBoost.fieldType.getTextSearchInfo(); canGenerateSynonymsPhraseQuery &= textSearchInfo.hasPositions(); } + */ - var builder = new CombinedFieldsBuilder(fieldsAndBoosts, group.getKey(), canGenerateSynonymsPhraseQuery, context); - Query query = builder.createBooleanQuery(placeholderFieldName, value.toString(), operator.toBooleanClauseOccur()); + disMax.add(new CombinedFieldsQueryBuilder(value, group.getValue().toArray(new String[0]))); + } - query = Queries.maybeApplyMinimumShouldMatch(query, minimumShouldMatch); - if (query == null) { - query = zeroTermsQuery.asQuery(); - } - queries.add(query); + /* + TODO + Query query = disMax.createBooleanQuery(placeholderFieldName, value.toString(), operator.toBooleanClauseOccur()); + query = Queries.maybeApplyMinimumShouldMatch(query, minimumShouldMatch); + if (query == null) { + query = zeroTermsQuery.asQuery(); } - // TODO: combine queries. - return queries.getFirst(); + */ + return disMax.doToQuery(context); } private static void validateSimilarity(SearchExecutionContext context, Map fields) { @@ -359,89 +350,6 @@ private static void validateSimilarity(SearchExecutionContext context, Map fields; - private final SearchExecutionContext context; - - CombinedFieldsBuilder( - List fields, - Analyzer analyzer, - boolean autoGenerateSynonymsPhraseQuery, - SearchExecutionContext context - ) { - super(analyzer); - this.fields = fields; - setAutoGenerateMultiTermSynonymsPhraseQuery(autoGenerateSynonymsPhraseQuery); - this.context = context; - } - - @Override - protected Query createFieldQuery(TokenStream source, BooleanClause.Occur operator, String field, boolean quoted, int phraseSlop) { - if (source.hasAttribute(DisableGraphAttribute.class)) { - /* - * A {@link TokenFilter} in this {@link TokenStream} disabled the graph analysis to avoid - * paths explosion. See {@link org.elasticsearch.index.analysis.ShingleTokenFilterFactory} for details. - */ - setEnableGraphQueries(false); - } - try { - return super.createFieldQuery(source, operator, field, quoted, phraseSlop); - } finally { - setEnableGraphQueries(true); - } - } - - @Override - public Query createPhraseQuery(String field, String queryText, int phraseSlop) { - throw new IllegalArgumentException("[multi_field_match] queries don't support phrases"); - } - - @Override - protected Query newSynonymQuery(String field, TermAndBoost[] terms) { - CombinedFieldQuery.Builder query = new CombinedFieldQuery.Builder(); - for (TermAndBoost termAndBoost : terms) { - assert termAndBoost.boost() == BoostAttribute.DEFAULT_BOOST; - BytesRef bytes = termAndBoost.term(); - query.addTerm(bytes); - } - for (FieldAndBoost fieldAndBoost : fields) { - MappedFieldType fieldType = fieldAndBoost.fieldType; - float fieldBoost = fieldAndBoost.boost; - query.addField(fieldType.name(), fieldBoost); - } - return query.build(); - } - - @Override - protected Query newTermQuery(Term term, float boost) { - TermAndBoost termAndBoost = new TermAndBoost(term.bytes(), boost); - return newSynonymQuery(term.field(), new TermAndBoost[] { termAndBoost }); - } - - @Override - protected Query analyzePhrase(String field, TokenStream stream, int slop) throws IOException { - BooleanQuery.Builder builder = new BooleanQuery.Builder(); - for (FieldAndBoost fieldAndBoost : fields) { - Query query = fieldAndBoost.fieldType.phraseQuery(stream, slop, enablePositionIncrements, context); - if (fieldAndBoost.boost != 1f) { - query = new BoostQuery(query, fieldAndBoost.boost); - } - builder.add(query, BooleanClause.Occur.SHOULD); - } - return builder.build(); - } - } - @Override protected int doHashCode() { return Objects.hash(value, fieldsAndBoosts, operator, minimumShouldMatch, zeroTermsQuery, autoGenerateSynonymsPhraseQuery); From 81026bcda50b967be3f1b7187ee8f417e2bd981d Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Mon, 23 Jun 2025 14:55:46 -0400 Subject: [PATCH 28/33] AND by default --- .../index/query/MultiFieldMatchQueryBuilder.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java index 86731acf97eb5..82940760f6281 100644 --- a/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/MultiFieldMatchQueryBuilder.java @@ -51,7 +51,7 @@ public class MultiFieldMatchQueryBuilder extends AbstractQueryBuilder Date: Mon, 23 Jun 2025 18:53:23 -0400 Subject: [PATCH 29/33] Always apply lenient --- .../src/main/resources/multi-match-function.csv-spec | 3 +-- .../xpack/esql/expression/function/fulltext/Match.java | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multi-match-function.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multi-match-function.csv-spec index d80ba8233c563..6fcede9433dc5 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multi-match-function.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/multi-match-function.csv-spec @@ -43,12 +43,11 @@ testMultiMatchWithOptionsMinimumShouldMatch required_capability: multi_match_unified_function from books -| where match(title, description, "here back again", {"minimum_should_match": 2, "operator": "OR"}) +| where match(title, description, "there back again", {"minimum_should_match": 2, "operator": "AND"}) | sort book_no | keep title; title:text -My First 100 Words in Spanish/English (My First 100 Words Pull-Tab Book) The Hobbit or There and Back Again ; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java index 595ee00a2e3a4..f710fba5d65d2 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Match.java @@ -11,6 +11,7 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.index.query.MatchQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.xpack.esql.capabilities.PostAnalysisPlanVerificationAware; import org.elasticsearch.xpack.esql.common.Failure; @@ -512,6 +513,7 @@ protected Query translate(TranslatorHandler handler) { return new MultiMatchQuery(source(), Objects.toString(queryAsObject()), fieldsWithBoost, options); } else { // Translate to Match when having exactly one field. + options.put(MatchQueryBuilder.LENIENT_FIELD.getPreferredName(), true); return new MatchQuery(source(), fieldsWithBoost.keySet().stream().findFirst().get(), queryAsObject(), options); } } From ffc0178939268db77017d4be82c01102d8fe9cb4 Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Tue, 24 Jun 2025 09:27:35 -0400 Subject: [PATCH 30/33] Re-add lenient --- .../LocalPhysicalPlanOptimizerTests.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java index 4eb7a62f391f5..00f88f2a02cc4 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java @@ -1222,7 +1222,7 @@ private void checkMatchFunctionPushDown( var fieldExtract = as(project.child(), FieldExtractExec.class); var actualLuceneQuery = as(fieldExtract.child(), EsQueryExec.class).query(); - var expectedLuceneQuery = new MatchQueryBuilder(fieldName, expectedValueProvider.apply(queryValue)); + var expectedLuceneQuery = new MatchQueryBuilder(fieldName, expectedValueProvider.apply(queryValue)).lenient(true); assertThat("Unexpected match query for data type " + fieldDataType, actualLuceneQuery, equalTo(expectedLuceneQuery)); } catch (ParsingException e) { fail("Error parsing ESQL query: " + esqlQuery + "\n" + e.getMessage()); @@ -1288,7 +1288,8 @@ public void testMatchOptionsPushDown() { .boost(2.1f) .minimumShouldMatch("2") .operator(Operator.AND) - .prefixLength(3); + .prefixLength(3) + .lenient(true); assertThat(actualLuceneQuery.toString(), is(expectedLuceneQuery.toString())); } @@ -1952,7 +1953,8 @@ public void testMatchFunctionWithStatsWherePushable() { var agg = as(limit.child(), AggregateExec.class); var exchange = as(agg.child(), ExchangeExec.class); var stats = as(exchange.child(), EsStatsQueryExec.class); - QueryBuilder expected = new MatchQueryBuilder("last_name", "Smith"); + var expected = new MatchQueryBuilder("last_name", "Smith"); + expected = expected.lenient(true); assertThat(stats.query().toString(), equalTo(expected.toString())); } @@ -1972,7 +1974,8 @@ public void testMatchFunctionWithStatsPushableAndNonPushableCondition() { assertTrue(filter.condition() instanceof GreaterThan); var fieldExtract = as(filter.child(), FieldExtractExec.class); var esQuery = as(fieldExtract.child(), EsQueryExec.class); - QueryBuilder expected = new MatchQueryBuilder("last_name", "Smith"); + var expected = new MatchQueryBuilder("last_name", "Smith"); + expected = expected.lenient(true); assertThat(esQuery.query().toString(), equalTo(expected.toString())); } @@ -2122,7 +2125,7 @@ private class MatchFunctionTestCase extends FullTextFunctionTestCase { @Override public QueryBuilder queryBuilder() { - return new MatchQueryBuilder(fieldName(), queryString()); + return new MatchQueryBuilder(fieldName(), queryString()).lenient(true); } @Override @@ -2138,7 +2141,7 @@ private class MatchOperatorTestCase extends FullTextFunctionTestCase { @Override public QueryBuilder queryBuilder() { - return new MatchQueryBuilder(fieldName(), queryString()); + return new MatchQueryBuilder(fieldName(), queryString()).lenient(true); } @Override From b2fee0a0b6edb7314e63bfb5a633649160040405 Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Tue, 24 Jun 2025 10:00:45 -0400 Subject: [PATCH 31/33] Register new builder --- .../src/main/java/org/elasticsearch/search/SearchModule.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/main/java/org/elasticsearch/search/SearchModule.java b/server/src/main/java/org/elasticsearch/search/SearchModule.java index 56b203700b362..c2734e806e6fd 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchModule.java +++ b/server/src/main/java/org/elasticsearch/search/SearchModule.java @@ -44,6 +44,7 @@ import org.elasticsearch.index.query.MatchPhraseQueryBuilder; import org.elasticsearch.index.query.MatchQueryBuilder; import org.elasticsearch.index.query.MoreLikeThisQueryBuilder; +import org.elasticsearch.index.query.MultiFieldMatchQueryBuilder; import org.elasticsearch.index.query.MultiMatchQueryBuilder; import org.elasticsearch.index.query.NestedQueryBuilder; import org.elasticsearch.index.query.PrefixQueryBuilder; @@ -1190,6 +1191,9 @@ private void registerQueryParsers(List plugins) { registerQuery( new QuerySpec<>(RandomSamplingQueryBuilder.NAME, RandomSamplingQueryBuilder::new, RandomSamplingQueryBuilder::fromXContent) ); + registerQuery( + new QuerySpec<>(MultiFieldMatchQueryBuilder.NAME, MultiFieldMatchQueryBuilder::new, MultiFieldMatchQueryBuilder::fromXContent) + ); registerFromPlugin(plugins, SearchPlugin::getQueries, this::registerQuery); } From 36eea009e3c74f564ffb90a3ec8c6f9151d4ddc1 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Tue, 24 Jun 2025 10:28:45 -0400 Subject: [PATCH 32/33] Register query --- .../src/main/java/org/elasticsearch/search/SearchModule.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/main/java/org/elasticsearch/search/SearchModule.java b/server/src/main/java/org/elasticsearch/search/SearchModule.java index 56b203700b362..184ea69aad0c3 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchModule.java +++ b/server/src/main/java/org/elasticsearch/search/SearchModule.java @@ -44,6 +44,7 @@ import org.elasticsearch.index.query.MatchPhraseQueryBuilder; import org.elasticsearch.index.query.MatchQueryBuilder; import org.elasticsearch.index.query.MoreLikeThisQueryBuilder; +import org.elasticsearch.index.query.MultiFieldMatchQueryBuilder; import org.elasticsearch.index.query.MultiMatchQueryBuilder; import org.elasticsearch.index.query.NestedQueryBuilder; import org.elasticsearch.index.query.PrefixQueryBuilder; @@ -1137,6 +1138,9 @@ private void registerQueryParsers(List plugins) { registerQuery(new QuerySpec<>(SpanNearQueryBuilder.NAME, SpanNearQueryBuilder::new, SpanNearQueryBuilder::fromXContent)); registerQuery(new QuerySpec<>(SpanGapQueryBuilder.NAME, SpanGapQueryBuilder::new, SpanGapQueryBuilder::fromXContent)); registerQuery(new QuerySpec<>(SpanOrQueryBuilder.NAME, SpanOrQueryBuilder::new, SpanOrQueryBuilder::fromXContent)); + namedWriteables.add( + new NamedWriteableRegistry.Entry(QueryBuilder.class, MultiFieldMatchQueryBuilder.NAME, MultiFieldMatchQueryBuilder::new) + ); registerQuery( new QuerySpec<>(MoreLikeThisQueryBuilder.NAME, MoreLikeThisQueryBuilder::new, MoreLikeThisQueryBuilder::fromXContent) ); From c0c6d0a77d48618fc73b27e96a5fdbb7c459d4de Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Tue, 24 Jun 2025 15:26:49 -0400 Subject: [PATCH 33/33] remove duplicate registration --- .../src/main/java/org/elasticsearch/search/SearchModule.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/SearchModule.java b/server/src/main/java/org/elasticsearch/search/SearchModule.java index 15fac38b51ec1..184ea69aad0c3 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchModule.java +++ b/server/src/main/java/org/elasticsearch/search/SearchModule.java @@ -1194,9 +1194,6 @@ private void registerQueryParsers(List plugins) { registerQuery( new QuerySpec<>(RandomSamplingQueryBuilder.NAME, RandomSamplingQueryBuilder::new, RandomSamplingQueryBuilder::fromXContent) ); - registerQuery( - new QuerySpec<>(MultiFieldMatchQueryBuilder.NAME, MultiFieldMatchQueryBuilder::new, MultiFieldMatchQueryBuilder::fromXContent) - ); registerFromPlugin(plugins, SearchPlugin::getQueries, this::registerQuery); }