diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0f10b3c980..9ef4423196 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- Add transparent gRPC transport with HybridTransport (bulk over gRPC, REST fallback), translation layer, TLS, basic auth, AWS SigV4, and JWT support ([#2062](https://github.com/opensearch-project/opensearch-java/pull/2062))
- Add search over gRPC with match_all query support, SearchRequestConverter, SearchResponseConverter, and _source deserialization ([#2071](https://github.com/opensearch-project/opensearch-java/pull/2071))
- Add `setAutomaticRetriesDisabled` to `ApacheHttpClient5TransportBuilder` to allow enabling automatic retries ([#2086](https://github.com/opensearch-project/opensearch-java/pull/2086))
+- Added typed support for scored named queries by mapping the polymorphic `Hit.matched_queries` field to a new `MatchedQueries` tagged union, whose `names` variant carries a `List` and whose `scores` variant carries a `Map`, and by sending `include_named_queries_score` as a query parameter ([#2098](https://github.com/opensearch-project/opensearch-java/pull/2098))
### Fixed
- Fix `unitTest` task not running the tests in the `test` source set ([#2074](https://github.com/opensearch-project/opensearch-java/pull/2074))
diff --git a/java-client/src/generated/java/org/opensearch/client/opensearch/core/SearchRequest.java b/java-client/src/generated/java/org/opensearch/client/opensearch/core/SearchRequest.java
index cdcf7777df..12df02a1df 100644
--- a/java-client/src/generated/java/org/opensearch/client/opensearch/core/SearchRequest.java
+++ b/java-client/src/generated/java/org/opensearch/client/opensearch/core/SearchRequest.java
@@ -571,7 +571,8 @@ public final Boolean ignoreUnavailable() {
}
/**
- * Whether to return scores with named queries. Default is false.
+ * Indicates whether hit.matched_queries should be rendered as a map that includes the name of the matched query associated
+ * with its score (true) or as an array containing the name of the matched queries (false)
*
* API name: {@code include_named_queries_score}
*
@@ -1055,11 +1056,6 @@ protected void serializeInternal(JsonGenerator generator, JsonpMapper mapper) {
this.highlight.serialize(generator, mapper);
}
- if (this.includeNamedQueriesScore != null) {
- generator.writeKey("include_named_queries_score");
- generator.write(this.includeNamedQueriesScore);
- }
-
if (ApiTypeHelper.isDefined(this.indicesBoost)) {
generator.writeKey("indices_boost");
generator.writeStartArray();
@@ -1954,7 +1950,8 @@ public final Builder ignoreUnavailable(@Nullable Boolean value) {
}
/**
- * Whether to return scores with named queries. Default is false.
+ * Indicates whether hit.matched_queries should be rendered as a map that includes the name of the matched query
+ * associated with its score (true) or as an array containing the name of the matched queries (false)
*
* API name: {@code include_named_queries_score}
*
@@ -2728,7 +2725,6 @@ protected static void setupSearchRequestDeserializer(ObjectDeserializer params) {
if (this.ignoreUnavailable != null) {
params.put("ignore_unavailable", String.valueOf(this.ignoreUnavailable));
}
+ if (this.includeNamedQueriesScore != null) {
+ params.put("include_named_queries_score", String.valueOf(this.includeNamedQueriesScore));
+ }
if (this.lenient != null) {
params.put("lenient", String.valueOf(this.lenient));
}
diff --git a/java-client/src/generated/java/org/opensearch/client/opensearch/core/search/Hit.java b/java-client/src/generated/java/org/opensearch/client/opensearch/core/search/Hit.java
index 9914352a3b..308f163ef5 100644
--- a/java-client/src/generated/java/org/opensearch/client/opensearch/core/search/Hit.java
+++ b/java-client/src/generated/java/org/opensearch/client/opensearch/core/search/Hit.java
@@ -91,7 +91,7 @@ public class Hit implements PlainJsonSerializable, ToCopyableBuilder<
private final Map innerHits;
@Nullable
- private final JsonData matchedQueries;
+ private final MatchedQueries matchedQueries;
@Nonnull
private final Map metaFields;
@@ -231,7 +231,7 @@ public final Map innerHits() {
*
*/
@Nullable
- public final JsonData matchedQueries() {
+ public final MatchedQueries matchedQueries() {
return this.matchedQueries;
}
@@ -516,7 +516,7 @@ public static class Builder extends ObjectBuilderBase implements Copy
@Nullable
private Map innerHits;
@Nullable
- private JsonData matchedQueries;
+ private MatchedQueries matchedQueries;
@Nullable
private Map metaFields;
@Nullable
@@ -783,11 +783,23 @@ public final Builder innerHits(String key, Function
*/
@Nonnull
- public final Builder matchedQueries(@Nullable JsonData value) {
+ public final Builder matchedQueries(@Nullable MatchedQueries value) {
this.matchedQueries = value;
return this;
}
+ /**
+ * The names of queries that matched the document. When include_named_queries_score is false (default), returns an
+ * array of query names. When true, returns an object mapping query names to their scores.
+ *
+ * API name: {@code matched_queries}
+ *
+ */
+ @Nonnull
+ public final Builder matchedQueries(Function> fn) {
+ return matchedQueries(fn.apply(new MatchedQueries.Builder()).build());
+ }
+
/**
* Contains metadata values for the documents.
*
@@ -996,7 +1008,7 @@ protected static void setupHitDeserializer(
);
op.add(Builder::index, JsonpDeserializer.stringDeserializer(), "_index");
op.add(Builder::innerHits, JsonpDeserializer.stringMapDeserializer(InnerHitsResult._DESERIALIZER), "inner_hits");
- op.add(Builder::matchedQueries, JsonData._DESERIALIZER, "matched_queries");
+ op.add(Builder::matchedQueries, MatchedQueries._DESERIALIZER, "matched_queries");
op.add(Builder::nested, NestedIdentity._DESERIALIZER, "_nested");
op.add(Builder::node, JsonpDeserializer.stringDeserializer(), "_node");
op.add(Builder::primaryTerm, JsonpDeserializer.longDeserializer(), "_primary_term");
diff --git a/java-client/src/main/java/org/opensearch/client/opensearch/core/search/MatchedQueries.java b/java-client/src/main/java/org/opensearch/client/opensearch/core/search/MatchedQueries.java
new file mode 100644
index 0000000000..aa46dff338
--- /dev/null
+++ b/java-client/src/main/java/org/opensearch/client/opensearch/core/search/MatchedQueries.java
@@ -0,0 +1,207 @@
+/*
+ * 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.client.opensearch.core.search;
+
+import jakarta.json.stream.JsonGenerator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import javax.annotation.Nonnull;
+import org.opensearch.client.json.JsonpDeserializable;
+import org.opensearch.client.json.JsonpDeserializer;
+import org.opensearch.client.json.JsonpMapper;
+import org.opensearch.client.json.PlainJsonSerializable;
+import org.opensearch.client.json.UnionDeserializer;
+import org.opensearch.client.util.ApiTypeHelper;
+import org.opensearch.client.util.ObjectBuilder;
+import org.opensearch.client.util.ObjectBuilderBase;
+import org.opensearch.client.util.TaggedUnion;
+import org.opensearch.client.util.TaggedUnionUtils;
+
+// typedef: core.search.MatchedQueries
+
+/**
+ * The names of queries that matched a hit, with optional per-query scores.
+ *
+ *
+ * The server returns matched_queries in one of two shapes, and never both: an array of query names when
+ * {@code include_named_queries_score} is not set, or an object mapping query names to scores when
+ * {@code include_named_queries_score} is true.
+ *
+ */
+@JsonpDeserializable
+public class MatchedQueries implements TaggedUnion, PlainJsonSerializable {
+
+ /**
+ * {@link MatchedQueries} variant kinds.
+ */
+ public enum Kind {
+ Names,
+ Scores
+ }
+
+ private final Kind _kind;
+ private final Object _value;
+
+ @Override
+ public final Kind _kind() {
+ return _kind;
+ }
+
+ @Override
+ public final Object _get() {
+ return _value;
+ }
+
+ private MatchedQueries(Kind kind, Object value) {
+ this._kind = kind;
+ this._value = value;
+ }
+
+ private MatchedQueries(Builder builder) {
+ this._kind = ApiTypeHelper.requireNonNull(builder._kind, builder, "");
+ this._value = ApiTypeHelper.requireNonNull(builder._value, builder, "");
+ }
+
+ public static MatchedQueries of(Function> fn) {
+ return fn.apply(new Builder()).build();
+ }
+
+ public static MatchedQueries ofNames(List names) {
+ return new MatchedQueries(Kind.Names, ApiTypeHelper.requireNonNull(names, MatchedQueries.class, "names"));
+ }
+
+ public static MatchedQueries ofScores(Map scores) {
+ return new MatchedQueries(Kind.Scores, ApiTypeHelper.requireNonNull(scores, MatchedQueries.class, "scores"));
+ }
+
+ /**
+ * Is this variant instance of kind {@code names}?
+ */
+ public boolean isNames() {
+ return _kind == Kind.Names;
+ }
+
+ /**
+ * Get the {@code names} variant value: the names of the queries that matched the hit, returned by the server when
+ * {@code include_named_queries_score} is not set.
+ *
+ * @throws IllegalStateException if the current variant is not the {@code names} kind.
+ */
+ public List names() {
+ return TaggedUnionUtils.get(this, Kind.Names);
+ }
+
+ /**
+ * Is this variant instance of kind {@code scores}?
+ */
+ public boolean isScores() {
+ return _kind == Kind.Scores;
+ }
+
+ /**
+ * Get the {@code scores} variant value: a map from matched query name to score, returned by the server when
+ * {@code include_named_queries_score} is true.
+ *
+ * @throws IllegalStateException if the current variant is not the {@code scores} kind.
+ */
+ public Map scores() {
+ return TaggedUnionUtils.get(this, Kind.Scores);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void serialize(JsonGenerator generator, JsonpMapper mapper) {
+ switch (_kind) {
+ case Names:
+ generator.writeStartArray();
+ for (String name : (List) _value) {
+ generator.write(name);
+ }
+ generator.writeEnd();
+ break;
+ case Scores:
+ generator.writeStartObject();
+ for (Map.Entry entry : ((Map) _value).entrySet()) {
+ generator.writeKey(entry.getKey());
+ generator.write(entry.getValue());
+ }
+ generator.writeEnd();
+ break;
+ }
+ }
+
+ @Nonnull
+ public Builder toBuilder() {
+ return new Builder(this);
+ }
+
+ @Nonnull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder extends ObjectBuilderBase implements ObjectBuilder {
+ private Kind _kind;
+ private Object _value;
+
+ public Builder() {}
+
+ private Builder(MatchedQueries o) {
+ this._kind = o._kind;
+ this._value = o._value;
+ }
+
+ public ObjectBuilder names(List v) {
+ this._kind = Kind.Names;
+ this._value = v;
+ return this;
+ }
+
+ public ObjectBuilder scores(Map v) {
+ this._kind = Kind.Scores;
+ this._value = v;
+ return this;
+ }
+
+ @Override
+ public MatchedQueries build() {
+ _checkSingleUse();
+ return new MatchedQueries(this);
+ }
+ }
+
+ private static JsonpDeserializer buildMatchedQueriesDeserializer() {
+ return new UnionDeserializer.Builder(MatchedQueries::new, false).addMember(
+ Kind.Names,
+ JsonpDeserializer.arrayDeserializer(JsonpDeserializer.stringDeserializer())
+ ).addMember(Kind.Scores, JsonpDeserializer.stringMapDeserializer(JsonpDeserializer.doubleDeserializer())).build();
+ }
+
+ public static final JsonpDeserializer _DESERIALIZER = JsonpDeserializer.lazy(
+ MatchedQueries::buildMatchedQueriesDeserializer
+ );
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + Objects.hashCode(this._kind);
+ result = 31 * result + Objects.hashCode(this._value);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || this.getClass() != o.getClass()) return false;
+ MatchedQueries other = (MatchedQueries) o;
+ return Objects.equals(this._kind, other._kind) && Objects.equals(this._value, other._value);
+ }
+}
diff --git a/java-client/src/test/java/org/opensearch/client/opensearch/core/search/MatchedQueriesTest.java b/java-client/src/test/java/org/opensearch/client/opensearch/core/search/MatchedQueriesTest.java
new file mode 100644
index 0000000000..70b0043e4f
--- /dev/null
+++ b/java-client/src/test/java/org/opensearch/client/opensearch/core/search/MatchedQueriesTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.client.opensearch.core.search;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.opensearch.client.opensearch.model.ModelTestCase.toJson;
+
+import jakarta.json.stream.JsonParser;
+import java.io.StringReader;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.junit.Test;
+import org.opensearch.client.json.JsonData;
+import org.opensearch.client.json.JsonpDeserializer;
+import org.opensearch.client.json.JsonpMapper;
+import org.opensearch.client.json.jsonb.JsonbJsonpMapper;
+
+public class MatchedQueriesTest {
+ private final JsonpMapper mapper = new JsonbJsonpMapper();
+ private final JsonpDeserializer> hitDeserializer = Hit.createHitDeserializer(JsonData._DESERIALIZER);
+
+ private Hit parseHit(String json) {
+ JsonParser parser = mapper.jsonProvider().createParser(new StringReader(json));
+ return hitDeserializer.deserialize(parser, mapper);
+ }
+
+ @Test
+ public void deserializesArrayFormAsNamesVariant() {
+ Hit hit = parseHit("{\"_index\":\"i\",\"matched_queries\":[\"a\",\"b\"]}");
+ MatchedQueries mq = hit.matchedQueries();
+ assertTrue(mq.isNames());
+ assertFalse(mq.isScores());
+ assertEquals(Arrays.asList("a", "b"), mq.names());
+ }
+
+ @Test
+ public void deserializesObjectFormAsScoresVariant() {
+ Hit hit = parseHit("{\"_index\":\"i\",\"matched_queries\":{\"a\":1.5,\"b\":2.5}}");
+ MatchedQueries mq = hit.matchedQueries();
+ assertTrue(mq.isScores());
+ assertFalse(mq.isNames());
+ Map scores = mq.scores();
+ assertEquals(Double.valueOf(1.5), scores.get("a"));
+ assertEquals(Double.valueOf(2.5), scores.get("b"));
+ }
+
+ @Test
+ public void returnsNullWhenAbsent() {
+ Hit hit = parseHit("{\"_index\":\"i\"}");
+ assertNull(hit.matchedQueries());
+ }
+
+ @Test
+ public void serializesNamesVariantAsArray() {
+ Hit hit = parseHit("{\"_index\":\"i\",\"matched_queries\":[\"a\",\"b\"]}");
+ assertTrue(toJson(hit, mapper).contains("\"matched_queries\":[\"a\",\"b\"]"));
+ }
+
+ @Test
+ public void serializesScoresVariantAsObject() {
+ Map scores = new LinkedHashMap<>();
+ scores.put("a", 1.5);
+ scores.put("b", 2.5);
+ Hit hit = new Hit.Builder().index("i").matchedQueries(MatchedQueries.ofScores(scores)).build();
+ assertTrue(toJson(hit, mapper).contains("\"matched_queries\":{\"a\":1.5,\"b\":2.5}"));
+ }
+}
diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/types/Types.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/types/Types.java
index 75390a355a..3de97b2579 100644
--- a/java-codegen/src/main/java/org/opensearch/client/codegen/model/types/Types.java
+++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/types/Types.java
@@ -230,6 +230,16 @@ public static final class Cat {
public static final String PACKAGE = OpenSearch.PACKAGE + ".cat";
public static final Type CatRequestBase = type(PACKAGE, "CatRequestBase");
}
+
+ public static final class Core {
+ public static final String PACKAGE = OpenSearch.PACKAGE + ".core";
+
+ public static final class Search {
+ public static final String PACKAGE = Core.PACKAGE + ".search";
+
+ public static final Type MatchedQueries = type(PACKAGE, "MatchedQueries");
+ }
+ }
}
public static final class Transport {
diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/transformer/overrides/Overrides.java b/java-codegen/src/main/java/org/opensearch/client/codegen/transformer/overrides/Overrides.java
index b41842eda3..8a2fac1cc0 100644
--- a/java-codegen/src/main/java/org/opensearch/client/codegen/transformer/overrides/Overrides.java
+++ b/java-codegen/src/main/java/org/opensearch/client/codegen/transformer/overrides/Overrides.java
@@ -157,6 +157,12 @@ private static JsonPointer schema(String namespace, String name) {
.with(schema("_core.search", "Rescore"), so -> so.withShouldGenerate(ShouldGenerate.Always))
.with(schema("_core.search", "Suggester"), so -> so.withShouldGenerate(ShouldGenerate.Always))
.with(schema("_core.search", "TermSuggestOption"), so -> so.withShouldGenerate(ShouldGenerate.Always))
+ .with(
+ schema("_core.search", "Hit"),
+ so -> so.withProperties(
+ p -> p.with("matched_queries", po -> po.withMappedType(Types.Client.OpenSearch.Core.Search.MatchedQueries))
+ )
+ )
.with(
schema("_core.search", "HitsMetadata"),
so -> so.withProperties(
@@ -206,7 +212,12 @@ private static JsonPointer schema(String namespace, String name) {
.with(
requestBodySchema("search"),
so -> so.withProperties(
- p -> p.with("aggregations", po -> po.withAliases(Set.of("aggs"))).with("aggs", po -> po.withIgnore(true))
+ p -> p.with("aggregations", po -> po.withAliases(Set.of("aggs")))
+ .with("aggs", po -> po.withIgnore(true))
+ // The server ignores include_named_queries_score in the request body and renders named query scores only when
+ // it is sent as a query parameter. See https://github.com/opensearch-project/OpenSearch/issues/22689. Remove
+ // this once the server honors the body value.
+ .with("include_named_queries_score", po -> po.withIgnore(true))
)
)
)