diff --git a/CHANGELOG.md b/CHANGELOG.md index 8587f69d71ff1..c4471ca3ca1fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Add range validations in query builder and field mapper ([#20497](https://github.com/opensearch-project/OpenSearch/issues/20497)) - [Workload Management] Enhance Scroll API support for autotagging ([#20151](https://github.com/opensearch-project/OpenSearch/pull/20151)) - Add indices to search request slowlog ([#20588](https://github.com/opensearch-project/OpenSearch/pull/20588)) +- Implement gRPC Boosting and SimpleQueryString queries ([#20487](https://github.com/opensearch-project/OpenSearch/pull/20487)) + ### Changed - Move Randomness from server to libs/common ([#20570](https://github.com/opensearch-project/OpenSearch/pull/20570)) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 707310853a3bb..3a5b696e4305c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,7 +24,7 @@ kotlin = "1.7.10" antlr4 = "4.13.1" guava = "33.2.1-jre" gson = "2.13.2" -opensearchprotobufs = "1.2.0" +opensearchprotobufs = "1.3.0" protobuf = "3.25.8" jakarta_annotation = "1.3.5" google_http_client = "1.44.1" diff --git a/modules/transport-grpc/licenses/protobufs-1.2.0.jar.sha1 b/modules/transport-grpc/licenses/protobufs-1.2.0.jar.sha1 deleted file mode 100644 index b122dd4dca041..0000000000000 --- a/modules/transport-grpc/licenses/protobufs-1.2.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -6688b30c1b5a73273e1062a6017b8de9f3ff7f33 \ No newline at end of file diff --git a/modules/transport-grpc/licenses/protobufs-1.3.0.jar.sha1 b/modules/transport-grpc/licenses/protobufs-1.3.0.jar.sha1 new file mode 100644 index 0000000000000..08f706404d3c8 --- /dev/null +++ b/modules/transport-grpc/licenses/protobufs-1.3.0.jar.sha1 @@ -0,0 +1 @@ +a141d00a9de80085436c648502d1b015fd89b9f6 \ No newline at end of file diff --git a/modules/transport-grpc/spi/licenses/protobufs-1.2.0.jar.sha1 b/modules/transport-grpc/spi/licenses/protobufs-1.2.0.jar.sha1 deleted file mode 100644 index b122dd4dca041..0000000000000 --- a/modules/transport-grpc/spi/licenses/protobufs-1.2.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -6688b30c1b5a73273e1062a6017b8de9f3ff7f33 \ No newline at end of file diff --git a/modules/transport-grpc/spi/licenses/protobufs-1.3.0.jar.sha1 b/modules/transport-grpc/spi/licenses/protobufs-1.3.0.jar.sha1 new file mode 100644 index 0000000000000..08f706404d3c8 --- /dev/null +++ b/modules/transport-grpc/spi/licenses/protobufs-1.3.0.jar.sha1 @@ -0,0 +1 @@ +a141d00a9de80085436c648502d1b015fd89b9f6 \ No newline at end of file diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/BoostingQueryBuilderProtoConverter.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/BoostingQueryBuilderProtoConverter.java new file mode 100644 index 0000000000000..0af4664acc5ad --- /dev/null +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/BoostingQueryBuilderProtoConverter.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.transport.grpc.proto.request.search.query; + +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.protobufs.QueryContainer; +import org.opensearch.transport.grpc.spi.QueryBuilderProtoConverter; +import org.opensearch.transport.grpc.spi.QueryBuilderProtoConverterRegistry; + +/** + * Converter for Boosting queries. + * This class implements the QueryBuilderProtoConverter interface to provide Boosting query support + * for the gRPC transport module. + */ +public class BoostingQueryBuilderProtoConverter implements QueryBuilderProtoConverter { + + /** + * Default constructor for BoostingQueryBuilderProtoConverter. + */ + public BoostingQueryBuilderProtoConverter() {} + + private QueryBuilderProtoConverterRegistry registry; + + @Override + public void setRegistry(QueryBuilderProtoConverterRegistry registry) { + this.registry = registry; + } + + @Override + public QueryContainer.QueryContainerCase getHandledQueryCase() { + return QueryContainer.QueryContainerCase.BOOSTING; + } + + @Override + public QueryBuilder fromProto(QueryContainer queryContainer) { + if (queryContainer == null || queryContainer.getQueryContainerCase() != QueryContainer.QueryContainerCase.BOOSTING) { + throw new IllegalArgumentException("QueryContainer does not contain a Boosting query"); + } + + return BoostingQueryBuilderProtoUtils.fromProto(queryContainer.getBoosting(), registry); + } +} diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/BoostingQueryBuilderProtoUtils.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/BoostingQueryBuilderProtoUtils.java new file mode 100644 index 0000000000000..2b72d4bd64746 --- /dev/null +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/BoostingQueryBuilderProtoUtils.java @@ -0,0 +1,95 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.transport.grpc.proto.request.search.query; + +import org.opensearch.index.query.AbstractQueryBuilder; +import org.opensearch.index.query.BoostingQueryBuilder; +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.protobufs.BoostingQuery; +import org.opensearch.protobufs.QueryContainer; +import org.opensearch.transport.grpc.spi.QueryBuilderProtoConverterRegistry; + +/** + * Utility class for converting BoostingQuery Protocol Buffers to OpenSearch query objects. + * This class provides methods to transform Protocol Buffer representations of boosting queries + * into their corresponding OpenSearch BoostingQueryBuilder implementations for search operations. + */ +class BoostingQueryBuilderProtoUtils { + + private BoostingQueryBuilderProtoUtils() { + // Utility class, no instances + } + + /** + * Converts a Protocol Buffer BoostingQuery to an OpenSearch BoostingQueryBuilder. + * Similar to {@link BoostingQueryBuilder#fromXContent(org.opensearch.core.xcontent.XContentParser)}, this method + * parses the Protocol Buffer representation and creates a properly configured + * BoostingQueryBuilder with the appropriate positive, negative, negative_boost, boost, and name settings. + * + * @param boostingQueryProto The Protocol Buffer BoostingQuery to convert + * @param registry The registry to use for converting nested queries + * @return A configured BoostingQueryBuilder instance + * @throws IllegalArgumentException if required fields are missing + */ + static BoostingQueryBuilder fromProto(BoostingQuery boostingQueryProto, QueryBuilderProtoConverterRegistry registry) { + // Variables mirror fromXContent exactly + QueryBuilder positiveQuery = null; + boolean positiveQueryFound = false; + QueryBuilder negativeQuery = null; + boolean negativeQueryFound = false; + float boost = AbstractQueryBuilder.DEFAULT_BOOST; + float negativeBoost = -1; + String queryName = null; + + // Process positive query + if (boostingQueryProto.hasPositive()) { + QueryContainer positiveContainer = boostingQueryProto.getPositive(); + positiveQuery = registry.fromProto(positiveContainer); + positiveQueryFound = true; + } + + // Process negative query + if (boostingQueryProto.hasNegative()) { + QueryContainer negativeContainer = boostingQueryProto.getNegative(); + negativeQuery = registry.fromProto(negativeContainer); + negativeQueryFound = true; + } + + // Process negative_boost + negativeBoost = boostingQueryProto.getNegativeBoost(); + + // Process boost (optional) + if (boostingQueryProto.hasBoost()) { + boost = boostingQueryProto.getBoost(); + } + + // Process queryName (optional) + if (boostingQueryProto.hasXName()) { + queryName = boostingQueryProto.getXName(); + } + + // Validation matches fromXContent exactly + if (!positiveQueryFound) { + throw new IllegalArgumentException(BoostingQueryBuilder.POSITIVE_QUERY_REQUIRED); + } + if (!negativeQueryFound) { + throw new IllegalArgumentException(BoostingQueryBuilder.NEGATIVE_QUERY_REQUIRED); + } + if (negativeBoost < 0) { + throw new IllegalArgumentException(BoostingQueryBuilder.NEGATIVE_BOOST_POSITIVE_VALUE_REQUIRED); + } + + // Build the query in the same order as fromXContent + BoostingQueryBuilder boostingQuery = new BoostingQueryBuilder(positiveQuery, negativeQuery); + boostingQuery.negativeBoost(negativeBoost); + boostingQuery.boost(boost); + boostingQuery.queryName(queryName); + + return boostingQuery; + } +} diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/QueryBuilderProtoConverterRegistryImpl.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/QueryBuilderProtoConverterRegistryImpl.java index da655d8d86768..87508223af26c 100644 --- a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/QueryBuilderProtoConverterRegistryImpl.java +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/QueryBuilderProtoConverterRegistryImpl.java @@ -69,6 +69,8 @@ protected void registerBuiltInConverters() { delegate.registerConverter(new MatchBoolPrefixQueryBuilderProtoConverter()); delegate.registerConverter(new MatchPhrasePrefixQueryBuilderProtoConverter()); delegate.registerConverter(new FunctionScoreQueryBuilderProtoConverter()); + delegate.registerConverter(new BoostingQueryBuilderProtoConverter()); + delegate.registerConverter(new SimpleQueryStringBuilderProtoConverter()); // Set the registry on all converters so they can access each other delegate.setRegistryOnAllConverters(this); diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/SimpleQueryStringBuilderProtoConverter.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/SimpleQueryStringBuilderProtoConverter.java new file mode 100644 index 0000000000000..6cac1260beb71 --- /dev/null +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/SimpleQueryStringBuilderProtoConverter.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.transport.grpc.proto.request.search.query; + +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.protobufs.QueryContainer; +import org.opensearch.transport.grpc.spi.QueryBuilderProtoConverter; +import org.opensearch.transport.grpc.spi.QueryBuilderProtoConverterRegistry; + +/** + * Converter for SimpleQueryString queries. + * This class implements the QueryBuilderProtoConverter interface to provide SimpleQueryString query support + * for the gRPC transport module. + */ +public class SimpleQueryStringBuilderProtoConverter implements QueryBuilderProtoConverter { + + /** + * Default constructor for SimpleQueryStringBuilderProtoConverter. + */ + public SimpleQueryStringBuilderProtoConverter() {} + + private QueryBuilderProtoConverterRegistry registry; + + @Override + public void setRegistry(QueryBuilderProtoConverterRegistry registry) { + this.registry = registry; + } + + @Override + public QueryContainer.QueryContainerCase getHandledQueryCase() { + return QueryContainer.QueryContainerCase.SIMPLE_QUERY_STRING; + } + + @Override + public QueryBuilder fromProto(QueryContainer queryContainer) { + if (queryContainer == null || queryContainer.getQueryContainerCase() != QueryContainer.QueryContainerCase.SIMPLE_QUERY_STRING) { + throw new IllegalArgumentException("QueryContainer does not contain a SimpleQueryString query"); + } + + return SimpleQueryStringBuilderProtoUtils.fromProto(queryContainer.getSimpleQueryString()); + } +} diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/SimpleQueryStringBuilderProtoUtils.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/SimpleQueryStringBuilderProtoUtils.java new file mode 100644 index 0000000000000..84842911b6bd4 --- /dev/null +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/SimpleQueryStringBuilderProtoUtils.java @@ -0,0 +1,308 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.transport.grpc.proto.request.search.query; + +import org.opensearch.index.query.AbstractQueryBuilder; +import org.opensearch.index.query.Operator; +import org.opensearch.index.query.SimpleQueryStringBuilder; +import org.opensearch.index.query.SimpleQueryStringFlag; +import org.opensearch.protobufs.SimpleQueryStringQuery; +import org.opensearch.transport.grpc.proto.request.search.OperatorProtoUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * Utility class for converting SimpleQueryStringQuery Protocol Buffers to OpenSearch query objects. + * This class provides methods to transform Protocol Buffer representations of simple query string queries + * into their corresponding OpenSearch SimpleQueryStringBuilder implementations for search operations. + */ +class SimpleQueryStringBuilderProtoUtils { + + // Error message constants matching SimpleQueryStringBuilder + static final String QUERY_TEXT_MISSING = "[" + SimpleQueryStringBuilder.NAME + "] query text missing"; + static final String UNKNOWN_FLAG_PREFIX = "Unknown " + SimpleQueryStringBuilder.NAME + " flag ["; + + private SimpleQueryStringBuilderProtoUtils() { + // Utility class, no instances + } + + /** + * Converts a Protocol Buffer SimpleQueryStringQuery to an OpenSearch SimpleQueryStringBuilder. + * Similar to {@link SimpleQueryStringBuilder#fromXContent(org.opensearch.core.xcontent.XContentParser)}, this method + * parses the Protocol Buffer representation and creates a properly configured + * SimpleQueryStringBuilder with all appropriate settings. + * + * @param simpleQueryStringProto The Protocol Buffer SimpleQueryStringQuery to convert + * @return A configured SimpleQueryStringBuilder instance + * @throws IllegalArgumentException if required fields are missing + */ + static SimpleQueryStringBuilder fromProto(SimpleQueryStringQuery simpleQueryStringProto) { + // Variables mirror fromXContent (with correct spelling for fuzzyPrefixLength) + String queryBody = null; + float boost = AbstractQueryBuilder.DEFAULT_BOOST; + String queryName = null; + String minimumShouldMatch = null; + Map fieldsAndWeights = null; + Operator defaultOperator = null; + String analyzerName = null; + int flags = SimpleQueryStringFlag.ALL.value(); + Boolean lenient = null; + boolean analyzeWildcard = SimpleQueryStringBuilder.DEFAULT_ANALYZE_WILDCARD; + String quoteFieldSuffix = null; + boolean autoGenerateSynonymsPhraseQuery = true; + int fuzzyPrefixLength = SimpleQueryStringBuilder.DEFAULT_FUZZY_PREFIX_LENGTH; + int fuzzyMaxExpansions = SimpleQueryStringBuilder.DEFAULT_FUZZY_MAX_EXPANSIONS; + boolean fuzzyTranspositions = SimpleQueryStringBuilder.DEFAULT_FUZZY_TRANSPOSITIONS; + + // Process query text (required) + queryBody = simpleQueryStringProto.getQuery(); + + // Process fields (optional) + if (simpleQueryStringProto.getFieldsCount() > 0) { + fieldsAndWeights = new HashMap<>(); + for (String field : simpleQueryStringProto.getFieldsList()) { + // Fields are stored as "field^boost" format or just "field" + String[] fieldAndBoost = field.split("\\^"); + if (fieldAndBoost.length == 2) { + try { + float fieldBoost = Float.parseFloat(fieldAndBoost[1]); + fieldsAndWeights.put(fieldAndBoost[0], fieldBoost); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("invalid boost value in field [" + field + "]", e); + } + } else { + fieldsAndWeights.put(field, AbstractQueryBuilder.DEFAULT_BOOST); + } + } + } + + // Process boost (optional) + if (simpleQueryStringProto.hasBoost()) { + boost = simpleQueryStringProto.getBoost(); + } + + // Process analyzer (optional) + if (simpleQueryStringProto.hasAnalyzer()) { + analyzerName = simpleQueryStringProto.getAnalyzer(); + } + + // Process default_operator (optional) + if (simpleQueryStringProto.hasDefaultOperator() + && simpleQueryStringProto.getDefaultOperator() != org.opensearch.protobufs.Operator.OPERATOR_UNSPECIFIED) { + defaultOperator = OperatorProtoUtils.fromEnum(simpleQueryStringProto.getDefaultOperator()); + } + + // Process flags (optional) + if (simpleQueryStringProto.hasFlags()) { + flags = parseFlags(simpleQueryStringProto.getFlags()); + } + + // Process lenient (optional) + if (simpleQueryStringProto.hasLenient()) { + lenient = simpleQueryStringProto.getLenient(); + } + + // Process analyze_wildcard (optional) + if (simpleQueryStringProto.hasAnalyzeWildcard()) { + analyzeWildcard = simpleQueryStringProto.getAnalyzeWildcard(); + } + + // Process queryName (optional) + if (simpleQueryStringProto.hasXName()) { + queryName = simpleQueryStringProto.getXName(); + } + + // Process minimum_should_match (optional) + if (simpleQueryStringProto.hasMinimumShouldMatch()) { + switch (simpleQueryStringProto.getMinimumShouldMatch().getMinimumShouldMatchCase()) { + case INT32: + minimumShouldMatch = String.valueOf(simpleQueryStringProto.getMinimumShouldMatch().getInt32()); + break; + case STRING: + minimumShouldMatch = simpleQueryStringProto.getMinimumShouldMatch().getString(); + break; + default: + // No minimum_should_match specified + break; + } + } + + // Process quote_field_suffix (optional) + if (simpleQueryStringProto.hasQuoteFieldSuffix()) { + quoteFieldSuffix = simpleQueryStringProto.getQuoteFieldSuffix(); + } + + // Process auto_generate_synonyms_phrase_query (optional) + if (simpleQueryStringProto.hasAutoGenerateSynonymsPhraseQuery()) { + autoGenerateSynonymsPhraseQuery = simpleQueryStringProto.getAutoGenerateSynonymsPhraseQuery(); + } + + // Process fuzzy_prefix_length (optional) + if (simpleQueryStringProto.hasFuzzyPrefixLength()) { + fuzzyPrefixLength = simpleQueryStringProto.getFuzzyPrefixLength(); + } + + // Process fuzzy_max_expansions (optional) + if (simpleQueryStringProto.hasFuzzyMaxExpansions()) { + fuzzyMaxExpansions = simpleQueryStringProto.getFuzzyMaxExpansions(); + } + + // Process fuzzy_transpositions (optional) + if (simpleQueryStringProto.hasFuzzyTranspositions()) { + fuzzyTranspositions = simpleQueryStringProto.getFuzzyTranspositions(); + } + + // Query text is required - validate AFTER all field processing (matches fromXContent) + // Note: XContent only checks for null (allows empty strings) + // Proto strings are never null (default to empty), so this check will never trigger + // but we keep it for consistency with XContent pattern + if (queryBody == null) { + throw new IllegalArgumentException(SimpleQueryStringBuilder.QUERY_TEXT_MISSING); + } + + // Build the query in the same order as fromXContent + SimpleQueryStringBuilder qb = new SimpleQueryStringBuilder(queryBody); + if (fieldsAndWeights != null) { + qb.fields(fieldsAndWeights); + } + qb.boost(boost).analyzer(analyzerName).queryName(queryName).minimumShouldMatch(minimumShouldMatch); + qb.flags(convertIntToFlags(flags)).defaultOperator(defaultOperator); + if (lenient != null) { + qb.lenient(lenient); + } + qb.analyzeWildcard(analyzeWildcard).boost(boost).quoteFieldSuffix(quoteFieldSuffix); + qb.autoGenerateSynonymsPhraseQuery(autoGenerateSynonymsPhraseQuery); + qb.fuzzyPrefixLength(fuzzyPrefixLength); + qb.fuzzyMaxExpansions(fuzzyMaxExpansions); + qb.fuzzyTranspositions(fuzzyTranspositions); + + return qb; + } + + /** + * Converts an integer flags value to SimpleQueryStringFlag array. + * This is needed because SimpleQueryStringBuilder.flags(int) is package-private. + * + * @param flagsValue The integer flags value + * @return Array of SimpleQueryStringFlag + */ + private static SimpleQueryStringFlag[] convertIntToFlags(int flagsValue) { + if (flagsValue == -1) { + return new SimpleQueryStringFlag[] { SimpleQueryStringFlag.ALL }; + } + if (flagsValue == 0) { + return new SimpleQueryStringFlag[] { SimpleQueryStringFlag.NONE }; + } + + // Decompose the flags value into individual flags + java.util.List flagsList = new java.util.ArrayList<>(); + for (SimpleQueryStringFlag flag : SimpleQueryStringFlag.values()) { + if (flag == SimpleQueryStringFlag.ALL || flag == SimpleQueryStringFlag.NONE) { + continue; // Skip ALL and NONE as they're special cases + } + if ((flagsValue & flag.value()) != 0) { + flagsList.add(flag); + } + } + + return flagsList.toArray(new SimpleQueryStringFlag[0]); + } + + /** + * Parses the flags from the Protocol Buffer format. + * Handles both single flag and multiple flags (string format). + * + * @param flagsProto The Protocol Buffer SimpleQueryStringFlags + * @return The integer flag value + */ + private static int parseFlags(org.opensearch.protobufs.SimpleQueryStringFlags flagsProto) { + switch (flagsProto.getSimpleQueryStringFlagsCase()) { + case SINGLE: + return convertSingleFlag(flagsProto.getSingle()); + case MULTIPLE: + return resolveFlagsString(flagsProto.getMultiple()); + default: + return SimpleQueryStringFlag.ALL.value(); + } + } + + /** + * Resolves flags from a pipe-delimited string (e.g., "AND|OR|NOT"). + * This is our own implementation since SimpleQueryStringFlag.resolveFlags is package-private. + * + * @param flags The pipe-delimited flags string + * @return The integer flag value + */ + private static int resolveFlagsString(String flags) { + if (flags == null || flags.isEmpty()) { + return SimpleQueryStringFlag.ALL.value(); + } + + int magic = SimpleQueryStringFlag.NONE.value(); + for (String s : flags.split("\\|")) { + s = s.trim(); + if (s.isEmpty()) { + continue; + } + try { + SimpleQueryStringFlag flag = SimpleQueryStringFlag.valueOf(s.toUpperCase(java.util.Locale.ROOT)); + switch (flag) { + case NONE: + return 0; + case ALL: + return -1; + default: + magic |= flag.value(); + } + } catch (IllegalArgumentException iae) { + throw new IllegalArgumentException(SimpleQueryStringBuilder.UNKNOWN_FLAG_PREFIX + s + "]"); + } + } + return magic; + } + + /** + * Converts a single Protocol Buffer flag to the integer value. + * + * @param flag The Protocol Buffer SimpleQueryStringFlag + * @return The integer flag value + */ + private static int convertSingleFlag(org.opensearch.protobufs.SimpleQueryStringFlag flag) { + switch (flag) { + case SIMPLE_QUERY_STRING_FLAG_ALL: + return SimpleQueryStringFlag.ALL.value(); + case SIMPLE_QUERY_STRING_FLAG_AND: + return SimpleQueryStringFlag.AND.value(); + case SIMPLE_QUERY_STRING_FLAG_ESCAPE: + return SimpleQueryStringFlag.ESCAPE.value(); + case SIMPLE_QUERY_STRING_FLAG_FUZZY: + return SimpleQueryStringFlag.FUZZY.value(); + case SIMPLE_QUERY_STRING_FLAG_NEAR: + return SimpleQueryStringFlag.NEAR.value(); + case SIMPLE_QUERY_STRING_FLAG_NONE: + return SimpleQueryStringFlag.NONE.value(); + case SIMPLE_QUERY_STRING_FLAG_NOT: + return SimpleQueryStringFlag.NOT.value(); + case SIMPLE_QUERY_STRING_FLAG_OR: + return SimpleQueryStringFlag.OR.value(); + case SIMPLE_QUERY_STRING_FLAG_PHRASE: + return SimpleQueryStringFlag.PHRASE.value(); + case SIMPLE_QUERY_STRING_FLAG_PRECEDENCE: + return SimpleQueryStringFlag.PRECEDENCE.value(); + case SIMPLE_QUERY_STRING_FLAG_PREFIX: + return SimpleQueryStringFlag.PREFIX.value(); + case SIMPLE_QUERY_STRING_FLAG_SLOP: + return SimpleQueryStringFlag.SLOP.value(); + case SIMPLE_QUERY_STRING_FLAG_WHITESPACE: + return SimpleQueryStringFlag.WHITESPACE.value(); + default: + return SimpleQueryStringFlag.ALL.value(); + } + } +} diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/BoostingQueryBuilderProtoConverterTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/BoostingQueryBuilderProtoConverterTests.java new file mode 100644 index 0000000000000..70e79ffe5b2d9 --- /dev/null +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/BoostingQueryBuilderProtoConverterTests.java @@ -0,0 +1,90 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.transport.grpc.proto.request.search.query; + +import org.opensearch.index.query.BoostingQueryBuilder; +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.protobufs.BoostingQuery; +import org.opensearch.protobufs.FieldValue; +import org.opensearch.protobufs.MatchAllQuery; +import org.opensearch.protobufs.QueryContainer; +import org.opensearch.protobufs.TermQuery; +import org.opensearch.test.OpenSearchTestCase; + +public class BoostingQueryBuilderProtoConverterTests extends OpenSearchTestCase { + + private BoostingQueryBuilderProtoConverter converter; + private QueryBuilderProtoConverterRegistryImpl registry; + + @Override + public void setUp() throws Exception { + super.setUp(); + converter = new BoostingQueryBuilderProtoConverter(); + registry = new QueryBuilderProtoConverterRegistryImpl(); + converter.setRegistry(registry); + } + + public void testGetHandledQueryCase() { + // Test that the converter returns the correct QueryContainerCase + assertEquals("Converter should handle BOOSTING case", QueryContainer.QueryContainerCase.BOOSTING, converter.getHandledQueryCase()); + } + + public void testFromProto() { + // Create a QueryContainer with BoostingQuery + BoostingQuery boostingQuery = BoostingQuery.newBuilder() + .setPositive(QueryContainer.newBuilder().setMatchAll(MatchAllQuery.newBuilder().build()).build()) + .setNegative( + QueryContainer.newBuilder() + .setTerm( + TermQuery.newBuilder().setField("field1").setValue(FieldValue.newBuilder().setString("value1").build()).build() + ) + .build() + ) + .setNegativeBoost(0.5f) + .setBoost(2.0f) + .setXName("test_query") + .build(); + QueryContainer queryContainer = QueryContainer.newBuilder().setBoosting(boostingQuery).build(); + + // Convert the query + QueryBuilder queryBuilder = converter.fromProto(queryContainer); + + // Verify the result + assertNotNull("QueryBuilder should not be null", queryBuilder); + assertTrue("QueryBuilder should be a BoostingQueryBuilder", queryBuilder instanceof BoostingQueryBuilder); + BoostingQueryBuilder boostingQueryBuilder = (BoostingQueryBuilder) queryBuilder; + assertEquals("Boost should match", 2.0f, boostingQueryBuilder.boost(), 0.0f); + assertEquals("Query name should match", "test_query", boostingQueryBuilder.queryName()); + assertEquals("Negative boost should match", 0.5f, boostingQueryBuilder.negativeBoost(), 0.0f); + } + + public void testFromProtoWithInvalidContainer() { + // Create a QueryContainer with a different query type + QueryContainer emptyContainer = QueryContainer.newBuilder().build(); + + // Test that the converter throws an exception + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> converter.fromProto(emptyContainer)); + + // Verify the exception message + assertTrue( + "Exception message should mention 'does not contain a Boosting query'", + exception.getMessage().contains("does not contain a Boosting query") + ); + } + + public void testFromProtoWithNullContainer() { + // Test that the converter throws an exception with null container + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> converter.fromProto(null)); + + // Verify the exception message + assertTrue( + "Exception message should mention 'does not contain a Boosting query'", + exception.getMessage().contains("does not contain a Boosting query") + ); + } +} diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/BoostingQueryBuilderProtoUtilsTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/BoostingQueryBuilderProtoUtilsTests.java new file mode 100644 index 0000000000000..50112317c1e66 --- /dev/null +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/BoostingQueryBuilderProtoUtilsTests.java @@ -0,0 +1,243 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.transport.grpc.proto.request.search.query; + +import org.opensearch.index.query.BoostingQueryBuilder; +import org.opensearch.index.query.MatchAllQueryBuilder; +import org.opensearch.index.query.TermQueryBuilder; +import org.opensearch.protobufs.BoostingQuery; +import org.opensearch.protobufs.FieldValue; +import org.opensearch.protobufs.MatchAllQuery; +import org.opensearch.protobufs.QueryContainer; +import org.opensearch.protobufs.TermQuery; +import org.opensearch.test.OpenSearchTestCase; + +public class BoostingQueryBuilderProtoUtilsTests extends OpenSearchTestCase { + + private QueryBuilderProtoConverterRegistryImpl registry; + + @Override + public void setUp() throws Exception { + super.setUp(); + // Set up the registry with all built-in converters + registry = new QueryBuilderProtoConverterRegistryImpl(); + } + + public void testFromProtoWithAllFields() { + // Create a protobuf BoostingQuery with all fields + BoostingQuery boostingQuery = BoostingQuery.newBuilder() + .setPositive(createMatchAllQueryContainer()) + .setNegative(createTermQueryContainer("field1", "value1")) + .setNegativeBoost(0.5f) + .setBoost(2.0f) + .setXName("test_boosting") + .build(); + + // Call the method under test + BoostingQueryBuilder result = BoostingQueryBuilderProtoUtils.fromProto(boostingQuery, registry); + + // Verify the result + assertNotNull("BoostingQueryBuilder should not be null", result); + assertEquals("Boost should match", 2.0f, result.boost(), 0.0f); + assertEquals("Query name should match", "test_boosting", result.queryName()); + assertEquals("Negative boost should match", 0.5f, result.negativeBoost(), 0.0f); + + // Verify positive query + assertNotNull("Positive query should not be null", result.positiveQuery()); + assertTrue("Positive query should be MatchAllQueryBuilder", result.positiveQuery() instanceof MatchAllQueryBuilder); + + // Verify negative query + assertNotNull("Negative query should not be null", result.negativeQuery()); + assertTrue("Negative query should be TermQueryBuilder", result.negativeQuery() instanceof TermQueryBuilder); + TermQueryBuilder negativeQuery = (TermQueryBuilder) result.negativeQuery(); + assertEquals("Negative query field should match", "field1", negativeQuery.fieldName()); + assertEquals("Negative query value should match", "value1", negativeQuery.value()); + } + + public void testFromProtoWithMinimalFields() { + // Create a protobuf BoostingQuery with only required fields + BoostingQuery boostingQuery = BoostingQuery.newBuilder() + .setPositive(createMatchAllQueryContainer()) + .setNegative(createTermQueryContainer("field1", "value1")) + .setNegativeBoost(0.3f) + .build(); + + // Call the method under test + BoostingQueryBuilder result = BoostingQueryBuilderProtoUtils.fromProto(boostingQuery, registry); + + // Verify the result + assertNotNull("BoostingQueryBuilder should not be null", result); + assertEquals("Boost should be default", 1.0f, result.boost(), 0.0f); + assertNull("Query name should be null", result.queryName()); + assertEquals("Negative boost should match", 0.3f, result.negativeBoost(), 0.0f); + assertNotNull("Positive query should not be null", result.positiveQuery()); + assertNotNull("Negative query should not be null", result.negativeQuery()); + } + + public void testFromProtoWithMissingPositiveQuery() { + // Create a protobuf BoostingQuery without positive query (empty QueryContainer) + BoostingQuery boostingQuery = BoostingQuery.newBuilder() + .setNegative(createTermQueryContainer("field1", "value1")) + .setNegativeBoost(0.5f) + .build(); + + // Call the method under test and expect exception + // Note: Since positive/negative are required proto fields, they default to empty QueryContainer + // The registry will throw "Unsupported query type" for QUERYCONTAINER_NOT_SET + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> BoostingQueryBuilderProtoUtils.fromProto(boostingQuery, registry) + ); + + assertTrue( + "Exception should mention unsupported query or missing positive", + exception.getMessage().contains("Unsupported query type") || exception.getMessage().contains("positive") + ); + } + + public void testFromProtoWithMissingNegativeQuery() { + // Create a protobuf BoostingQuery without negative query (empty QueryContainer) + BoostingQuery boostingQuery = BoostingQuery.newBuilder().setPositive(createMatchAllQueryContainer()).setNegativeBoost(0.5f).build(); + + // Call the method under test and expect exception + // Note: Since positive/negative are required proto fields, they default to empty QueryContainer + // The registry will throw "Unsupported query type" for QUERYCONTAINER_NOT_SET + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> BoostingQueryBuilderProtoUtils.fromProto(boostingQuery, registry) + ); + + assertTrue( + "Exception should mention unsupported query or missing negative", + exception.getMessage().contains("Unsupported query type") || exception.getMessage().contains("negative") + ); + } + + public void testFromProtoWithZeroNegativeBoost() { + // Create a protobuf BoostingQuery with negative_boost = 0 (valid, matches XContent behavior) + BoostingQuery boostingQuery = BoostingQuery.newBuilder() + .setPositive(createMatchAllQueryContainer()) + .setNegative(createTermQueryContainer("field1", "value1")) + .setNegativeBoost(0.0f) + .build(); + + // Call the method under test - should succeed (0 is not < 0) + BoostingQueryBuilder result = BoostingQueryBuilderProtoUtils.fromProto(boostingQuery, registry); + assertNotNull("BoostingQueryBuilder should not be null", result); + assertEquals("Negative boost should be 0", 0.0f, result.negativeBoost(), 0.0f); + } + + public void testFromProtoWithNegativeNegativeBoost() { + // Create a protobuf BoostingQuery with negative_boost < 0 (invalid) + BoostingQuery boostingQuery = BoostingQuery.newBuilder() + .setPositive(createMatchAllQueryContainer()) + .setNegative(createTermQueryContainer("field1", "value1")) + .setNegativeBoost(-0.5f) + .build(); + + // Call the method under test and expect exception + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> BoostingQueryBuilderProtoUtils.fromProto(boostingQuery, registry) + ); + + assertEquals( + "Exception message should match constant", + BoostingQueryBuilder.NEGATIVE_BOOST_POSITIVE_VALUE_REQUIRED, + exception.getMessage() + ); + } + + public void testFromProtoWithOnlyBoost() { + // Create a protobuf BoostingQuery with only boost (no name) + BoostingQuery boostingQuery = BoostingQuery.newBuilder() + .setPositive(createMatchAllQueryContainer()) + .setNegative(createTermQueryContainer("field1", "value1")) + .setNegativeBoost(0.5f) + .setBoost(3.5f) + .build(); + + // Call the method under test + BoostingQueryBuilder result = BoostingQueryBuilderProtoUtils.fromProto(boostingQuery, registry); + + // Verify the result + assertNotNull("BoostingQueryBuilder should not be null", result); + assertEquals("Boost should match", 3.5f, result.boost(), 0.0f); + assertNull("Query name should be null", result.queryName()); + } + + public void testFromProtoWithOnlyName() { + // Create a protobuf BoostingQuery with only name (no boost) + BoostingQuery boostingQuery = BoostingQuery.newBuilder() + .setPositive(createMatchAllQueryContainer()) + .setNegative(createTermQueryContainer("field1", "value1")) + .setNegativeBoost(0.5f) + .setXName("my_boosting_query") + .build(); + + // Call the method under test + BoostingQueryBuilder result = BoostingQueryBuilderProtoUtils.fromProto(boostingQuery, registry); + + // Verify the result + assertNotNull("BoostingQueryBuilder should not be null", result); + assertEquals("Boost should be default", 1.0f, result.boost(), 0.0f); + assertEquals("Query name should match", "my_boosting_query", result.queryName()); + } + + public void testFromProtoWithComplexNestedQueries() { + // Create nested queries for both positive and negative + QueryContainer positiveQuery = QueryContainer.newBuilder() + .setTerm(TermQuery.newBuilder().setField("status").setValue(FieldValue.newBuilder().setString("active").build()).build()) + .build(); + + QueryContainer negativeQuery = QueryContainer.newBuilder() + .setTerm(TermQuery.newBuilder().setField("category").setValue(FieldValue.newBuilder().setString("spam").build()).build()) + .build(); + + BoostingQuery boostingQuery = BoostingQuery.newBuilder() + .setPositive(positiveQuery) + .setNegative(negativeQuery) + .setNegativeBoost(0.2f) + .setBoost(1.5f) + .setXName("complex_boosting") + .build(); + + // Call the method under test + BoostingQueryBuilder result = BoostingQueryBuilderProtoUtils.fromProto(boostingQuery, registry); + + // Verify the result + assertNotNull("BoostingQueryBuilder should not be null", result); + assertEquals("Boost should match", 1.5f, result.boost(), 0.0f); + assertEquals("Query name should match", "complex_boosting", result.queryName()); + assertEquals("Negative boost should match", 0.2f, result.negativeBoost(), 0.0f); + + // Verify positive query + assertTrue("Positive query should be TermQueryBuilder", result.positiveQuery() instanceof TermQueryBuilder); + TermQueryBuilder positiveTermQuery = (TermQueryBuilder) result.positiveQuery(); + assertEquals("Positive query field should match", "status", positiveTermQuery.fieldName()); + assertEquals("Positive query value should match", "active", positiveTermQuery.value()); + + // Verify negative query + assertTrue("Negative query should be TermQueryBuilder", result.negativeQuery() instanceof TermQueryBuilder); + TermQueryBuilder negativeTermQuery = (TermQueryBuilder) result.negativeQuery(); + assertEquals("Negative query field should match", "category", negativeTermQuery.fieldName()); + assertEquals("Negative query value should match", "spam", negativeTermQuery.value()); + } + + // Helper methods to create QueryContainers + private QueryContainer createMatchAllQueryContainer() { + return QueryContainer.newBuilder().setMatchAll(MatchAllQuery.newBuilder().build()).build(); + } + + private QueryContainer createTermQueryContainer(String field, String value) { + return QueryContainer.newBuilder() + .setTerm(TermQuery.newBuilder().setField(field).setValue(FieldValue.newBuilder().setString(value).build()).build()) + .build(); + } +} diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/SimpleQueryStringBuilderProtoConverterTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/SimpleQueryStringBuilderProtoConverterTests.java new file mode 100644 index 0000000000000..7747c3a3c4a87 --- /dev/null +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/SimpleQueryStringBuilderProtoConverterTests.java @@ -0,0 +1,82 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.transport.grpc.proto.request.search.query; + +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.index.query.SimpleQueryStringBuilder; +import org.opensearch.protobufs.QueryContainer; +import org.opensearch.protobufs.SimpleQueryStringQuery; +import org.opensearch.test.OpenSearchTestCase; + +public class SimpleQueryStringBuilderProtoConverterTests extends OpenSearchTestCase { + + private SimpleQueryStringBuilderProtoConverter converter; + + @Override + public void setUp() throws Exception { + super.setUp(); + converter = new SimpleQueryStringBuilderProtoConverter(); + } + + public void testGetHandledQueryCase() { + // Test that the converter returns the correct QueryContainerCase + assertEquals( + "Converter should handle SIMPLE_QUERY_STRING case", + QueryContainer.QueryContainerCase.SIMPLE_QUERY_STRING, + converter.getHandledQueryCase() + ); + } + + public void testFromProto() { + // Create a QueryContainer with SimpleQueryStringQuery + SimpleQueryStringQuery simpleQueryString = SimpleQueryStringQuery.newBuilder() + .setQuery("search text") + .setBoost(2.0f) + .setXName("test_query") + .setAnalyzer("standard") + .build(); + QueryContainer queryContainer = QueryContainer.newBuilder().setSimpleQueryString(simpleQueryString).build(); + + // Convert the query + QueryBuilder queryBuilder = converter.fromProto(queryContainer); + + // Verify the result + assertNotNull("QueryBuilder should not be null", queryBuilder); + assertTrue("QueryBuilder should be a SimpleQueryStringBuilder", queryBuilder instanceof SimpleQueryStringBuilder); + SimpleQueryStringBuilder simpleQueryStringBuilder = (SimpleQueryStringBuilder) queryBuilder; + assertEquals("Query text should match", "search text", simpleQueryStringBuilder.value()); + assertEquals("Boost should match", 2.0f, simpleQueryStringBuilder.boost(), 0.0f); + assertEquals("Query name should match", "test_query", simpleQueryStringBuilder.queryName()); + assertEquals("Analyzer should match", "standard", simpleQueryStringBuilder.analyzer()); + } + + public void testFromProtoWithInvalidContainer() { + // Create a QueryContainer with a different query type + QueryContainer emptyContainer = QueryContainer.newBuilder().build(); + + // Test that the converter throws an exception + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> converter.fromProto(emptyContainer)); + + // Verify the exception message + assertTrue( + "Exception message should mention 'does not contain a SimpleQueryString query'", + exception.getMessage().contains("does not contain a SimpleQueryString query") + ); + } + + public void testFromProtoWithNullContainer() { + // Test that the converter throws an exception with null container + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> converter.fromProto(null)); + + // Verify the exception message + assertTrue( + "Exception message should mention 'does not contain a SimpleQueryString query'", + exception.getMessage().contains("does not contain a SimpleQueryString query") + ); + } +} diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/SimpleQueryStringBuilderProtoUtilsTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/SimpleQueryStringBuilderProtoUtilsTests.java new file mode 100644 index 0000000000000..738cafbed2302 --- /dev/null +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/SimpleQueryStringBuilderProtoUtilsTests.java @@ -0,0 +1,348 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.transport.grpc.proto.request.search.query; + +import org.opensearch.index.query.Operator; +import org.opensearch.index.query.SimpleQueryStringBuilder; +import org.opensearch.protobufs.MinimumShouldMatch; +import org.opensearch.protobufs.SimpleQueryStringFlags; +import org.opensearch.protobufs.SimpleQueryStringQuery; +import org.opensearch.test.OpenSearchTestCase; + +import java.util.Map; + +public class SimpleQueryStringBuilderProtoUtilsTests extends OpenSearchTestCase { + + public void testFromProtoWithOnlyQuery() { + // Create a protobuf SimpleQueryStringQuery with only required field + SimpleQueryStringQuery query = SimpleQueryStringQuery.newBuilder().setQuery("test query").build(); + + // Call the method under test + SimpleQueryStringBuilder result = SimpleQueryStringBuilderProtoUtils.fromProto(query); + + // Verify the result + assertNotNull("SimpleQueryStringBuilder should not be null", result); + assertEquals("Query text should match", "test query", result.value()); + assertEquals("Boost should be default", 1.0f, result.boost(), 0.0f); + assertNull("Query name should be null", result.queryName()); + assertNull("Analyzer should be null", result.analyzer()); + assertEquals("Default operator should be OR", Operator.OR, result.defaultOperator()); + // Note: flags() is package-private, so we can't directly test it here + // The default value is ALL which gets set correctly by the implementation + assertFalse("Lenient should be false", result.lenient()); + assertFalse("Analyze wildcard should be false", result.analyzeWildcard()); + assertNull("Minimum should match should be null", result.minimumShouldMatch()); + assertNull("Quote field suffix should be null", result.quoteFieldSuffix()); + assertTrue("Auto generate synonyms phrase query should be true", result.autoGenerateSynonymsPhraseQuery()); + assertEquals( + "Fuzzy prefix length should be default", + SimpleQueryStringBuilder.DEFAULT_FUZZY_PREFIX_LENGTH, + result.fuzzyPrefixLength() + ); + assertEquals( + "Fuzzy max expansions should be default", + SimpleQueryStringBuilder.DEFAULT_FUZZY_MAX_EXPANSIONS, + result.fuzzyMaxExpansions() + ); + assertTrue("Fuzzy transpositions should be true", result.fuzzyTranspositions()); + } + + public void testFromProtoWithAllFields() { + // Create a protobuf SimpleQueryStringQuery with all fields + SimpleQueryStringQuery query = SimpleQueryStringQuery.newBuilder() + .setQuery("search text") + .addFields("title^2.0") + .addFields("content") + .addFields("author^1.5") + .setBoost(2.5f) + .setAnalyzer("standard") + .setDefaultOperator(org.opensearch.protobufs.Operator.OPERATOR_AND) + .setFlags( + SimpleQueryStringFlags.newBuilder() + .setSingle(org.opensearch.protobufs.SimpleQueryStringFlag.SIMPLE_QUERY_STRING_FLAG_AND) + .build() + ) + .setLenient(true) + .setAnalyzeWildcard(true) + .setXName("my_query") + .setMinimumShouldMatch(MinimumShouldMatch.newBuilder().setString("75%").build()) + .setQuoteFieldSuffix(".exact") + .setAutoGenerateSynonymsPhraseQuery(false) + .setFuzzyPrefixLength(2) + .setFuzzyMaxExpansions(100) + .setFuzzyTranspositions(false) + .build(); + + // Call the method under test + SimpleQueryStringBuilder result = SimpleQueryStringBuilderProtoUtils.fromProto(query); + + // Verify the result + assertNotNull("SimpleQueryStringBuilder should not be null", result); + assertEquals("Query text should match", "search text", result.value()); + assertEquals("Boost should match", 2.5f, result.boost(), 0.0f); + assertEquals("Query name should match", "my_query", result.queryName()); + assertEquals("Analyzer should match", "standard", result.analyzer()); + assertEquals("Default operator should be AND", Operator.AND, result.defaultOperator()); + // Note: flags() is package-private, verified by successful creation + assertTrue("Lenient should be true", result.lenient()); + assertTrue("Analyze wildcard should be true", result.analyzeWildcard()); + assertEquals("Minimum should match should be 75%", "75%", result.minimumShouldMatch()); + assertEquals("Quote field suffix should match", ".exact", result.quoteFieldSuffix()); + assertFalse("Auto generate synonyms phrase query should be false", result.autoGenerateSynonymsPhraseQuery()); + assertEquals("Fuzzy prefix length should match", 2, result.fuzzyPrefixLength()); + assertEquals("Fuzzy max expansions should match", 100, result.fuzzyMaxExpansions()); + assertFalse("Fuzzy transpositions should be false", result.fuzzyTranspositions()); + + // Verify fields + Map fields = result.fields(); + assertEquals("Should have 3 fields", 3, fields.size()); + assertTrue("Should contain title field", fields.containsKey("title")); + assertEquals("title boost should be 2.0", 2.0f, fields.get("title"), 0.0f); + assertTrue("Should contain content field", fields.containsKey("content")); + assertEquals("content boost should be 1.0", 1.0f, fields.get("content"), 0.0f); + assertTrue("Should contain author field", fields.containsKey("author")); + assertEquals("author boost should be 1.5", 1.5f, fields.get("author"), 0.0f); + } + + public void testFromProtoWithMissingQuery() { + // Create a protobuf SimpleQueryStringQuery without setting query text + // Proto strings default to empty string, which XContent allows (only null is rejected) + SimpleQueryStringQuery query = SimpleQueryStringQuery.newBuilder().build(); + + // Call the method under test - should succeed with empty string (matching XContent behavior) + SimpleQueryStringBuilder result = SimpleQueryStringBuilderProtoUtils.fromProto(query); + + assertNotNull("SimpleQueryStringBuilder should not be null", result); + assertEquals("Query text should be empty string (proto default)", "", result.value()); + } + + public void testFromProtoWithEmptyQuery() { + // Create a protobuf SimpleQueryStringQuery with explicitly empty query text + // XContent allows empty strings (only null is rejected), so proto should too + SimpleQueryStringQuery query = SimpleQueryStringQuery.newBuilder().setQuery("").build(); + + // Call the method under test - should succeed with empty string + SimpleQueryStringBuilder result = SimpleQueryStringBuilderProtoUtils.fromProto(query); + + assertNotNull("SimpleQueryStringBuilder should not be null", result); + assertEquals("Query text should be empty string", "", result.value()); + } + + public void testFromProtoWithFieldsNoBoost() { + // Create a protobuf SimpleQueryStringQuery with fields without boost notation + SimpleQueryStringQuery query = SimpleQueryStringQuery.newBuilder().setQuery("test").addFields("title").addFields("content").build(); + + // Call the method under test + SimpleQueryStringBuilder result = SimpleQueryStringBuilderProtoUtils.fromProto(query); + + // Verify fields + Map fields = result.fields(); + assertEquals("Should have 2 fields", 2, fields.size()); + assertTrue("Should contain title field", fields.containsKey("title")); + assertEquals("title boost should be 1.0", 1.0f, fields.get("title"), 0.0f); + assertTrue("Should contain content field", fields.containsKey("content")); + assertEquals("content boost should be 1.0", 1.0f, fields.get("content"), 0.0f); + } + + public void testFromProtoWithInvalidBoostNotation() { + // Create a protobuf SimpleQueryStringQuery with invalid boost notation + SimpleQueryStringQuery query = SimpleQueryStringQuery.newBuilder().setQuery("test").addFields("title^invalid").build(); + + // Call the method under test - should throw exception on invalid boost + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> SimpleQueryStringBuilderProtoUtils.fromProto(query) + ); + + // Verify the exception message mentions the invalid field + assertTrue( + "Exception message should mention invalid boost value", + exception.getMessage().contains("invalid boost value") && exception.getMessage().contains("title^invalid") + ); + } + + public void testFromProtoWithFlagsSingle() { + // Test all single flag values - we can't directly test the flags() method + // because it's package-private, but we can verify the query builds successfully + org.opensearch.protobufs.SimpleQueryStringFlag[] protoFlags = { + org.opensearch.protobufs.SimpleQueryStringFlag.SIMPLE_QUERY_STRING_FLAG_ALL, + org.opensearch.protobufs.SimpleQueryStringFlag.SIMPLE_QUERY_STRING_FLAG_NONE, + org.opensearch.protobufs.SimpleQueryStringFlag.SIMPLE_QUERY_STRING_FLAG_AND, + org.opensearch.protobufs.SimpleQueryStringFlag.SIMPLE_QUERY_STRING_FLAG_OR, + org.opensearch.protobufs.SimpleQueryStringFlag.SIMPLE_QUERY_STRING_FLAG_NOT, + org.opensearch.protobufs.SimpleQueryStringFlag.SIMPLE_QUERY_STRING_FLAG_PREFIX, + org.opensearch.protobufs.SimpleQueryStringFlag.SIMPLE_QUERY_STRING_FLAG_PHRASE, + org.opensearch.protobufs.SimpleQueryStringFlag.SIMPLE_QUERY_STRING_FLAG_PRECEDENCE, + org.opensearch.protobufs.SimpleQueryStringFlag.SIMPLE_QUERY_STRING_FLAG_ESCAPE, + org.opensearch.protobufs.SimpleQueryStringFlag.SIMPLE_QUERY_STRING_FLAG_WHITESPACE, + org.opensearch.protobufs.SimpleQueryStringFlag.SIMPLE_QUERY_STRING_FLAG_FUZZY, + org.opensearch.protobufs.SimpleQueryStringFlag.SIMPLE_QUERY_STRING_FLAG_NEAR, + org.opensearch.protobufs.SimpleQueryStringFlag.SIMPLE_QUERY_STRING_FLAG_SLOP }; + + for (org.opensearch.protobufs.SimpleQueryStringFlag protoFlag : protoFlags) { + SimpleQueryStringQuery query = SimpleQueryStringQuery.newBuilder() + .setQuery("test") + .setFlags(SimpleQueryStringFlags.newBuilder().setSingle(protoFlag).build()) + .build(); + + SimpleQueryStringBuilder result = SimpleQueryStringBuilderProtoUtils.fromProto(query); + // Note: flags() is package-private, so we just verify the query builds successfully + assertNotNull("Query should build successfully for flag " + protoFlag, result); + assertEquals("Query text should match", "test", result.value()); + } + } + + public void testFromProtoWithFlagsMultiple() { + // Test multiple flags as pipe-delimited string + // Note: flags() is package-private, so we just verify the query builds successfully + SimpleQueryStringQuery query = SimpleQueryStringQuery.newBuilder() + .setQuery("test") + .setFlags(SimpleQueryStringFlags.newBuilder().setMultiple("AND|OR|NOT").build()) + .build(); + + SimpleQueryStringBuilder result = SimpleQueryStringBuilderProtoUtils.fromProto(query); + assertNotNull("Query should build successfully", result); + assertEquals("Query text should match", "test", result.value()); + } + + public void testFromProtoWithFlagsMultipleAll() { + // Test multiple flags with ALL + SimpleQueryStringQuery query = SimpleQueryStringQuery.newBuilder() + .setQuery("test") + .setFlags(SimpleQueryStringFlags.newBuilder().setMultiple("ALL").build()) + .build(); + + SimpleQueryStringBuilder result = SimpleQueryStringBuilderProtoUtils.fromProto(query); + assertNotNull("Query should build successfully", result); + assertEquals("Query text should match", "test", result.value()); + } + + public void testFromProtoWithFlagsMultipleNone() { + // Test multiple flags with NONE + SimpleQueryStringQuery query = SimpleQueryStringQuery.newBuilder() + .setQuery("test") + .setFlags(SimpleQueryStringFlags.newBuilder().setMultiple("NONE").build()) + .build(); + + SimpleQueryStringBuilder result = SimpleQueryStringBuilderProtoUtils.fromProto(query); + assertNotNull("Query should build successfully", result); + assertEquals("Query text should match", "test", result.value()); + } + + public void testFromProtoWithFlagsMultipleInvalid() { + // Test multiple flags with invalid flag + SimpleQueryStringQuery query = SimpleQueryStringQuery.newBuilder() + .setQuery("test") + .setFlags(SimpleQueryStringFlags.newBuilder().setMultiple("INVALID_FLAG").build()) + .build(); + + // Call the method under test and expect exception + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> SimpleQueryStringBuilderProtoUtils.fromProto(query) + ); + + assertTrue("Exception message should mention unknown flag", exception.getMessage().contains("Unknown")); + assertTrue("Exception message should mention the flag name", exception.getMessage().contains("INVALID_FLAG")); + } + + public void testFromProtoWithMinimumShouldMatchInt() { + // Test minimum_should_match as int32 + SimpleQueryStringQuery query = SimpleQueryStringQuery.newBuilder() + .setQuery("test") + .setMinimumShouldMatch(MinimumShouldMatch.newBuilder().setInt32(2).build()) + .build(); + + SimpleQueryStringBuilder result = SimpleQueryStringBuilderProtoUtils.fromProto(query); + assertEquals("Minimum should match should be '2'", "2", result.minimumShouldMatch()); + } + + public void testFromProtoWithMinimumShouldMatchString() { + // Test minimum_should_match as string + SimpleQueryStringQuery query = SimpleQueryStringQuery.newBuilder() + .setQuery("test") + .setMinimumShouldMatch(MinimumShouldMatch.newBuilder().setString("75%").build()) + .build(); + + SimpleQueryStringBuilder result = SimpleQueryStringBuilderProtoUtils.fromProto(query); + assertEquals("Minimum should match should be '75%'", "75%", result.minimumShouldMatch()); + } + + public void testFromProtoWithOperatorOr() { + // Test default_operator as OR + SimpleQueryStringQuery query = SimpleQueryStringQuery.newBuilder() + .setQuery("test") + .setDefaultOperator(org.opensearch.protobufs.Operator.OPERATOR_OR) + .build(); + + SimpleQueryStringBuilder result = SimpleQueryStringBuilderProtoUtils.fromProto(query); + assertEquals("Default operator should be OR", Operator.OR, result.defaultOperator()); + } + + public void testFromProtoWithOperatorAnd() { + // Test default_operator as AND + SimpleQueryStringQuery query = SimpleQueryStringQuery.newBuilder() + .setQuery("test") + .setDefaultOperator(org.opensearch.protobufs.Operator.OPERATOR_AND) + .build(); + + SimpleQueryStringBuilder result = SimpleQueryStringBuilderProtoUtils.fromProto(query); + assertEquals("Default operator should be AND", Operator.AND, result.defaultOperator()); + } + + public void testFromProtoWithEmptyFlagsString() { + // Test with empty flags string - should default to ALL + SimpleQueryStringQuery query = SimpleQueryStringQuery.newBuilder() + .setQuery("test") + .setFlags(SimpleQueryStringFlags.newBuilder().setMultiple("").build()) + .build(); + + SimpleQueryStringBuilder result = SimpleQueryStringBuilderProtoUtils.fromProto(query); + assertNotNull("Query should build successfully", result); + assertEquals("Query text should match", "test", result.value()); + } + + public void testFromProtoWithMultipleFlagsWithSpaces() { + // Test multiple flags with spaces around pipe + SimpleQueryStringQuery query = SimpleQueryStringQuery.newBuilder() + .setQuery("test") + .setFlags(SimpleQueryStringFlags.newBuilder().setMultiple("AND | OR | NOT").build()) + .build(); + + SimpleQueryStringBuilder result = SimpleQueryStringBuilderProtoUtils.fromProto(query); + assertNotNull("Query should build successfully", result); + assertEquals("Query text should match", "test", result.value()); + } + + public void testFromProtoWithEmptyFieldsList() { + // Test with no fields specified + SimpleQueryStringQuery query = SimpleQueryStringQuery.newBuilder().setQuery("test").build(); + + SimpleQueryStringBuilder result = SimpleQueryStringBuilderProtoUtils.fromProto(query); + + // Verify fields map is empty (will use default fields from index settings) + Map fields = result.fields(); + assertEquals("Fields should be empty", 0, fields.size()); + } + + public void testFromProtoWithFuzzySettings() { + // Test fuzzy-related settings + SimpleQueryStringQuery query = SimpleQueryStringQuery.newBuilder() + .setQuery("test") + .setFuzzyPrefixLength(3) + .setFuzzyMaxExpansions(150) + .setFuzzyTranspositions(true) + .build(); + + SimpleQueryStringBuilder result = SimpleQueryStringBuilderProtoUtils.fromProto(query); + assertEquals("Fuzzy prefix length should match", 3, result.fuzzyPrefixLength()); + assertEquals("Fuzzy max expansions should match", 150, result.fuzzyMaxExpansions()); + assertTrue("Fuzzy transpositions should be true", result.fuzzyTranspositions()); + } +} diff --git a/server/src/main/java/org/opensearch/index/query/BoostingQueryBuilder.java b/server/src/main/java/org/opensearch/index/query/BoostingQueryBuilder.java index 830eef1c5fd0e..bea07ba0f8684 100644 --- a/server/src/main/java/org/opensearch/index/query/BoostingQueryBuilder.java +++ b/server/src/main/java/org/opensearch/index/query/BoostingQueryBuilder.java @@ -63,6 +63,12 @@ public class BoostingQueryBuilder extends AbstractQueryBuilder { public static final String NAME = "boosting"; + // Error message constants for validation + public static final String POSITIVE_QUERY_REQUIRED = "[boosting] query requires 'positive' query to be set"; + public static final String NEGATIVE_QUERY_REQUIRED = "[boosting] query requires 'negative' query to be set"; + public static final String NEGATIVE_BOOST_POSITIVE_VALUE_REQUIRED = + "[boosting] query requires 'negative_boost' to be set to be a positive value"; + private static final ParseField POSITIVE_FIELD = new ParseField("positive"); private static final ParseField NEGATIVE_FIELD = new ParseField("negative"); private static final ParseField NEGATIVE_BOOST_FIELD = new ParseField("negative_boost"); @@ -189,16 +195,13 @@ public static BoostingQueryBuilder fromXContent(XContentParser parser) throws IO } if (!positiveQueryFound) { - throw new ParsingException(parser.getTokenLocation(), "[boosting] query requires 'positive' query to be set'"); + throw new ParsingException(parser.getTokenLocation(), POSITIVE_QUERY_REQUIRED); } if (!negativeQueryFound) { - throw new ParsingException(parser.getTokenLocation(), "[boosting] query requires 'negative' query to be set'"); + throw new ParsingException(parser.getTokenLocation(), NEGATIVE_QUERY_REQUIRED); } if (negativeBoost < 0) { - throw new ParsingException( - parser.getTokenLocation(), - "[boosting] query requires 'negative_boost' to be set to be a positive value'" - ); + throw new ParsingException(parser.getTokenLocation(), NEGATIVE_BOOST_POSITIVE_VALUE_REQUIRED); } BoostingQueryBuilder boostingQuery = new BoostingQueryBuilder(positiveQuery, negativeQuery); diff --git a/server/src/main/java/org/opensearch/index/query/SimpleQueryStringBuilder.java b/server/src/main/java/org/opensearch/index/query/SimpleQueryStringBuilder.java index 57ae7dd0ea5e9..7b2b7efbe24ca 100644 --- a/server/src/main/java/org/opensearch/index/query/SimpleQueryStringBuilder.java +++ b/server/src/main/java/org/opensearch/index/query/SimpleQueryStringBuilder.java @@ -108,6 +108,10 @@ public class SimpleQueryStringBuilder extends AbstractQueryBuilder