From fd08ef719084ea2c79208910288f6238ec20f6b2 Mon Sep 17 00:00:00 2001 From: Karen X Date: Thu, 4 Dec 2025 21:43:03 +0000 Subject: [PATCH 1/2] [GRPC] Add support for missing proto fields in FunctionScore and Highlight Signed-off-by: Karen X --- CHANGELOG.md | 2 + .../search/HighlightBuilderProtoUtils.java | 43 ++++++ .../DecayFunctionProtoUtils.java | 38 +++++ .../ExpDecayFunctionProtoUtils.java | 21 ++- .../GaussDecayFunctionProtoUtils.java | 21 ++- .../LinearDecayFunctionProtoUtils.java | 21 ++- .../RandomScoreFunctionProtoUtils.java | 2 + .../HighlightBuilderProtoUtilsTests.java | 131 ++++++++++++++++++ .../TermsQueryBuilderProtoUtilsTests.java | 30 ++++ .../ExpDecayFunctionProtoUtilsTests.java | 77 ++++++++++ .../GaussDecayFunctionProtoUtilsTests.java | 77 ++++++++++ .../LinearDecayFunctionProtoUtilsTests.java | 77 ++++++++++ .../RandomScoreFunctionProtoUtilsTests.java | 15 ++ 13 files changed, 537 insertions(+), 18 deletions(-) create mode 100644 modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/DecayFunctionProtoUtils.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 4433e1bbfe9fb..d31e7af17e1bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Bump opensearch-protobufs dependency to 0.24.0 and update transport-grpc module compatibility ([#20059](https://github.com/opensearch-project/OpenSearch/pull/20059)) - Refactor the ShardStats, WarmerStats and IndexingPressureStats class to use the Builder pattern instead of constructors ([#19966](https://github.com/opensearch-project/OpenSearch/pull/19966)) +- Add support for missing proto fields in GRPC FunctionScore and Highlight ([#20169](https://github.com/opensearch-project/OpenSearch/pull/20169)) + ### Fixed - Fix Allocation and Rebalance Constraints of WeightFunction are incorrectly reset ([#19012](https://github.com/opensearch-project/OpenSearch/pull/19012)) diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/HighlightBuilderProtoUtils.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/HighlightBuilderProtoUtils.java index 405771270347b..51f19e1139e87 100644 --- a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/HighlightBuilderProtoUtils.java +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/HighlightBuilderProtoUtils.java @@ -152,6 +152,9 @@ static HighlightBuilder fromProto(Highlight highlightProto, QueryBuilderProtoCon highlightBuilder.encoder(ProtobufEnumUtils.convertToString(highlightProto.getEncoder())); } + // TODO: Support useExplicitFieldOrder + // A spec fix is required and a corresponding protobuf version upgrade. + if (highlightProto.getFieldsCount() > 0) { for (java.util.Map.Entry entry : highlightProto.getFieldsMap().entrySet()) { String fieldName = entry.getKey(); @@ -252,6 +255,22 @@ static HighlightBuilder fromProto(Highlight highlightProto, QueryBuilderProtoCon fieldBuilder.forceSource(fieldProto.getForceSource()); } + if (fieldProto.hasOrder() && fieldProto.getOrder() == HighlighterOrder.HIGHLIGHTER_ORDER_SCORE) { + fieldBuilder.order(HighlightBuilder.Order.SCORE); + } + + if (fieldProto.hasPhraseLimit()) { + fieldBuilder.phraseLimit(fieldProto.getPhraseLimit()); + } + + if (fieldProto.hasRequireFieldMatch()) { + fieldBuilder.requireFieldMatch(fieldProto.getRequireFieldMatch()); + } + + if (fieldProto.hasTagsSchema() && fieldProto.getTagsSchema() != HighlighterTagsSchema.HIGHLIGHTER_TAGS_SCHEMA_UNSPECIFIED) { + applyTagsSchemaToField(fieldBuilder, fieldProto.getTagsSchema()); + } + highlightBuilder.field(fieldBuilder); } } @@ -274,4 +293,28 @@ private static HighlightBuilder.BoundaryScannerType parseBoundaryScanner(Boundar throw new IllegalArgumentException("Unknown BoundaryScanner value: " + boundaryScanner); } } + + /** + * Apply tags schema to a highlight field by setting pre/post tags. + * This mirrors the behavior of {@link HighlightBuilder#tagsSchema(String)} but for field-level settings. + *

+ * The {@code tagsSchema} method on {@link HighlightBuilder} sets built-in pre and post tags based on a schema name. + * Since {@link HighlightBuilder.Field} doesn't have a {@code tagsSchema} method, this helper applies the same + * logic by directly setting the pre/post tags on the field. + * + * @param fieldBuilder the highlight field builder to apply the tags schema to + * @param tagsSchema the tags schema to apply (e.g., STYLED) + * @see HighlightBuilder#tagsSchema(String) + */ + private static void applyTagsSchemaToField(HighlightBuilder.Field fieldBuilder, HighlighterTagsSchema tagsSchema) { + switch (tagsSchema) { + // TODO add HIGHLIGHTER_TAGS_SCHEMA_DEFAULT once new protobufs are published after spec fix + case HIGHLIGHTER_TAGS_SCHEMA_STYLED: + fieldBuilder.preTags(HighlightBuilder.DEFAULT_STYLED_PRE_TAG); + fieldBuilder.postTags(HighlightBuilder.DEFAULT_STYLED_POST_TAGS); + break; + default: + throw new IllegalArgumentException("Unknown tags schema: " + tagsSchema); + } + } } diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/DecayFunctionProtoUtils.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/DecayFunctionProtoUtils.java new file mode 100644 index 0000000000000..3197697105db2 --- /dev/null +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/DecayFunctionProtoUtils.java @@ -0,0 +1,38 @@ +/* + * 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.protobufs.MultiValueMode; + +/** + * Common utility class for decay function Protocol Buffer conversions. + * Contains shared methods used by {@link ExpDecayFunctionProtoUtils}, + * {@link GaussDecayFunctionProtoUtils}, and {@link LinearDecayFunctionProtoUtils}. + */ +class DecayFunctionProtoUtils { + + private DecayFunctionProtoUtils() { + // Utility class, no instances + } + + /** + * Converts a Protocol Buffer MultiValueMode enum to OpenSearch MultiValueMode. + * + * @param multiValueMode the Protocol Buffer MultiValueMode enum value + * @return the corresponding OpenSearch MultiValueMode + * @throws IllegalArgumentException if the multiValueMode is unsupported + */ + static org.opensearch.search.MultiValueMode parseMultiValueMode(MultiValueMode multiValueMode) { + return switch (multiValueMode) { + case MULTI_VALUE_MODE_AVG -> org.opensearch.search.MultiValueMode.AVG; + case MULTI_VALUE_MODE_MAX -> org.opensearch.search.MultiValueMode.MAX; + case MULTI_VALUE_MODE_MIN -> org.opensearch.search.MultiValueMode.MIN; + default -> throw new IllegalArgumentException("Unsupported multi value mode: " + multiValueMode); + }; + } +} 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 index 9b99d686398ce..2bb73b128d3dd 100644 --- 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 @@ -14,6 +14,7 @@ import org.opensearch.protobufs.DecayFunction; import org.opensearch.protobufs.DecayPlacement; import org.opensearch.protobufs.GeoDecayPlacement; +import org.opensearch.protobufs.MultiValueMode; import org.opensearch.protobufs.NumericDecayPlacement; import org.opensearch.transport.grpc.proto.request.common.GeoPointProtoUtils; @@ -49,21 +50,29 @@ static ScoreFunctionBuilder fromProto(DecayFunction decayFunction) { String fieldName = entry.getKey(); DecayPlacement decayPlacement = entry.getValue(); + ExponentialDecayFunctionBuilder builder; if (decayPlacement.hasNumericDecayPlacement()) { - return parseNumericExpDecay(fieldName, decayPlacement.getNumericDecayPlacement()); + builder = parseNumericExpDecay(fieldName, decayPlacement.getNumericDecayPlacement()); } else if (decayPlacement.hasGeoDecayPlacement()) { - return parseGeoExpDecay(fieldName, decayPlacement.getGeoDecayPlacement()); + builder = parseGeoExpDecay(fieldName, decayPlacement.getGeoDecayPlacement()); } else if (decayPlacement.hasDateDecayPlacement()) { - return parseDateExpDecay(fieldName, decayPlacement.getDateDecayPlacement()); + builder = parseDateExpDecay(fieldName, decayPlacement.getDateDecayPlacement()); } else { throw new IllegalArgumentException("Unsupported decay placement type"); } + + // Set multi_value_mode if present + if (decayFunction.hasMultiValueMode() && decayFunction.getMultiValueMode() != MultiValueMode.MULTI_VALUE_MODE_UNSPECIFIED) { + builder.setMultiValueMode(DecayFunctionProtoUtils.parseMultiValueMode(decayFunction.getMultiValueMode())); + } + + return builder; } /** * Parses a numeric decay placement for exponential decay. */ - private static ScoreFunctionBuilder parseNumericExpDecay(String fieldName, NumericDecayPlacement numericPlacement) { + private static ExponentialDecayFunctionBuilder parseNumericExpDecay(String fieldName, NumericDecayPlacement numericPlacement) { ExponentialDecayFunctionBuilder builder; if (numericPlacement.hasDecay()) { builder = new ExponentialDecayFunctionBuilder( @@ -88,7 +97,7 @@ private static ScoreFunctionBuilder parseNumericExpDecay(String fieldName, Nu /** * Parses a geo decay placement for exponential decay. */ - private static ScoreFunctionBuilder parseGeoExpDecay(String fieldName, GeoDecayPlacement geoPlacement) { + private static ExponentialDecayFunctionBuilder parseGeoExpDecay(String fieldName, GeoDecayPlacement geoPlacement) { GeoPoint geoPoint = GeoPointProtoUtils.parseGeoPoint(geoPlacement.getOrigin()); ExponentialDecayFunctionBuilder builder; @@ -115,7 +124,7 @@ private static ScoreFunctionBuilder parseGeoExpDecay(String fieldName, GeoDec /** * Parses a date decay placement for exponential decay. */ - private static ScoreFunctionBuilder parseDateExpDecay(String fieldName, DateDecayPlacement datePlacement) { + private static ExponentialDecayFunctionBuilder parseDateExpDecay(String fieldName, DateDecayPlacement datePlacement) { Object origin = datePlacement.hasOrigin() ? datePlacement.getOrigin() : null; ExponentialDecayFunctionBuilder builder; 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 index 91de13a0ac4f7..cb41aedc588e3 100644 --- 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 @@ -14,6 +14,7 @@ import org.opensearch.protobufs.DecayFunction; import org.opensearch.protobufs.DecayPlacement; import org.opensearch.protobufs.GeoDecayPlacement; +import org.opensearch.protobufs.MultiValueMode; import org.opensearch.protobufs.NumericDecayPlacement; import org.opensearch.transport.grpc.proto.request.common.GeoPointProtoUtils; @@ -49,21 +50,29 @@ static ScoreFunctionBuilder fromProto(DecayFunction decayFunction) { String fieldName = entry.getKey(); DecayPlacement decayPlacement = entry.getValue(); + GaussDecayFunctionBuilder builder; if (decayPlacement.hasNumericDecayPlacement()) { - return parseNumericGaussDecay(fieldName, decayPlacement.getNumericDecayPlacement()); + builder = parseNumericGaussDecay(fieldName, decayPlacement.getNumericDecayPlacement()); } else if (decayPlacement.hasGeoDecayPlacement()) { - return parseGeoGaussDecay(fieldName, decayPlacement.getGeoDecayPlacement()); + builder = parseGeoGaussDecay(fieldName, decayPlacement.getGeoDecayPlacement()); } else if (decayPlacement.hasDateDecayPlacement()) { - return parseDateGaussDecay(fieldName, decayPlacement.getDateDecayPlacement()); + builder = parseDateGaussDecay(fieldName, decayPlacement.getDateDecayPlacement()); } else { throw new IllegalArgumentException("Unsupported decay placement type"); } + + // Set multi_value_mode if present + if (decayFunction.hasMultiValueMode() && decayFunction.getMultiValueMode() != MultiValueMode.MULTI_VALUE_MODE_UNSPECIFIED) { + builder.setMultiValueMode(DecayFunctionProtoUtils.parseMultiValueMode(decayFunction.getMultiValueMode())); + } + + return builder; } /** * Parses a numeric decay placement for Gaussian decay. */ - private static ScoreFunctionBuilder parseNumericGaussDecay(String fieldName, NumericDecayPlacement numericPlacement) { + private static GaussDecayFunctionBuilder parseNumericGaussDecay(String fieldName, NumericDecayPlacement numericPlacement) { GaussDecayFunctionBuilder builder; if (numericPlacement.hasDecay()) { builder = new GaussDecayFunctionBuilder( @@ -88,7 +97,7 @@ private static ScoreFunctionBuilder parseNumericGaussDecay(String fieldName, /** * Parses a geo decay placement for Gaussian decay. */ - private static ScoreFunctionBuilder parseGeoGaussDecay(String fieldName, GeoDecayPlacement geoPlacement) { + private static GaussDecayFunctionBuilder parseGeoGaussDecay(String fieldName, GeoDecayPlacement geoPlacement) { GeoPoint geoPoint = GeoPointProtoUtils.parseGeoPoint(geoPlacement.getOrigin()); GaussDecayFunctionBuilder builder; @@ -115,7 +124,7 @@ private static ScoreFunctionBuilder parseGeoGaussDecay(String fieldName, GeoD /** * Parses a date decay placement for Gaussian decay. */ - private static ScoreFunctionBuilder parseDateGaussDecay(String fieldName, DateDecayPlacement datePlacement) { + private static GaussDecayFunctionBuilder parseDateGaussDecay(String fieldName, DateDecayPlacement datePlacement) { Object origin = datePlacement.hasOrigin() ? datePlacement.getOrigin() : null; GaussDecayFunctionBuilder 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 index 42d9c640da0cb..ddc049c657d7b 100644 --- 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 @@ -14,6 +14,7 @@ import org.opensearch.protobufs.DecayFunction; import org.opensearch.protobufs.DecayPlacement; import org.opensearch.protobufs.GeoDecayPlacement; +import org.opensearch.protobufs.MultiValueMode; import org.opensearch.protobufs.NumericDecayPlacement; import org.opensearch.transport.grpc.proto.request.common.GeoPointProtoUtils; @@ -49,15 +50,23 @@ static ScoreFunctionBuilder fromProto(DecayFunction decayFunction) { String fieldName = entry.getKey(); DecayPlacement decayPlacement = entry.getValue(); + LinearDecayFunctionBuilder builder; if (decayPlacement.hasNumericDecayPlacement()) { - return parseNumericLinearDecay(fieldName, decayPlacement.getNumericDecayPlacement()); + builder = parseNumericLinearDecay(fieldName, decayPlacement.getNumericDecayPlacement()); } else if (decayPlacement.hasGeoDecayPlacement()) { - return parseGeoLinearDecay(fieldName, decayPlacement.getGeoDecayPlacement()); + builder = parseGeoLinearDecay(fieldName, decayPlacement.getGeoDecayPlacement()); } else if (decayPlacement.hasDateDecayPlacement()) { - return parseDateLinearDecay(fieldName, decayPlacement.getDateDecayPlacement()); + builder = parseDateLinearDecay(fieldName, decayPlacement.getDateDecayPlacement()); } else { throw new IllegalArgumentException("Unsupported decay placement type"); } + + // Set multi_value_mode if present + if (decayFunction.hasMultiValueMode() && decayFunction.getMultiValueMode() != MultiValueMode.MULTI_VALUE_MODE_UNSPECIFIED) { + builder.setMultiValueMode(DecayFunctionProtoUtils.parseMultiValueMode(decayFunction.getMultiValueMode())); + } + + return builder; } /** @@ -69,7 +78,7 @@ static ScoreFunctionBuilder fromProto(DecayFunction decayFunction) { * @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) { + private static LinearDecayFunctionBuilder parseNumericLinearDecay(String fieldName, NumericDecayPlacement numericPlacement) { LinearDecayFunctionBuilder builder; if (numericPlacement.hasDecay()) { builder = new LinearDecayFunctionBuilder( @@ -100,7 +109,7 @@ private static ScoreFunctionBuilder parseNumericLinearDecay(String fieldName, * @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) { + private static LinearDecayFunctionBuilder parseGeoLinearDecay(String fieldName, GeoDecayPlacement geoPlacement) { GeoPoint geoPoint = GeoPointProtoUtils.parseGeoPoint(geoPlacement.getOrigin()); LinearDecayFunctionBuilder builder; @@ -133,7 +142,7 @@ private static ScoreFunctionBuilder parseGeoLinearDecay(String fieldName, Geo * @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) { + private static LinearDecayFunctionBuilder parseDateLinearDecay(String fieldName, DateDecayPlacement datePlacement) { Object origin = datePlacement.hasOrigin() ? datePlacement.getOrigin() : null; LinearDecayFunctionBuilder 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 index bd125781f7a1f..5fc34cbd73dc1 100644 --- 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 @@ -48,6 +48,8 @@ static ScoreFunctionBuilder fromProto(RandomScoreFunction randomScore) { org.opensearch.protobufs.RandomScoreFunctionSeed seed = randomScore.getSeed(); if (seed.hasInt32()) { builder.seed(seed.getInt32()); + } else if (seed.hasInt64()) { + builder.seed(seed.getInt64()); } else if (seed.hasString()) { builder.seed(seed.getString()); } diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/HighlightBuilderProtoUtilsTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/HighlightBuilderProtoUtilsTests.java index 8c8f411086095..2841fa19ebaeb 100644 --- a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/HighlightBuilderProtoUtilsTests.java +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/HighlightBuilderProtoUtilsTests.java @@ -403,4 +403,135 @@ public void testFromProto_WithFieldHighlightQueryAndNoMatchSize() { assertEquals("match_all", field.highlightQuery().getName()); assertEquals(250, (int) field.noMatchSize()); } + + public void testFromProto_WithFieldOrder() { + HighlightField fieldProto = HighlightField.newBuilder().setOrder(HighlighterOrder.HIGHLIGHTER_ORDER_SCORE).build(); + + Highlight highlightProto = Highlight.newBuilder().putFields("title", fieldProto).build(); + HighlightBuilder result = HighlightBuilderProtoUtils.fromProto(highlightProto, registry); + + assertNotNull(result); + List fields = result.fields(); + assertEquals(1, fields.size()); + + HighlightBuilder.Field field = fields.get(0); + assertEquals(HighlightBuilder.Order.SCORE, field.order()); + } + + public void testFromProto_WithFieldPhraseLimit() { + HighlightField fieldProto = HighlightField.newBuilder().setPhraseLimit(512).build(); + + Highlight highlightProto = Highlight.newBuilder().putFields("title", fieldProto).build(); + HighlightBuilder result = HighlightBuilderProtoUtils.fromProto(highlightProto, registry); + + assertNotNull(result); + List fields = result.fields(); + assertEquals(1, fields.size()); + + HighlightBuilder.Field field = fields.get(0); + assertEquals(512, (int) field.phraseLimit()); + } + + public void testFromProto_WithFieldRequireFieldMatch() { + HighlightField fieldProto = HighlightField.newBuilder().setRequireFieldMatch(false).build(); + + Highlight highlightProto = Highlight.newBuilder().putFields("title", fieldProto).build(); + HighlightBuilder result = HighlightBuilderProtoUtils.fromProto(highlightProto, registry); + + assertNotNull(result); + List fields = result.fields(); + assertEquals(1, fields.size()); + + HighlightBuilder.Field field = fields.get(0); + assertFalse(field.requireFieldMatch()); + } + + public void testFromProto_WithFieldTagsSchema() { + HighlightField fieldProto = HighlightField.newBuilder().setTagsSchema(HighlighterTagsSchema.HIGHLIGHTER_TAGS_SCHEMA_STYLED).build(); + + Highlight highlightProto = Highlight.newBuilder().putFields("title", fieldProto).build(); + HighlightBuilder result = HighlightBuilderProtoUtils.fromProto(highlightProto, registry); + + assertNotNull(result); + List fields = result.fields(); + assertEquals(1, fields.size()); + + // The tags schema should have been applied - verify the field was created + HighlightBuilder.Field field = fields.get(0); + assertEquals("title", field.name()); + } + + public void testFromProto_WithFieldForceSource() { + HighlightField fieldProto = HighlightField.newBuilder().setForceSource(true).build(); + + Highlight highlightProto = Highlight.newBuilder().putFields("content", fieldProto).build(); + HighlightBuilder result = HighlightBuilderProtoUtils.fromProto(highlightProto, registry); + + assertNotNull(result); + List fields = result.fields(); + assertEquals(1, fields.size()); + + HighlightBuilder.Field field = fields.get(0); + assertTrue(field.forceSource()); + } + + public void testFromProto_WithAllFieldSettings() { + // Test with all field settings combined (excluding tagsSchema to test explicit pre/post tags) + HighlightField fieldProto = HighlightField.newBuilder() + .setFragmentSize(150) + .setNumberOfFragments(3) + .setFragmentOffset(10) + .setOrder(HighlighterOrder.HIGHLIGHTER_ORDER_SCORE) + .setPhraseLimit(256) + .setRequireFieldMatch(true) + .setForceSource(true) + .setNoMatchSize(100) + .setType(HighlighterType.newBuilder().setBuiltin(BuiltinHighlighterType.BUILTIN_HIGHLIGHTER_TYPE_FVH).build()) + .setBoundaryScanner(BoundaryScanner.BOUNDARY_SCANNER_WORD) + .setBoundaryMaxScan(30) + .setBoundaryChars(".,!?") + .setBoundaryScannerLocale("en-US") + .setFragmenter(HighlighterFragmenter.HIGHLIGHTER_FRAGMENTER_SPAN) + .setHighlightFilter(true) + .setMaxAnalyzerOffset(1000) + .addPreTags("") + .addPostTags("") + .addMatchedFields("title") + .addMatchedFields("title.raw") + .build(); + + Highlight highlightProto = Highlight.newBuilder().putFields("title", fieldProto).build(); + HighlightBuilder result = HighlightBuilderProtoUtils.fromProto(highlightProto, registry); + + assertNotNull(result); + List fields = result.fields(); + assertEquals(1, fields.size()); + + HighlightBuilder.Field field = fields.get(0); + assertEquals("title", field.name()); + assertEquals(150, (int) field.fragmentSize()); + assertEquals(3, (int) field.numOfFragments()); + // fragmentOffset is package-private, can't verify directly + assertEquals(HighlightBuilder.Order.SCORE, field.order()); + assertEquals(256, (int) field.phraseLimit()); + assertTrue(field.requireFieldMatch()); + assertTrue(field.forceSource()); + assertEquals(100, (int) field.noMatchSize()); + assertEquals("fvh", field.highlighterType()); + assertEquals(HighlightBuilder.BoundaryScannerType.WORD, field.boundaryScannerType()); + assertEquals(30, (int) field.boundaryMaxScan()); + assertEquals("span", field.fragmenter()); + assertTrue(field.highlightFilter()); + assertEquals(1000, (int) field.maxAnalyzerOffset()); + + String[] preTags = field.preTags(); + assertEquals(1, preTags.length); + assertEquals("", preTags[0]); + + String[] postTags = field.postTags(); + assertEquals(1, postTags.length); + assertEquals("", postTags[0]); + + // matchedFields is package-private, can't verify directly + } } diff --git a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/TermsQueryBuilderProtoUtilsTests.java b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/TermsQueryBuilderProtoUtilsTests.java index 2f68f4a84e853..50ab3c57942ea 100644 --- a/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/TermsQueryBuilderProtoUtilsTests.java +++ b/modules/transport-grpc/src/test/java/org/opensearch/transport/grpc/proto/request/search/query/TermsQueryBuilderProtoUtilsTests.java @@ -172,6 +172,36 @@ public void testFromProtoNewOverloadWithLookup() { assertEquals(fieldName, builder.fieldName()); assertEquals(TermsQueryBuilder.ValueType.DEFAULT, builder.valueType()); assertNotNull("termsLookup should be set for lookup path", builder.termsLookup()); + assertEquals("idx", builder.termsLookup().index()); + assertEquals("1", builder.termsLookup().id()); + assertEquals("tags", builder.termsLookup().path()); + assertEquals("r", builder.termsLookup().routing()); + assertTrue("store should be true", builder.termsLookup().store()); + } + + public void testFromProtoWithLookupStoreDefaultFalse() { + String fieldName = "tags"; + + // Test without setting store (should default to false) + org.opensearch.protobufs.TermsLookup lookup = org.opensearch.protobufs.TermsLookup.newBuilder() + .setIndex("idx") + .setId("1") + .setPath("tags") + .build(); + + org.opensearch.protobufs.TermsQueryField termsQueryField = org.opensearch.protobufs.TermsQueryField.newBuilder() + .setLookup(lookup) + .build(); + + TermsQueryBuilder builder = TermsQueryBuilderProtoUtils.fromProto( + fieldName, + termsQueryField, + org.opensearch.protobufs.TermsQueryValueType.TERMS_QUERY_VALUE_TYPE_DEFAULT + ); + + assertNotNull(builder); + assertNotNull("termsLookup should be set for lookup path", builder.termsLookup()); + assertFalse("store should default to false", builder.termsLookup().store()); } public void testFromProtoNewOverloadBitmapDecoding() { 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 index d1375f7f43929..737fe334a2cfb 100644 --- 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 @@ -178,4 +178,81 @@ public void testFromProtoWithGeoPlacementWithoutDecay() { assertEquals("geo_field", expFunction.getFieldName()); } + + public void testFromProtoWithMultiValueModeMin() { + NumericDecayPlacement numericPlacement = NumericDecayPlacement.newBuilder().setOrigin(10.0).setScale(5.0).build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setNumericDecayPlacement(numericPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder() + .putPlacement("price", decayPlacement) + .setMultiValueMode(org.opensearch.protobufs.MultiValueMode.MULTI_VALUE_MODE_MIN) + .build(); + + ScoreFunctionBuilder result = ExpDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(ExponentialDecayFunctionBuilder.class)); + ExponentialDecayFunctionBuilder expFunction = (ExponentialDecayFunctionBuilder) result; + + assertEquals("price", expFunction.getFieldName()); + assertEquals(org.opensearch.search.MultiValueMode.MIN, expFunction.getMultiValueMode()); + } + + public void testFromProtoWithMultiValueModeMax() { + NumericDecayPlacement numericPlacement = NumericDecayPlacement.newBuilder().setOrigin(50.0).setScale(25.0).build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setNumericDecayPlacement(numericPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder() + .putPlacement("rating", decayPlacement) + .setMultiValueMode(org.opensearch.protobufs.MultiValueMode.MULTI_VALUE_MODE_MAX) + .build(); + + ScoreFunctionBuilder result = ExpDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(ExponentialDecayFunctionBuilder.class)); + ExponentialDecayFunctionBuilder expFunction = (ExponentialDecayFunctionBuilder) result; + + assertEquals("rating", expFunction.getFieldName()); + assertEquals(org.opensearch.search.MultiValueMode.MAX, expFunction.getMultiValueMode()); + } + + public void testFromProtoWithMultiValueModeAvg() { + NumericDecayPlacement numericPlacement = NumericDecayPlacement.newBuilder().setOrigin(100.0).setScale(10.0).build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setNumericDecayPlacement(numericPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder() + .putPlacement("score", decayPlacement) + .setMultiValueMode(org.opensearch.protobufs.MultiValueMode.MULTI_VALUE_MODE_AVG) + .build(); + + ScoreFunctionBuilder result = ExpDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(ExponentialDecayFunctionBuilder.class)); + ExponentialDecayFunctionBuilder expFunction = (ExponentialDecayFunctionBuilder) result; + + assertEquals("score", expFunction.getFieldName()); + assertEquals(org.opensearch.search.MultiValueMode.AVG, expFunction.getMultiValueMode()); + } + + public void testFromProtoWithMultiValueModeUnspecified() { + NumericDecayPlacement numericPlacement = NumericDecayPlacement.newBuilder().setOrigin(10.0).setScale(5.0).build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setNumericDecayPlacement(numericPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder() + .putPlacement("price", decayPlacement) + .setMultiValueMode(org.opensearch.protobufs.MultiValueMode.MULTI_VALUE_MODE_UNSPECIFIED) + .build(); + + ScoreFunctionBuilder result = ExpDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(ExponentialDecayFunctionBuilder.class)); + ExponentialDecayFunctionBuilder expFunction = (ExponentialDecayFunctionBuilder) result; + + assertEquals("price", expFunction.getFieldName()); + // When UNSPECIFIED, multi_value_mode should remain at default (MIN) + assertEquals(org.opensearch.search.MultiValueMode.MIN, expFunction.getMultiValueMode()); + } } 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 index effe9b3e09406..8174ea0f87a82 100644 --- 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 @@ -191,4 +191,81 @@ public void testFromProtoWithUnsetDecayPlacement() { assertThat(exception.getMessage(), containsString("Unsupported decay placement type")); } + + public void testFromProtoWithMultiValueModeMin() { + NumericDecayPlacement numericPlacement = NumericDecayPlacement.newBuilder().setOrigin(20.0).setScale(10.0).build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setNumericDecayPlacement(numericPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder() + .putPlacement("rating", decayPlacement) + .setMultiValueMode(org.opensearch.protobufs.MultiValueMode.MULTI_VALUE_MODE_MIN) + .build(); + + ScoreFunctionBuilder result = GaussDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(GaussDecayFunctionBuilder.class)); + GaussDecayFunctionBuilder gaussFunction = (GaussDecayFunctionBuilder) result; + + assertEquals("rating", gaussFunction.getFieldName()); + assertEquals(org.opensearch.search.MultiValueMode.MIN, gaussFunction.getMultiValueMode()); + } + + public void testFromProtoWithMultiValueModeMax() { + NumericDecayPlacement numericPlacement = NumericDecayPlacement.newBuilder().setOrigin(50.0).setScale(25.0).build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setNumericDecayPlacement(numericPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder() + .putPlacement("price", decayPlacement) + .setMultiValueMode(org.opensearch.protobufs.MultiValueMode.MULTI_VALUE_MODE_MAX) + .build(); + + ScoreFunctionBuilder result = GaussDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(GaussDecayFunctionBuilder.class)); + GaussDecayFunctionBuilder gaussFunction = (GaussDecayFunctionBuilder) result; + + assertEquals("price", gaussFunction.getFieldName()); + assertEquals(org.opensearch.search.MultiValueMode.MAX, gaussFunction.getMultiValueMode()); + } + + public void testFromProtoWithMultiValueModeAvg() { + NumericDecayPlacement numericPlacement = NumericDecayPlacement.newBuilder().setOrigin(100.0).setScale(10.0).build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setNumericDecayPlacement(numericPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder() + .putPlacement("score", decayPlacement) + .setMultiValueMode(org.opensearch.protobufs.MultiValueMode.MULTI_VALUE_MODE_AVG) + .build(); + + ScoreFunctionBuilder result = GaussDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(GaussDecayFunctionBuilder.class)); + GaussDecayFunctionBuilder gaussFunction = (GaussDecayFunctionBuilder) result; + + assertEquals("score", gaussFunction.getFieldName()); + assertEquals(org.opensearch.search.MultiValueMode.AVG, gaussFunction.getMultiValueMode()); + } + + public void testFromProtoWithMultiValueModeUnspecified() { + NumericDecayPlacement numericPlacement = NumericDecayPlacement.newBuilder().setOrigin(20.0).setScale(10.0).build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setNumericDecayPlacement(numericPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder() + .putPlacement("rating", decayPlacement) + .setMultiValueMode(org.opensearch.protobufs.MultiValueMode.MULTI_VALUE_MODE_UNSPECIFIED) + .build(); + + ScoreFunctionBuilder result = GaussDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(GaussDecayFunctionBuilder.class)); + GaussDecayFunctionBuilder gaussFunction = (GaussDecayFunctionBuilder) result; + + assertEquals("rating", gaussFunction.getFieldName()); + // When UNSPECIFIED, multi_value_mode should remain at default (MIN) + assertEquals(org.opensearch.search.MultiValueMode.MIN, gaussFunction.getMultiValueMode()); + } } 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 index e92f8a881d297..06da1d3c96ea2 100644 --- 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 @@ -193,4 +193,81 @@ public void testFromProtoWithNumericPlacementWithoutDecay() { assertEquals("price", linearFunction.getFieldName()); } + public void testFromProtoWithMultiValueModeMin() { + NumericDecayPlacement numericPlacement = NumericDecayPlacement.newBuilder().setOrigin(100.0).setScale(50.0).build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setNumericDecayPlacement(numericPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder() + .putPlacement("distance", decayPlacement) + .setMultiValueMode(org.opensearch.protobufs.MultiValueMode.MULTI_VALUE_MODE_MIN) + .build(); + + ScoreFunctionBuilder result = LinearDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(LinearDecayFunctionBuilder.class)); + LinearDecayFunctionBuilder linearFunction = (LinearDecayFunctionBuilder) result; + + assertEquals("distance", linearFunction.getFieldName()); + assertEquals(org.opensearch.search.MultiValueMode.MIN, linearFunction.getMultiValueMode()); + } + + public void testFromProtoWithMultiValueModeMax() { + NumericDecayPlacement numericPlacement = NumericDecayPlacement.newBuilder().setOrigin(50.0).setScale(25.0).build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setNumericDecayPlacement(numericPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder() + .putPlacement("price", decayPlacement) + .setMultiValueMode(org.opensearch.protobufs.MultiValueMode.MULTI_VALUE_MODE_MAX) + .build(); + + ScoreFunctionBuilder result = LinearDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(LinearDecayFunctionBuilder.class)); + LinearDecayFunctionBuilder linearFunction = (LinearDecayFunctionBuilder) result; + + assertEquals("price", linearFunction.getFieldName()); + assertEquals(org.opensearch.search.MultiValueMode.MAX, linearFunction.getMultiValueMode()); + } + + public void testFromProtoWithMultiValueModeAvg() { + NumericDecayPlacement numericPlacement = NumericDecayPlacement.newBuilder().setOrigin(100.0).setScale(10.0).build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setNumericDecayPlacement(numericPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder() + .putPlacement("score", decayPlacement) + .setMultiValueMode(org.opensearch.protobufs.MultiValueMode.MULTI_VALUE_MODE_AVG) + .build(); + + ScoreFunctionBuilder result = LinearDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(LinearDecayFunctionBuilder.class)); + LinearDecayFunctionBuilder linearFunction = (LinearDecayFunctionBuilder) result; + + assertEquals("score", linearFunction.getFieldName()); + assertEquals(org.opensearch.search.MultiValueMode.AVG, linearFunction.getMultiValueMode()); + } + + public void testFromProtoWithMultiValueModeUnspecified() { + NumericDecayPlacement numericPlacement = NumericDecayPlacement.newBuilder().setOrigin(100.0).setScale(50.0).build(); + + DecayPlacement decayPlacement = DecayPlacement.newBuilder().setNumericDecayPlacement(numericPlacement).build(); + + DecayFunction decayFunction = DecayFunction.newBuilder() + .putPlacement("distance", decayPlacement) + .setMultiValueMode(org.opensearch.protobufs.MultiValueMode.MULTI_VALUE_MODE_UNSPECIFIED) + .build(); + + ScoreFunctionBuilder result = LinearDecayFunctionProtoUtils.fromProto(decayFunction); + + assertThat(result, instanceOf(LinearDecayFunctionBuilder.class)); + LinearDecayFunctionBuilder linearFunction = (LinearDecayFunctionBuilder) result; + + assertEquals("distance", linearFunction.getFieldName()); + // When UNSPECIFIED, multi_value_mode should remain at default (MIN) + assertEquals(org.opensearch.search.MultiValueMode.MIN, linearFunction.getMultiValueMode()); + } + } 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 index aaabba3c3477d..1eb2086b0ac4f 100644 --- 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 @@ -31,6 +31,21 @@ public void testFromProtoWithValidRandomScoreFunction() { assertEquals(Integer.valueOf(12345), randomScoreBuilder.getSeed()); } + public void testFromProtoWithInt64Seed() { + // Create a random score function with int64 seed + RandomScoreFunctionSeed seed = RandomScoreFunctionSeed.newBuilder().setInt64(9876543210L).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()); + // Long seeds are converted to int via Long.hashCode() internally + assertEquals(Integer.valueOf(Long.hashCode(9876543210L)), randomScoreBuilder.getSeed()); + } + public void testFromProtoWithStringSeed() { // Create a random score function with string seed RandomScoreFunctionSeed seed = RandomScoreFunctionSeed.newBuilder().setString("test_seed").build(); From 05123ebbbb8888486f9bd3cbd6d693d03eac16ad Mon Sep 17 00:00:00 2001 From: Karen X Date: Fri, 5 Dec 2025 08:49:27 +0000 Subject: [PATCH 2/2] add MULTI_VALUE_MODE_SUM Signed-off-by: Karen X --- .../search/query/functionscore/DecayFunctionProtoUtils.java | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/DecayFunctionProtoUtils.java b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/DecayFunctionProtoUtils.java index 3197697105db2..f898a00f30dd8 100644 --- a/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/DecayFunctionProtoUtils.java +++ b/modules/transport-grpc/src/main/java/org/opensearch/transport/grpc/proto/request/search/query/functionscore/DecayFunctionProtoUtils.java @@ -32,6 +32,7 @@ static org.opensearch.search.MultiValueMode parseMultiValueMode(MultiValueMode m case MULTI_VALUE_MODE_AVG -> org.opensearch.search.MultiValueMode.AVG; case MULTI_VALUE_MODE_MAX -> org.opensearch.search.MultiValueMode.MAX; case MULTI_VALUE_MODE_MIN -> org.opensearch.search.MultiValueMode.MIN; + case MULTI_VALUE_MODE_SUM -> org.opensearch.search.MultiValueMode.SUM; default -> throw new IllegalArgumentException("Unsupported multi value mode: " + multiValueMode); }; }