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..a941bf531a 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 @@ -27,7 +27,9 @@ public class OpenSearchFunctions { */ public void register(BuiltinFunctionRepository repository) { repository.register(match_bool_prefix()); - repository.register(match()); + repository.register(match(BuiltinFunctionName.MATCH)); + repository.register(match(BuiltinFunctionName.MATCHQUERY)); + repository.register(match(BuiltinFunctionName.MATCH_QUERY)); repository.register(multi_match()); repository.register(simple_query_string()); repository.register(query()); @@ -44,8 +46,8 @@ private static FunctionResolver match_bool_prefix() { return new RelevanceFunctionResolver(name, STRING); } - private static FunctionResolver match() { - FunctionName funcName = BuiltinFunctionName.MATCH.getName(); + private static FunctionResolver match(BuiltinFunctionName match) { + FunctionName funcName = match.getName(); return new RelevanceFunctionResolver(funcName, STRING); } diff --git a/docs/user/dql/functions.rst b/docs/user/dql/functions.rst index 788cac0433..b50fb89ff3 100644 --- a/docs/user/dql/functions.rst +++ b/docs/user/dql/functions.rst @@ -2730,6 +2730,99 @@ Another example to show how to set custom values for the optional parameters:: +------------+ +MATCHQUERY +----- + +Description +>>>>>>>>>>> + +``matchquery(field_expression, query_expression[, option=]*)`` + +The matchquery function maps to the match query used in search engine, to return the documents that match a provided text, number, date or boolean value with a given field. This is alternate syntax for the `match`_ function. Available parameters include: + +- analyzer +- auto_generate_synonyms_phrase +- fuzziness +- max_expansions +- prefix_length +- fuzzy_transpositions +- fuzzy_rewrite +- lenient +- operator +- minimum_should_match +- zero_terms_query +- boost + +For backwards compatibility, matchquery is supported and mapped to the match query. + +Example with only ``field`` and ``query`` expressions, and all other parameters are set default values:: + + os> SELECT lastname, address FROM accounts WHERE matchquery(address, 'Street'); + fetched rows / total rows = 2/2 + +------------+--------------------+ + | lastname | address | + |------------+--------------------| + | Bond | 671 Bristol Street | + | Bates | 789 Madison Street | + +------------+--------------------+ + +Another example to show how to set custom values for the optional parameters:: + + os> SELECT lastname FROM accounts WHERE matchquery(firstname, 'Hattie', operator='AND', boost=2.0); + fetched rows / total rows = 1/1 + +------------+ + | lastname | + |------------| + | Bond | + +------------+ + +MATCH_QUERY +----- + +Description +>>>>>>>>>>> + +``match_query(field_expression, query_expression[, option=]*)`` + +The match_query function maps to the match query used in search engine, to return the documents that match_query a provided text, number, date or boolean value with a given field. This is alternate syntax for the `match`_ function. Available parameters include: + +- analyzer +- auto_generate_synonyms_phrase +- fuzziness +- max_expansions +- prefix_length +- fuzzy_transpositions +- fuzzy_rewrite +- lenient +- operator +- minimum_should_match +- zero_terms_query +- boost + +For backwards compatibility, match_query is supported and mapped to the match query. + +Example with only ``field`` and ``query`` expressions, and all other parameters are set default values:: + + os> SELECT lastname, address FROM accounts WHERE match_query(address, 'Street'); + fetched rows / total rows = 2/2 + +------------+--------------------+ + | lastname | address | + |------------+--------------------| + | Bond | 671 Bristol Street | + | Bates | 789 Madison Street | + +------------+--------------------+ + +Another example to show how to set custom values for the optional parameters:: + + os> SELECT lastname FROM accounts WHERE match_query(firstname, 'Hattie', operator='AND', boost=2.0); + fetched rows / total rows = 1/1 + +------------+ + | lastname | + |------------| + | Bond | + +------------+ + + MATCH_PHRASE ------------ diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/MatchIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/MatchIT.java index 813f6b7a9c..09e3504e4a 100644 --- a/integ-test/src/test/java/org/opensearch/sql/sql/MatchIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/sql/MatchIT.java @@ -35,4 +35,48 @@ public void match_in_having() throws IOException { verifySchema(result, schema("lastname", "text")); verifyDataRows(result, rows("Bates")); } + + @Test + public void matchquery_in_where() throws IOException { + JSONObject result = executeJdbcRequest("SELECT firstname FROM " + TEST_INDEX_ACCOUNT + " WHERE matchquery(lastname, 'Bates')"); + verifySchema(result, schema("firstname", "text")); + verifyDataRows(result, rows("Nanette")); + } + + @Test + public void matchquery_in_having() throws IOException { + JSONObject result = executeJdbcRequest("SELECT lastname FROM " + TEST_INDEX_ACCOUNT + " HAVING matchquery(firstname, 'Nanette')"); + verifySchema(result, schema("lastname", "text")); + verifyDataRows(result, rows("Bates")); + } + + @Test + public void match_query_in_where() throws IOException { + JSONObject result = executeJdbcRequest("SELECT firstname FROM " + TEST_INDEX_ACCOUNT + " WHERE match_query(lastname, 'Bates')"); + verifySchema(result, schema("firstname", "text")); + verifyDataRows(result, rows("Nanette")); + } + + @Test + public void match_query_in_having() throws IOException { + JSONObject result = executeJdbcRequest( + "SELECT lastname FROM " + TEST_INDEX_ACCOUNT + " HAVING match_query(firstname, 'Nanette')"); + verifySchema(result, schema("lastname", "text")); + verifyDataRows(result, rows("Bates")); + } + + @Test + public void alternate_syntaxes_return_the_same_results() throws IOException { + String query1 = "SELECT lastname FROM " + + TEST_INDEX_ACCOUNT + " HAVING match(firstname, 'Nanette')"; + JSONObject result1 = executeJdbcRequest(query1); + String query2 = "SELECT lastname FROM " + + TEST_INDEX_ACCOUNT + " HAVING matchquery(firstname, 'Nanette')"; + JSONObject result2 = executeJdbcRequest(query2); + String query3 = "SELECT lastname FROM " + + TEST_INDEX_ACCOUNT + " HAVING match_query(firstname, 'Nanette')"; + JSONObject result3 = executeJdbcRequest(query3); + assertEquals(result1.getInt("total"), result2.getInt("total")); + assertEquals(result1.getInt("total"), result3.getInt("total")); + } } diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/MatchQueryTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/MatchQueryTest.java index 60dd938f78..5b1684dac9 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/MatchQueryTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/MatchQueryTest.java @@ -32,7 +32,11 @@ public class MatchQueryTest { private final DSL dsl = new ExpressionConfig().dsl(new ExpressionConfig().functionRepository()); private final MatchQuery matchQuery = new MatchQuery(); - private final FunctionName match = FunctionName.of("match"); + private final FunctionName matchName = FunctionName.of("match"); + private final FunctionName matchQueryName = FunctionName.of("matchquery"); + private final FunctionName matchQueryWithUnderscoreName = FunctionName.of("match_query"); + private final FunctionName[] functionNames = + {matchName,matchQueryName, matchQueryWithUnderscoreName}; static Stream> generateValidData() { final DSL dsl = new ExpressionConfig().dsl(new ExpressionConfig().functionRepository()); @@ -139,13 +143,87 @@ public void test_SemanticCheckException_when_invalid_parameter() { () -> matchQuery.build(new MatchExpression(arguments))); } + @ParameterizedTest + @MethodSource("generateValidData") + public void test_valid_parameters_matchquery_syntax(List validArgs) { + Assertions.assertNotNull(matchQuery.build( + new MatchExpression(validArgs, MatchQueryTest.this.matchQueryName))); + } + + @Test + public void test_SyntaxCheckException_when_no_arguments_matchquery_syntax() { + List arguments = List.of(); + assertThrows(SyntaxCheckException.class, + () -> matchQuery.build( + new MatchExpression(arguments, MatchQueryTest.this.matchQueryName))); + } + + @Test + public void test_SyntaxCheckException_when_one_argument_matchquery_syntax() { + List arguments = List.of(namedArgument("field", "field_value")); + assertThrows(SyntaxCheckException.class, + () -> matchQuery.build( + new MatchExpression(arguments, MatchQueryTest.this.matchQueryName))); + } + + @Test + public void test_SemanticCheckException_when_invalid_parameter_matchquery_syntax() { + List arguments = List.of( + namedArgument("field", "field_value"), + namedArgument("query", "query_value"), + namedArgument("unsupported", "unsupported_value")); + Assertions.assertThrows(SemanticCheckException.class, + () -> matchQuery.build( + new MatchExpression(arguments, MatchQueryTest.this.matchQueryName))); + } + + @ParameterizedTest + @MethodSource("generateValidData") + public void test_valid_parameters_match_query_syntax(List validArgs) { + Assertions.assertNotNull(matchQuery.build( + new MatchExpression(validArgs, MatchQueryTest.this.matchQueryWithUnderscoreName))); + } + + @Test + public void test_SyntaxCheckException_when_no_arguments_match_query_syntax() { + List arguments = List.of(); + assertThrows(SyntaxCheckException.class, + () -> matchQuery.build( + new MatchExpression(arguments, MatchQueryTest.this.matchQueryWithUnderscoreName))); + } + + @Test + public void test_SyntaxCheckException_when_one_argument_match_query_syntax() { + List arguments = List.of(namedArgument("field", "field_value")); + assertThrows(SyntaxCheckException.class, + () -> matchQuery.build( + new MatchExpression(arguments, MatchQueryTest.this.matchQueryWithUnderscoreName))); + } + + @Test + public void test_SemanticCheckException_when_invalid_parameter_match_query_syntax() { + List arguments = List.of( + namedArgument("field", "field_value"), + namedArgument("query", "query_value"), + namedArgument("unsupported", "unsupported_value")); + Assertions.assertThrows(SemanticCheckException.class, + () -> matchQuery.build( + new MatchExpression(arguments, MatchQueryTest.this.matchQueryWithUnderscoreName))); + } + + private NamedArgumentExpression namedArgument(String name, String value) { return dsl.namedArgument(name, DSL.literal(value)); } private class MatchExpression extends FunctionExpression { + public MatchExpression(List arguments) { - super(MatchQueryTest.this.match, arguments); + super(MatchQueryTest.this.matchName, arguments); + } + + public MatchExpression(List arguments, FunctionName funcName) { + super(funcName, arguments); } @Override diff --git a/sql/src/main/antlr/OpenSearchSQLParser.g4 b/sql/src/main/antlr/OpenSearchSQLParser.g4 index c803f2b5c3..0cb445edfe 100644 --- a/sql/src/main/antlr/OpenSearchSQLParser.g4 +++ b/sql/src/main/antlr/OpenSearchSQLParser.g4 @@ -425,7 +425,8 @@ systemFunctionName ; singleFieldRelevanceFunctionName - : MATCH | MATCH_PHRASE | MATCHPHRASE + : MATCH | MATCHQUERY | MATCH_QUERY + | MATCH_PHRASE | MATCHPHRASE | MATCH_BOOL_PREFIX | MATCH_PHRASE_PREFIX ; @@ -435,10 +436,6 @@ multiFieldRelevanceFunctionName | QUERY_STRING ; -legacyRelevanceFunctionName - : QUERY | MATCH_QUERY | MATCHQUERY - ; - functionArgs : (functionArg (COMMA functionArg)*)? ; 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..0d999640b0 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 @@ -375,7 +375,31 @@ public void can_parse_match_relevance_function() { assertNotNull(parser.parse("SELECT * FROM test WHERE match(`column`, \"this is a test\")")); assertNotNull(parser.parse("SELECT * FROM test WHERE match(`column`, 'this is a test')")); assertNotNull(parser.parse("SELECT * FROM test WHERE match(column, 100500)")); + } + + @Test + public void can_parse_matchquery_relevance_function() { + assertNotNull(parser.parse("SELECT * FROM test WHERE matchquery(column, \"this is a test\")")); + assertNotNull(parser.parse("SELECT * FROM test WHERE matchquery(column, 'this is a test')")); + assertNotNull(parser.parse( + "SELECT * FROM test WHERE matchquery(`column`, \"this is a test\")")); + assertNotNull(parser.parse("SELECT * FROM test WHERE matchquery(`column`, 'this is a test')")); + assertNotNull(parser.parse("SELECT * FROM test WHERE matchquery(column, 100500)")); + } + @Test + public void can_parse_match_query_relevance_function() { + assertNotNull(parser.parse( + "SELECT * FROM test WHERE match_query(column, \"this is a test\")")); + assertNotNull(parser.parse("SELECT * FROM test WHERE match_query(column, 'this is a test')")); + assertNotNull(parser.parse( + "SELECT * FROM test WHERE match_query(`column`, \"this is a test\")")); + assertNotNull(parser.parse("SELECT * FROM test WHERE match_query(`column`, 'this is a test')")); + assertNotNull(parser.parse("SELECT * FROM test WHERE match_query(column, 100500)")); + } + + @Test + public void can_parse_match_phrase_relevance_function() { assertNotNull( parser.parse("SELECT * FROM test WHERE match_phrase(column, \"this is a test\")")); assertNotNull(parser.parse("SELECT * FROM test WHERE match_phrase(column, 'this is a test')")); 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..f71717e1b9 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 @@ -485,6 +485,38 @@ public void relevanceMatch() { buildExprAst("match('message', 'search query', analyzer='keyword', operator='AND')")); } + @Test + public void relevanceMatchQuery() { + assertEquals(AstDSL.function("matchquery", + unresolvedArg("field", stringLiteral("message")), + unresolvedArg("query", stringLiteral("search query"))), + buildExprAst("matchquery('message', 'search query')") + ); + + assertEquals(AstDSL.function("matchquery", + unresolvedArg("field", stringLiteral("message")), + unresolvedArg("query", stringLiteral("search query")), + unresolvedArg("analyzer", stringLiteral("keyword")), + unresolvedArg("operator", stringLiteral("AND"))), + buildExprAst("matchquery('message', 'search query', analyzer='keyword', operator='AND')")); + } + + @Test + public void relevanceMatch_Query() { + assertEquals(AstDSL.function("match_query", + unresolvedArg("field", stringLiteral("message")), + unresolvedArg("query", stringLiteral("search query"))), + buildExprAst("match_query('message', 'search query')") + ); + + assertEquals(AstDSL.function("match_query", + unresolvedArg("field", stringLiteral("message")), + unresolvedArg("query", stringLiteral("search query")), + unresolvedArg("analyzer", stringLiteral("keyword")), + unresolvedArg("operator", stringLiteral("AND"))), + buildExprAst("match_query('message', 'search query', analyzer='keyword', operator='AND')")); + } + @Test public void relevanceMulti_match() { assertEquals(AstDSL.function("multi_match",