Skip to content

Commit 9bf9c15

Browse files
authored
Support named query scores in Hit.matchedQueries() (#2098)
* Support named query scores in Hit.matchedQueries() matched_queries is now generated as Map<String, Double> instead of the generic JsonData fallback. A schema driven rule in the code generator detects the oneOf of a string array and a scalar value map and maps it to a typed map. The deserializer reads the object form as name to score and the array form as names with null values. include_named_queries_score is now sent as a query parameter rather than in the request body. The server enables the scored map rendering only from the query parameter, so sending the flag in the body returns names without scores. The request body property is ignored in the code generator so the generated client emits the query parameter. See opensearch-project/OpenSearch#22689 for the server behavior. Adds MatchedQueriesTest covering the array and object forms. Signed-off-by: Sotaro Hikita <bering1814@gmail.com> * Apply spotless formatting to Overrides.java Signed-off-by: Sotaro Hikita <bering1814@gmail.com> * Rework matched_queries as a MatchedQueries tagged union" -m "Replace the previous Map<String, Double> shape for Hit.matched_queries with a MatchedQueries tagged union that carries a Names variant (List<String>) and a Scores variant (Map<String, Double>). The Union is bound through Overrides.PropertyOverride.withMappedType and reuses UnionDeserializer, following the SourceConfig and FieldValue precedent, so no changes are needed to the object shape templates or to Field, SpecTransformer, and TypeMapper. Remove the previously added stringArrayOrMapDeserializer along with the arrayOrMapUnion flag and its wiring. Signed-off-by: Sotaro Hikita <bering1814@gmail.com> --------- Signed-off-by: Sotaro Hikita <bering1814@gmail.com>
1 parent e421982 commit 9bf9c15

7 files changed

Lines changed: 331 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
2525
- 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))
2626
- Add search over gRPC with match_all query support, SearchRequestConverter, SearchResponseConverter, and _source deserialization ([#2071](https://github.com/opensearch-project/opensearch-java/pull/2071))
2727
- Add `setAutomaticRetriesDisabled` to `ApacheHttpClient5TransportBuilder` to allow enabling automatic retries ([#2086](https://github.com/opensearch-project/opensearch-java/pull/2086))
28+
- 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<String>` and whose `scores` variant carries a `Map<String, Double>`, and by sending `include_named_queries_score` as a query parameter ([#2098](https://github.com/opensearch-project/opensearch-java/pull/2098))
2829

2930
### Fixed
3031
- Fix `unitTest` task not running the tests in the `test` source set ([#2074](https://github.com/opensearch-project/opensearch-java/pull/2074))

java-client/src/generated/java/org/opensearch/client/opensearch/core/SearchRequest.java

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,8 @@ public final Boolean ignoreUnavailable() {
571571
}
572572

573573
/**
574-
* Whether to return scores with named queries. Default is false.
574+
* Indicates whether <code>hit.matched_queries</code> should be rendered as a map that includes the name of the matched query associated
575+
* with its score (true) or as an array containing the name of the matched queries (false)
575576
* <p>
576577
* API name: {@code include_named_queries_score}
577578
* </p>
@@ -1055,11 +1056,6 @@ protected void serializeInternal(JsonGenerator generator, JsonpMapper mapper) {
10551056
this.highlight.serialize(generator, mapper);
10561057
}
10571058

1058-
if (this.includeNamedQueriesScore != null) {
1059-
generator.writeKey("include_named_queries_score");
1060-
generator.write(this.includeNamedQueriesScore);
1061-
}
1062-
10631059
if (ApiTypeHelper.isDefined(this.indicesBoost)) {
10641060
generator.writeKey("indices_boost");
10651061
generator.writeStartArray();
@@ -1954,7 +1950,8 @@ public final Builder ignoreUnavailable(@Nullable Boolean value) {
19541950
}
19551951

19561952
/**
1957-
* Whether to return scores with named queries. Default is false.
1953+
* Indicates whether <code>hit.matched_queries</code> should be rendered as a map that includes the name of the matched query
1954+
* associated with its score (true) or as an array containing the name of the matched queries (false)
19581955
* <p>
19591956
* API name: {@code include_named_queries_score}
19601957
* </p>
@@ -2728,7 +2725,6 @@ protected static void setupSearchRequestDeserializer(ObjectDeserializer<SearchRe
27282725
op.add(Builder::fields, JsonpDeserializer.arrayDeserializer(FieldAndFormat._DESERIALIZER), "fields");
27292726
op.add(Builder::from, JsonpDeserializer.integerDeserializer(), "from");
27302727
op.add(Builder::highlight, Highlight._DESERIALIZER, "highlight");
2731-
op.add(Builder::includeNamedQueriesScore, JsonpDeserializer.booleanDeserializer(), "include_named_queries_score");
27322728
op.add(
27332729
Builder::indicesBoost,
27342730
JsonpDeserializer.arrayDeserializer(JsonpDeserializer.stringMapDeserializer(JsonpDeserializer.floatDeserializer())),
@@ -2801,6 +2797,9 @@ protected void applyQueryParameters(@Nonnull Map<String, String> params) {
28012797
if (this.ignoreUnavailable != null) {
28022798
params.put("ignore_unavailable", String.valueOf(this.ignoreUnavailable));
28032799
}
2800+
if (this.includeNamedQueriesScore != null) {
2801+
params.put("include_named_queries_score", String.valueOf(this.includeNamedQueriesScore));
2802+
}
28042803
if (this.lenient != null) {
28052804
params.put("lenient", String.valueOf(this.lenient));
28062805
}

java-client/src/generated/java/org/opensearch/client/opensearch/core/search/Hit.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public class Hit<TDocument> implements PlainJsonSerializable, ToCopyableBuilder<
9191
private final Map<String, InnerHitsResult> innerHits;
9292

9393
@Nullable
94-
private final JsonData matchedQueries;
94+
private final MatchedQueries matchedQueries;
9595

9696
@Nonnull
9797
private final Map<String, JsonData> metaFields;
@@ -231,7 +231,7 @@ public final Map<String, InnerHitsResult> innerHits() {
231231
* </p>
232232
*/
233233
@Nullable
234-
public final JsonData matchedQueries() {
234+
public final MatchedQueries matchedQueries() {
235235
return this.matchedQueries;
236236
}
237237

@@ -516,7 +516,7 @@ public static class Builder<TDocument> extends ObjectBuilderBase implements Copy
516516
@Nullable
517517
private Map<String, InnerHitsResult> innerHits;
518518
@Nullable
519-
private JsonData matchedQueries;
519+
private MatchedQueries matchedQueries;
520520
@Nullable
521521
private Map<String, JsonData> metaFields;
522522
@Nullable
@@ -783,11 +783,23 @@ public final Builder<TDocument> innerHits(String key, Function<InnerHitsResult.B
783783
* </p>
784784
*/
785785
@Nonnull
786-
public final Builder<TDocument> matchedQueries(@Nullable JsonData value) {
786+
public final Builder<TDocument> matchedQueries(@Nullable MatchedQueries value) {
787787
this.matchedQueries = value;
788788
return this;
789789
}
790790

791+
/**
792+
* The names of queries that matched the document. When <code>include_named_queries_score</code> is false (default), returns an
793+
* array of query names. When true, returns an object mapping query names to their scores.
794+
* <p>
795+
* API name: {@code matched_queries}
796+
* </p>
797+
*/
798+
@Nonnull
799+
public final Builder<TDocument> matchedQueries(Function<MatchedQueries.Builder, ObjectBuilder<MatchedQueries>> fn) {
800+
return matchedQueries(fn.apply(new MatchedQueries.Builder()).build());
801+
}
802+
791803
/**
792804
* Contains metadata values for the documents.
793805
*
@@ -996,7 +1008,7 @@ protected static <TDocument> void setupHitDeserializer(
9961008
);
9971009
op.add(Builder::index, JsonpDeserializer.stringDeserializer(), "_index");
9981010
op.add(Builder::innerHits, JsonpDeserializer.stringMapDeserializer(InnerHitsResult._DESERIALIZER), "inner_hits");
999-
op.add(Builder::matchedQueries, JsonData._DESERIALIZER, "matched_queries");
1011+
op.add(Builder::matchedQueries, MatchedQueries._DESERIALIZER, "matched_queries");
10001012
op.add(Builder::nested, NestedIdentity._DESERIALIZER, "_nested");
10011013
op.add(Builder::node, JsonpDeserializer.stringDeserializer(), "_node");
10021014
op.add(Builder::primaryTerm, JsonpDeserializer.longDeserializer(), "_primary_term");
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.client.opensearch.core.search;
10+
11+
import jakarta.json.stream.JsonGenerator;
12+
import java.util.List;
13+
import java.util.Map;
14+
import java.util.Objects;
15+
import java.util.function.Function;
16+
import javax.annotation.Nonnull;
17+
import org.opensearch.client.json.JsonpDeserializable;
18+
import org.opensearch.client.json.JsonpDeserializer;
19+
import org.opensearch.client.json.JsonpMapper;
20+
import org.opensearch.client.json.PlainJsonSerializable;
21+
import org.opensearch.client.json.UnionDeserializer;
22+
import org.opensearch.client.util.ApiTypeHelper;
23+
import org.opensearch.client.util.ObjectBuilder;
24+
import org.opensearch.client.util.ObjectBuilderBase;
25+
import org.opensearch.client.util.TaggedUnion;
26+
import org.opensearch.client.util.TaggedUnionUtils;
27+
28+
// typedef: core.search.MatchedQueries
29+
30+
/**
31+
* The names of queries that matched a hit, with optional per-query scores.
32+
*
33+
* <p>
34+
* The server returns matched_queries in one of two shapes, and never both: an array of query names when
35+
* {@code include_named_queries_score} is not set, or an object mapping query names to scores when
36+
* {@code include_named_queries_score} is true.
37+
* </p>
38+
*/
39+
@JsonpDeserializable
40+
public class MatchedQueries implements TaggedUnion<MatchedQueries.Kind, Object>, PlainJsonSerializable {
41+
42+
/**
43+
* {@link MatchedQueries} variant kinds.
44+
*/
45+
public enum Kind {
46+
Names,
47+
Scores
48+
}
49+
50+
private final Kind _kind;
51+
private final Object _value;
52+
53+
@Override
54+
public final Kind _kind() {
55+
return _kind;
56+
}
57+
58+
@Override
59+
public final Object _get() {
60+
return _value;
61+
}
62+
63+
private MatchedQueries(Kind kind, Object value) {
64+
this._kind = kind;
65+
this._value = value;
66+
}
67+
68+
private MatchedQueries(Builder builder) {
69+
this._kind = ApiTypeHelper.requireNonNull(builder._kind, builder, "<variant kind>");
70+
this._value = ApiTypeHelper.requireNonNull(builder._value, builder, "<variant value>");
71+
}
72+
73+
public static MatchedQueries of(Function<MatchedQueries.Builder, ObjectBuilder<MatchedQueries>> fn) {
74+
return fn.apply(new Builder()).build();
75+
}
76+
77+
public static MatchedQueries ofNames(List<String> names) {
78+
return new MatchedQueries(Kind.Names, ApiTypeHelper.requireNonNull(names, MatchedQueries.class, "names"));
79+
}
80+
81+
public static MatchedQueries ofScores(Map<String, Double> scores) {
82+
return new MatchedQueries(Kind.Scores, ApiTypeHelper.requireNonNull(scores, MatchedQueries.class, "scores"));
83+
}
84+
85+
/**
86+
* Is this variant instance of kind {@code names}?
87+
*/
88+
public boolean isNames() {
89+
return _kind == Kind.Names;
90+
}
91+
92+
/**
93+
* Get the {@code names} variant value: the names of the queries that matched the hit, returned by the server when
94+
* {@code include_named_queries_score} is not set.
95+
*
96+
* @throws IllegalStateException if the current variant is not the {@code names} kind.
97+
*/
98+
public List<String> names() {
99+
return TaggedUnionUtils.get(this, Kind.Names);
100+
}
101+
102+
/**
103+
* Is this variant instance of kind {@code scores}?
104+
*/
105+
public boolean isScores() {
106+
return _kind == Kind.Scores;
107+
}
108+
109+
/**
110+
* Get the {@code scores} variant value: a map from matched query name to score, returned by the server when
111+
* {@code include_named_queries_score} is true.
112+
*
113+
* @throws IllegalStateException if the current variant is not the {@code scores} kind.
114+
*/
115+
public Map<String, Double> scores() {
116+
return TaggedUnionUtils.get(this, Kind.Scores);
117+
}
118+
119+
@Override
120+
@SuppressWarnings("unchecked")
121+
public void serialize(JsonGenerator generator, JsonpMapper mapper) {
122+
switch (_kind) {
123+
case Names:
124+
generator.writeStartArray();
125+
for (String name : (List<String>) _value) {
126+
generator.write(name);
127+
}
128+
generator.writeEnd();
129+
break;
130+
case Scores:
131+
generator.writeStartObject();
132+
for (Map.Entry<String, Double> entry : ((Map<String, Double>) _value).entrySet()) {
133+
generator.writeKey(entry.getKey());
134+
generator.write(entry.getValue());
135+
}
136+
generator.writeEnd();
137+
break;
138+
}
139+
}
140+
141+
@Nonnull
142+
public Builder toBuilder() {
143+
return new Builder(this);
144+
}
145+
146+
@Nonnull
147+
public static Builder builder() {
148+
return new Builder();
149+
}
150+
151+
public static class Builder extends ObjectBuilderBase implements ObjectBuilder<MatchedQueries> {
152+
private Kind _kind;
153+
private Object _value;
154+
155+
public Builder() {}
156+
157+
private Builder(MatchedQueries o) {
158+
this._kind = o._kind;
159+
this._value = o._value;
160+
}
161+
162+
public ObjectBuilder<MatchedQueries> names(List<String> v) {
163+
this._kind = Kind.Names;
164+
this._value = v;
165+
return this;
166+
}
167+
168+
public ObjectBuilder<MatchedQueries> scores(Map<String, Double> v) {
169+
this._kind = Kind.Scores;
170+
this._value = v;
171+
return this;
172+
}
173+
174+
@Override
175+
public MatchedQueries build() {
176+
_checkSingleUse();
177+
return new MatchedQueries(this);
178+
}
179+
}
180+
181+
private static JsonpDeserializer<MatchedQueries> buildMatchedQueriesDeserializer() {
182+
return new UnionDeserializer.Builder<MatchedQueries, Kind, Object>(MatchedQueries::new, false).addMember(
183+
Kind.Names,
184+
JsonpDeserializer.arrayDeserializer(JsonpDeserializer.stringDeserializer())
185+
).addMember(Kind.Scores, JsonpDeserializer.stringMapDeserializer(JsonpDeserializer.doubleDeserializer())).build();
186+
}
187+
188+
public static final JsonpDeserializer<MatchedQueries> _DESERIALIZER = JsonpDeserializer.lazy(
189+
MatchedQueries::buildMatchedQueriesDeserializer
190+
);
191+
192+
@Override
193+
public int hashCode() {
194+
int result = 17;
195+
result = 31 * result + Objects.hashCode(this._kind);
196+
result = 31 * result + Objects.hashCode(this._value);
197+
return result;
198+
}
199+
200+
@Override
201+
public boolean equals(Object o) {
202+
if (this == o) return true;
203+
if (o == null || this.getClass() != o.getClass()) return false;
204+
MatchedQueries other = (MatchedQueries) o;
205+
return Objects.equals(this._kind, other._kind) && Objects.equals(this._value, other._value);
206+
}
207+
}

0 commit comments

Comments
 (0)