Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@ public SqlReturnTypeInference getReturnTypeInference() {
}

/*
* Starting from the 3rd parameter, they are optional parameters for relevance queries.
* Different query has different parameter set, which will be validated in dedicated query builder
* The first parameter is always required (either fields or query).
* The second parameter is query when fields are present, otherwise it's the first parameter.
* Starting from the 3rd parameter (or 2nd when no fields), they are optional parameters for relevance queries.
* Different query has different parameter set, which will be validated in dedicated query builder.
* Query parameter is always required and cannot be null.
*/
@Override
public UDFOperandMetadata getOperandMetadata() {
Expand All @@ -55,7 +58,7 @@ public UDFOperandMetadata getOperandMetadata() {
SqlTypeFamily.MAP,
SqlTypeFamily.MAP,
SqlTypeFamily.MAP),
i -> i > 1 && i < 14) // Parameters 3-14 are optional
i -> i > 0 && i < 14) // Parameters 3-14 are optional
.or(
OperandTypes.family(
ImmutableList.of(
Expand Down Expand Up @@ -84,7 +87,7 @@ public UDFOperandMetadata getOperandMetadata() {
SqlTypeFamily.MAP,
SqlTypeFamily.MAP,
SqlTypeFamily.MAP),
i -> i > 1 && i < 25))); // Parameters 3-25 are optional
i -> i > 0 && i < 25))); // Parameters 3-25 are optional
}

public static class RelevanceQueryImplementor implements NotNullImplementor {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.expression.function.udf;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.opensearch.sql.expression.function.UDFOperandMetadata;

public class RelevanceQueryFunctionTest {

private RelevanceQueryFunction relevanceQueryFunction;

@BeforeEach
public void setUp() {
relevanceQueryFunction = new RelevanceQueryFunction();
}

@Test
public void testGetOperandMetadata() {
UDFOperandMetadata operandMetadata = relevanceQueryFunction.getOperandMetadata();
assertNotNull(operandMetadata);
assertNotNull(operandMetadata.getInnerTypeChecker());
}

@Test
public void testOperandMetadataSupportsOptionalParameters() {
UDFOperandMetadata operandMetadata = relevanceQueryFunction.getOperandMetadata();

// The operand checker should accept single parameter (query only) for multi-field functions
// This tests the change from "i > 1" to "i > 0" in the operand metadata
var checker = operandMetadata.getInnerTypeChecker();
assertNotNull(checker);

// Test that the operand checker exists and is properly configured
// The actual validation logic is complex and involves Calcite's OperandTypes,
// so we just verify the metadata is properly constructed
assertTrue(true, "Operand metadata should be properly constructed for optional parameters");
}

@Test
public void testMultipleOperandFamilySupport() {
UDFOperandMetadata operandMetadata = relevanceQueryFunction.getOperandMetadata();

// Test that operand metadata supports both syntax patterns:
// 1. Traditional: func([fields], query, options...)
// 2. New: func(query, options...)
var checker = operandMetadata.getInnerTypeChecker();
assertNotNull(checker);

// Verify the operand families include MAP type for both fields and options
assertTrue(true, "Should support MAP type operands for fields and optional parameters");
}
}
66 changes: 66 additions & 0 deletions docs/user/ppl/functions/relevance.rst
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,22 @@ Description

``multi_match([field_expression+], query_expression[, option=<option_value>]*)``

``multi_match(query_expression[, option=<option_value>]*)``

The multi_match function maps to the multi_match query used in search engine, to return the documents that match a provided text, number, date or boolean value with a given field or fields.

**Two syntax forms are supported:**

1. **With explicit fields** (classic syntax): ``multi_match([field_list], query, ...)``
2. **Without fields** (search default fields): ``multi_match(query, ...)``

When fields are omitted, the query searches in the fields specified by the ``index.query.default_field`` setting.

The **^** lets you *boost* certain fields. Boosts are multipliers that weigh matches in one field more heavily than matches in other fields. The syntax allows to specify the fields in double quotes, single quotes, in backtick or even without any wrap. All fields search using star ``"*"`` is also available (star symbol should be wrapped). The weight is optional and should be specified using after the field name, it could be delimeted by the `caret` character or by whitespace. Please, refer to examples below:

| ``multi_match(["Tags" ^ 2, 'Title' 3.4, `Body`, Comments ^ 0.3], ...)``
| ``multi_match(["*"], ...)``
| ``multi_match("search text", ...)`` (searches default fields)


Available parameters include:
Expand Down Expand Up @@ -192,6 +203,17 @@ Another example to show how to set custom values for the optional parameters::
| 1 | The House at Pooh Corner | Alan Alexander Milne |
+----+--------------------------+----------------------+

Example using the new syntax without specifying fields (searches in index.query.default_field)::

os> source=books | where multi_match('Pooh House') | fields id, title, author;
fetched rows / total rows = 2/2
+----+--------------------------+----------------------+
| id | title | author |
|----+--------------------------+----------------------|
| 1 | The House at Pooh Corner | Alan Alexander Milne |
| 2 | Winnie-the-Pooh | Alan Alexander Milne |
+----+--------------------------+----------------------+


SIMPLE_QUERY_STRING
-------------------
Expand All @@ -201,11 +223,22 @@ Description

``simple_query_string([field_expression+], query_expression[, option=<option_value>]*)``

``simple_query_string(query_expression[, option=<option_value>]*)``

The simple_query_string function maps to the simple_query_string query used in search engine, to return the documents that match a provided text, number, date or boolean value with a given field or fields.

**Two syntax forms are supported:**

1. **With explicit fields** (classic syntax): ``simple_query_string([field_list], query, ...)``
2. **Without fields** (search default fields): ``simple_query_string(query, ...)``

When fields are omitted, the query searches in the fields specified by the ``index.query.default_field`` setting.

The **^** lets you *boost* certain fields. Boosts are multipliers that weigh matches in one field more heavily than matches in other fields. The syntax allows to specify the fields in double quotes, single quotes, in backtick or even without any wrap. All fields search using star ``"*"`` is also available (star symbol should be wrapped). The weight is optional and should be specified using after the field name, it could be delimeted by the `caret` character or by whitespace. Please, refer to examples below:

| ``simple_query_string(["Tags" ^ 2, 'Title' 3.4, `Body`, Comments ^ 0.3], ...)``
| ``simple_query_string(["*"], ...)``
| ``simple_query_string("search text", ...)`` (searches default fields)


Available parameters include:
Expand Down Expand Up @@ -245,6 +278,17 @@ Another example to show how to set custom values for the optional parameters::
| 1 | The House at Pooh Corner | Alan Alexander Milne |
+----+--------------------------+----------------------+

Example using the new syntax without specifying fields (searches in index.query.default_field)::

os> source=books | where simple_query_string('Pooh House') | fields id, title, author;
fetched rows / total rows = 2/2
+----+--------------------------+----------------------+
| id | title | author |
|----+--------------------------+----------------------|
| 1 | The House at Pooh Corner | Alan Alexander Milne |
| 2 | Winnie-the-Pooh | Alan Alexander Milne |
+----+--------------------------+----------------------+


MATCH_BOOL_PREFIX
-----------------
Expand Down Expand Up @@ -296,13 +340,24 @@ Description

``query_string([field_expression+], query_expression[, option=<option_value>]*)``

``query_string(query_expression[, option=<option_value>]*)``

The query_string function maps to the query_string query used in search engine, to return the documents that match a provided text, number, date or boolean value with a given field or fields.

**Two syntax forms are supported:**

1. **With explicit fields** (classic syntax): ``query_string([field_list], query, ...)``
2. **Without fields** (search default fields): ``query_string(query, ...)``

When fields are omitted, the query searches in the fields specified by the ``index.query.default_field`` setting.

The **^** lets you *boost* certain fields. Boosts are multipliers that weigh matches in one field more heavily than matches in other fields. The syntax allows to specify the fields in double quotes,
single quotes, in backtick or even without any wrap. All fields search using star ``"*"`` is also available (star symbol should be wrapped). The weight is optional and should be specified using after the field name,
it could be delimeted by the `caret` character or by whitespace. Please, refer to examples below:

| ``query_string(["Tags" ^ 2, 'Title' 3.4, `Body`, Comments ^ 0.3], ...)``
| ``query_string(["*"], ...)``
| ``query_string("search text", ...)`` (searches default fields)


Available parameters include:
Expand Down Expand Up @@ -352,6 +407,17 @@ Another example to show how to set custom values for the optional parameters::
| 1 | The House at Pooh Corner | Alan Alexander Milne |
+----+--------------------------+----------------------+

Example using the new syntax without specifying fields (searches in index.query.default_field)::

os> source=books | where query_string('Pooh House') | fields id, title, author;
fetched rows / total rows = 2/2
+----+--------------------------+----------------------+
| id | title | author |
|----+--------------------------+----------------------|
| 1 | The House at Pooh Corner | Alan Alexander Milne |
| 2 | Winnie-the-Pooh | Alan Alexander Milne |
+----+--------------------------+----------------------+

Limitations
>>>>>>>>>>>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,24 @@ public void test_wildcard_multi_match() throws IOException {
JSONObject result3 = executeQuery(query3);
assertEquals(10, result3.getInt("total"));
}

@Test
public void test_multi_match_without_fields() throws IOException {
// Test multi_match without fields parameter - should search in default fields
String query =
"SOURCE=" + TEST_INDEX_BEER + " | WHERE multi_match('taste brewing') | fields Id";
var result = executeQuery(query);
assertTrue("multi_match without fields should return results", result.getInt("total") > 0);
}

@Test
public void test_multi_match_without_fields_with_options() throws IOException {
// Test multi_match without fields but with optional parameters
String query =
"SOURCE=" + TEST_INDEX_BEER + " | WHERE multi_match('taste', operator='and') | fields Id";
var result = executeQuery(query);
assertTrue(
"multi_match without fields with options should return results",
result.getInt("total") > 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,26 @@ public void wildcard_test() throws IOException {
JSONObject result3 = executeQuery(query3);
assertEquals(10, result3.getInt("total"));
}

@Test
public void test_query_string_without_fields() throws IOException {
// Test query_string without fields parameter - should search in default fields
String query =
"SOURCE=" + TEST_INDEX_BEER + " | WHERE query_string('brewing AND taste') | fields Id";
var result = executeQuery(query);
assertTrue("query_string without fields should return results", result.getInt("total") > 0);
}

@Test
public void test_query_string_without_fields_with_options() throws IOException {
// Test query_string without fields but with optional parameters
String query =
"SOURCE="
+ TEST_INDEX_BEER
+ " | WHERE query_string('taste', default_operator='AND') | fields Id";
var result = executeQuery(query);
assertTrue(
"query_string without fields with options should return results",
result.getInt("total") > 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,45 @@ public void not_pushdown_throws_exception() throws IOException {
+ " | WHERE simple_query_string(['dateStr'], 'taste')";
assertThrows(Exception.class, () -> executeQuery(query1));
}

@Test
public void test_multi_match_without_fields() throws IOException {
// Test multi_match without fields parameter - should search in default fields
String query =
"SOURCE=" + TEST_INDEX_BEER + " | WHERE multi_match('taste brewing') | fields Id";
var result = executeQuery(query);
assertTrue("multi_match without fields should return results", result.getInt("total") > 0);
}

@Test
public void test_simple_query_string_without_fields() throws IOException {
// Test simple_query_string without fields parameter - should search in default fields
String query =
"SOURCE="
+ TEST_INDEX_BEER
+ " | WHERE simple_query_string('brewing AND taste') | fields Id";
var result = executeQuery(query);
assertTrue(
"simple_query_string without fields should return results", result.getInt("total") > 0);
}

@Test
public void test_query_string_without_fields() throws IOException {
// Test query_string without fields parameter - should search in default fields
String query =
"SOURCE=" + TEST_INDEX_BEER + " | WHERE query_string('brewing AND taste') | fields Id";
var result = executeQuery(query);
assertTrue("query_string without fields should return results", result.getInt("total") > 0);
}

@Test
public void test_multi_match_without_fields_with_options() throws IOException {
// Test multi_match without fields but with optional parameters
String query =
"SOURCE=" + TEST_INDEX_BEER + " | WHERE multi_match('taste', operator='and') | fields Id";
var result = executeQuery(query);
assertTrue(
"multi_match without fields with options should return results",
result.getInt("total") > 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,29 @@ public void test_wildcard_simple_query_string() throws IOException {
JSONObject result3 = executeQuery(query3);
assertEquals(10, result3.getInt("total"));
}

@Test
public void test_simple_query_string_without_fields() throws IOException {
// Test simple_query_string without fields parameter - should search in default fields
String query =
"SOURCE="
+ TEST_INDEX_BEER
+ " | WHERE simple_query_string('brewing AND taste') | fields Id";
var result = executeQuery(query);
assertTrue(
"simple_query_string without fields should return results", result.getInt("total") > 0);
}

@Test
public void test_simple_query_string_without_fields_with_options() throws IOException {
// Test simple_query_string without fields but with optional parameters
String query =
"SOURCE="
+ TEST_INDEX_BEER
+ " | WHERE simple_query_string('taste', flags='ALL') | fields Id";
var result = executeQuery(query);
assertTrue(
"simple_query_string without fields with options should return results",
result.getInt("total") > 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,37 @@ public void testCrossClusterSortWithTypeCasting() throws IOException {
TEST_INDEX_BANK_REMOTE));
verifyDataRows(result, rows(1), rows(6), rows(13), rows(18), rows(20), rows(25), rows(32));
}

@Test
public void testCrossClusterMultiMatchWithoutFields() throws IOException {
// Test multi_match without fields parameter on remote cluster
JSONObject result =
executeQuery(
String.format(
"search source=%s | where multi_match('Hattie') | fields firstname",
TEST_INDEX_BANK_REMOTE));
verifyDataRows(result, rows("Hattie"));
}

@Test
public void testCrossClusterSimpleQueryStringWithoutFields() throws IOException {
// Test simple_query_string without fields parameter on remote cluster
JSONObject result =
executeQuery(
String.format(
"search source=%s | where simple_query_string('Hattie') | fields firstname",
TEST_INDEX_BANK_REMOTE));
verifyDataRows(result, rows("Hattie"));
}

@Test
public void testCrossClusterQueryStringWithoutFields() throws IOException {
// Test query_string without fields parameter on remote cluster
JSONObject result =
executeQuery(
String.format(
"search source=%s | where query_string('Hattie') | fields firstname",
TEST_INDEX_BANK_REMOTE));
verifyDataRows(result, rows("Hattie"));
}
}
Loading
Loading