diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c2f7652a1faa..0d223d6cabd19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Implement GRPC ConstantScoreQuery, FuzzyQuery, MatchBoolPrefixQuery, MatchPhrasePrefix, PrefixQuery, MatchQuery ([#19854](https://github.com/opensearch-project/OpenSearch/pull/19854)) - Add async periodic flush task support for pull-based ingestion ([#19878](https://github.com/opensearch-project/OpenSearch/pull/19878)) - Add support for context aware segments ([#19098](https://github.com/opensearch-project/OpenSearch/pull/19098)) +- Implement GRPC FunctionScoreQuery ([#19888](https://github.com/opensearch-project/OpenSearch/pull/19888)) ### Changed - Faster `terms` query creation for `keyword` field with index and docValues enabled ([#19350](https://github.com/opensearch-project/OpenSearch/pull/19350)) 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 6d11a964fb177..da655d8d86768 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 @@ -13,6 +13,7 @@ import org.opensearch.common.inject.Singleton; import org.opensearch.index.query.QueryBuilder; import org.opensearch.protobufs.QueryContainer; +import org.opensearch.transport.grpc.proto.request.search.query.functionscore.FunctionScoreQueryBuilderProtoConverter; import org.opensearch.transport.grpc.spi.QueryBuilderProtoConverter; import org.opensearch.transport.grpc.spi.QueryBuilderProtoConverterRegistry; @@ -67,6 +68,7 @@ protected void registerBuiltInConverters() { delegate.registerConverter(new MatchQueryBuilderProtoConverter()); delegate.registerConverter(new MatchBoolPrefixQueryBuilderProtoConverter()); delegate.registerConverter(new MatchPhrasePrefixQueryBuilderProtoConverter()); + delegate.registerConverter(new FunctionScoreQueryBuilderProtoConverter()); // 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/functionscore/ExpDecayFunctionProtoUtils.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/ExpDecayFunctionProtoUtils.java new file mode 100644 index 0000000000000..9b99d686398ce --- /dev/null +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/ExpDecayFunctionProtoUtils.java @@ -0,0 +1,141 @@ +/* + * 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.functionscore; + +import org.opensearch.common.geo.GeoPoint; +import org.opensearch.index.query.functionscore.ExponentialDecayFunctionBuilder; +import org.opensearch.index.query.functionscore.ScoreFunctionBuilder; +import org.opensearch.protobufs.DateDecayPlacement; +import org.opensearch.protobufs.DecayFunction; +import org.opensearch.protobufs.DecayPlacement; +import org.opensearch.protobufs.GeoDecayPlacement; +import org.opensearch.protobufs.NumericDecayPlacement; +import org.opensearch.transport.grpc.proto.request.common.GeoPointProtoUtils; + +import java.util.Map; + +/** + * Utility class for converting Protocol Buffer DecayFunction to OpenSearch ExponentialDecayFunctionBuilder. + * This utility handles the transformation of Protocol Buffer DecayFunction objects + * into OpenSearch ExponentialDecayFunctionBuilder instances. + */ +class ExpDecayFunctionProtoUtils { + + private ExpDecayFunctionProtoUtils() { + // Utility class, no instances + } + + /** + * Converts a Protocol Buffer DecayFunction to an OpenSearch ScoreFunctionBuilder. + * Similar to {@link org.opensearch.index.query.functionscore.DecayFunctionParser#fromXContent(org.opensearch.core.xcontent.XContentParser)}, + * this method parses the Protocol Buffer representation and creates a properly configured + * ExponentialDecayFunctionBuilder with decay placement parameters (numeric, geo, or date). + * + * @param decayFunction the Protocol Buffer DecayFunction + * @return the corresponding OpenSearch ScoreFunctionBuilder + * @throws IllegalArgumentException if the decayFunction is null or doesn't contain placements + */ + static ScoreFunctionBuilder fromProto(DecayFunction decayFunction) { + if (decayFunction == null || decayFunction.getPlacementCount() == 0) { + throw new IllegalArgumentException("DecayFunction must have at least one placement"); + } + + Map.Entry entry = decayFunction.getPlacementMap().entrySet().iterator().next(); + String fieldName = entry.getKey(); + DecayPlacement decayPlacement = entry.getValue(); + + if (decayPlacement.hasNumericDecayPlacement()) { + return parseNumericExpDecay(fieldName, decayPlacement.getNumericDecayPlacement()); + } else if (decayPlacement.hasGeoDecayPlacement()) { + return parseGeoExpDecay(fieldName, decayPlacement.getGeoDecayPlacement()); + } else if (decayPlacement.hasDateDecayPlacement()) { + return parseDateExpDecay(fieldName, decayPlacement.getDateDecayPlacement()); + } else { + throw new IllegalArgumentException("Unsupported decay placement type"); + } + } + + /** + * Parses a numeric decay placement for exponential decay. + */ + private static ScoreFunctionBuilder parseNumericExpDecay(String fieldName, NumericDecayPlacement numericPlacement) { + ExponentialDecayFunctionBuilder builder; + if (numericPlacement.hasDecay()) { + builder = new ExponentialDecayFunctionBuilder( + fieldName, + numericPlacement.getOrigin(), + numericPlacement.getScale(), + numericPlacement.hasOffset() ? numericPlacement.getOffset() : null, + numericPlacement.getDecay() + ); + } else { + builder = new ExponentialDecayFunctionBuilder( + fieldName, + numericPlacement.getOrigin(), + numericPlacement.getScale(), + numericPlacement.hasOffset() ? numericPlacement.getOffset() : null + ); + } + + return builder; + } + + /** + * Parses a geo decay placement for exponential decay. + */ + private static ScoreFunctionBuilder parseGeoExpDecay(String fieldName, GeoDecayPlacement geoPlacement) { + GeoPoint geoPoint = GeoPointProtoUtils.parseGeoPoint(geoPlacement.getOrigin()); + + ExponentialDecayFunctionBuilder builder; + if (geoPlacement.hasDecay()) { + builder = new ExponentialDecayFunctionBuilder( + fieldName, + geoPoint, + geoPlacement.getScale(), + geoPlacement.hasOffset() ? geoPlacement.getOffset() : null, + geoPlacement.getDecay() + ); + } else { + builder = new ExponentialDecayFunctionBuilder( + fieldName, + geoPoint, + geoPlacement.getScale(), + geoPlacement.hasOffset() ? geoPlacement.getOffset() : null + ); + } + + return builder; + } + + /** + * Parses a date decay placement for exponential decay. + */ + private static ScoreFunctionBuilder parseDateExpDecay(String fieldName, DateDecayPlacement datePlacement) { + Object origin = datePlacement.hasOrigin() ? datePlacement.getOrigin() : null; + + ExponentialDecayFunctionBuilder builder; + if (datePlacement.hasDecay()) { + builder = new ExponentialDecayFunctionBuilder( + fieldName, + origin, + datePlacement.getScale(), + datePlacement.hasOffset() ? datePlacement.getOffset() : null, + datePlacement.getDecay() + ); + } else { + builder = new ExponentialDecayFunctionBuilder( + fieldName, + origin, + datePlacement.getScale(), + datePlacement.hasOffset() ? datePlacement.getOffset() : null + ); + } + + return builder; + } +} diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/FieldValueFactorFunctionProtoUtils.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/FieldValueFactorFunctionProtoUtils.java new file mode 100644 index 0000000000000..ede302036b1a6 --- /dev/null +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/FieldValueFactorFunctionProtoUtils.java @@ -0,0 +1,84 @@ +/* + * 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.functionscore; + +import org.opensearch.index.query.functionscore.FieldValueFactorFunctionBuilder; +import org.opensearch.index.query.functionscore.ScoreFunctionBuilder; +import org.opensearch.protobufs.FieldValueFactorModifier; +import org.opensearch.protobufs.FieldValueFactorScoreFunction; + +/** + * Utility class for converting Protocol Buffer FieldValueFactorScoreFunction to OpenSearch objects. + * This utility handles the transformation of Protocol Buffer FieldValueFactorScoreFunction objects + * into OpenSearch FieldValueFactorFunctionBuilder instances. + */ +class FieldValueFactorFunctionProtoUtils { + + private FieldValueFactorFunctionProtoUtils() { + // Utility class, no instances + } + + /** + * Converts a Protocol Buffer FieldValueFactorScoreFunction to an OpenSearch ScoreFunctionBuilder. + * Similar to {@link FieldValueFactorFunctionBuilder#fromXContent(XContentParser)}, this method + * parses the field, factor, missing value, and modifier parameters. + * + * @param fieldValueFactor the Protocol Buffer FieldValueFactorScoreFunction + * @return the corresponding OpenSearch ScoreFunctionBuilder + * @throws IllegalArgumentException if the fieldValueFactor is null + */ + static ScoreFunctionBuilder fromProto(FieldValueFactorScoreFunction fieldValueFactor) { + if (fieldValueFactor == null) { + throw new IllegalArgumentException("FieldValueFactorScoreFunction cannot be null"); + } + + FieldValueFactorFunctionBuilder builder = new FieldValueFactorFunctionBuilder(fieldValueFactor.getField()); + + if (fieldValueFactor.hasFactor()) { + builder.factor(fieldValueFactor.getFactor()); + } + + if (fieldValueFactor.hasMissing()) { + builder.missing(fieldValueFactor.getMissing()); + } + + if (fieldValueFactor.getModifier() != FieldValueFactorModifier.FIELD_VALUE_FACTOR_MODIFIER_NONE) { + builder.modifier(parseFieldValueFactorModifier(fieldValueFactor.getModifier())); + } + + return builder; + } + + /** + * Parses a protobuf FieldValueFactorModifier and returns the corresponding OpenSearch modifier. + * + * @param modifier the protobuf FieldValueFactorModifier + * @return the corresponding OpenSearch FieldValueFactorFunction.Modifier + */ + private static org.opensearch.common.lucene.search.function.FieldValueFactorFunction.Modifier parseFieldValueFactorModifier( + FieldValueFactorModifier modifier + ) { + return switch (modifier) { + case FIELD_VALUE_FACTOR_MODIFIER_NONE -> org.opensearch.common.lucene.search.function.FieldValueFactorFunction.Modifier.NONE; + case FIELD_VALUE_FACTOR_MODIFIER_LOG -> org.opensearch.common.lucene.search.function.FieldValueFactorFunction.Modifier.LOG; + case FIELD_VALUE_FACTOR_MODIFIER_LOG1P -> org.opensearch.common.lucene.search.function.FieldValueFactorFunction.Modifier.LOG1P; + case FIELD_VALUE_FACTOR_MODIFIER_LOG2P -> org.opensearch.common.lucene.search.function.FieldValueFactorFunction.Modifier.LOG2P; + case FIELD_VALUE_FACTOR_MODIFIER_LN -> org.opensearch.common.lucene.search.function.FieldValueFactorFunction.Modifier.LN; + case FIELD_VALUE_FACTOR_MODIFIER_LN1P -> org.opensearch.common.lucene.search.function.FieldValueFactorFunction.Modifier.LN1P; + case FIELD_VALUE_FACTOR_MODIFIER_LN2P -> org.opensearch.common.lucene.search.function.FieldValueFactorFunction.Modifier.LN2P; + case FIELD_VALUE_FACTOR_MODIFIER_SQUARE -> + org.opensearch.common.lucene.search.function.FieldValueFactorFunction.Modifier.SQUARE; + case FIELD_VALUE_FACTOR_MODIFIER_SQRT -> org.opensearch.common.lucene.search.function.FieldValueFactorFunction.Modifier.SQRT; + case FIELD_VALUE_FACTOR_MODIFIER_RECIPROCAL -> + org.opensearch.common.lucene.search.function.FieldValueFactorFunction.Modifier.RECIPROCAL; + case FIELD_VALUE_FACTOR_MODIFIER_UNSPECIFIED -> + org.opensearch.common.lucene.search.function.FieldValueFactorFunction.Modifier.NONE; + default -> throw new IllegalArgumentException("Unknown FieldValueFactorModifier: " + modifier); + }; + } +} diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/FunctionScoreQueryBuilderProtoConverter.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/FunctionScoreQueryBuilderProtoConverter.java new file mode 100644 index 0000000000000..f75703b90ea41 --- /dev/null +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/FunctionScoreQueryBuilderProtoConverter.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.functionscore; + +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 FunctionScore queries. + * This class implements the QueryBuilderProtoConverter interface to provide FunctionScore query support + * for the gRPC transport module. + */ +public class FunctionScoreQueryBuilderProtoConverter implements QueryBuilderProtoConverter { + + /** + * Default constructor for FunctionScoreQueryBuilderProtoConverter. + */ + public FunctionScoreQueryBuilderProtoConverter() {} + + private QueryBuilderProtoConverterRegistry registry; + + @Override + public void setRegistry(QueryBuilderProtoConverterRegistry registry) { + this.registry = registry; + } + + @Override + public QueryContainer.QueryContainerCase getHandledQueryCase() { + return QueryContainer.QueryContainerCase.FUNCTION_SCORE; + } + + @Override + public QueryBuilder fromProto(QueryContainer queryContainer) { + if (queryContainer == null || !queryContainer.hasFunctionScore()) { + throw new IllegalArgumentException("QueryContainer does not contain a FunctionScore query"); + } + + return FunctionScoreQueryBuilderProtoUtils.fromProto(queryContainer.getFunctionScore(), registry); + } +} diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/FunctionScoreQueryBuilderProtoUtils.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/FunctionScoreQueryBuilderProtoUtils.java new file mode 100644 index 0000000000000..4275c898d3b22 --- /dev/null +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/FunctionScoreQueryBuilderProtoUtils.java @@ -0,0 +1,231 @@ +/* + * 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.functionscore; + +import org.opensearch.common.lucene.search.function.CombineFunction; +import org.opensearch.index.query.AbstractQueryBuilder; +import org.opensearch.index.query.MatchAllQueryBuilder; +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.index.query.functionscore.FunctionScoreQueryBuilder; +import org.opensearch.index.query.functionscore.ScoreFunctionBuilder; +import org.opensearch.index.query.functionscore.WeightBuilder; +import org.opensearch.protobufs.FunctionBoostMode; +import org.opensearch.protobufs.FunctionScoreContainer; +import org.opensearch.protobufs.FunctionScoreMode; +import org.opensearch.protobufs.FunctionScoreQuery; +import org.opensearch.protobufs.QueryContainer; +import org.opensearch.transport.grpc.spi.QueryBuilderProtoConverterRegistry; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utility class for converting FunctionScoreQuery Protocol Buffers to OpenSearch objects. + * This class provides methods to transform Protocol Buffer representations of function score queries + * into their corresponding OpenSearch FunctionScoreQueryBuilder implementations for search operations. + */ +class FunctionScoreQueryBuilderProtoUtils { + + private FunctionScoreQueryBuilderProtoUtils() { + // Utility class, no instances + } + + /** + * Converts a Protocol Buffer FunctionScoreQuery to an OpenSearch FunctionScoreQueryBuilder. + * Similar to {@link FunctionScoreQueryBuilder#fromXContent(XContentParser)}, this method + * parses the Protocol Buffer representation and creates a properly configured + * FunctionScoreQueryBuilder with the appropriate query, functions, boost mode, score mode, + * max boost, min score, boost, and query name settings. + * + * @param functionScoreQueryProto The Protocol Buffer FunctionScoreQuery object + * @param registry The registry to use for converting nested queries + * @return A configured FunctionScoreQueryBuilder instance + * @throws IllegalArgumentException if the function score query is invalid or contains unsupported function types + */ + static FunctionScoreQueryBuilder fromProto(FunctionScoreQuery functionScoreQueryProto, QueryBuilderProtoConverterRegistry registry) { + if (functionScoreQueryProto == null) { + throw new IllegalArgumentException("FunctionScoreQuery cannot be null"); + } + + QueryBuilder query = null; + float boost = AbstractQueryBuilder.DEFAULT_BOOST; + String queryName = null; + + org.opensearch.common.lucene.search.function.FunctionScoreQuery.ScoreMode scoreMode = FunctionScoreQueryBuilder.DEFAULT_SCORE_MODE; + float maxBoost = org.opensearch.common.lucene.search.function.FunctionScoreQuery.DEFAULT_MAX_BOOST; + Float minScore = null; + + CombineFunction combineFunction = null; + List filterFunctionBuilders = new ArrayList<>(); + + if (functionScoreQueryProto.hasQuery()) { + QueryContainer queryContainer = functionScoreQueryProto.getQuery(); + query = registry.fromProto(queryContainer); + } + + if (functionScoreQueryProto.getFunctionsCount() > 0) { + for (FunctionScoreContainer container : functionScoreQueryProto.getFunctionsList()) { + FunctionScoreQueryBuilder.FilterFunctionBuilder filterFunctionBuilder = parseFunctionScoreContainer(container, registry); + filterFunctionBuilders.add(filterFunctionBuilder); + } + } + + if (query == null) { + query = new MatchAllQueryBuilder(); + } + + FunctionScoreQueryBuilder functionScoreQueryBuilder = new FunctionScoreQueryBuilder( + query, + filterFunctionBuilders.toArray(new FunctionScoreQueryBuilder.FilterFunctionBuilder[0]) + ); + + if (functionScoreQueryProto.hasBoostMode() + && functionScoreQueryProto.getBoostMode() != FunctionBoostMode.FUNCTION_BOOST_MODE_UNSPECIFIED) { + combineFunction = parseBoostMode(functionScoreQueryProto.getBoostMode()); + } + + if (functionScoreQueryProto.hasScoreMode() + && functionScoreQueryProto.getScoreMode() != FunctionScoreMode.FUNCTION_SCORE_MODE_UNSPECIFIED) { + scoreMode = parseScoreMode(functionScoreQueryProto.getScoreMode()); + } + + if (functionScoreQueryProto.hasMaxBoost()) { + maxBoost = functionScoreQueryProto.getMaxBoost(); + } + + if (functionScoreQueryProto.hasMinScore()) { + minScore = functionScoreQueryProto.getMinScore(); + } + + if (functionScoreQueryProto.hasBoost()) { + boost = functionScoreQueryProto.getBoost(); + } + + if (functionScoreQueryProto.hasXName()) { + queryName = functionScoreQueryProto.getXName(); + } + + functionScoreQueryBuilder.boost(boost); + if (queryName != null) { + functionScoreQueryBuilder.queryName(queryName); + } + functionScoreQueryBuilder.scoreMode(scoreMode); + functionScoreQueryBuilder.maxBoost(maxBoost); + if (minScore != null) { + functionScoreQueryBuilder.setMinScore(minScore); + } + if (combineFunction != null) { + functionScoreQueryBuilder.boostMode(combineFunction); + } + + return functionScoreQueryBuilder; + } + + /** + * Parses a FunctionScoreContainer and creates the appropriate FilterFunctionBuilder. + */ + private static FunctionScoreQueryBuilder.FilterFunctionBuilder parseFunctionScoreContainer( + FunctionScoreContainer container, + QueryBuilderProtoConverterRegistry registry + ) { + if (container == null) { + throw new IllegalArgumentException("FunctionScoreContainer cannot be null"); + } + + QueryBuilder filter = null; + ScoreFunctionBuilder scoreFunction = null; + Float functionWeight = null; + if (container.hasFilter()) { + QueryContainer filterContainer = container.getFilter(); + filter = registry.fromProto(filterContainer); + } + + if (container.hasWeight()) { + functionWeight = container.getWeight(); + } + + if (container.getFunctionScoreContainerCase() == FunctionScoreContainer.FunctionScoreContainerCase.FUNCTIONSCORECONTAINER_NOT_SET) { + scoreFunction = null; + } else { + scoreFunction = parseScoreFunction(container); + } + + if (functionWeight != null) { + if (scoreFunction == null) { + scoreFunction = new WeightBuilder().setWeight(functionWeight); + } else { + scoreFunction.setWeight(functionWeight); + } + } + + if (filter == null) { + filter = new MatchAllQueryBuilder(); + } + + return new FunctionScoreQueryBuilder.FilterFunctionBuilder(filter, scoreFunction); + } + + /** + * Parses a FunctionScoreContainer and creates the appropriate ScoreFunctionBuilder. + */ + private static ScoreFunctionBuilder parseScoreFunction(FunctionScoreContainer container) { + if (container == null) { + throw new IllegalArgumentException("FunctionScoreContainer cannot be null"); + } + + FunctionScoreContainer.FunctionScoreContainerCase functionCase = container.getFunctionScoreContainerCase(); + + return switch (functionCase) { + case FIELD_VALUE_FACTOR -> FieldValueFactorFunctionProtoUtils.fromProto(container.getFieldValueFactor()); + case RANDOM_SCORE -> RandomScoreFunctionProtoUtils.fromProto(container.getRandomScore()); + case SCRIPT_SCORE -> ScriptScoreFunctionProtoUtils.fromProto(container.getScriptScore()); + case EXP -> ExpDecayFunctionProtoUtils.fromProto(container.getExp()); + case GAUSS -> GaussDecayFunctionProtoUtils.fromProto(container.getGauss()); + case LINEAR -> LinearDecayFunctionProtoUtils.fromProto(container.getLinear()); + default -> throw new IllegalArgumentException("Unsupported function score type: " + functionCase); + }; + } + + /** + * Parses a FunctionBoostMode enum to CombineFunction. * + * @param boostMode the FunctionBoostMode to parse + * @return the corresponding CombineFunction + * @throws IllegalArgumentException if the boostMode is unknown or unsupported + */ + private static CombineFunction parseBoostMode(FunctionBoostMode boostMode) { + return switch (boostMode) { + case FUNCTION_BOOST_MODE_AVG -> CombineFunction.AVG; + case FUNCTION_BOOST_MODE_MAX -> CombineFunction.MAX; + case FUNCTION_BOOST_MODE_MIN -> CombineFunction.MIN; + case FUNCTION_BOOST_MODE_MULTIPLY -> CombineFunction.MULTIPLY; + case FUNCTION_BOOST_MODE_REPLACE -> CombineFunction.REPLACE; + case FUNCTION_BOOST_MODE_SUM -> CombineFunction.SUM; + default -> throw new IllegalArgumentException("Unsupported boost mode: " + boostMode); + }; + } + + /** + * Parses a FunctionScoreMode enum to ScoreMode. + * + * @param scoreMode the FunctionScoreMode to parse + * @return the corresponding FunctionScoreQuery.ScoreMode + * @throws IllegalArgumentException if the scoreMode is unknown or unsupported + */ + private static org.opensearch.common.lucene.search.function.FunctionScoreQuery.ScoreMode parseScoreMode(FunctionScoreMode scoreMode) { + return switch (scoreMode) { + case FUNCTION_SCORE_MODE_AVG -> org.opensearch.common.lucene.search.function.FunctionScoreQuery.ScoreMode.AVG; + case FUNCTION_SCORE_MODE_FIRST -> org.opensearch.common.lucene.search.function.FunctionScoreQuery.ScoreMode.FIRST; + case FUNCTION_SCORE_MODE_MAX -> org.opensearch.common.lucene.search.function.FunctionScoreQuery.ScoreMode.MAX; + case FUNCTION_SCORE_MODE_MIN -> org.opensearch.common.lucene.search.function.FunctionScoreQuery.ScoreMode.MIN; + case FUNCTION_SCORE_MODE_MULTIPLY -> org.opensearch.common.lucene.search.function.FunctionScoreQuery.ScoreMode.MULTIPLY; + case FUNCTION_SCORE_MODE_SUM -> org.opensearch.common.lucene.search.function.FunctionScoreQuery.ScoreMode.SUM; + default -> throw new IllegalArgumentException("Unsupported score mode: " + scoreMode); + }; + } + +} diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/GaussDecayFunctionProtoUtils.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/GaussDecayFunctionProtoUtils.java new file mode 100644 index 0000000000000..91de13a0ac4f7 --- /dev/null +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/GaussDecayFunctionProtoUtils.java @@ -0,0 +1,141 @@ +/* + * 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.functionscore; + +import org.opensearch.common.geo.GeoPoint; +import org.opensearch.index.query.functionscore.GaussDecayFunctionBuilder; +import org.opensearch.index.query.functionscore.ScoreFunctionBuilder; +import org.opensearch.protobufs.DateDecayPlacement; +import org.opensearch.protobufs.DecayFunction; +import org.opensearch.protobufs.DecayPlacement; +import org.opensearch.protobufs.GeoDecayPlacement; +import org.opensearch.protobufs.NumericDecayPlacement; +import org.opensearch.transport.grpc.proto.request.common.GeoPointProtoUtils; + +import java.util.Map; + +/** + * Utility class for converting Protocol Buffer DecayFunction to OpenSearch GaussDecayFunctionBuilder. + * This utility handles the transformation of Protocol Buffer DecayFunction objects + * into OpenSearch GaussDecayFunctionBuilder instances. + */ +class GaussDecayFunctionProtoUtils { + + private GaussDecayFunctionProtoUtils() { + // Utility class, no instances + } + + /** + * Converts a Protocol Buffer DecayFunction to an OpenSearch ScoreFunctionBuilder. + * Similar to {@link org.opensearch.index.query.functionscore.DecayFunctionParser#fromXContent(org.opensearch.core.xcontent.XContentParser)}, + * this method parses the Protocol Buffer representation and creates a properly configured + * GaussDecayFunctionBuilder with decay placement parameters (numeric, geo, or date). + * + * @param decayFunction the Protocol Buffer DecayFunction + * @return the corresponding OpenSearch ScoreFunctionBuilder + * @throws IllegalArgumentException if the decayFunction is null or doesn't contain placements + */ + static ScoreFunctionBuilder fromProto(DecayFunction decayFunction) { + if (decayFunction == null || decayFunction.getPlacementCount() == 0) { + throw new IllegalArgumentException("DecayFunction must have at least one placement"); + } + + Map.Entry entry = decayFunction.getPlacementMap().entrySet().iterator().next(); + String fieldName = entry.getKey(); + DecayPlacement decayPlacement = entry.getValue(); + + if (decayPlacement.hasNumericDecayPlacement()) { + return parseNumericGaussDecay(fieldName, decayPlacement.getNumericDecayPlacement()); + } else if (decayPlacement.hasGeoDecayPlacement()) { + return parseGeoGaussDecay(fieldName, decayPlacement.getGeoDecayPlacement()); + } else if (decayPlacement.hasDateDecayPlacement()) { + return parseDateGaussDecay(fieldName, decayPlacement.getDateDecayPlacement()); + } else { + throw new IllegalArgumentException("Unsupported decay placement type"); + } + } + + /** + * Parses a numeric decay placement for Gaussian decay. + */ + private static ScoreFunctionBuilder parseNumericGaussDecay(String fieldName, NumericDecayPlacement numericPlacement) { + GaussDecayFunctionBuilder builder; + if (numericPlacement.hasDecay()) { + builder = new GaussDecayFunctionBuilder( + fieldName, + numericPlacement.getOrigin(), + numericPlacement.getScale(), + numericPlacement.hasOffset() ? numericPlacement.getOffset() : null, + numericPlacement.getDecay() + ); + } else { + builder = new GaussDecayFunctionBuilder( + fieldName, + numericPlacement.getOrigin(), + numericPlacement.getScale(), + numericPlacement.hasOffset() ? numericPlacement.getOffset() : null + ); + } + + return builder; + } + + /** + * Parses a geo decay placement for Gaussian decay. + */ + private static ScoreFunctionBuilder parseGeoGaussDecay(String fieldName, GeoDecayPlacement geoPlacement) { + GeoPoint geoPoint = GeoPointProtoUtils.parseGeoPoint(geoPlacement.getOrigin()); + + GaussDecayFunctionBuilder builder; + if (geoPlacement.hasDecay()) { + builder = new GaussDecayFunctionBuilder( + fieldName, + geoPoint, + geoPlacement.getScale(), + geoPlacement.hasOffset() ? geoPlacement.getOffset() : null, + geoPlacement.getDecay() + ); + } else { + builder = new GaussDecayFunctionBuilder( + fieldName, + geoPoint, + geoPlacement.getScale(), + geoPlacement.hasOffset() ? geoPlacement.getOffset() : null + ); + } + + return builder; + } + + /** + * Parses a date decay placement for Gaussian decay. + */ + private static ScoreFunctionBuilder parseDateGaussDecay(String fieldName, DateDecayPlacement datePlacement) { + Object origin = datePlacement.hasOrigin() ? datePlacement.getOrigin() : null; + + GaussDecayFunctionBuilder builder; + if (datePlacement.hasDecay()) { + builder = new GaussDecayFunctionBuilder( + fieldName, + origin, + datePlacement.getScale(), + datePlacement.hasOffset() ? datePlacement.getOffset() : null, + datePlacement.getDecay() + ); + } else { + builder = new GaussDecayFunctionBuilder( + fieldName, + origin, + datePlacement.getScale(), + datePlacement.hasOffset() ? datePlacement.getOffset() : null + ); + } + + return builder; + } +} diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/LinearDecayFunctionProtoUtils.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/LinearDecayFunctionProtoUtils.java new file mode 100644 index 0000000000000..42d9c640da0cb --- /dev/null +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/LinearDecayFunctionProtoUtils.java @@ -0,0 +1,159 @@ +/* + * 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.functionscore; + +import org.opensearch.common.geo.GeoPoint; +import org.opensearch.index.query.functionscore.LinearDecayFunctionBuilder; +import org.opensearch.index.query.functionscore.ScoreFunctionBuilder; +import org.opensearch.protobufs.DateDecayPlacement; +import org.opensearch.protobufs.DecayFunction; +import org.opensearch.protobufs.DecayPlacement; +import org.opensearch.protobufs.GeoDecayPlacement; +import org.opensearch.protobufs.NumericDecayPlacement; +import org.opensearch.transport.grpc.proto.request.common.GeoPointProtoUtils; + +import java.util.Map; + +/** + * Utility class for converting Protocol Buffer DecayFunction to OpenSearch LinearDecayFunctionBuilder. + * This utility handles the transformation of Protocol Buffer DecayFunction objects + * into OpenSearch LinearDecayFunctionBuilder instances. + */ +class LinearDecayFunctionProtoUtils { + + private LinearDecayFunctionProtoUtils() { + // Utility class, no instances + } + + /** + * Converts a Protocol Buffer DecayFunction to an OpenSearch ScoreFunctionBuilder. + * Similar to {@link org.opensearch.index.query.functionscore.DecayFunctionParser#fromXContent(org.opensearch.core.xcontent.XContentParser)}, + * this method parses the Protocol Buffer representation and creates a properly configured + * LinearDecayFunctionBuilder with decay placement parameters (numeric, geo, or date). + * + * @param decayFunction the Protocol Buffer DecayFunction + * @return the corresponding OpenSearch ScoreFunctionBuilder + * @throws IllegalArgumentException if the decayFunction is null or doesn't contain placements + */ + static ScoreFunctionBuilder fromProto(DecayFunction decayFunction) { + if (decayFunction == null || decayFunction.getPlacementCount() == 0) { + throw new IllegalArgumentException("DecayFunction must have at least one placement"); + } + + Map.Entry entry = decayFunction.getPlacementMap().entrySet().iterator().next(); + String fieldName = entry.getKey(); + DecayPlacement decayPlacement = entry.getValue(); + + if (decayPlacement.hasNumericDecayPlacement()) { + return parseNumericLinearDecay(fieldName, decayPlacement.getNumericDecayPlacement()); + } else if (decayPlacement.hasGeoDecayPlacement()) { + return parseGeoLinearDecay(fieldName, decayPlacement.getGeoDecayPlacement()); + } else if (decayPlacement.hasDateDecayPlacement()) { + return parseDateLinearDecay(fieldName, decayPlacement.getDateDecayPlacement()); + } else { + throw new IllegalArgumentException("Unsupported decay placement type"); + } + } + + /** + * Parses a numeric decay placement and creates a LinearDecayFunctionBuilder. + * Similar to {@link org.opensearch.index.query.functionscore.DecayFunctionParser}, + * this method constructs a decay function for numeric field values. + * + * @param fieldName the field name to apply linear decay + * @param numericPlacement the protobuf numeric decay placement containing origin, scale, offset, and decay + * @return the corresponding OpenSearch LinearDecayFunctionBuilder + */ + private static ScoreFunctionBuilder parseNumericLinearDecay(String fieldName, NumericDecayPlacement numericPlacement) { + LinearDecayFunctionBuilder builder; + if (numericPlacement.hasDecay()) { + builder = new LinearDecayFunctionBuilder( + fieldName, + numericPlacement.getOrigin(), + numericPlacement.getScale(), + numericPlacement.hasOffset() ? numericPlacement.getOffset() : null, + numericPlacement.getDecay() + ); + } else { + builder = new LinearDecayFunctionBuilder( + fieldName, + numericPlacement.getOrigin(), + numericPlacement.getScale(), + numericPlacement.hasOffset() ? numericPlacement.getOffset() : null + ); + } + + return builder; + } + + /** + * Parses a geo decay placement and creates a LinearDecayFunctionBuilder. + * Similar to {@link org.opensearch.index.query.functionscore.DecayFunctionParser}, + * this method constructs a decay function for geographic field values. + * + * @param fieldName the field name to apply linear decay + * @param geoPlacement the protobuf geo decay placement containing origin (lat/lon), scale, offset, and decay + * @return the corresponding OpenSearch LinearDecayFunctionBuilder + */ + private static ScoreFunctionBuilder parseGeoLinearDecay(String fieldName, GeoDecayPlacement geoPlacement) { + GeoPoint geoPoint = GeoPointProtoUtils.parseGeoPoint(geoPlacement.getOrigin()); + + LinearDecayFunctionBuilder builder; + if (geoPlacement.hasDecay()) { + builder = new LinearDecayFunctionBuilder( + fieldName, + geoPoint, + geoPlacement.getScale(), + geoPlacement.hasOffset() ? geoPlacement.getOffset() : null, + geoPlacement.getDecay() + ); + } else { + builder = new LinearDecayFunctionBuilder( + fieldName, + geoPoint, + geoPlacement.getScale(), + geoPlacement.hasOffset() ? geoPlacement.getOffset() : null + ); + } + + return builder; + } + + /** + * Parses a date decay placement and creates a LinearDecayFunctionBuilder. + * Similar to {@link org.opensearch.index.query.functionscore.DecayFunctionParser}, + * this method constructs a decay function for date field values. + * + * @param fieldName the field name to apply linear decay + * @param datePlacement the protobuf date decay placement containing origin (date), scale, offset, and decay + * @return the corresponding OpenSearch LinearDecayFunctionBuilder + */ + private static ScoreFunctionBuilder parseDateLinearDecay(String fieldName, DateDecayPlacement datePlacement) { + Object origin = datePlacement.hasOrigin() ? datePlacement.getOrigin() : null; + + LinearDecayFunctionBuilder builder; + if (datePlacement.hasDecay()) { + builder = new LinearDecayFunctionBuilder( + fieldName, + origin, + datePlacement.getScale(), + datePlacement.hasOffset() ? datePlacement.getOffset() : null, + datePlacement.getDecay() + ); + } else { + builder = new LinearDecayFunctionBuilder( + fieldName, + origin, + datePlacement.getScale(), + datePlacement.hasOffset() ? datePlacement.getOffset() : null + ); + } + + return builder; + } +} diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/RandomScoreFunctionProtoUtils.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/RandomScoreFunctionProtoUtils.java new file mode 100644 index 0000000000000..bd125781f7a1f --- /dev/null +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/RandomScoreFunctionProtoUtils.java @@ -0,0 +1,58 @@ +/* + * 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.functionscore; + +import org.opensearch.index.query.functionscore.RandomScoreFunctionBuilder; +import org.opensearch.index.query.functionscore.ScoreFunctionBuilder; +import org.opensearch.protobufs.RandomScoreFunction; + +/** + * Utility class for converting Protocol Buffer RandomScoreFunction to OpenSearch objects. + * This utility handles the transformation of Protocol Buffer RandomScoreFunction objects + * into OpenSearch RandomScoreFunctionBuilder instances. + */ +class RandomScoreFunctionProtoUtils { + + private RandomScoreFunctionProtoUtils() { + // Utility class, no instances + } + + /** + * Converts a Protocol Buffer RandomScoreFunction to an OpenSearch ScoreFunctionBuilder. + * Similar to {@link RandomScoreFunctionBuilder#fromXContent(XContentParser)}, this method + * parses the seed and optional field parameters. + * + * @param randomScore the Protocol Buffer RandomScoreFunction + * @return the corresponding OpenSearch ScoreFunctionBuilder + * @throws IllegalArgumentException if the randomScore is null + */ + static ScoreFunctionBuilder fromProto(RandomScoreFunction randomScore) { + if (randomScore == null) { + throw new IllegalArgumentException("RandomScoreFunction cannot be null"); + } + + RandomScoreFunctionBuilder builder = new RandomScoreFunctionBuilder(); + + // Set field if present + if (!randomScore.getField().isEmpty()) { + builder.setField(randomScore.getField()); + } + + // Set seed if present + if (randomScore.hasSeed()) { + org.opensearch.protobufs.RandomScoreFunctionSeed seed = randomScore.getSeed(); + if (seed.hasInt32()) { + builder.seed(seed.getInt32()); + } else if (seed.hasString()) { + builder.seed(seed.getString()); + } + } + + return builder; + } +} diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/ScriptScoreFunctionProtoUtils.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/ScriptScoreFunctionProtoUtils.java new file mode 100644 index 0000000000000..1336b3eea114e --- /dev/null +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/ScriptScoreFunctionProtoUtils.java @@ -0,0 +1,49 @@ +/* + * 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.functionscore; + +import org.opensearch.index.query.functionscore.ScoreFunctionBuilder; +import org.opensearch.protobufs.ScriptScoreFunction; +import org.opensearch.script.Script; +import org.opensearch.transport.grpc.proto.request.common.ScriptProtoUtils; + +/** + * Utility class for converting Protocol Buffer ScriptScoreFunction to OpenSearch objects. + * This utility converts Protocol Buffer ScriptScoreFunction objects into OpenSearch ScriptScoreFunctionBuilder instances. + */ +class ScriptScoreFunctionProtoUtils { + + private ScriptScoreFunctionProtoUtils() { + // Utility class, no instances + } + + /** + * Converts a Protocol Buffer ScriptScoreFunction to an OpenSearch ScoreFunctionBuilder. + * Similar to {@link org.opensearch.index.query.functionscore.ScriptScoreFunctionBuilder#fromXContent(org.opensearch.core.xcontent.XContentParser)}, this method + * parses the script parameter and constructs the builder. + * + * @param scriptScore the Protocol Buffer ScriptScoreFunction + * @return the corresponding OpenSearch ScoreFunctionBuilder + * @throws IllegalArgumentException if the scriptScore is null or doesn't contain a script + */ + static ScoreFunctionBuilder fromProto(ScriptScoreFunction scriptScore) { + if (scriptScore == null) { + throw new IllegalArgumentException("ScriptScoreFunction cannot be null"); + } + + if (!scriptScore.hasScript()) { + throw new IllegalArgumentException("ScriptScoreFunction must have a script"); + } + + // Convert protobuf Script to OpenSearch Script using existing utility + Script script = ScriptProtoUtils.parseFromProtoRequest(scriptScore.getScript()); + + // Create and return ScriptScoreFunctionBuilder + return new org.opensearch.index.query.functionscore.ScriptScoreFunctionBuilder(script); + } +} diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/package-info.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/package-info.java new file mode 100644 index 0000000000000..93f2ab6ded360 --- /dev/null +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/package-info.java @@ -0,0 +1,15 @@ +/* + * 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. + */ + +/** + * This package contains utility classes for converting function score components between OpenSearch + * and Protocol Buffers formats. These utilities handle the transformation of score function builders, + * function parameters, and function configurations to ensure proper communication between gRPC clients + * and the OpenSearch server. + */ +package org.opensearch.transport.grpc.proto.request.search.query.functionscore; diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/QueryBuilderProtoConverterRegistryTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/QueryBuilderProtoConverterRegistryTests.java index 14eee9221a0a2..e83c13e2b50b6 100644 --- a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/QueryBuilderProtoConverterRegistryTests.java +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/QueryBuilderProtoConverterRegistryTests.java @@ -16,6 +16,7 @@ import org.opensearch.protobufs.ExistsQuery; import org.opensearch.protobufs.FieldValue; import org.opensearch.protobufs.FieldValueArray; +import org.opensearch.protobufs.FunctionScoreQuery; import org.opensearch.protobufs.Fuzziness; import org.opensearch.protobufs.FuzzyQuery; import org.opensearch.protobufs.GeoBoundingBoxQuery; @@ -742,4 +743,28 @@ public void testMatchPhrasePrefixQueryConversion() { queryBuilder.getClass().getName() ); } + + public void testFunctionScoreQueryConversion() { + // Create a FunctionScore query container with a base MatchAll query + QueryContainer baseQuery = QueryContainer.newBuilder().setMatchAll(MatchAllQuery.newBuilder().build()).build(); + + FunctionScoreQuery functionScoreQuery = FunctionScoreQuery.newBuilder() + .setQuery(baseQuery) + .setBoost(1.5f) + .setXName("test_function_score_query") + .build(); + + QueryContainer queryContainer = QueryContainer.newBuilder().setFunctionScore(functionScoreQuery).build(); + + // Convert using the registry + QueryBuilder queryBuilder = registry.fromProto(queryContainer); + + // Verify the result + assertNotNull("QueryBuilder should not be null", queryBuilder); + assertEquals( + "Should be a FunctionScoreQueryBuilder", + "org.opensearch.index.query.functionscore.FunctionScoreQueryBuilder", + queryBuilder.getClass().getName() + ); + } } diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/ExpDecayFunctionProtoUtilsTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/ExpDecayFunctionProtoUtilsTests.java new file mode 100644 index 0000000000000..d1375f7f43929 --- /dev/null +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/ExpDecayFunctionProtoUtilsTests.java @@ -0,0 +1,181 @@ +/* + * 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.functionscore; + +import org.opensearch.index.query.functionscore.ExponentialDecayFunctionBuilder; +import org.opensearch.index.query.functionscore.ScoreFunctionBuilder; +import org.opensearch.protobufs.DecayFunction; +import org.opensearch.protobufs.DecayPlacement; +import org.opensearch.protobufs.NumericDecayPlacement; +import org.opensearch.test.OpenSearchTestCase; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; + +public class ExpDecayFunctionProtoUtilsTests extends OpenSearchTestCase { + + public void testFromProtoWithNumericPlacement() { + NumericDecayPlacement numericPlacement = NumericDecayPlacement.newBuilder() + .setOrigin(10.0) + .setScale(5.0) + .setOffset(1.0) + .setDecay(0.3) + .build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setNumericDecayPlacement(numericPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder().putPlacement("price", decayPlacement).build(); + + ScoreFunctionBuilder result = ExpDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(ExponentialDecayFunctionBuilder.class)); + ExponentialDecayFunctionBuilder expFunction = (ExponentialDecayFunctionBuilder) result; + + assertEquals("price", expFunction.getFieldName()); + } + + public void testFromProtoWithNumericPlacementWithoutDecay() { + NumericDecayPlacement numericPlacement = NumericDecayPlacement.newBuilder().setOrigin(100.0).setScale(50.0).setOffset(5.0).build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setNumericDecayPlacement(numericPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder().putPlacement("distance", decayPlacement).build(); + + ScoreFunctionBuilder result = ExpDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(ExponentialDecayFunctionBuilder.class)); + ExponentialDecayFunctionBuilder expFunction = (ExponentialDecayFunctionBuilder) result; + + assertEquals("distance", expFunction.getFieldName()); + } + + public void testFromProtoWithDatePlacement() { + org.opensearch.protobufs.DateDecayPlacement datePlacement = org.opensearch.protobufs.DateDecayPlacement.newBuilder() + .setOrigin("2024-01-01") + .setScale("30d") + .setOffset("5d") + .setDecay(0.35) + .build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setDateDecayPlacement(datePlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder().putPlacement("timestamp", decayPlacement).build(); + + ScoreFunctionBuilder result = ExpDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(ExponentialDecayFunctionBuilder.class)); + ExponentialDecayFunctionBuilder expFunction = (ExponentialDecayFunctionBuilder) result; + + assertEquals("timestamp", expFunction.getFieldName()); + } + + public void testFromProtoWithDatePlacementWithoutDecay() { + org.opensearch.protobufs.DateDecayPlacement datePlacement = org.opensearch.protobufs.DateDecayPlacement.newBuilder() + .setOrigin("now") + .setScale("7d") + .build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setDateDecayPlacement(datePlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder().putPlacement("date_field", decayPlacement).build(); + + ScoreFunctionBuilder result = ExpDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(ExponentialDecayFunctionBuilder.class)); + ExponentialDecayFunctionBuilder expFunction = (ExponentialDecayFunctionBuilder) result; + + assertEquals("date_field", expFunction.getFieldName()); + } + + public void testFromProtoWithNullDecayFunction() { + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> ExpDecayFunctionProtoUtils.fromProto(null)); + + assertThat(exception.getMessage(), containsString("DecayFunction must have at least one placement")); + } + + public void testFromProtoWithEmptyDecayFunction() { + DecayFunction decayFunction = DecayFunction.newBuilder().build(); + + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> ExpDecayFunctionProtoUtils.fromProto(decayFunction) + ); + + assertThat(exception.getMessage(), containsString("DecayFunction must have at least one placement")); + } + + public void testFromProtoWithUnsetDecayPlacement() { + DecayPlacement decayPlacement = DecayPlacement.newBuilder().build(); + + DecayFunction decayFunction = DecayFunction.newBuilder().putPlacement("field", decayPlacement).build(); + + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> ExpDecayFunctionProtoUtils.fromProto(decayFunction) + ); + + assertThat(exception.getMessage(), containsString("Unsupported decay placement type")); + } + + public void testFromProtoWithGeoPlacement() { + org.opensearch.protobufs.LatLonGeoLocation latLonLocation = org.opensearch.protobufs.LatLonGeoLocation.newBuilder() + .setLat(40.7128) + .setLon(-74.0060) + .build(); + + org.opensearch.protobufs.GeoLocation geoLocation = org.opensearch.protobufs.GeoLocation.newBuilder() + .setLatlon(latLonLocation) + .build(); + + org.opensearch.protobufs.GeoDecayPlacement geoPlacement = org.opensearch.protobufs.GeoDecayPlacement.newBuilder() + .setOrigin(geoLocation) + .setScale("10km") + .setOffset("1km") + .setDecay(0.5) + .build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setGeoDecayPlacement(geoPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder().putPlacement("location", decayPlacement).build(); + + ScoreFunctionBuilder result = ExpDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(ExponentialDecayFunctionBuilder.class)); + ExponentialDecayFunctionBuilder expFunction = (ExponentialDecayFunctionBuilder) result; + + assertEquals("location", expFunction.getFieldName()); + } + + public void testFromProtoWithGeoPlacementWithoutDecay() { + org.opensearch.protobufs.LatLonGeoLocation latLonLocation = org.opensearch.protobufs.LatLonGeoLocation.newBuilder() + .setLat(51.5074) + .setLon(-0.1278) + .build(); + + org.opensearch.protobufs.GeoLocation geoLocation = org.opensearch.protobufs.GeoLocation.newBuilder() + .setLatlon(latLonLocation) + .build(); + + org.opensearch.protobufs.GeoDecayPlacement geoPlacement = org.opensearch.protobufs.GeoDecayPlacement.newBuilder() + .setOrigin(geoLocation) + .setScale("5km") + .build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setGeoDecayPlacement(geoPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder().putPlacement("geo_field", decayPlacement).build(); + + ScoreFunctionBuilder result = ExpDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(ExponentialDecayFunctionBuilder.class)); + ExponentialDecayFunctionBuilder expFunction = (ExponentialDecayFunctionBuilder) result; + + assertEquals("geo_field", expFunction.getFieldName()); + } +} diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/FieldValueFactorFunctionProtoUtilsTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/FieldValueFactorFunctionProtoUtilsTests.java new file mode 100644 index 0000000000000..2678ca8532de9 --- /dev/null +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/FieldValueFactorFunctionProtoUtilsTests.java @@ -0,0 +1,219 @@ +/* + * 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.functionscore; + +import org.opensearch.index.query.functionscore.FieldValueFactorFunctionBuilder; +import org.opensearch.index.query.functionscore.ScoreFunctionBuilder; +import org.opensearch.protobufs.FieldValueFactorModifier; +import org.opensearch.protobufs.FieldValueFactorScoreFunction; +import org.opensearch.test.OpenSearchTestCase; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; + +public class FieldValueFactorFunctionProtoUtilsTests extends OpenSearchTestCase { + + public void testFromProtoWithValidFieldValueFactorFunction() { + // Create a field value factor function + FieldValueFactorScoreFunction fieldValueFactor = FieldValueFactorScoreFunction.newBuilder() + .setField("popularity") + .setFactor(1.2f) + .setMissing(1.0f) + .setModifier(FieldValueFactorModifier.FIELD_VALUE_FACTOR_MODIFIER_LOG) + .build(); + + ScoreFunctionBuilder result = FieldValueFactorFunctionProtoUtils.fromProto(fieldValueFactor); + + assertThat(result, instanceOf(FieldValueFactorFunctionBuilder.class)); + FieldValueFactorFunctionBuilder fieldValueFactorFunction = (FieldValueFactorFunctionBuilder) result; + + } + + public void testFromProtoWithMinimalParameters() { + // Create a minimal field value factor function (only field required) + FieldValueFactorScoreFunction fieldValueFactor = FieldValueFactorScoreFunction.newBuilder().setField("score").build(); + + ScoreFunctionBuilder result = FieldValueFactorFunctionProtoUtils.fromProto(fieldValueFactor); + + assertThat(result, instanceOf(FieldValueFactorFunctionBuilder.class)); + FieldValueFactorFunctionBuilder fieldValueFactorFunction = (FieldValueFactorFunctionBuilder) result; + + } + + public void testFromProtoWithAllParameters() { + // Create a field value factor function with all parameters + FieldValueFactorScoreFunction fieldValueFactor = FieldValueFactorScoreFunction.newBuilder() + .setField("rating") + .setFactor(2.5f) + .setMissing(0.5f) + .setModifier(FieldValueFactorModifier.FIELD_VALUE_FACTOR_MODIFIER_SQRT) + .build(); + + ScoreFunctionBuilder result = FieldValueFactorFunctionProtoUtils.fromProto(fieldValueFactor); + + assertThat(result, instanceOf(FieldValueFactorFunctionBuilder.class)); + FieldValueFactorFunctionBuilder fieldValueFactorFunction = (FieldValueFactorFunctionBuilder) result; + } + + public void testFromProtoWithDifferentModifiers() { + // Test LOG modifier + FieldValueFactorScoreFunction logFunction = FieldValueFactorScoreFunction.newBuilder() + .setField("views") + .setModifier(FieldValueFactorModifier.FIELD_VALUE_FACTOR_MODIFIER_LOG) + .build(); + + ScoreFunctionBuilder logResult = FieldValueFactorFunctionProtoUtils.fromProto(logFunction); + assertThat(logResult, instanceOf(FieldValueFactorFunctionBuilder.class)); + + // Test LOG1P modifier + FieldValueFactorScoreFunction log1pFunction = FieldValueFactorScoreFunction.newBuilder() + .setField("clicks") + .setModifier(FieldValueFactorModifier.FIELD_VALUE_FACTOR_MODIFIER_LOG1P) + .build(); + + ScoreFunctionBuilder log1pResult = FieldValueFactorFunctionProtoUtils.fromProto(log1pFunction); + assertThat(log1pResult, instanceOf(FieldValueFactorFunctionBuilder.class)); + + // Test LOG2P modifier + FieldValueFactorScoreFunction log2pFunction = FieldValueFactorScoreFunction.newBuilder() + .setField("likes") + .setModifier(FieldValueFactorModifier.FIELD_VALUE_FACTOR_MODIFIER_LOG2P) + .build(); + + ScoreFunctionBuilder log2pResult = FieldValueFactorFunctionProtoUtils.fromProto(log2pFunction); + assertThat(log2pResult, instanceOf(FieldValueFactorFunctionBuilder.class)); + + // Test LN modifier + FieldValueFactorScoreFunction lnFunction = FieldValueFactorScoreFunction.newBuilder() + .setField("shares") + .setModifier(FieldValueFactorModifier.FIELD_VALUE_FACTOR_MODIFIER_LN) + .build(); + + ScoreFunctionBuilder lnResult = FieldValueFactorFunctionProtoUtils.fromProto(lnFunction); + assertThat(lnResult, instanceOf(FieldValueFactorFunctionBuilder.class)); + + // Test LN1P modifier + FieldValueFactorScoreFunction ln1pFunction = FieldValueFactorScoreFunction.newBuilder() + .setField("comments") + .setModifier(FieldValueFactorModifier.FIELD_VALUE_FACTOR_MODIFIER_LN1P) + .build(); + + ScoreFunctionBuilder ln1pResult = FieldValueFactorFunctionProtoUtils.fromProto(ln1pFunction); + assertThat(ln1pResult, instanceOf(FieldValueFactorFunctionBuilder.class)); + + // Test LN2P modifier + FieldValueFactorScoreFunction ln2pFunction = FieldValueFactorScoreFunction.newBuilder() + .setField("downloads") + .setModifier(FieldValueFactorModifier.FIELD_VALUE_FACTOR_MODIFIER_LN2P) + .build(); + + ScoreFunctionBuilder ln2pResult = FieldValueFactorFunctionProtoUtils.fromProto(ln2pFunction); + assertThat(ln2pResult, instanceOf(FieldValueFactorFunctionBuilder.class)); + + // Test SQUARE modifier + FieldValueFactorScoreFunction squareFunction = FieldValueFactorScoreFunction.newBuilder() + .setField("revenue") + .setModifier(FieldValueFactorModifier.FIELD_VALUE_FACTOR_MODIFIER_SQUARE) + .build(); + + ScoreFunctionBuilder squareResult = FieldValueFactorFunctionProtoUtils.fromProto(squareFunction); + assertThat(squareResult, instanceOf(FieldValueFactorFunctionBuilder.class)); + + // Test SQRT modifier + FieldValueFactorScoreFunction sqrtFunction = FieldValueFactorScoreFunction.newBuilder() + .setField("distance") + .setModifier(FieldValueFactorModifier.FIELD_VALUE_FACTOR_MODIFIER_SQRT) + .build(); + + ScoreFunctionBuilder sqrtResult = FieldValueFactorFunctionProtoUtils.fromProto(sqrtFunction); + assertThat(sqrtResult, instanceOf(FieldValueFactorFunctionBuilder.class)); + + // Test RECIPROCAL modifier + FieldValueFactorScoreFunction reciprocalFunction = FieldValueFactorScoreFunction.newBuilder() + .setField("time") + .setModifier(FieldValueFactorModifier.FIELD_VALUE_FACTOR_MODIFIER_RECIPROCAL) + .build(); + + ScoreFunctionBuilder reciprocalResult = FieldValueFactorFunctionProtoUtils.fromProto(reciprocalFunction); + assertThat(reciprocalResult, instanceOf(FieldValueFactorFunctionBuilder.class)); + + // Test NONE modifier + FieldValueFactorScoreFunction noneFunction = FieldValueFactorScoreFunction.newBuilder() + .setField("count") + .setModifier(FieldValueFactorModifier.FIELD_VALUE_FACTOR_MODIFIER_NONE) + .build(); + + ScoreFunctionBuilder noneResult = FieldValueFactorFunctionProtoUtils.fromProto(noneFunction); + assertThat(noneResult, instanceOf(FieldValueFactorFunctionBuilder.class)); + } + + public void testFromProtoWithNullFieldValueFactor() { + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> FieldValueFactorFunctionProtoUtils.fromProto(null) + ); + + assertThat(exception.getMessage(), containsString("FieldValueFactorScoreFunction cannot be null")); + } + + public void testFromProtoWithZeroFactor() { + // Test with zero factor + FieldValueFactorScoreFunction fieldValueFactor = FieldValueFactorScoreFunction.newBuilder() + .setField("zero_field") + .setFactor(0.0f) + .build(); + + ScoreFunctionBuilder result = FieldValueFactorFunctionProtoUtils.fromProto(fieldValueFactor); + + assertThat(result, instanceOf(FieldValueFactorFunctionBuilder.class)); + FieldValueFactorFunctionBuilder fieldValueFactorFunction = (FieldValueFactorFunctionBuilder) result; + } + + public void testFromProtoWithNegativeFactor() { + // Test with negative factor + FieldValueFactorScoreFunction fieldValueFactor = FieldValueFactorScoreFunction.newBuilder() + .setField("negative_field") + .setFactor(-1.5f) + .build(); + + ScoreFunctionBuilder result = FieldValueFactorFunctionProtoUtils.fromProto(fieldValueFactor); + + assertThat(result, instanceOf(FieldValueFactorFunctionBuilder.class)); + FieldValueFactorFunctionBuilder fieldValueFactorFunction = (FieldValueFactorFunctionBuilder) result; + + } + + public void testFromProtoWithZeroMissing() { + // Test with zero missing value + FieldValueFactorScoreFunction fieldValueFactor = FieldValueFactorScoreFunction.newBuilder() + .setField("missing_zero") + .setMissing(0.0f) + .build(); + + ScoreFunctionBuilder result = FieldValueFactorFunctionProtoUtils.fromProto(fieldValueFactor); + + assertThat(result, instanceOf(FieldValueFactorFunctionBuilder.class)); + FieldValueFactorFunctionBuilder fieldValueFactorFunction = (FieldValueFactorFunctionBuilder) result; + + } + + public void testFromProtoWithNegativeMissing() { + // Test with negative missing value + FieldValueFactorScoreFunction fieldValueFactor = FieldValueFactorScoreFunction.newBuilder() + .setField("missing_negative") + .setMissing(-0.5f) + .build(); + + ScoreFunctionBuilder result = FieldValueFactorFunctionProtoUtils.fromProto(fieldValueFactor); + + assertThat(result, instanceOf(FieldValueFactorFunctionBuilder.class)); + FieldValueFactorFunctionBuilder fieldValueFactorFunction = (FieldValueFactorFunctionBuilder) result; + + } +} diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/FunctionScoreQueryBuilderProtoConverterTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/FunctionScoreQueryBuilderProtoConverterTests.java new file mode 100644 index 0000000000000..2c2f9332942c4 --- /dev/null +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/FunctionScoreQueryBuilderProtoConverterTests.java @@ -0,0 +1,71 @@ +/* + * 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.functionscore; + +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.index.query.functionscore.FunctionScoreQueryBuilder; +import org.opensearch.protobufs.FunctionScoreQuery; +import org.opensearch.protobufs.QueryContainer; +import org.opensearch.test.OpenSearchTestCase; +import org.opensearch.transport.grpc.proto.request.search.query.QueryBuilderProtoConverterRegistryImpl; + +/** + * Tests for {@link FunctionScoreQueryBuilderProtoConverter}. + */ +public class FunctionScoreQueryBuilderProtoConverterTests extends OpenSearchTestCase { + + private FunctionScoreQueryBuilderProtoConverter converter; + + @Override + public void setUp() throws Exception { + super.setUp(); + converter = new FunctionScoreQueryBuilderProtoConverter(); + // Set up the registry with all built-in converters + QueryBuilderProtoConverterRegistryImpl registry = new QueryBuilderProtoConverterRegistryImpl(); + converter.setRegistry(registry); + } + + public void testGetHandledQueryCase() { + assertEquals(QueryContainer.QueryContainerCase.FUNCTION_SCORE, converter.getHandledQueryCase()); + } + + public void testFromProto_ValidFunctionScoreQuery() { + // Create a simple function score query + FunctionScoreQuery functionScoreQuery = FunctionScoreQuery.newBuilder().build(); + + QueryContainer queryContainer = QueryContainer.newBuilder().setFunctionScore(functionScoreQuery).build(); + + QueryBuilder result = converter.fromProto(queryContainer); + + assertNotNull(result); + assertTrue("Should be FunctionScoreQueryBuilder", result instanceof FunctionScoreQueryBuilder); + } + + public void testFromProto_NullQueryContainer() { + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> { converter.fromProto(null); }); + assertEquals("QueryContainer does not contain a FunctionScore query", exception.getMessage()); + } + + public void testFromProto_MissingFunctionScore() { + // Create a query container without function score + QueryContainer queryContainer = QueryContainer.newBuilder().build(); + + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> { converter.fromProto(queryContainer); }); + assertEquals("QueryContainer does not contain a FunctionScore query", exception.getMessage()); + } + + public void testFromProto_DifferentQueryType() { + // Create a query container with a different query type + QueryContainer queryContainer = QueryContainer.newBuilder() + .setMatchAll(org.opensearch.protobufs.MatchAllQuery.newBuilder().build()) + .build(); + + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> { converter.fromProto(queryContainer); }); + assertEquals("QueryContainer does not contain a FunctionScore query", exception.getMessage()); + } +} diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/FunctionScoreQueryBuilderProtoUtilsTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/FunctionScoreQueryBuilderProtoUtilsTests.java new file mode 100644 index 0000000000000..b599e86342851 --- /dev/null +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/FunctionScoreQueryBuilderProtoUtilsTests.java @@ -0,0 +1,661 @@ +/* + * 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.functionscore; + +import org.opensearch.index.query.MatchAllQueryBuilder; +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.index.query.functionscore.FunctionScoreQueryBuilder; +import org.opensearch.protobufs.FieldValue; +import org.opensearch.protobufs.FieldValueFactorModifier; +import org.opensearch.protobufs.FieldValueFactorScoreFunction; +import org.opensearch.protobufs.FunctionBoostMode; +import org.opensearch.protobufs.FunctionScoreContainer; +import org.opensearch.protobufs.FunctionScoreMode; +import org.opensearch.protobufs.FunctionScoreQuery; +import org.opensearch.protobufs.QueryContainer; +import org.opensearch.protobufs.RandomScoreFunction; +import org.opensearch.protobufs.RandomScoreFunctionSeed; +import org.opensearch.protobufs.TermQuery; +import org.opensearch.test.OpenSearchTestCase; +import org.opensearch.transport.grpc.proto.request.search.query.QueryBuilderProtoConverterRegistryImpl; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; + +public class FunctionScoreQueryBuilderProtoUtilsTests extends OpenSearchTestCase { + + private QueryBuilderProtoConverterRegistryImpl registry; + + @Override + public void setUp() throws Exception { + super.setUp(); + // Initialize the registry for FunctionScoreQueryBuilderProtoUtils + registry = new QueryBuilderProtoConverterRegistryImpl(); + } + + public void testFromProtoWithBasicFunctionScoreQuery() { + // Create a basic function score query with a term query + TermQuery termQuery = TermQuery.newBuilder() + .setField("test_field") + .setValue(FieldValue.newBuilder().setString("test_value").build()) + .build(); + + QueryContainer queryContainer = QueryContainer.newBuilder().setTerm(termQuery).build(); + + FunctionScoreQuery functionScoreQuery = FunctionScoreQuery.newBuilder() + .setQuery(queryContainer) + .setBoostMode(FunctionBoostMode.FUNCTION_BOOST_MODE_MULTIPLY) + .setScoreMode(FunctionScoreMode.FUNCTION_SCORE_MODE_MULTIPLY) + .setMaxBoost(10.0f) + .setMinScore(0.5f) + .build(); + + QueryBuilder result = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery, registry); + + assertThat(result, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder functionScoreQueryBuilder = (FunctionScoreQueryBuilder) result; + + assertEquals(10.0f, functionScoreQueryBuilder.maxBoost(), 0.001f); + assertEquals(0.5f, functionScoreQueryBuilder.getMinScore(), 0.001f); + } + + public void testFromProtoWithFieldValueFactorFunction() { + // Create a function score query with field_value_factor function + FieldValueFactorScoreFunction fieldValueFactor = FieldValueFactorScoreFunction.newBuilder() + .setField("popularity") + .setFactor(1.2f) + .setMissing(1.0f) + .setModifier(FieldValueFactorModifier.FIELD_VALUE_FACTOR_MODIFIER_LOG) + .build(); + + FunctionScoreContainer container = FunctionScoreContainer.newBuilder() + .setFieldValueFactor(fieldValueFactor) + .setWeight(2.0f) + .build(); + + FunctionScoreQuery functionScoreQuery = FunctionScoreQuery.newBuilder().addFunctions(container).build(); + + QueryBuilder result = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery, registry); + + assertThat(result, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder functionScoreQueryBuilder = (FunctionScoreQueryBuilder) result; + + // Verify that functions were added + assertNotNull(functionScoreQueryBuilder.filterFunctionBuilders()); + assertEquals(1, functionScoreQueryBuilder.filterFunctionBuilders().length); + } + + public void testFromProtoWithRandomScoreFunction() { + // Create a function score query with random_score function + RandomScoreFunction randomScore = RandomScoreFunction.newBuilder() + .setField("_id") + .setSeed(RandomScoreFunctionSeed.newBuilder().setInt32(42).build()) + .build(); + + FunctionScoreContainer container = FunctionScoreContainer.newBuilder().setRandomScore(randomScore).setWeight(1.5f).build(); + + FunctionScoreQuery functionScoreQuery = FunctionScoreQuery.newBuilder().addFunctions(container).build(); + + QueryBuilder result = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery, registry); + + assertThat(result, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder functionScoreQueryBuilder = (FunctionScoreQueryBuilder) result; + + // Verify that functions were added + assertNotNull(functionScoreQueryBuilder.filterFunctionBuilders()); + assertEquals(1, functionScoreQueryBuilder.filterFunctionBuilders().length); + } + + public void testFromProtoWithNullInput() { + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> FunctionScoreQueryBuilderProtoUtils.fromProto(null, registry) + ); + + assertThat(exception.getMessage(), containsString("FunctionScoreQuery cannot be null")); + } + + public void testFromProtoWithEmptyQueryAndNoFunctions() { + // Create a function score query with neither query nor functions + // This should default to MatchAllQueryBuilder (matching fromXContent behavior) + FunctionScoreQuery functionScoreQuery = FunctionScoreQuery.newBuilder().build(); + + QueryBuilder result = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery, registry); + + assertThat(result, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder functionScoreQueryBuilder = (FunctionScoreQueryBuilder) result; + + // Should have a MatchAllQueryBuilder as the base query + assertThat(functionScoreQueryBuilder.query(), instanceOf(MatchAllQueryBuilder.class)); + // Should have no functions + assertEquals(0, functionScoreQueryBuilder.filterFunctionBuilders().length); + } + + public void testFromProtoWithWeightOnlyFunction() { + // Create a function score query with only weight (no specific function) + FunctionScoreContainer container = FunctionScoreContainer.newBuilder().setWeight(3.0f).build(); + + FunctionScoreQuery functionScoreQuery = FunctionScoreQuery.newBuilder().addFunctions(container).build(); + + QueryBuilder result = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery, registry); + + assertThat(result, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder functionScoreQueryBuilder = (FunctionScoreQueryBuilder) result; + + // Verify that functions were added + assertNotNull(functionScoreQueryBuilder.filterFunctionBuilders()); + assertEquals(1, functionScoreQueryBuilder.filterFunctionBuilders().length); + } + + public void testFromProtoWithScriptScoreFunction() { + // Create an inline script + org.opensearch.protobufs.InlineScript inlineScript = org.opensearch.protobufs.InlineScript.newBuilder() + .setSource("Math.log(doc['popularity'].value) * params.multiplier") + .setLang( + org.opensearch.protobufs.ScriptLanguage.newBuilder() + .setBuiltin(org.opensearch.protobufs.BuiltinScriptLanguage.BUILTIN_SCRIPT_LANGUAGE_PAINLESS) + .build() + ) + .setParams( + org.opensearch.protobufs.ObjectMap.newBuilder() + .putFields("multiplier", org.opensearch.protobufs.ObjectMap.Value.newBuilder().setDouble(2.0).build()) + .build() + ) + .build(); + + org.opensearch.protobufs.Script script = org.opensearch.protobufs.Script.newBuilder().setInline(inlineScript).build(); + + org.opensearch.protobufs.ScriptScoreFunction scriptScoreFunction = org.opensearch.protobufs.ScriptScoreFunction.newBuilder() + .setScript(script) + .build(); + + FunctionScoreContainer container = FunctionScoreContainer.newBuilder().setScriptScore(scriptScoreFunction).setWeight(1.5f).build(); + + FunctionScoreQuery functionScoreQuery = FunctionScoreQuery.newBuilder().addFunctions(container).build(); + + QueryBuilder result = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery, registry); + + assertThat(result, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder functionScoreQueryBuilder = (FunctionScoreQueryBuilder) result; + + // Verify that functions were added + assertNotNull(functionScoreQueryBuilder.filterFunctionBuilders()); + assertEquals(1, functionScoreQueryBuilder.filterFunctionBuilders().length); + + // Verify the script score function + FunctionScoreQueryBuilder.FilterFunctionBuilder filterFunction = functionScoreQueryBuilder.filterFunctionBuilders()[0]; + assertThat( + filterFunction.getScoreFunction(), + instanceOf(org.opensearch.index.query.functionscore.ScriptScoreFunctionBuilder.class) + ); + + org.opensearch.index.query.functionscore.ScriptScoreFunctionBuilder scriptFunction = + (org.opensearch.index.query.functionscore.ScriptScoreFunctionBuilder) filterFunction.getScoreFunction(); + + // Verify script properties + org.opensearch.script.Script openSearchScript = scriptFunction.getScript(); + assertEquals(org.opensearch.script.ScriptType.INLINE, openSearchScript.getType()); + assertEquals("painless", openSearchScript.getLang()); + assertEquals("Math.log(doc['popularity'].value) * params.multiplier", openSearchScript.getIdOrCode()); + assertEquals(2.0, openSearchScript.getParams().get("multiplier")); + } + + public void testFromProtoWithExponentialDecayFunction() { + // Create a numeric decay placement + org.opensearch.protobufs.NumericDecayPlacement numericPlacement = org.opensearch.protobufs.NumericDecayPlacement.newBuilder() + .setOrigin(10.0) + .setScale(5.0) + .setOffset(1.0) + .setDecay(0.3) + .build(); + + // Create a decay placement + org.opensearch.protobufs.DecayPlacement decayPlacement = org.opensearch.protobufs.DecayPlacement.newBuilder() + .setNumericDecayPlacement(numericPlacement) + .build(); + + // Create a decay function with the placement + org.opensearch.protobufs.DecayFunction decayFunction = org.opensearch.protobufs.DecayFunction.newBuilder() + .putPlacement("price", decayPlacement) + .build(); + + // Create a function score container with the exponential decay function + FunctionScoreContainer container = FunctionScoreContainer.newBuilder().setExp(decayFunction).setWeight(1.5f).build(); + + FunctionScoreQuery functionScoreQuery = FunctionScoreQuery.newBuilder().addFunctions(container).build(); + + QueryBuilder result = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery, registry); + + assertThat(result, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder functionScoreQueryBuilder = (FunctionScoreQueryBuilder) result; + + // Verify that functions were added + assertNotNull(functionScoreQueryBuilder.filterFunctionBuilders()); + assertEquals(1, functionScoreQueryBuilder.filterFunctionBuilders().length); + + // Verify the exponential decay function + FunctionScoreQueryBuilder.FilterFunctionBuilder filterFunction = functionScoreQueryBuilder.filterFunctionBuilders()[0]; + assertThat( + filterFunction.getScoreFunction(), + instanceOf(org.opensearch.index.query.functionscore.ExponentialDecayFunctionBuilder.class) + ); + + org.opensearch.index.query.functionscore.ExponentialDecayFunctionBuilder expFunction = + (org.opensearch.index.query.functionscore.ExponentialDecayFunctionBuilder) filterFunction.getScoreFunction(); + + // Verify the field name + assertEquals("price", expFunction.getFieldName()); + } + + public void testFromProtoWithGaussianDecayFunction() { + // Create a numeric decay placement + org.opensearch.protobufs.NumericDecayPlacement numericPlacement = org.opensearch.protobufs.NumericDecayPlacement.newBuilder() + .setOrigin(20.0) + .setScale(10.0) + .setOffset(2.0) + .setDecay(0.4) + .build(); + + // Create a decay placement + org.opensearch.protobufs.DecayPlacement decayPlacement = org.opensearch.protobufs.DecayPlacement.newBuilder() + .setNumericDecayPlacement(numericPlacement) + .build(); + + // Create a decay function with the placement + org.opensearch.protobufs.DecayFunction decayFunction = org.opensearch.protobufs.DecayFunction.newBuilder() + .putPlacement("rating", decayPlacement) + .build(); + + // Create a function score container with the gaussian decay function + FunctionScoreContainer container = FunctionScoreContainer.newBuilder().setGauss(decayFunction).setWeight(2.0f).build(); + + FunctionScoreQuery functionScoreQuery = FunctionScoreQuery.newBuilder().addFunctions(container).build(); + + QueryBuilder result = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery, registry); + + assertThat(result, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder functionScoreQueryBuilder = (FunctionScoreQueryBuilder) result; + + // Verify that functions were added + assertNotNull(functionScoreQueryBuilder.filterFunctionBuilders()); + assertEquals(1, functionScoreQueryBuilder.filterFunctionBuilders().length); + + // Verify the gaussian decay function + FunctionScoreQueryBuilder.FilterFunctionBuilder filterFunction = functionScoreQueryBuilder.filterFunctionBuilders()[0]; + assertThat(filterFunction.getScoreFunction(), instanceOf(org.opensearch.index.query.functionscore.GaussDecayFunctionBuilder.class)); + + org.opensearch.index.query.functionscore.GaussDecayFunctionBuilder gaussFunction = + (org.opensearch.index.query.functionscore.GaussDecayFunctionBuilder) filterFunction.getScoreFunction(); + + // Verify the field name + assertEquals("rating", gaussFunction.getFieldName()); + } + + public void testFromProtoWithLinearDecayFunction() { + // Create a numeric decay placement + org.opensearch.protobufs.NumericDecayPlacement numericPlacement = org.opensearch.protobufs.NumericDecayPlacement.newBuilder() + .setOrigin(100.0) + .setScale(50.0) + .setOffset(5.0) + .setDecay(0.2) + .build(); + + // Create a decay placement + org.opensearch.protobufs.DecayPlacement decayPlacement = org.opensearch.protobufs.DecayPlacement.newBuilder() + .setNumericDecayPlacement(numericPlacement) + .build(); + + // Create a decay function with the placement + org.opensearch.protobufs.DecayFunction decayFunction = org.opensearch.protobufs.DecayFunction.newBuilder() + .putPlacement("distance", decayPlacement) + .build(); + + // Create a function score container with the linear decay function + FunctionScoreContainer container = FunctionScoreContainer.newBuilder().setLinear(decayFunction).setWeight(1.0f).build(); + + FunctionScoreQuery functionScoreQuery = FunctionScoreQuery.newBuilder().addFunctions(container).build(); + + QueryBuilder result = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery, registry); + + assertThat(result, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder functionScoreQueryBuilder = (FunctionScoreQueryBuilder) result; + + // Verify that functions were added + assertNotNull(functionScoreQueryBuilder.filterFunctionBuilders()); + assertEquals(1, functionScoreQueryBuilder.filterFunctionBuilders().length); + + // Verify the linear decay function + FunctionScoreQueryBuilder.FilterFunctionBuilder filterFunction = functionScoreQueryBuilder.filterFunctionBuilders()[0]; + assertThat( + filterFunction.getScoreFunction(), + instanceOf(org.opensearch.index.query.functionscore.LinearDecayFunctionBuilder.class) + ); + + org.opensearch.index.query.functionscore.LinearDecayFunctionBuilder linearFunction = + (org.opensearch.index.query.functionscore.LinearDecayFunctionBuilder) filterFunction.getScoreFunction(); + + // Verify the field name + assertEquals("distance", linearFunction.getFieldName()); + } + + public void testFromProtoWithMultipleFunctions() { + // Create first function - field value factor + FieldValueFactorScoreFunction fieldValueFactor = FieldValueFactorScoreFunction.newBuilder() + .setField("popularity") + .setFactor(1.5f) + .setModifier(FieldValueFactorModifier.FIELD_VALUE_FACTOR_MODIFIER_LOG) + .build(); + + FunctionScoreContainer container1 = FunctionScoreContainer.newBuilder() + .setFieldValueFactor(fieldValueFactor) + .setWeight(2.0f) + .build(); + + // Create second function - random score + RandomScoreFunction randomScore = RandomScoreFunction.newBuilder() + .setField("_id") + .setSeed(RandomScoreFunctionSeed.newBuilder().setString("test_seed").build()) + .build(); + + FunctionScoreContainer container2 = FunctionScoreContainer.newBuilder().setRandomScore(randomScore).setWeight(1.0f).build(); + + // Create third function - exponential decay + org.opensearch.protobufs.NumericDecayPlacement numericPlacement = org.opensearch.protobufs.NumericDecayPlacement.newBuilder() + .setOrigin(15.0) + .setScale(8.0) + .setDecay(0.3) + .build(); + + org.opensearch.protobufs.DecayPlacement decayPlacement = org.opensearch.protobufs.DecayPlacement.newBuilder() + .setNumericDecayPlacement(numericPlacement) + .build(); + + org.opensearch.protobufs.DecayFunction decayFunction = org.opensearch.protobufs.DecayFunction.newBuilder() + .putPlacement("price", decayPlacement) + .build(); + + FunctionScoreContainer container3 = FunctionScoreContainer.newBuilder().setExp(decayFunction).setWeight(1.5f).build(); + + FunctionScoreQuery functionScoreQuery = FunctionScoreQuery.newBuilder() + .addFunctions(container1) + .addFunctions(container2) + .addFunctions(container3) + .build(); + + QueryBuilder result = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery, registry); + + assertThat(result, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder functionScoreQueryBuilder = (FunctionScoreQueryBuilder) result; + + // Verify that all three functions were added + assertNotNull(functionScoreQueryBuilder.filterFunctionBuilders()); + assertEquals(3, functionScoreQueryBuilder.filterFunctionBuilders().length); + + // Verify the first function (field value factor) + FunctionScoreQueryBuilder.FilterFunctionBuilder filterFunction1 = functionScoreQueryBuilder.filterFunctionBuilders()[0]; + assertThat( + filterFunction1.getScoreFunction(), + instanceOf(org.opensearch.index.query.functionscore.FieldValueFactorFunctionBuilder.class) + ); + + // Verify the second function (random score) + FunctionScoreQueryBuilder.FilterFunctionBuilder filterFunction2 = functionScoreQueryBuilder.filterFunctionBuilders()[1]; + assertThat( + filterFunction2.getScoreFunction(), + instanceOf(org.opensearch.index.query.functionscore.RandomScoreFunctionBuilder.class) + ); + + // Verify the third function (exponential decay) + FunctionScoreQueryBuilder.FilterFunctionBuilder filterFunction3 = functionScoreQueryBuilder.filterFunctionBuilders()[2]; + assertThat( + filterFunction3.getScoreFunction(), + instanceOf(org.opensearch.index.query.functionscore.ExponentialDecayFunctionBuilder.class) + ); + } + + public void testFromProtoWithDifferentBoostModes() { + // Test MULTIPLY boost mode + FunctionScoreQuery functionScoreQuery1 = FunctionScoreQuery.newBuilder() + .setBoostMode(FunctionBoostMode.FUNCTION_BOOST_MODE_MULTIPLY) + .build(); + + QueryBuilder result1 = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery1, registry); + assertThat(result1, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder builder1 = (FunctionScoreQueryBuilder) result1; + assertEquals(org.opensearch.common.lucene.search.function.CombineFunction.MULTIPLY, builder1.boostMode()); + + // Test REPLACE boost mode + FunctionScoreQuery functionScoreQuery2 = FunctionScoreQuery.newBuilder() + .setBoostMode(FunctionBoostMode.FUNCTION_BOOST_MODE_REPLACE) + .build(); + + QueryBuilder result2 = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery2, registry); + assertThat(result2, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder builder2 = (FunctionScoreQueryBuilder) result2; + assertEquals(org.opensearch.common.lucene.search.function.CombineFunction.REPLACE, builder2.boostMode()); + + // Test SUM boost mode + FunctionScoreQuery functionScoreQuery3 = FunctionScoreQuery.newBuilder() + .setBoostMode(FunctionBoostMode.FUNCTION_BOOST_MODE_SUM) + .build(); + + QueryBuilder result3 = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery3, registry); + assertThat(result3, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder builder3 = (FunctionScoreQueryBuilder) result3; + assertEquals(org.opensearch.common.lucene.search.function.CombineFunction.SUM, builder3.boostMode()); + + // Test AVG boost mode + FunctionScoreQuery functionScoreQuery4 = FunctionScoreQuery.newBuilder() + .setBoostMode(FunctionBoostMode.FUNCTION_BOOST_MODE_AVG) + .build(); + + QueryBuilder result4 = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery4, registry); + assertThat(result4, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder builder4 = (FunctionScoreQueryBuilder) result4; + assertEquals(org.opensearch.common.lucene.search.function.CombineFunction.AVG, builder4.boostMode()); + + // Test MAX boost mode + FunctionScoreQuery functionScoreQuery5 = FunctionScoreQuery.newBuilder() + .setBoostMode(FunctionBoostMode.FUNCTION_BOOST_MODE_MAX) + .build(); + + QueryBuilder result5 = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery5, registry); + assertThat(result5, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder builder5 = (FunctionScoreQueryBuilder) result5; + assertEquals(org.opensearch.common.lucene.search.function.CombineFunction.MAX, builder5.boostMode()); + + // Test MIN boost mode + FunctionScoreQuery functionScoreQuery6 = FunctionScoreQuery.newBuilder() + .setBoostMode(FunctionBoostMode.FUNCTION_BOOST_MODE_MIN) + .build(); + + QueryBuilder result6 = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery6, registry); + assertThat(result6, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder builder6 = (FunctionScoreQueryBuilder) result6; + assertEquals(org.opensearch.common.lucene.search.function.CombineFunction.MIN, builder6.boostMode()); + } + + public void testFromProtoWithDifferentScoreModes() { + // Test MULTIPLY score mode + FunctionScoreQuery functionScoreQuery1 = FunctionScoreQuery.newBuilder() + .setScoreMode(FunctionScoreMode.FUNCTION_SCORE_MODE_MULTIPLY) + .build(); + + QueryBuilder result1 = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery1, registry); + assertThat(result1, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder builder1 = (FunctionScoreQueryBuilder) result1; + assertEquals(org.opensearch.common.lucene.search.function.FunctionScoreQuery.ScoreMode.MULTIPLY, builder1.scoreMode()); + + // Test SUM score mode + FunctionScoreQuery functionScoreQuery2 = FunctionScoreQuery.newBuilder() + .setScoreMode(FunctionScoreMode.FUNCTION_SCORE_MODE_SUM) + .build(); + + QueryBuilder result2 = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery2, registry); + assertThat(result2, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder builder2 = (FunctionScoreQueryBuilder) result2; + assertEquals(org.opensearch.common.lucene.search.function.FunctionScoreQuery.ScoreMode.SUM, builder2.scoreMode()); + + // Test AVG score mode + FunctionScoreQuery functionScoreQuery3 = FunctionScoreQuery.newBuilder() + .setScoreMode(FunctionScoreMode.FUNCTION_SCORE_MODE_AVG) + .build(); + + QueryBuilder result3 = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery3, registry); + assertThat(result3, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder builder3 = (FunctionScoreQueryBuilder) result3; + assertEquals(org.opensearch.common.lucene.search.function.FunctionScoreQuery.ScoreMode.AVG, builder3.scoreMode()); + + // Test FIRST score mode + FunctionScoreQuery functionScoreQuery4 = FunctionScoreQuery.newBuilder() + .setScoreMode(FunctionScoreMode.FUNCTION_SCORE_MODE_FIRST) + .build(); + + QueryBuilder result4 = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery4, registry); + assertThat(result4, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder builder4 = (FunctionScoreQueryBuilder) result4; + assertEquals(org.opensearch.common.lucene.search.function.FunctionScoreQuery.ScoreMode.FIRST, builder4.scoreMode()); + + // Test MAX score mode + FunctionScoreQuery functionScoreQuery5 = FunctionScoreQuery.newBuilder() + .setScoreMode(FunctionScoreMode.FUNCTION_SCORE_MODE_MAX) + .build(); + + QueryBuilder result5 = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery5, registry); + assertThat(result5, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder builder5 = (FunctionScoreQueryBuilder) result5; + assertEquals(org.opensearch.common.lucene.search.function.FunctionScoreQuery.ScoreMode.MAX, builder5.scoreMode()); + + // Test MIN score mode + FunctionScoreQuery functionScoreQuery6 = FunctionScoreQuery.newBuilder() + .setScoreMode(FunctionScoreMode.FUNCTION_SCORE_MODE_MIN) + .build(); + + QueryBuilder result6 = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery6, registry); + assertThat(result6, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder builder6 = (FunctionScoreQueryBuilder) result6; + assertEquals(org.opensearch.common.lucene.search.function.FunctionScoreQuery.ScoreMode.MIN, builder6.scoreMode()); + } + + public void testFromProtoWithFunctionAndFilter() { + // Create a term query for the filter + TermQuery termQuery = TermQuery.newBuilder() + .setField("category") + .setValue(FieldValue.newBuilder().setString("electronics").build()) + .build(); + + QueryContainer queryContainer = QueryContainer.newBuilder().setTerm(termQuery).build(); + + // Create a field value factor function + FieldValueFactorScoreFunction fieldValueFactor = FieldValueFactorScoreFunction.newBuilder() + .setField("rating") + .setFactor(2.0f) + .setModifier(FieldValueFactorModifier.FIELD_VALUE_FACTOR_MODIFIER_SQRT) + .build(); + + // Create a function score container with both function and filter + FunctionScoreContainer container = FunctionScoreContainer.newBuilder() + .setFieldValueFactor(fieldValueFactor) + .setFilter(queryContainer) + .setWeight(1.5f) + .build(); + + FunctionScoreQuery functionScoreQuery = FunctionScoreQuery.newBuilder().addFunctions(container).build(); + + QueryBuilder result = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery, registry); + + assertThat(result, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder functionScoreQueryBuilder = (FunctionScoreQueryBuilder) result; + + // Verify that functions were added + assertNotNull(functionScoreQueryBuilder.filterFunctionBuilders()); + assertEquals(1, functionScoreQueryBuilder.filterFunctionBuilders().length); + + // Verify the function has a filter + FunctionScoreQueryBuilder.FilterFunctionBuilder filterFunction = functionScoreQueryBuilder.filterFunctionBuilders()[0]; + assertNotNull(filterFunction.getFilter()); + assertThat(filterFunction.getFilter(), instanceOf(org.opensearch.index.query.TermQueryBuilder.class)); + } + + public void testFromProtoWithEmptyDecayFunction() { + // Create an empty decay function (no placements) + org.opensearch.protobufs.DecayFunction decayFunction = org.opensearch.protobufs.DecayFunction.newBuilder().build(); + + FunctionScoreContainer container = FunctionScoreContainer.newBuilder().setExp(decayFunction).setWeight(1.0f).build(); + + FunctionScoreQuery functionScoreQuery = FunctionScoreQuery.newBuilder().addFunctions(container).build(); + + // This should throw an exception because decay function has no placements + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery, registry) + ); + + assertThat(exception.getMessage(), containsString("DecayFunction must have at least one placement")); + } + + public void testFromProtoWithAllParameters() { + // Create a comprehensive function score query with all parameters + TermQuery termQuery = TermQuery.newBuilder() + .setField("title") + .setValue(FieldValue.newBuilder().setString("search").build()) + .build(); + + QueryContainer queryContainer = QueryContainer.newBuilder().setTerm(termQuery).build(); + + // Create multiple functions + FieldValueFactorScoreFunction fieldValueFactor = FieldValueFactorScoreFunction.newBuilder() + .setField("popularity") + .setFactor(1.2f) + .setMissing(1.0f) + .setModifier(FieldValueFactorModifier.FIELD_VALUE_FACTOR_MODIFIER_LOG) + .build(); + + FunctionScoreContainer container1 = FunctionScoreContainer.newBuilder() + .setFieldValueFactor(fieldValueFactor) + .setWeight(2.0f) + .build(); + + RandomScoreFunction randomScore = RandomScoreFunction.newBuilder() + .setField("_id") + // .setSeed(RandomScoreFunctionSeed.newBuilder().setInt32Value(123).build()) // Method not available in 0.16.0 + .build(); + + FunctionScoreContainer container2 = FunctionScoreContainer.newBuilder().setRandomScore(randomScore).setWeight(0.5f).build(); + + FunctionScoreQuery functionScoreQuery = FunctionScoreQuery.newBuilder() + .setQuery(queryContainer) + .setBoostMode(FunctionBoostMode.FUNCTION_BOOST_MODE_MULTIPLY) + .setScoreMode(FunctionScoreMode.FUNCTION_SCORE_MODE_SUM) + .setMaxBoost(15.0f) + .setMinScore(0.1f) + .setBoost(2.0f) + .addFunctions(container1) + .addFunctions(container2) + .build(); + + QueryBuilder result = FunctionScoreQueryBuilderProtoUtils.fromProto(functionScoreQuery, registry); + + assertThat(result, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder functionScoreQueryBuilder = (FunctionScoreQueryBuilder) result; + + // Verify all parameters + assertThat(functionScoreQueryBuilder.query(), instanceOf(org.opensearch.index.query.TermQueryBuilder.class)); + assertEquals(org.opensearch.common.lucene.search.function.CombineFunction.MULTIPLY, functionScoreQueryBuilder.boostMode()); + assertEquals(org.opensearch.common.lucene.search.function.FunctionScoreQuery.ScoreMode.SUM, functionScoreQueryBuilder.scoreMode()); + assertEquals(15.0f, functionScoreQueryBuilder.maxBoost(), 0.001f); + assertEquals(0.1f, functionScoreQueryBuilder.getMinScore(), 0.001f); + assertEquals(2.0f, functionScoreQueryBuilder.boost(), 0.001f); + + // Verify functions + assertNotNull(functionScoreQueryBuilder.filterFunctionBuilders()); + assertEquals(2, functionScoreQueryBuilder.filterFunctionBuilders().length); + } + +} diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/GaussDecayFunctionProtoUtilsTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/GaussDecayFunctionProtoUtilsTests.java new file mode 100644 index 0000000000000..effe9b3e09406 --- /dev/null +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/GaussDecayFunctionProtoUtilsTests.java @@ -0,0 +1,194 @@ +/* + * 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.functionscore; + +import org.opensearch.index.query.functionscore.GaussDecayFunctionBuilder; +import org.opensearch.index.query.functionscore.ScoreFunctionBuilder; +import org.opensearch.protobufs.DecayFunction; +import org.opensearch.protobufs.DecayPlacement; +import org.opensearch.protobufs.NumericDecayPlacement; +import org.opensearch.test.OpenSearchTestCase; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; + +public class GaussDecayFunctionProtoUtilsTests extends OpenSearchTestCase { + + public void testFromProtoWithNumericPlacement() { + // Test with numeric decay placement + NumericDecayPlacement numericPlacement = NumericDecayPlacement.newBuilder() + .setOrigin(20.0) + .setScale(10.0) + .setOffset(2.0) + .setDecay(0.4) + .build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setNumericDecayPlacement(numericPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder().putPlacement("rating", decayPlacement).build(); + + ScoreFunctionBuilder result = GaussDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(GaussDecayFunctionBuilder.class)); + GaussDecayFunctionBuilder gaussFunction = (GaussDecayFunctionBuilder) result; + + assertEquals("rating", gaussFunction.getFieldName()); + } + + public void testFromProtoWithNumericPlacementWithoutDecay() { + // Test with numeric decay placement without decay parameter + NumericDecayPlacement numericPlacement = NumericDecayPlacement.newBuilder().setOrigin(50.0).setScale(25.0).setOffset(5.0).build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setNumericDecayPlacement(numericPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder().putPlacement("price", decayPlacement).build(); + + ScoreFunctionBuilder result = GaussDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(GaussDecayFunctionBuilder.class)); + GaussDecayFunctionBuilder gaussFunction = (GaussDecayFunctionBuilder) result; + + assertEquals("price", gaussFunction.getFieldName()); + } + + public void testFromProtoWithDatePlacement() { + // Test with DateDecayPlacement with all parameters + org.opensearch.protobufs.DateDecayPlacement datePlacement = org.opensearch.protobufs.DateDecayPlacement.newBuilder() + .setOrigin("2024-06-15") + .setScale("60d") + .setOffset("10d") + .setDecay(0.55) + .build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setDateDecayPlacement(datePlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder().putPlacement("created_date", decayPlacement).build(); + + ScoreFunctionBuilder result = GaussDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(GaussDecayFunctionBuilder.class)); + GaussDecayFunctionBuilder gaussFunction = (GaussDecayFunctionBuilder) result; + + assertEquals("created_date", gaussFunction.getFieldName()); + } + + public void testFromProtoWithDatePlacementWithoutDecay() { + // Test with DateDecayPlacement without decay parameter + org.opensearch.protobufs.DateDecayPlacement datePlacement = org.opensearch.protobufs.DateDecayPlacement.newBuilder() + .setOrigin("now") + .setScale("7d") + .build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setDateDecayPlacement(datePlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder().putPlacement("date_field", decayPlacement).build(); + + ScoreFunctionBuilder result = GaussDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(GaussDecayFunctionBuilder.class)); + GaussDecayFunctionBuilder gaussFunction = (GaussDecayFunctionBuilder) result; + + assertEquals("date_field", gaussFunction.getFieldName()); + } + + public void testFromProtoWithGeoPlacement() { + // Test with GeoDecayPlacement + org.opensearch.protobufs.LatLonGeoLocation latLonLocation = org.opensearch.protobufs.LatLonGeoLocation.newBuilder() + .setLat(40.7128) + .setLon(-74.0060) + .build(); + + org.opensearch.protobufs.GeoLocation geoLocation = org.opensearch.protobufs.GeoLocation.newBuilder() + .setLatlon(latLonLocation) + .build(); + + org.opensearch.protobufs.GeoDecayPlacement geoPlacement = org.opensearch.protobufs.GeoDecayPlacement.newBuilder() + .setOrigin(geoLocation) + .setScale("10km") + .setOffset("1km") + .setDecay(0.5) + .build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setGeoDecayPlacement(geoPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder().putPlacement("location", decayPlacement).build(); + + ScoreFunctionBuilder result = GaussDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(GaussDecayFunctionBuilder.class)); + GaussDecayFunctionBuilder gaussFunction = (GaussDecayFunctionBuilder) result; + + assertEquals("location", gaussFunction.getFieldName()); + } + + public void testFromProtoWithGeoPlacementWithoutDecay() { + // Test with GeoDecayPlacement without decay parameter + org.opensearch.protobufs.LatLonGeoLocation latLonLocation = org.opensearch.protobufs.LatLonGeoLocation.newBuilder() + .setLat(51.5074) + .setLon(-0.1278) + .build(); + + org.opensearch.protobufs.GeoLocation geoLocation = org.opensearch.protobufs.GeoLocation.newBuilder() + .setLatlon(latLonLocation) + .build(); + + org.opensearch.protobufs.GeoDecayPlacement geoPlacement = org.opensearch.protobufs.GeoDecayPlacement.newBuilder() + .setOrigin(geoLocation) + .setScale("5km") + .build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setGeoDecayPlacement(geoPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder().putPlacement("geo_field", decayPlacement).build(); + + ScoreFunctionBuilder result = GaussDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(GaussDecayFunctionBuilder.class)); + GaussDecayFunctionBuilder gaussFunction = (GaussDecayFunctionBuilder) result; + + assertEquals("geo_field", gaussFunction.getFieldName()); + } + + public void testFromProtoWithNullContainer() { + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> GaussDecayFunctionProtoUtils.fromProto(null) + ); + + assertThat(exception.getMessage(), containsString("DecayFunction must have at least one placement")); + } + + public void testFromProtoWithEmptyDecayFunction() { + // Create an empty decay function (no placements) + DecayFunction decayFunction = DecayFunction.newBuilder().build(); + + // This should throw an exception because decay function has no placements + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> GaussDecayFunctionProtoUtils.fromProto(decayFunction) + ); + + assertThat(exception.getMessage(), containsString("DecayFunction must have at least one placement")); + } + + public void testFromProtoWithUnsetDecayPlacement() { + // Create a decay placement with no specific type set + DecayPlacement decayPlacement = DecayPlacement.newBuilder().build(); + + DecayFunction decayFunction = DecayFunction.newBuilder().putPlacement("field", decayPlacement).build(); + + // This should throw an exception because decay placement has no valid type + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> GaussDecayFunctionProtoUtils.fromProto(decayFunction) + ); + + assertThat(exception.getMessage(), containsString("Unsupported decay placement type")); + } +} diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/LinearDecayFunctionProtoUtilsTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/LinearDecayFunctionProtoUtilsTests.java new file mode 100644 index 0000000000000..e92f8a881d297 --- /dev/null +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/LinearDecayFunctionProtoUtilsTests.java @@ -0,0 +1,196 @@ +/* + * 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.functionscore; + +import org.opensearch.index.query.functionscore.LinearDecayFunctionBuilder; +import org.opensearch.index.query.functionscore.ScoreFunctionBuilder; +import org.opensearch.protobufs.DecayFunction; +import org.opensearch.protobufs.DecayPlacement; +import org.opensearch.protobufs.FunctionScoreContainer; +import org.opensearch.protobufs.NumericDecayPlacement; +import org.opensearch.test.OpenSearchTestCase; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; + +public class LinearDecayFunctionProtoUtilsTests extends OpenSearchTestCase { + + public void testFromProtoWithNumericPlacement() { + NumericDecayPlacement numericPlacement = NumericDecayPlacement.newBuilder() + .setOrigin(100.0) + .setScale(50.0) + .setOffset(5.0) + .setDecay(0.2) + .build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setNumericDecayPlacement(numericPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder().putPlacement("distance", decayPlacement).build(); + + FunctionScoreContainer container = FunctionScoreContainer.newBuilder().setLinear(decayFunction).setWeight(1.0f).build(); + + ScoreFunctionBuilder result = LinearDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(LinearDecayFunctionBuilder.class)); + LinearDecayFunctionBuilder linearFunction = (LinearDecayFunctionBuilder) result; + + assertEquals("distance", linearFunction.getFieldName()); + } + + public void testFromProtoWithDatePlacement() { + org.opensearch.protobufs.DateDecayPlacement datePlacement = org.opensearch.protobufs.DateDecayPlacement.newBuilder() + .setOrigin("2024-01-01") + .setScale("30d") + .setOffset("5d") + .setDecay(0.35) + .build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setDateDecayPlacement(datePlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder().putPlacement("timestamp", decayPlacement).build(); + + FunctionScoreContainer container = FunctionScoreContainer.newBuilder().setLinear(decayFunction).build(); + + ScoreFunctionBuilder result = LinearDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(LinearDecayFunctionBuilder.class)); + LinearDecayFunctionBuilder linearFunction = (LinearDecayFunctionBuilder) result; + + assertEquals("timestamp", linearFunction.getFieldName()); + } + + public void testFromProtoWithDatePlacementWithoutDecay() { + org.opensearch.protobufs.DateDecayPlacement datePlacement = org.opensearch.protobufs.DateDecayPlacement.newBuilder() + .setOrigin("now") + .setScale("7d") + .build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setDateDecayPlacement(datePlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder().putPlacement("date_field", decayPlacement).build(); + + FunctionScoreContainer container = FunctionScoreContainer.newBuilder().setLinear(decayFunction).build(); + + ScoreFunctionBuilder result = LinearDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(LinearDecayFunctionBuilder.class)); + LinearDecayFunctionBuilder linearFunction = (LinearDecayFunctionBuilder) result; + + assertEquals("date_field", linearFunction.getFieldName()); + } + + public void testFromProtoWithNullDecayFunction() { + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> LinearDecayFunctionProtoUtils.fromProto(null) + ); + + assertThat(exception.getMessage(), containsString("DecayFunction must have at least one placement")); + } + + public void testFromProtoWithEmptyDecayFunction() { + DecayFunction decayFunction = DecayFunction.newBuilder().build(); + + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> LinearDecayFunctionProtoUtils.fromProto(decayFunction) + ); + + assertThat(exception.getMessage(), containsString("DecayFunction must have at least one placement")); + } + + public void testFromProtoWithUnsetDecayPlacement() { + DecayPlacement decayPlacement = DecayPlacement.newBuilder().build(); + + DecayFunction decayFunction = DecayFunction.newBuilder().putPlacement("field", decayPlacement).build(); + + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> LinearDecayFunctionProtoUtils.fromProto(decayFunction) + ); + + assertThat(exception.getMessage(), containsString("Unsupported decay placement type")); + } + + public void testFromProtoWithGeoPlacement() { + org.opensearch.protobufs.LatLonGeoLocation latLonLocation = org.opensearch.protobufs.LatLonGeoLocation.newBuilder() + .setLat(40.7128) + .setLon(-74.0060) + .build(); + + org.opensearch.protobufs.GeoLocation geoLocation = org.opensearch.protobufs.GeoLocation.newBuilder() + .setLatlon(latLonLocation) + .build(); + + org.opensearch.protobufs.GeoDecayPlacement geoPlacement = org.opensearch.protobufs.GeoDecayPlacement.newBuilder() + .setOrigin(geoLocation) + .setScale("10km") + .setOffset("1km") + .setDecay(0.5) + .build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setGeoDecayPlacement(geoPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder().putPlacement("location", decayPlacement).build(); + + FunctionScoreContainer container = FunctionScoreContainer.newBuilder().setLinear(decayFunction).build(); + + ScoreFunctionBuilder result = LinearDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(LinearDecayFunctionBuilder.class)); + LinearDecayFunctionBuilder linearFunction = (LinearDecayFunctionBuilder) result; + + assertEquals("location", linearFunction.getFieldName()); + } + + public void testFromProtoWithGeoPlacementWithoutDecay() { + org.opensearch.protobufs.LatLonGeoLocation latLonLocation = org.opensearch.protobufs.LatLonGeoLocation.newBuilder() + .setLat(51.5074) + .setLon(-0.1278) + .build(); + + org.opensearch.protobufs.GeoLocation geoLocation = org.opensearch.protobufs.GeoLocation.newBuilder() + .setLatlon(latLonLocation) + .build(); + + org.opensearch.protobufs.GeoDecayPlacement geoPlacement = org.opensearch.protobufs.GeoDecayPlacement.newBuilder() + .setOrigin(geoLocation) + .setScale("5km") + .build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setGeoDecayPlacement(geoPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder().putPlacement("geo_field", decayPlacement).build(); + + FunctionScoreContainer container = FunctionScoreContainer.newBuilder().setLinear(decayFunction).build(); + + ScoreFunctionBuilder result = LinearDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(LinearDecayFunctionBuilder.class)); + LinearDecayFunctionBuilder linearFunction = (LinearDecayFunctionBuilder) result; + + assertEquals("geo_field", linearFunction.getFieldName()); + } + + public void testFromProtoWithNumericPlacementWithoutDecay() { + NumericDecayPlacement numericPlacement = NumericDecayPlacement.newBuilder().setOrigin(75.0).setScale(25.0).setOffset(5.0).build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setNumericDecayPlacement(numericPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder().putPlacement("price", decayPlacement).build(); + + ScoreFunctionBuilder result = LinearDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(LinearDecayFunctionBuilder.class)); + LinearDecayFunctionBuilder linearFunction = (LinearDecayFunctionBuilder) result; + + assertEquals("price", linearFunction.getFieldName()); + } + +} diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/RandomScoreFunctionProtoUtilsTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/RandomScoreFunctionProtoUtilsTests.java new file mode 100644 index 0000000000000..aaabba3c3477d --- /dev/null +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/RandomScoreFunctionProtoUtilsTests.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.functionscore; + +import org.opensearch.index.query.functionscore.RandomScoreFunctionBuilder; +import org.opensearch.index.query.functionscore.ScoreFunctionBuilder; +import org.opensearch.protobufs.RandomScoreFunction; +import org.opensearch.protobufs.RandomScoreFunctionSeed; +import org.opensearch.test.OpenSearchTestCase; + +import static org.hamcrest.Matchers.instanceOf; + +public class RandomScoreFunctionProtoUtilsTests extends OpenSearchTestCase { + + public void testFromProtoWithValidRandomScoreFunction() { + // Create a random score function with int32 seed + RandomScoreFunctionSeed seed = RandomScoreFunctionSeed.newBuilder().setInt32(12345).build(); + + RandomScoreFunction randomScore = RandomScoreFunction.newBuilder().setField("_seq_no").setSeed(seed).build(); + + ScoreFunctionBuilder result = RandomScoreFunctionProtoUtils.fromProto(randomScore); + + assertThat(result, instanceOf(RandomScoreFunctionBuilder.class)); + RandomScoreFunctionBuilder randomScoreBuilder = (RandomScoreFunctionBuilder) result; + assertEquals("_seq_no", randomScoreBuilder.getField()); + assertEquals(Integer.valueOf(12345), randomScoreBuilder.getSeed()); + } + + public void testFromProtoWithStringSeed() { + // Create a random score function with string seed + RandomScoreFunctionSeed seed = RandomScoreFunctionSeed.newBuilder().setString("test_seed").build(); + + RandomScoreFunction randomScore = RandomScoreFunction.newBuilder().setField("_id").setSeed(seed).build(); + + ScoreFunctionBuilder result = RandomScoreFunctionProtoUtils.fromProto(randomScore); + + assertThat(result, instanceOf(RandomScoreFunctionBuilder.class)); + RandomScoreFunctionBuilder randomScoreBuilder = (RandomScoreFunctionBuilder) result; + assertEquals("_id", randomScoreBuilder.getField()); + assertEquals(Integer.valueOf("test_seed".hashCode()), randomScoreBuilder.getSeed()); + } + + public void testFromProtoWithEmptyField() { + // Create a random score function with empty field + RandomScoreFunctionSeed seed = RandomScoreFunctionSeed.newBuilder().setInt32(42).build(); + + RandomScoreFunction randomScore = RandomScoreFunction.newBuilder().setField("").setSeed(seed).build(); + + ScoreFunctionBuilder result = RandomScoreFunctionProtoUtils.fromProto(randomScore); + + assertThat(result, instanceOf(RandomScoreFunctionBuilder.class)); + RandomScoreFunctionBuilder randomScoreBuilder = (RandomScoreFunctionBuilder) result; + assertNull(randomScoreBuilder.getField()); + assertEquals(Integer.valueOf(42), randomScoreBuilder.getSeed()); + } + + public void testFromProtoWithNoSeed() { + // Create a random score function without seed + RandomScoreFunction randomScore = RandomScoreFunction.newBuilder().setField("_seq_no").build(); + + ScoreFunctionBuilder result = RandomScoreFunctionProtoUtils.fromProto(randomScore); + + assertThat(result, instanceOf(RandomScoreFunctionBuilder.class)); + RandomScoreFunctionBuilder randomScoreBuilder = (RandomScoreFunctionBuilder) result; + assertEquals("_seq_no", randomScoreBuilder.getField()); + // Seed should be null when not set + assertNull(randomScoreBuilder.getSeed()); + } + + public void testFromProtoWithNullRandomScoreFunction() { + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> { RandomScoreFunctionProtoUtils.fromProto(null); } + ); + assertEquals("RandomScoreFunction cannot be null", exception.getMessage()); + } +} diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/ScriptScoreFunctionProtoUtilsTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/ScriptScoreFunctionProtoUtilsTests.java new file mode 100644 index 0000000000000..e1b0a91c8ce51 --- /dev/null +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/ScriptScoreFunctionProtoUtilsTests.java @@ -0,0 +1,88 @@ +/* + * 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.functionscore; + +import org.opensearch.index.query.functionscore.ScoreFunctionBuilder; +import org.opensearch.index.query.functionscore.ScriptScoreFunctionBuilder; +import org.opensearch.protobufs.BuiltinScriptLanguage; +import org.opensearch.protobufs.FunctionScoreContainer; +import org.opensearch.protobufs.InlineScript; +import org.opensearch.protobufs.ObjectMap; +import org.opensearch.protobufs.ObjectMap.Value; +import org.opensearch.protobufs.Script; +import org.opensearch.protobufs.ScriptLanguage; +import org.opensearch.protobufs.ScriptScoreFunction; +import org.opensearch.test.OpenSearchTestCase; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; + +public class ScriptScoreFunctionProtoUtilsTests extends OpenSearchTestCase { + + public void testFromProtoWithScript() { + InlineScript inlineScript = InlineScript.newBuilder() + .setSource("doc['score'].value * params.factor") + .setLang(ScriptLanguage.newBuilder().setBuiltin(BuiltinScriptLanguage.BUILTIN_SCRIPT_LANGUAGE_PAINLESS).build()) + .setParams(ObjectMap.newBuilder().putFields("factor", Value.newBuilder().setDouble(2.0).build()).build()) + .build(); + + Script script = Script.newBuilder().setInline(inlineScript).build(); + ScriptScoreFunction scriptScoreFunction = ScriptScoreFunction.newBuilder().setScript(script).build(); + FunctionScoreContainer container = FunctionScoreContainer.newBuilder().setScriptScore(scriptScoreFunction).setWeight(1.5f).build(); + + ScoreFunctionBuilder result = ScriptScoreFunctionProtoUtils.fromProto(scriptScoreFunction); + + assertThat(result, instanceOf(ScriptScoreFunctionBuilder.class)); + ScriptScoreFunctionBuilder scriptFunction = (ScriptScoreFunctionBuilder) result; + org.opensearch.script.Script openSearchScript = scriptFunction.getScript(); + assertEquals(org.opensearch.script.ScriptType.INLINE, openSearchScript.getType()); + assertEquals("painless", openSearchScript.getLang()); + assertEquals("doc['score'].value * params.factor", openSearchScript.getIdOrCode()); + assertEquals(2.0, openSearchScript.getParams().get("factor")); + } + + public void testFromProtoWithNullScriptScoreFunction() { + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> ScriptScoreFunctionProtoUtils.fromProto(null) + ); + assertThat(exception.getMessage(), containsString("ScriptScoreFunction cannot be null")); + } + + public void testFromProtoWithNullScript() { + // Test that setting null script throws NullPointerException at protobuf level + NullPointerException exception = expectThrows( + NullPointerException.class, + () -> ScriptScoreFunction.newBuilder().setScript((org.opensearch.protobufs.Script) null) + ); + // This is expected behavior - protobuf doesn't allow null values + } + + public void testFromProtoWithEmptyScript() { + Script script = Script.newBuilder().build(); + ScriptScoreFunction scriptScoreFunction = ScriptScoreFunction.newBuilder().setScript(script).build(); + FunctionScoreContainer container = FunctionScoreContainer.newBuilder().setScriptScore(scriptScoreFunction).build(); + + UnsupportedOperationException exception = expectThrows( + UnsupportedOperationException.class, + () -> ScriptScoreFunctionProtoUtils.fromProto(scriptScoreFunction) + ); + assertThat(exception.getMessage(), containsString("No valid script type detected")); + } + + public void testFromProtoWithoutScript() { + ScriptScoreFunction scriptScoreFunction = ScriptScoreFunction.newBuilder().build(); + + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> ScriptScoreFunctionProtoUtils.fromProto(scriptScoreFunction) + ); + assertThat(exception.getMessage(), containsString("ScriptScoreFunction must have a script")); + } +}