Skip to content

Commit 94a700a

Browse files
committed
Adding alternate multi_match syntax.
Signed-off-by: forestmvey <forestv@bitquilltech.com>
1 parent 662a938 commit 94a700a

File tree

10 files changed

+247
-21
lines changed

10 files changed

+247
-21
lines changed

core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,9 @@ public enum BuiltinFunctionName {
220220
QUERY(FunctionName.of("query")),
221221
MATCH_QUERY(FunctionName.of("match_query")),
222222
MATCHQUERY(FunctionName.of("matchquery")),
223-
MULTI_MATCH(FunctionName.of("multi_match"));
223+
MULTI_MATCH(FunctionName.of("multi_match")),
224+
MULTIMATCH(FunctionName.of("multimatch")),
225+
MULTIMATCHQUERY(FunctionName.of("multimatchquery"));
224226

225227
private final FunctionName name;
226228

core/src/main/java/org/opensearch/sql/expression/function/OpenSearchFunctions.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ public class OpenSearchFunctions {
2828
public void register(BuiltinFunctionRepository repository) {
2929
repository.register(match_bool_prefix());
3030
repository.register(match());
31-
repository.register(multi_match());
31+
repository.register(multi_match(BuiltinFunctionName.MULTI_MATCH));
32+
repository.register(multi_match(BuiltinFunctionName.MULTIMATCH));
33+
repository.register(multi_match(BuiltinFunctionName.MULTIMATCHQUERY));
3234
repository.register(simple_query_string());
3335
repository.register(query());
3436
repository.register(query_string());
@@ -59,8 +61,8 @@ private static FunctionResolver match_phrase(BuiltinFunctionName matchPhrase) {
5961
return new RelevanceFunctionResolver(funcName, STRING);
6062
}
6163

62-
private static FunctionResolver multi_match() {
63-
FunctionName funcName = BuiltinFunctionName.MULTI_MATCH.getName();
64+
private static FunctionResolver multi_match(BuiltinFunctionName matchPhrase) {
65+
FunctionName funcName = matchPhrase.getName();
6466
return new RelevanceFunctionResolver(funcName, STRUCT);
6567
}
6668

integ-test/src/test/java/org/opensearch/sql/sql/MultiMatchIT.java

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
package org.opensearch.sql.sql;
77

88
import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_BEER;
9+
import static org.opensearch.sql.util.MatcherUtils.rows;
10+
import static org.opensearch.sql.util.MatcherUtils.verifyDataRows;
911

1012
import java.io.IOException;
1113
import org.json.JSONObject;
@@ -27,15 +29,15 @@ public void init() throws IOException {
2729
*/
2830

2931
@Test
30-
public void test_mandatory_params() throws IOException {
32+
public void test_mandatory_params() {
3133
String query = "SELECT Id FROM " + TEST_INDEX_BEER
3234
+ " WHERE multi_match([\\\"Tags\\\" ^ 1.5, Title, `Body` 4.2], 'taste')";
3335
var result = new JSONObject(executeQuery(query, "jdbc"));
3436
assertEquals(16, result.getInt("total"));
3537
}
3638

3739
@Test
38-
public void test_all_params() throws IOException {
40+
public void test_all_params() {
3941
String query = "SELECT Id FROM " + TEST_INDEX_BEER
4042
+ " WHERE multi_match(['Body', Tags], 'taste beer', operator='and', analyzer=english,"
4143
+ "auto_generate_synonyms_phrase_query=true, boost = 0.77, cutoff_frequency=0.33,"
@@ -47,7 +49,7 @@ public void test_all_params() throws IOException {
4749
}
4850

4951
@Test
50-
public void verify_wildcard_test() throws IOException {
52+
public void verify_wildcard_test() {
5153
String query1 = "SELECT Id FROM " + TEST_INDEX_BEER
5254
+ " WHERE multi_match(['Tags'], 'taste')";
5355
var result1 = new JSONObject(executeQuery(query1, "jdbc"));
@@ -61,4 +63,65 @@ public void verify_wildcard_test() throws IOException {
6163
var result = new JSONObject(executeQuery(query, "jdbc"));
6264
assertEquals(10, result.getInt("total"));
6365
}
66+
67+
@Test
68+
public void test_multimatch_alternate_parameter_syntax() {
69+
String query = "SELECT Tags FROM " + TEST_INDEX_BEER
70+
+ " WHERE multimatch('query'='taste', 'fields'='Tags')";
71+
var result = new JSONObject(executeQuery(query, "jdbc"));
72+
assertEquals(8, result.getInt("total"));
73+
}
74+
75+
@Test
76+
public void test_multimatchquery_alternate_parameter_syntax() {
77+
String query = "SELECT Tags FROM " + TEST_INDEX_BEER
78+
+ " WHERE multimatchquery(query='cicerone', fields='Tags')";
79+
var result = new JSONObject(executeQuery(query, "jdbc"));
80+
assertEquals(2, result.getInt("total"));
81+
verifyDataRows(result, rows("serving cicerone restaurants"),
82+
rows("taste cicerone"));
83+
}
84+
85+
@Test
86+
public void test_quoted_multi_match_alternate_parameter_syntax() {
87+
String query = "SELECT Tags FROM " + TEST_INDEX_BEER
88+
+ " WHERE multi_match('query'='cicerone', 'fields'='Tags')";
89+
var result = new JSONObject(executeQuery(query, "jdbc"));
90+
assertEquals(2, result.getInt("total"));
91+
verifyDataRows(result, rows("serving cicerone restaurants"),
92+
rows("taste cicerone"));
93+
}
94+
95+
@Test
96+
public void test_multi_match_alternate_parameter_syntax() {
97+
String query = "SELECT Tags FROM " + TEST_INDEX_BEER
98+
+ " WHERE multi_match(query='cicerone', fields='Tags')";
99+
var result = new JSONObject(executeQuery(query, "jdbc"));
100+
assertEquals(2, result.getInt("total"));
101+
verifyDataRows(result, rows("serving cicerone restaurants"),
102+
rows("taste cicerone"));
103+
}
104+
105+
@Test
106+
public void test_wildcard_multi_match_alternate_parameter_syntax() {
107+
String query = "SELECT Body FROM " + TEST_INDEX_BEER
108+
+ " WHERE multi_match(query='IPA', fields='B*') LIMIT 1";
109+
var result = new JSONObject(executeQuery(query, "jdbc"));
110+
verifyDataRows(result, rows("<p>I know what makes an IPA an IPA, but what are the unique" +
111+
" characteristics of it's common variants? To be specific, the ones I'm interested in are Double IPA" +
112+
" and Black IPA, but general differences between any other styles would be welcome too. </p>\n"));
113+
}
114+
115+
@Test
116+
public void test_all_params_multimatchquery_alternate_parameter_syntax() {
117+
String query = "SELECT Id FROM " + TEST_INDEX_BEER
118+
+ " WHERE multimatchquery(query='cicerone', fields='Tags', 'operator'='or', analyzer=english,"
119+
+ "auto_generate_synonyms_phrase_query=true, boost = 0.77, cutoff_frequency=0.33,"
120+
+ "fuzziness = 'AUTO:1,5', fuzzy_transpositions = false, lenient = true, max_expansions = 25,"
121+
+ "minimum_should_match = '2<-25% 9<-3', prefix_length = 7, tie_breaker = 0.3,"
122+
+ "type = most_fields, slop = 2, zero_terms_query = 'ALL');";
123+
124+
var result = new JSONObject(executeQuery(query, "jdbc"));
125+
assertEquals(2, result.getInt("total"));
126+
}
64127
}

opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/FilterQueryBuilder.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ public class FilterQueryBuilder extends ExpressionNodeVisitor<QueryBuilder, Obje
6565
.put(BuiltinFunctionName.MATCH_QUERY.getName(), new MatchQuery())
6666
.put(BuiltinFunctionName.MATCHQUERY.getName(), new MatchQuery())
6767
.put(BuiltinFunctionName.MULTI_MATCH.getName(), new MultiMatchQuery())
68+
.put(BuiltinFunctionName.MULTIMATCH.getName(), new MultiMatchQuery())
69+
.put(BuiltinFunctionName.MULTIMATCHQUERY.getName(), new MultiMatchQuery())
6870
.put(BuiltinFunctionName.SIMPLE_QUERY_STRING.getName(), new SimpleQueryStringQuery())
6971
.put(BuiltinFunctionName.QUERY_STRING.getName(), new QueryStringQuery())
7072
.put(BuiltinFunctionName.MATCH_BOOL_PREFIX.getName(), new MatchBoolPrefixQuery())

opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/MultiMatchTest.java

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ class MultiMatchTest {
3939
private static final DSL dsl = new ExpressionConfig()
4040
.dsl(new ExpressionConfig().functionRepository());
4141
private final MultiMatchQuery multiMatchQuery = new MultiMatchQuery();
42-
private final FunctionName multiMatch = FunctionName.of("multi_match");
42+
private final FunctionName multiMatchName = FunctionName.of("multimatch");
43+
private final FunctionName multiMatchWithUnderscoreName = FunctionName.of("multi_match");
44+
private final FunctionName multiMatchQueryName = FunctionName.of("multi_match");
45+
private final FunctionName[] functionNames =
46+
{multiMatchName, multiMatchWithUnderscoreName, multiMatchQueryName};
4347
private static final LiteralExpression fields_value = DSL.literal(
4448
new ExprTupleValue(new LinkedHashMap<>(ImmutableMap.of(
4549
"title", ExprValueUtils.floatValue(1.F),
@@ -133,22 +137,28 @@ static Stream<List<Expression>> generateValidData() {
133137
@ParameterizedTest
134138
@MethodSource("generateValidData")
135139
public void test_valid_parameters(List<Expression> validArgs) {
136-
Assertions.assertNotNull(multiMatchQuery.build(
137-
new MultiMatchExpression(validArgs)));
140+
for (FunctionName funcName: functionNames) {
141+
Assertions.assertNotNull(multiMatchQuery.build(
142+
new MultiMatchExpression(validArgs, funcName)));
143+
}
138144
}
139145

140146
@Test
141147
public void test_SyntaxCheckException_when_no_arguments() {
142148
List<Expression> arguments = List.of();
143-
assertThrows(SyntaxCheckException.class,
144-
() -> multiMatchQuery.build(new MultiMatchExpression(arguments)));
149+
for (FunctionName funcName: functionNames) {
150+
assertThrows(SyntaxCheckException.class,
151+
() -> multiMatchQuery.build(new MultiMatchExpression(arguments, funcName)));
152+
}
145153
}
146154

147155
@Test
148156
public void test_SyntaxCheckException_when_one_argument() {
149157
List<Expression> arguments = List.of(namedArgument("fields", fields_value));
150-
assertThrows(SyntaxCheckException.class,
151-
() -> multiMatchQuery.build(new MultiMatchExpression(arguments)));
158+
for (FunctionName funcName: functionNames) {
159+
assertThrows(SyntaxCheckException.class,
160+
() -> multiMatchQuery.build(new MultiMatchExpression(arguments, funcName)));
161+
}
152162
}
153163

154164
@Test
@@ -157,17 +167,19 @@ public void test_SemanticCheckException_when_invalid_parameter() {
157167
namedArgument("fields", fields_value),
158168
namedArgument("query", query_value),
159169
dsl.namedArgument("unsupported", "unsupported_value"));
160-
Assertions.assertThrows(SemanticCheckException.class,
161-
() -> multiMatchQuery.build(new MultiMatchExpression(arguments)));
170+
for (FunctionName funcName: functionNames) {
171+
Assertions.assertThrows(SemanticCheckException.class,
172+
() -> multiMatchQuery.build(new MultiMatchExpression(arguments, funcName)));
173+
}
162174
}
163175

164176
private NamedArgumentExpression namedArgument(String name, LiteralExpression value) {
165177
return dsl.namedArgument(name, value);
166178
}
167179

168180
private class MultiMatchExpression extends FunctionExpression {
169-
public MultiMatchExpression(List<Expression> arguments) {
170-
super(MultiMatchTest.this.multiMatch, arguments);
181+
public MultiMatchExpression(List<Expression> arguments, FunctionName funcName) {
182+
super(funcName, arguments);
171183
}
172184

173185
@Override

sql/src/main/antlr/OpenSearchSQLLexer.g4

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ MINUTE_OF_HOUR: 'MINUTE_OF_HOUR';
305305
MONTH_OF_YEAR: 'MONTH_OF_YEAR';
306306
MULTIMATCH: 'MULTIMATCH';
307307
MULTI_MATCH: 'MULTI_MATCH';
308+
MULTIMATCHQUERY: 'MULTIMATCHQUERY';
308309
NESTED: 'NESTED';
309310
PERCENTILES: 'PERCENTILES';
310311
REGEXP_QUERY: 'REGEXP_QUERY';

sql/src/main/antlr/OpenSearchSQLParser.g4

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,9 @@ multiFieldRelevanceFunction
347347
: multiFieldRelevanceFunctionName LR_BRACKET
348348
LT_SQR_PRTHS field=relevanceFieldAndWeight (COMMA field=relevanceFieldAndWeight)* RT_SQR_PRTHS
349349
COMMA query=relevanceQuery (COMMA relevanceArg)* RR_BRACKET
350+
| multiFieldRelevanceFunctionName LR_BRACKET
351+
alternateMultiMatchQueryField COMMA alternateMultiMatchQueryField
352+
(COMMA alternateMultiMatchOptionalArg)* RR_BRACKET
350353
;
351354

352355
convertedDataType
@@ -431,6 +434,8 @@ singleFieldRelevanceFunctionName
431434

432435
multiFieldRelevanceFunctionName
433436
: MULTI_MATCH
437+
| MULTIMATCH
438+
| MULTIMATCHQUERY
434439
| SIMPLE_QUERY_STRING
435440
| QUERY_STRING
436441
;
@@ -498,3 +503,22 @@ highlightArgValue
498503
: stringLiteral
499504
;
500505

506+
alternateMultiMatchArgName
507+
: FIELDS
508+
| QUERY
509+
| stringLiteral
510+
;
511+
512+
alternateMultiMatchArgVal
513+
: qualifiedName
514+
| stringLiteral
515+
;
516+
517+
alternateMultiMatchOptionalArg
518+
: relevanceArg
519+
| argName=stringLiteral EQUAL_SYMBOL argVal=relevanceArgValue
520+
;
521+
522+
alternateMultiMatchQueryField
523+
: argName=alternateMultiMatchArgName EQUAL_SYMBOL argVal=alternateMultiMatchArgVal
524+
;

sql/src/main/java/org/opensearch/sql/sql/parser/AstExpressionBuilder.java

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@
8080
import org.opensearch.sql.ast.expression.WindowFunction;
8181
import org.opensearch.sql.ast.tree.Sort.SortOption;
8282
import org.opensearch.sql.common.utils.StringUtils;
83+
import org.opensearch.sql.exception.SemanticCheckException;
84+
import org.opensearch.sql.expression.function.BuiltinFunctionName;
8385
import org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser;
8486
import org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.AndExpressionContext;
8587
import org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.ColumnNameContext;
@@ -399,9 +401,24 @@ public UnresolvedExpression visitSingleFieldRelevanceFunction(
399401
@Override
400402
public UnresolvedExpression visitMultiFieldRelevanceFunction(
401403
MultiFieldRelevanceFunctionContext ctx) {
402-
return new Function(
403-
ctx.multiFieldRelevanceFunctionName().getText().toLowerCase(),
404-
multiFieldRelevanceArguments(ctx));
404+
// To support alternate syntax for MULTI_MATCH like
405+
// 'MULTI_MATCH('query'='query_val', 'fields'='*fields_val')'
406+
if ((StringUtils.unquoteText(ctx.multiFieldRelevanceFunctionName().getText().toUpperCase())
407+
.equals(BuiltinFunctionName.MULTI_MATCH.toString())
408+
|| StringUtils.unquoteText(ctx.multiFieldRelevanceFunctionName().getText().toUpperCase())
409+
.equals(BuiltinFunctionName.MULTIMATCH.toString())
410+
|| StringUtils.unquoteText(ctx.multiFieldRelevanceFunctionName().getText().toUpperCase())
411+
.equals(BuiltinFunctionName.MULTIMATCHQUERY.toString()))
412+
&& ! ctx.getRuleContexts(OpenSearchSQLParser.AlternateMultiMatchQueryFieldContext.class)
413+
.isEmpty()) {
414+
return new Function(
415+
ctx.multiFieldRelevanceFunctionName().getText().toLowerCase(),
416+
alternateMultiMatchArguments(ctx));
417+
} else {
418+
return new Function(
419+
ctx.multiFieldRelevanceFunctionName().getText().toLowerCase(),
420+
multiFieldRelevanceArguments(ctx));
421+
}
405422
}
406423

407424
private Function visitFunction(String functionName, FunctionArgsContext args) {
@@ -487,4 +504,54 @@ private List<UnresolvedExpression> multiFieldRelevanceArguments(
487504
fillRelevanceArgs(ctx.relevanceArg(), builder);
488505
return builder.build();
489506
}
507+
508+
/**
509+
* Adds support for multi_match alternate syntax like
510+
* MULTI_MATCH('query'='Dale', 'fields'='*name').
511+
* @param ctx : Context for multi field relevance function.
512+
* @return : Returns list of all arguments for relevance function.
513+
*/
514+
private List<UnresolvedExpression> alternateMultiMatchArguments(
515+
OpenSearchSQLParser.MultiFieldRelevanceFunctionContext ctx) {
516+
// all the arguments are defaulted to string values
517+
// to skip environment resolving and function signature resolving
518+
ImmutableList.Builder<UnresolvedExpression> builder = ImmutableList.builder();
519+
String fields = "";
520+
String query = "";
521+
for (var arg : ctx.getRuleContexts(
522+
OpenSearchSQLParser.AlternateMultiMatchQueryFieldContext.class)) {
523+
switch (StringUtils.unquoteText(arg.argName.getText())) {
524+
case "query":
525+
query = StringUtils.unquoteText(arg.argVal.getText());
526+
break;
527+
528+
case "fields":
529+
fields = StringUtils.unquoteText(arg.argVal.getText());
530+
break;
531+
532+
default:
533+
throw new SemanticCheckException(
534+
String.format("can't resolve argument %s for %s",
535+
StringUtils.unquoteText(arg.argName.getText()),
536+
StringUtils.unquoteText(ctx.multiFieldRelevanceFunctionName().getText()))
537+
);
538+
}
539+
}
540+
541+
builder.add(new UnresolvedArgument("fields",
542+
new RelevanceFieldList(ImmutableMap.of(fields, 1F))));
543+
builder.add(new UnresolvedArgument("query",
544+
new Literal(query, DataType.STRING)));
545+
546+
// To support old syntax we must support argument keys as quoted strings.
547+
ctx.getRuleContexts(OpenSearchSQLParser.AlternateMultiMatchOptionalArgContext.class)
548+
.forEach(v -> builder.add(v.relevanceArg() != null
549+
? new UnresolvedArgument(v.relevanceArg().relevanceArgName().getText().toLowerCase(),
550+
new Literal(StringUtils.unquoteText(v.relevanceArg().relevanceArgValue().getText()),
551+
DataType.STRING))
552+
: new UnresolvedArgument(StringUtils.unquoteText(v.argName.getText()).toLowerCase(),
553+
new Literal(StringUtils.unquoteText(v.argVal.getText()), DataType.STRING))));
554+
555+
return builder.build();
556+
}
490557
}

sql/src/test/java/org/opensearch/sql/sql/antlr/SQLSyntaxParserTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,12 @@ public void can_parse_now_like_functions(String name, Boolean hasFsp, Boolean ha
193193

194194
@Test
195195
public void can_parse_multi_match_relevance_function() {
196+
assertNotNull(parser.parse(
197+
"SELECT id FROM test WHERE multi_match(\"fields\"=\"field\", \"query\"=\"query\")"));
198+
assertNotNull(parser.parse(
199+
"SELECT id FROM test WHERE multi_match(\'fields\'=\'field\', \'query\'=\'query\')"));
200+
assertNotNull(parser.parse(
201+
"SELECT id FROM test WHERE multi_match(fields=\'field\', query=\'query\')"));
196202
assertNotNull(parser.parse(
197203
"SELECT id FROM test WHERE multi_match(['address'], 'query')"));
198204
assertNotNull(parser.parse(

0 commit comments

Comments
 (0)