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..9dcd73a427 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 @@ -211,6 +211,7 @@ public enum BuiltinFunctionName { SIMPLE_QUERY_STRING(FunctionName.of("simple_query_string")), MATCH_PHRASE(FunctionName.of("match_phrase")), MATCHPHRASE(FunctionName.of("matchphrase")), + MATCHPHRASEQUERY(FunctionName.of("matchphrasequery")), QUERY_STRING(FunctionName.of("query_string")), MATCH_BOOL_PREFIX(FunctionName.of("match_bool_prefix")), HIGHLIGHT(FunctionName.of("highlight")), 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 a941bf531a..83d69062e1 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 @@ -38,6 +38,7 @@ public void register(BuiltinFunctionRepository repository) { // compatibility. repository.register(match_phrase(BuiltinFunctionName.MATCH_PHRASE)); repository.register(match_phrase(BuiltinFunctionName.MATCHPHRASE)); + repository.register(match_phrase(BuiltinFunctionName.MATCHPHRASEQUERY)); repository.register(match_phrase_prefix()); } diff --git a/docs/user/dql/functions.rst b/docs/user/dql/functions.rst index 200570dc3a..0644b970f5 100644 --- a/docs/user/dql/functions.rst +++ b/docs/user/dql/functions.rst @@ -2817,7 +2817,7 @@ The match_phrase function maps to the match_phrase query used in search engine, - slop - zero_terms_query -For backward compatibility, matchphrase is also supported and mapped to match_phrase query as well. +`matchphrase` and `matchphrasequery` are synonyms for `match_phrase`_ Example with only ``field`` and ``query`` expressions, and all other parameters are set default values:: diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/MatchPhraseIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/MatchPhraseIT.java index 0773238948..b870a60604 100644 --- a/integ-test/src/test/java/org/opensearch/sql/sql/MatchPhraseIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/sql/MatchPhraseIT.java @@ -36,10 +36,29 @@ public void test_matchphrase_legacy_function() throws IOException { verifyDataRows(result, rows("quick fox"), rows("quick fox here")); } + @Test + public void test_matchphrasequery_legacy_function() throws IOException { + String query = "SELECT phrase FROM %s WHERE matchphrasequery(phrase, 'quick fox')"; + JSONObject result = executeJdbcRequest(String.format(query, TEST_INDEX_PHRASE)); + verifyDataRows(result, rows("quick fox"), rows("quick fox here")); + } + @Test public void test_match_phrase_with_slop() throws IOException { String query = "SELECT phrase FROM %s WHERE match_phrase(phrase, 'brown fox', slop = 2)"; JSONObject result = executeJdbcRequest(String.format(query, TEST_INDEX_PHRASE)); verifyDataRows(result, rows("brown fox"), rows("fox brown")); } + + @Test + public void test_alternate_syntax_for_match_phrase_returns_same_result() throws IOException { + String query1 = "SELECT phrase FROM %s WHERE matchphrase(phrase, 'quick fox')"; + String query2 = "SELECT phrase FROM %s WHERE match_phrase(phrase, 'quick fox')"; + String query3 = "SELECT phrase FROM %s WHERE matchphrasequery(phrase, 'quick fox')"; + JSONObject result1 = executeJdbcRequest(String.format(query1, TEST_INDEX_PHRASE)); + JSONObject result2 = executeJdbcRequest(String.format(query2, TEST_INDEX_PHRASE)); + JSONObject result3 = executeJdbcRequest(String.format(query3, TEST_INDEX_PHRASE)); + assertTrue(result1.similar(result2)); + assertTrue(result1.similar(result3)); + } } 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..843096eee0 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 @@ -61,6 +61,7 @@ public class FilterQueryBuilder extends ExpressionNodeVisitor arguments = List.of(); + assertThrows(SyntaxCheckException.class, + () -> matchPhraseQuery.build(new MatchPhraseExpression( + arguments, matchPhraseWithUnderscoreName))); + } + + @Test + public void test_SyntaxCheckException_when_one_argument_match_phrase_syntax() { + List arguments = List.of(DSL.namedArgument("field", "test")); + assertThrows(SyntaxCheckException.class, + () -> matchPhraseQuery.build(new MatchPhraseExpression( + arguments, matchPhraseWithUnderscoreName))); + + } + + @Test + public void test_SyntaxCheckException_when_invalid_parameter_match_phrase_syntax() { + List arguments = List.of( + DSL.namedArgument("field", "test"), + DSL.namedArgument("query", "test2"), + DSL.namedArgument("unsupported", "3")); + Assertions.assertThrows(SemanticCheckException.class, + () -> matchPhraseQuery.build(new MatchPhraseExpression( + arguments, matchPhraseWithUnderscoreName))); + } + + @Test + public void test_analyzer_parameter_match_phrase_syntax() { + List arguments = List.of( + DSL.namedArgument("field", "t1"), + DSL.namedArgument("query", "t2"), + DSL.namedArgument("analyzer", "standard") + ); + Assertions.assertNotNull(matchPhraseQuery.build(new MatchPhraseExpression( + arguments, matchPhraseWithUnderscoreName))); + } + + @Test + public void build_succeeds_with_two_arguments_match_phrase_syntax() { + List arguments = List.of( + DSL.namedArgument("field", "test"), + DSL.namedArgument("query", "test2")); + Assertions.assertNotNull(matchPhraseQuery.build(new MatchPhraseExpression( + arguments, matchPhraseWithUnderscoreName))); + } + + @Test + public void test_slop_parameter_match_phrase_syntax() { + List arguments = List.of( + DSL.namedArgument("field", "t1"), + DSL.namedArgument("query", "t2"), + DSL.namedArgument("slop", "2") + ); + Assertions.assertNotNull(matchPhraseQuery.build(new MatchPhraseExpression( + arguments, matchPhraseWithUnderscoreName))); + } + + @Test + public void test_zero_terms_query_parameter_match_phrase_syntax() { + List arguments = List.of( + DSL.namedArgument("field", "t1"), + DSL.namedArgument("query", "t2"), + DSL.namedArgument("zero_terms_query", "ALL") + ); + Assertions.assertNotNull(matchPhraseQuery.build(new MatchPhraseExpression( + arguments, matchPhraseWithUnderscoreName))); + } + + @Test + public void test_zero_terms_query_parameter_lower_case_match_phrase_syntax() { + List arguments = List.of( + DSL.namedArgument("field", "t1"), + DSL.namedArgument("query", "t2"), + DSL.namedArgument("zero_terms_query", "all") + ); + Assertions.assertNotNull(matchPhraseQuery.build(new MatchPhraseExpression( + arguments, matchPhraseWithUnderscoreName))); + } + + @Test + public void test_SyntaxCheckException_when_no_arguments_matchphrase_syntax() { + List arguments = List.of(); + assertThrows(SyntaxCheckException.class, + () -> matchPhraseQuery.build(new MatchPhraseExpression( + arguments, matchPhraseQueryName))); + } + + @Test + public void test_SyntaxCheckException_when_one_argument_matchphrase_syntax() { + List arguments = List.of(DSL.namedArgument("field", "test")); + assertThrows(SyntaxCheckException.class, + () -> matchPhraseQuery.build(new MatchPhraseExpression( + arguments, matchPhraseQueryName))); + + } + + @Test + public void test_SyntaxCheckException_when_invalid_parameter_matchphrase_syntax() { + List arguments = List.of( + DSL.namedArgument("field", "test"), + DSL.namedArgument("query", "test2"), + DSL.namedArgument("unsupported", "3")); + Assertions.assertThrows(SemanticCheckException.class, + () -> matchPhraseQuery.build(new MatchPhraseExpression( + arguments, matchPhraseQueryName))); + } + + @Test + public void test_analyzer_parameter_matchphrase_syntax() { + List arguments = List.of( + DSL.namedArgument("field", "t1"), + DSL.namedArgument("query", "t2"), + DSL.namedArgument("analyzer", "standard") + ); + Assertions.assertNotNull(matchPhraseQuery.build(new MatchPhraseExpression( + arguments, matchPhraseQueryName))); + } + + @Test + public void build_succeeds_with_two_arguments_matchphrase_syntax() { + List arguments = List.of( + DSL.namedArgument("field", "test"), + DSL.namedArgument("query", "test2")); + Assertions.assertNotNull(matchPhraseQuery.build(new MatchPhraseExpression( + arguments, matchPhraseQueryName))); + } + + @Test + public void test_slop_parameter_matchphrase_syntax() { + List arguments = List.of( + DSL.namedArgument("field", "t1"), + DSL.namedArgument("query", "t2"), + DSL.namedArgument("slop", "2") + ); + Assertions.assertNotNull(matchPhraseQuery.build(new MatchPhraseExpression( + arguments, matchPhraseQueryName))); + } + + @Test + public void test_zero_terms_query_parameter_matchphrase_syntax() { + List arguments = List.of( + DSL.namedArgument("field", "t1"), + DSL.namedArgument("query", "t2"), + DSL.namedArgument("zero_terms_query", "ALL") + ); + Assertions.assertNotNull(matchPhraseQuery.build(new MatchPhraseExpression( + arguments, matchPhraseQueryName))); + } + + @Test + public void test_zero_terms_query_parameter_lower_case_matchphrase_syntax() { + List arguments = List.of( + DSL.namedArgument("field", "t1"), + DSL.namedArgument("query", "t2"), + DSL.namedArgument("zero_terms_query", "all") + ); + Assertions.assertNotNull(matchPhraseQuery.build(new MatchPhraseExpression( + arguments, matchPhraseQueryName))); + } + private class MatchPhraseExpression extends FunctionExpression { public MatchPhraseExpression(List arguments) { - super(MatchPhraseQueryTest.this.matchPhrase, arguments); + super(matchPhraseName, arguments); + } + + public MatchPhraseExpression(List arguments, FunctionName funcName) { + super(funcName, arguments); } @Override diff --git a/sql/src/main/antlr/OpenSearchSQLLexer.g4 b/sql/src/main/antlr/OpenSearchSQLLexer.g4 index 9e0a409401..df104d2a2a 100644 --- a/sql/src/main/antlr/OpenSearchSQLLexer.g4 +++ b/sql/src/main/antlr/OpenSearchSQLLexer.g4 @@ -295,6 +295,7 @@ INCLUDE: 'INCLUDE'; IN_TERMS: 'IN_TERMS'; MATCHPHRASE: 'MATCHPHRASE'; MATCH_PHRASE: 'MATCH_PHRASE'; +MATCHPHRASEQUERY: 'MATCHPHRASEQUERY'; SIMPLE_QUERY_STRING: 'SIMPLE_QUERY_STRING'; QUERY_STRING: 'QUERY_STRING'; MATCH_PHRASE_PREFIX: 'MATCH_PHRASE_PREFIX'; diff --git a/sql/src/main/antlr/OpenSearchSQLParser.g4 b/sql/src/main/antlr/OpenSearchSQLParser.g4 index ab9b4f09f7..e4c6f0a5e3 100644 --- a/sql/src/main/antlr/OpenSearchSQLParser.g4 +++ b/sql/src/main/antlr/OpenSearchSQLParser.g4 @@ -426,7 +426,7 @@ systemFunctionName singleFieldRelevanceFunctionName : MATCH | MATCHQUERY | MATCH_QUERY - | MATCH_PHRASE | MATCHPHRASE + | MATCH_PHRASE | MATCHPHRASE | MATCHPHRASEQUERY | MATCH_BOOL_PREFIX | MATCH_PHRASE_PREFIX ; 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 0d999640b0..bb5707539d 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 @@ -415,6 +415,7 @@ public void can_parse_match_phrase_relevance_function() { "matchPhraseComplexQueries", "matchPhraseGeneratedQueries", "generateMatchPhraseQueries", + "matchPhraseQueryComplexQueries" }) public void canParseComplexMatchPhraseArgsTest(String query) { assertNotNull(parser.parse(query)); @@ -444,6 +445,22 @@ private static Stream matchPhraseComplexQueries() { ); } + private static Stream matchPhraseQueryComplexQueries() { + return Stream.of( + "SELECT * FROM t WHERE matchphrasequery(c, 3)", + "SELECT * FROM t WHERE matchphrasequery(c, 3, fuzziness=AUTO)", + "SELECT * FROM t WHERE matchphrasequery(c, 3, zero_terms_query=\"all\")", + "SELECT * FROM t WHERE matchphrasequery(c, 3, lenient=true)", + "SELECT * FROM t WHERE matchphrasequery(c, 3, lenient='true')", + "SELECT * FROM t WHERE matchphrasequery(c, 3, operator=xor)", + "SELECT * FROM t WHERE matchphrasequery(c, 3, cutoff_frequency=0.04)", + "SELECT * FROM t WHERE matchphrasequery(c, 3, cutoff_frequency=0.04, analyzer = english, " + + "prefix_length=34, fuzziness='auto', minimum_should_match='2<-25% 9<-3')", + "SELECT * FROM t WHERE matchphrasequery(c, 3, minimum_should_match='2<-25% 9<-3')", + "SELECT * FROM t WHERE matchphrasequery(c, 3, operator='AUTO')" + ); + } + private static Stream matchPhraseGeneratedQueries() { var matchArgs = new HashMap(); matchArgs.put("fuzziness", new String[]{ "AUTO", "AUTO:1,5", "1" }); 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 f71717e1b9..72358986e6 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 @@ -451,6 +451,22 @@ public void filteredDistinctCount() { ); } + @Test + public void matchPhraseQueryAllParameters() { + assertEquals( + AstDSL.function("matchphrasequery", + unresolvedArg("field", stringLiteral("test")), + unresolvedArg("query", stringLiteral("search query")), + unresolvedArg("slop", stringLiteral("3")), + unresolvedArg("analyzer", stringLiteral("standard")), + unresolvedArg("zero_terms_query", stringLiteral("NONE")) + ), + buildExprAst("matchphrasequery(test, 'search query', slop = 3" + + ", analyzer = 'standard', zero_terms_query='NONE'" + + ")") + ); + } + @Test public void matchPhrasePrefixAllParameters() { assertEquals(