diff --git a/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java index cc3db47982..2696ca62b5 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java @@ -221,7 +221,9 @@ public enum BuiltinFunctionName { QUERY(FunctionName.of("query")), MATCH_QUERY(FunctionName.of("match_query")), MATCHQUERY(FunctionName.of("matchquery")), - MULTI_MATCH(FunctionName.of("multi_match")); + MULTI_MATCH(FunctionName.of("multi_match")), + MULTIMATCH(FunctionName.of("multimatch")), + MULTIMATCHQUERY(FunctionName.of("multimatchquery")); private final FunctionName name; diff --git a/core/src/main/java/org/opensearch/sql/expression/function/OpenSearchFunctions.java b/core/src/main/java/org/opensearch/sql/expression/function/OpenSearchFunctions.java index 97afe3675e..1364559482 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/OpenSearchFunctions.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/OpenSearchFunctions.java @@ -28,7 +28,9 @@ public class OpenSearchFunctions { public void register(BuiltinFunctionRepository repository) { repository.register(match_bool_prefix()); repository.register(match()); - repository.register(multi_match()); + repository.register(multi_match(BuiltinFunctionName.MULTI_MATCH)); + repository.register(multi_match(BuiltinFunctionName.MULTIMATCH)); + repository.register(multi_match(BuiltinFunctionName.MULTIMATCHQUERY)); repository.register(simple_query_string()); repository.register(query()); repository.register(query_string()); @@ -59,9 +61,8 @@ private static FunctionResolver match_phrase(BuiltinFunctionName matchPhrase) { return new RelevanceFunctionResolver(funcName, STRING); } - private static FunctionResolver multi_match() { - FunctionName funcName = BuiltinFunctionName.MULTI_MATCH.getName(); - return new RelevanceFunctionResolver(funcName, STRUCT); + private static FunctionResolver multi_match(BuiltinFunctionName multiMatchName) { + return new RelevanceFunctionResolver(multiMatchName.getName(), STRUCT); } private static FunctionResolver simple_query_string() { diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/MultiMatchIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/MultiMatchIT.java index ba62e6629f..24ce45fd20 100644 --- a/integ-test/src/test/java/org/opensearch/sql/sql/MultiMatchIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/sql/MultiMatchIT.java @@ -6,6 +6,8 @@ package org.opensearch.sql.sql; import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_BEER; +import static org.opensearch.sql.util.MatcherUtils.rows; +import static org.opensearch.sql.util.MatcherUtils.verifyDataRows; import java.io.IOException; import org.json.JSONObject; @@ -27,38 +29,99 @@ public void init() throws IOException { */ @Test - public void test_mandatory_params() throws IOException { + public void test_mandatory_params() { String query = "SELECT Id FROM " + TEST_INDEX_BEER + " WHERE multi_match([\\\"Tags\\\" ^ 1.5, Title, `Body` 4.2], 'taste')"; - var result = new JSONObject(executeQuery(query, "jdbc")); + JSONObject result = executeJdbcRequest(query); assertEquals(16, result.getInt("total")); } @Test - public void test_all_params() throws IOException { + public void test_all_params() { String query = "SELECT Id FROM " + TEST_INDEX_BEER + " WHERE multi_match(['Body', Tags], 'taste beer', operator='and', analyzer=english," + "auto_generate_synonyms_phrase_query=true, boost = 0.77, cutoff_frequency=0.33," + "fuzziness = 'AUTO:1,5', fuzzy_transpositions = false, lenient = true, max_expansions = 25," + "minimum_should_match = '2<-25% 9<-3', prefix_length = 7, tie_breaker = 0.3," + "type = most_fields, slop = 2, zero_terms_query = 'ALL');"; - var result = new JSONObject(executeQuery(query, "jdbc")); + JSONObject result = executeJdbcRequest(query); assertEquals(10, result.getInt("total")); } @Test - public void verify_wildcard_test() throws IOException { + public void verify_wildcard_test() { String query1 = "SELECT Id FROM " + TEST_INDEX_BEER + " WHERE multi_match(['Tags'], 'taste')"; - var result1 = new JSONObject(executeQuery(query1, "jdbc")); + JSONObject result1 = executeJdbcRequest(query1); String query2 = "SELECT Id FROM " + TEST_INDEX_BEER + " WHERE multi_match(['T*'], 'taste')"; - var result2 = new JSONObject(executeQuery(query2, "jdbc")); + JSONObject result2 = executeJdbcRequest(query2); assertNotEquals(result2.getInt("total"), result1.getInt("total")); String query = "SELECT Id FROM " + TEST_INDEX_BEER + " WHERE multi_match(['*Date'], '2014-01-22');"; - var result = new JSONObject(executeQuery(query, "jdbc")); + JSONObject result = executeJdbcRequest(query); assertEquals(10, result.getInt("total")); } + + @Test + public void test_multimatch_alternate_parameter_syntax() { + String query = "SELECT Tags FROM " + TEST_INDEX_BEER + + " WHERE multimatch('query'='taste', 'fields'='Tags')"; + JSONObject result = executeJdbcRequest(query); + assertEquals(8, result.getInt("total")); + } + + @Test + public void test_multimatchquery_alternate_parameter_syntax() { + String query = "SELECT Tags FROM " + TEST_INDEX_BEER + + " WHERE multimatchquery(query='cicerone', fields='Tags')"; + JSONObject result = executeJdbcRequest(query); + assertEquals(2, result.getInt("total")); + verifyDataRows(result, rows("serving cicerone restaurants"), + rows("taste cicerone")); + } + + @Test + public void test_quoted_multi_match_alternate_parameter_syntax() { + String query = "SELECT Tags FROM " + TEST_INDEX_BEER + + " WHERE multi_match('query'='cicerone', 'fields'='Tags')"; + JSONObject result = executeJdbcRequest(query); + assertEquals(2, result.getInt("total")); + verifyDataRows(result, rows("serving cicerone restaurants"), + rows("taste cicerone")); + } + + @Test + public void test_multi_match_alternate_parameter_syntax() { + String query = "SELECT Tags FROM " + TEST_INDEX_BEER + + " WHERE multi_match(query='cicerone', fields='Tags')"; + JSONObject result = executeJdbcRequest(query); + assertEquals(2, result.getInt("total")); + verifyDataRows(result, rows("serving cicerone restaurants"), + rows("taste cicerone")); + } + + @Test + public void test_wildcard_multi_match_alternate_parameter_syntax() { + String query = "SELECT Body FROM " + TEST_INDEX_BEER + + " WHERE multi_match(query='IPA', fields='B*') LIMIT 1"; + JSONObject result = executeJdbcRequest(query); + verifyDataRows(result, rows("

I know what makes an IPA an IPA, but what are the unique" + + " characteristics of it's common variants? To be specific, the ones I'm interested in are Double IPA" + + " and Black IPA, but general differences between any other styles would be welcome too.

\n")); + } + + @Test + public void test_all_params_multimatchquery_alternate_parameter_syntax() { + String query = "SELECT Id FROM " + TEST_INDEX_BEER + + " WHERE multimatchquery(query='cicerone', fields='Tags', 'operator'='or', analyzer=english," + + "auto_generate_synonyms_phrase_query=true, boost = 0.77, cutoff_frequency=0.33," + + "fuzziness = 'AUTO:1,5', fuzzy_transpositions = false, lenient = true, max_expansions = 25," + + "minimum_should_match = '2<-25% 9<-3', prefix_length = 7, tie_breaker = 0.3," + + "type = most_fields, slop = 2, zero_terms_query = 'ALL');"; + + JSONObject result = executeJdbcRequest(query); + assertEquals(2, result.getInt("total")); + } } diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/FilterQueryBuilder.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/FilterQueryBuilder.java index ab8fb562da..b754ea7112 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/FilterQueryBuilder.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/FilterQueryBuilder.java @@ -65,6 +65,8 @@ public class FilterQueryBuilder extends ExpressionNodeVisitor(ImmutableMap.of( "title", ExprValueUtils.floatValue(1.F), @@ -129,27 +131,69 @@ static Stream> generateValidData() { @ParameterizedTest @MethodSource("generateValidData") - public void test_valid_parameters(List validArgs) { + public void test_valid_parameters_multiMatch(List validArgs) { Assertions.assertNotNull(multiMatchQuery.build( new MultiMatchExpression(validArgs))); } + @ParameterizedTest + @MethodSource("generateValidData") + public void test_valid_parameters_multi_match(List validArgs) { + Assertions.assertNotNull(multiMatchQuery.build( + new MultiMatchExpression(validArgs, snakeCaseMultiMatchName))); + } + + @ParameterizedTest + @MethodSource("generateValidData") + public void test_valid_parameters_multiMatchQuery(List validArgs) { + Assertions.assertNotNull(multiMatchQuery.build( + new MultiMatchExpression(validArgs, multiMatchQueryName))); + } + @Test - public void test_SyntaxCheckException_when_no_arguments() { + public void test_SyntaxCheckException_when_no_arguments_multiMatch() { List arguments = List.of(); assertThrows(SyntaxCheckException.class, () -> multiMatchQuery.build(new MultiMatchExpression(arguments))); } @Test - public void test_SyntaxCheckException_when_one_argument() { + public void test_SyntaxCheckException_when_no_arguments_multi_match() { + List arguments = List.of(); + assertThrows(SyntaxCheckException.class, + () -> multiMatchQuery.build(new MultiMatchExpression(arguments, multiMatchName))); + } + + @Test + public void test_SyntaxCheckException_when_no_arguments_multiMatchQuery() { + List arguments = List.of(); + assertThrows(SyntaxCheckException.class, + () -> multiMatchQuery.build(new MultiMatchExpression(arguments, multiMatchQueryName))); + } + + @Test + public void test_SyntaxCheckException_when_one_argument_multiMatch() { List arguments = List.of(namedArgument("fields", fields_value)); assertThrows(SyntaxCheckException.class, () -> multiMatchQuery.build(new MultiMatchExpression(arguments))); } @Test - public void test_SemanticCheckException_when_invalid_parameter() { + public void test_SyntaxCheckException_when_one_argument_multi_match() { + List arguments = List.of(namedArgument("fields", fields_value)); + assertThrows(SyntaxCheckException.class, + () -> multiMatchQuery.build(new MultiMatchExpression(arguments, snakeCaseMultiMatchName))); + } + + @Test + public void test_SyntaxCheckException_when_one_argument_multiMatchQuery() { + List arguments = List.of(namedArgument("fields", fields_value)); + assertThrows(SyntaxCheckException.class, + () -> multiMatchQuery.build(new MultiMatchExpression(arguments, multiMatchQueryName))); + } + + @Test + public void test_SemanticCheckException_when_invalid_parameter_multiMatch() { List arguments = List.of( namedArgument("fields", fields_value), namedArgument("query", query_value), @@ -158,15 +202,40 @@ public void test_SemanticCheckException_when_invalid_parameter() { () -> multiMatchQuery.build(new MultiMatchExpression(arguments))); } + @Test + public void test_SemanticCheckException_when_invalid_parameter_multi_match() { + List arguments = List.of( + namedArgument("fields", fields_value), + namedArgument("query", query_value), + DSL.namedArgument("unsupported", "unsupported_value")); + Assertions.assertThrows(SemanticCheckException.class, + () -> multiMatchQuery.build(new MultiMatchExpression(arguments, snakeCaseMultiMatchName))); + } + + @Test + public void test_SemanticCheckException_when_invalid_parameter_multiMatchQuery() { + List arguments = List.of( + namedArgument("fields", fields_value), + namedArgument("query", query_value), + DSL.namedArgument("unsupported", "unsupported_value")); + Assertions.assertThrows(SemanticCheckException.class, + () -> multiMatchQuery.build(new MultiMatchExpression(arguments, multiMatchQueryName))); + } + private NamedArgumentExpression namedArgument(String name, LiteralExpression value) { return DSL.namedArgument(name, value); } private class MultiMatchExpression extends FunctionExpression { public MultiMatchExpression(List arguments) { - super(MultiMatchTest.this.multiMatch, arguments); + super(multiMatchName, arguments); } + public MultiMatchExpression(List arguments, FunctionName funcName) { + super(funcName, arguments); + } + + @Override public ExprValue valueOf(Environment valueEnv) { throw new UnsupportedOperationException("Invalid function call, " diff --git a/sql/src/main/antlr/OpenSearchSQLLexer.g4 b/sql/src/main/antlr/OpenSearchSQLLexer.g4 index 9e0a409401..3df9cd95ba 100644 --- a/sql/src/main/antlr/OpenSearchSQLLexer.g4 +++ b/sql/src/main/antlr/OpenSearchSQLLexer.g4 @@ -305,6 +305,7 @@ MINUTE_OF_HOUR: 'MINUTE_OF_HOUR'; MONTH_OF_YEAR: 'MONTH_OF_YEAR'; MULTIMATCH: 'MULTIMATCH'; MULTI_MATCH: 'MULTI_MATCH'; +MULTIMATCHQUERY: 'MULTIMATCHQUERY'; NESTED: 'NESTED'; PERCENTILES: 'PERCENTILES'; REGEXP_QUERY: 'REGEXP_QUERY'; diff --git a/sql/src/main/antlr/OpenSearchSQLParser.g4 b/sql/src/main/antlr/OpenSearchSQLParser.g4 index b3fd29b342..da30059d6f 100644 --- a/sql/src/main/antlr/OpenSearchSQLParser.g4 +++ b/sql/src/main/antlr/OpenSearchSQLParser.g4 @@ -347,6 +347,8 @@ multiFieldRelevanceFunction : multiFieldRelevanceFunctionName LR_BRACKET LT_SQR_PRTHS field=relevanceFieldAndWeight (COMMA field=relevanceFieldAndWeight)* RT_SQR_PRTHS COMMA query=relevanceQuery (COMMA relevanceArg)* RR_BRACKET + | multiFieldRelevanceFunctionName LR_BRACKET + alternateMultiMatchQuery COMMA alternateMultiMatchField (COMMA relevanceArg)* RR_BRACKET ; convertedDataType @@ -431,6 +433,8 @@ singleFieldRelevanceFunctionName multiFieldRelevanceFunctionName : MULTI_MATCH + | MULTIMATCH + | MULTIMATCHQUERY | SIMPLE_QUERY_STRING | QUERY_STRING ; @@ -449,6 +453,7 @@ functionArg relevanceArg : relevanceArgName EQUAL_SYMBOL relevanceArgValue + | argName=stringLiteral EQUAL_SYMBOL argVal=relevanceArgValue ; highlightArg @@ -498,3 +503,18 @@ highlightArgValue : stringLiteral ; +alternateMultiMatchArgName + : FIELDS + | QUERY + | stringLiteral + ; + +alternateMultiMatchQuery + : argName=alternateMultiMatchArgName EQUAL_SYMBOL argVal=relevanceArgValue + ; + +alternateMultiMatchField + : argName=alternateMultiMatchArgName EQUAL_SYMBOL argVal=relevanceArgValue + | argName=alternateMultiMatchArgName EQUAL_SYMBOL + LT_SQR_PRTHS argVal=relevanceArgValue RT_SQR_PRTHS + ; diff --git a/sql/src/main/java/org/opensearch/sql/sql/parser/AstExpressionBuilder.java b/sql/src/main/java/org/opensearch/sql/sql/parser/AstExpressionBuilder.java index 131b6d9116..1879d62142 100644 --- a/sql/src/main/java/org/opensearch/sql/sql/parser/AstExpressionBuilder.java +++ b/sql/src/main/java/org/opensearch/sql/sql/parser/AstExpressionBuilder.java @@ -52,7 +52,9 @@ import com.google.common.collect.ImmutableMap; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import org.antlr.v4.runtime.RuleContext; import org.apache.commons.lang3.tuple.ImmutablePair; @@ -80,6 +82,7 @@ import org.opensearch.sql.ast.expression.WindowFunction; import org.opensearch.sql.ast.tree.Sort.SortOption; import org.opensearch.sql.common.utils.StringUtils; +import org.opensearch.sql.expression.function.BuiltinFunctionName; import org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser; import org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.AndExpressionContext; import org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.ColumnNameContext; @@ -399,9 +402,22 @@ public UnresolvedExpression visitSingleFieldRelevanceFunction( @Override public UnresolvedExpression visitMultiFieldRelevanceFunction( MultiFieldRelevanceFunctionContext ctx) { - return new Function( - ctx.multiFieldRelevanceFunctionName().getText().toLowerCase(), - multiFieldRelevanceArguments(ctx)); + // To support alternate syntax for MULTI_MATCH like + // 'MULTI_MATCH('query'='query_val', 'fields'='*fields_val')' + String funcName = StringUtils.unquoteText(ctx.multiFieldRelevanceFunctionName().getText()); + if ((funcName.equalsIgnoreCase(BuiltinFunctionName.MULTI_MATCH.toString()) + || funcName.equalsIgnoreCase(BuiltinFunctionName.MULTIMATCH.toString()) + || funcName.equalsIgnoreCase(BuiltinFunctionName.MULTIMATCHQUERY.toString())) + && ! ctx.getRuleContexts(OpenSearchSQLParser.AlternateMultiMatchQueryContext.class) + .isEmpty()) { + return new Function( + ctx.multiFieldRelevanceFunctionName().getText().toLowerCase(), + alternateMultiMatchArguments(ctx)); + } else { + return new Function( + ctx.multiFieldRelevanceFunctionName().getText().toLowerCase(), + multiFieldRelevanceArguments(ctx)); + } } private Function visitFunction(String functionName, FunctionArgsContext args) { @@ -439,9 +455,13 @@ private QualifiedName visitIdentifiers(List identifiers) { private void fillRelevanceArgs(List args, ImmutableList.Builder builder) { - args.forEach(v -> builder.add(new UnresolvedArgument( - v.relevanceArgName().getText().toLowerCase(), new Literal(StringUtils.unquoteText( - v.relevanceArgValue().getText()), DataType.STRING)))); + // To support old syntax we must support argument keys as quoted strings. + args.forEach(v -> builder.add(v.argName == null + ? new UnresolvedArgument(v.relevanceArgName().getText().toLowerCase(), + new Literal(StringUtils.unquoteText(v.relevanceArgValue().getText()), + DataType.STRING)) + : new UnresolvedArgument(StringUtils.unquoteText(v.argName.getText()).toLowerCase(), + new Literal(StringUtils.unquoteText(v.argVal.getText()), DataType.STRING)))); } private List noFieldRelevanceArguments( @@ -487,4 +507,41 @@ private List multiFieldRelevanceArguments( fillRelevanceArgs(ctx.relevanceArg(), builder); return builder.build(); } + + /** + * Adds support for multi_match alternate syntax like + * MULTI_MATCH('query'='Dale', 'fields'='*name'). + * @param ctx : Context for multi field relevance function. + * @return : Returns list of all arguments for relevance function. + */ + private List alternateMultiMatchArguments( + OpenSearchSQLParser.MultiFieldRelevanceFunctionContext ctx) { + // all the arguments are defaulted to string values + // to skip environment resolving and function signature resolving + ImmutableList.Builder builder = ImmutableList.builder(); + Map fieldAndWeightMap = new HashMap<>(); + + String[] fieldAndWeights = StringUtils.unquoteText( + ctx.getRuleContexts(OpenSearchSQLParser.AlternateMultiMatchFieldContext.class) + .stream().findFirst().get().argVal.getText()).split(","); + + for (var fieldAndWeight : fieldAndWeights) { + String[] splitFieldAndWeights = fieldAndWeight.split("\\^"); + fieldAndWeightMap.put(splitFieldAndWeights[0], + splitFieldAndWeights.length > 1 ? Float.parseFloat(splitFieldAndWeights[1]) : 1F); + } + builder.add(new UnresolvedArgument("fields", + new RelevanceFieldList(fieldAndWeightMap))); + + ctx.getRuleContexts(OpenSearchSQLParser.AlternateMultiMatchQueryContext.class) + .stream().findFirst().ifPresent( + arg -> + builder.add(new UnresolvedArgument("query", + new Literal(StringUtils.unquoteText(arg.argVal.getText()), DataType.STRING))) + ); + + fillRelevanceArgs(ctx.relevanceArg(), builder); + + return builder.build(); + } } diff --git a/sql/src/test/java/org/opensearch/sql/sql/antlr/SQLSyntaxParserTest.java b/sql/src/test/java/org/opensearch/sql/sql/antlr/SQLSyntaxParserTest.java index 6b78376d45..c5676c2b6e 100644 --- a/sql/src/test/java/org/opensearch/sql/sql/antlr/SQLSyntaxParserTest.java +++ b/sql/src/test/java/org/opensearch/sql/sql/antlr/SQLSyntaxParserTest.java @@ -193,6 +193,16 @@ public void can_parse_now_like_functions(String name, Boolean hasFsp, Boolean ha @Test public void can_parse_multi_match_relevance_function() { + assertNotNull(parser.parse( + "SELECT id FROM test WHERE multimatch(\"fields\"=\"field\", query=\"query\")")); + assertNotNull(parser.parse( + "SELECT id FROM test WHERE multimatchquery(fields=\"field\", \"query\"=\"query\")")); + assertNotNull(parser.parse( + "SELECT id FROM test WHERE multi_match(\"fields\"=\"field\", \"query\"=\"query\")")); + assertNotNull(parser.parse( + "SELECT id FROM test WHERE multi_match(\'fields\'=\'field\', \'query\'=\'query\')")); + assertNotNull(parser.parse( + "SELECT id FROM test WHERE multi_match(fields=\'field\', query=\'query\')")); assertNotNull(parser.parse( "SELECT id FROM test WHERE multi_match(['address'], 'query')")); assertNotNull(parser.parse( diff --git a/sql/src/test/java/org/opensearch/sql/sql/parser/AstExpressionBuilderTest.java b/sql/src/test/java/org/opensearch/sql/sql/parser/AstExpressionBuilderTest.java index cb00ea2f18..8db5ae79c9 100644 --- a/sql/src/test/java/org/opensearch/sql/sql/parser/AstExpressionBuilderTest.java +++ b/sql/src/test/java/org/opensearch/sql/sql/parser/AstExpressionBuilderTest.java @@ -501,9 +501,47 @@ public void relevanceMulti_match() { unresolvedArg("analyzer", stringLiteral("keyword")), unresolvedArg("operator", stringLiteral("AND"))), buildExprAst("multi_match(['field1', 'field2' ^ 3.2], 'search query'," + + "analyzer='keyword', 'operator'='AND')")); + } + + @Test + public void relevanceMultimatch_alternate_parameter_syntax() { + assertEquals(AstDSL.function("multimatch", + unresolvedArg("fields", new RelevanceFieldList(ImmutableMap.of( + "field1", 1F, "field2", 2F))), + unresolvedArg("query", stringLiteral("search query"))), + buildExprAst("multimatch(query='search query', fields=['field1^1.0,field2^2.0'])") + ); + + assertEquals(AstDSL.function("multimatch", + unresolvedArg("fields", new RelevanceFieldList(ImmutableMap.of( + "field1", 1F, "field2", 2F))), + unresolvedArg("query", stringLiteral("search query")), + unresolvedArg("analyzer", stringLiteral("keyword")), + unresolvedArg("operator", stringLiteral("AND"))), + buildExprAst("multimatch(query='search query', fields=['field1^1.0,field2^2.0']," + "analyzer='keyword', operator='AND')")); } + @Test + public void relevanceMultimatchquery_alternate_parameter_syntax() { + assertEquals(AstDSL.function("multimatchquery", + unresolvedArg("fields", new RelevanceFieldList(ImmutableMap.of( + "field", 1F))), + unresolvedArg("query", stringLiteral("search query"))), + buildExprAst("multimatchquery(query='search query', fields='field')") + ); + + assertEquals(AstDSL.function("multimatchquery", + unresolvedArg("fields", new RelevanceFieldList(ImmutableMap.of( + "field", 1F))), + unresolvedArg("query", stringLiteral("search query")), + unresolvedArg("analyzer", stringLiteral("keyword")), + unresolvedArg("operator", stringLiteral("AND"))), + buildExprAst("multimatchquery(query='search query', fields='field'," + + "analyzer='keyword', 'operator'='AND')")); + } + @Test public void relevanceSimple_query_string() { assertEquals(AstDSL.function("simple_query_string",