Skip to content

Commit 3015d5a

Browse files
committed
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>
1 parent 7dbb28e commit 3015d5a

12 files changed

Lines changed: 262 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
1616
- Bump `gradle-wrapper` from 9.3.1 to 9.4.1 ([#1930](https://github.com/opensearch-project/opensearch-java/pull/1930), [#1934](https://github.com/opensearch-project/opensearch-java/pull/1934))
1717

1818
### Added
19+
- Added typed support for scored named queries by mapping the polymorphic `Hit.matched_queries` field to a `Map<String, Double>`, where the array form yields a map with `null` values and the object form yields query name to score entries ([#XXXX](https://github.com/opensearch-project/opensearch-java/pull/XXXX))
1920

2021
### Fixed
2122

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: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ public class Hit<TDocument> implements PlainJsonSerializable, ToCopyableBuilder<
9090
@Nonnull
9191
private final Map<String, InnerHitsResult> innerHits;
9292

93-
@Nullable
94-
private final JsonData matchedQueries;
93+
@Nonnull
94+
private final Map<String, Double> matchedQueries;
9595

9696
@Nonnull
9797
private final Map<String, JsonData> metaFields;
@@ -140,7 +140,7 @@ private Hit(Builder<TDocument> builder) {
140140
this.ignoredFieldValues = ApiTypeHelper.unmodifiable(builder.ignoredFieldValues);
141141
this.index = builder.index;
142142
this.innerHits = ApiTypeHelper.unmodifiable(builder.innerHits);
143-
this.matchedQueries = builder.matchedQueries;
143+
this.matchedQueries = ApiTypeHelper.unmodifiable(builder.matchedQueries);
144144
this.metaFields = ApiTypeHelper.unmodifiable(builder.metaFields);
145145
this.nested = builder.nested;
146146
this.node = builder.node;
@@ -230,8 +230,8 @@ public final Map<String, InnerHitsResult> innerHits() {
230230
* API name: {@code matched_queries}
231231
* </p>
232232
*/
233-
@Nullable
234-
public final JsonData matchedQueries() {
233+
@Nonnull
234+
public final Map<String, Double> matchedQueries() {
235235
return this.matchedQueries;
236236
}
237237

@@ -422,9 +422,22 @@ protected void serializeInternal(JsonGenerator generator, JsonpMapper mapper) {
422422
generator.writeEnd();
423423
}
424424

425-
if (this.matchedQueries != null) {
425+
if (ApiTypeHelper.isDefined(this.matchedQueries)) {
426426
generator.writeKey("matched_queries");
427-
this.matchedQueries.serialize(generator, mapper);
427+
if (this.matchedQueries.values().stream().allMatch(Objects::isNull)) {
428+
generator.writeStartArray();
429+
for (String item : this.matchedQueries.keySet()) {
430+
generator.write(item);
431+
}
432+
generator.writeEnd();
433+
} else {
434+
generator.writeStartObject();
435+
for (Map.Entry<String, Double> item0 : this.matchedQueries.entrySet()) {
436+
generator.writeKey(item0.getKey());
437+
generator.write(item0.getValue());
438+
}
439+
generator.writeEnd();
440+
}
428441
}
429442

430443
if (this.nested != null) {
@@ -516,7 +529,7 @@ public static class Builder<TDocument> extends ObjectBuilderBase implements Copy
516529
@Nullable
517530
private Map<String, InnerHitsResult> innerHits;
518531
@Nullable
519-
private JsonData matchedQueries;
532+
private Map<String, Double> matchedQueries;
520533
@Nullable
521534
private Map<String, JsonData> metaFields;
522535
@Nullable
@@ -553,7 +566,7 @@ private Builder(Hit<TDocument> o) {
553566
this.ignoredFieldValues = _mapCopy(o.ignoredFieldValues);
554567
this.index = o.index;
555568
this.innerHits = _mapCopy(o.innerHits);
556-
this.matchedQueries = o.matchedQueries;
569+
this.matchedQueries = _mapCopy(o.matchedQueries);
557570
this.metaFields = _mapCopy(o.metaFields);
558571
this.nested = o.nested;
559572
this.node = o.node;
@@ -577,7 +590,7 @@ private Builder(Builder<TDocument> o) {
577590
this.ignoredFieldValues = _mapCopy(o.ignoredFieldValues);
578591
this.index = o.index;
579592
this.innerHits = _mapCopy(o.innerHits);
580-
this.matchedQueries = o.matchedQueries;
593+
this.matchedQueries = _mapCopy(o.matchedQueries);
581594
this.metaFields = _mapCopy(o.metaFields);
582595
this.nested = o.nested;
583596
this.node = o.node;
@@ -781,10 +794,31 @@ public final Builder<TDocument> innerHits(String key, Function<InnerHitsResult.B
781794
* <p>
782795
* API name: {@code matched_queries}
783796
* </p>
797+
*
798+
* <p>
799+
* Adds all elements of <code>map</code> to <code>matchedQueries</code>.
800+
* </p>
784801
*/
785802
@Nonnull
786-
public final Builder<TDocument> matchedQueries(@Nullable JsonData value) {
787-
this.matchedQueries = value;
803+
public final Builder<TDocument> matchedQueries(Map<String, Double> map) {
804+
this.matchedQueries = _mapPutAll(this.matchedQueries, map);
805+
return this;
806+
}
807+
808+
/**
809+
* The names of queries that matched the document. When <code>include_named_queries_score</code> is false (default), returns an
810+
* array of query names. When true, returns an object mapping query names to their scores.
811+
* <p>
812+
* API name: {@code matched_queries}
813+
* </p>
814+
*
815+
* <p>
816+
* Adds an entry to <code>matchedQueries</code>.
817+
* </p>
818+
*/
819+
@Nonnull
820+
public final Builder<TDocument> matchedQueries(String key, Double value) {
821+
this.matchedQueries = _mapPut(this.matchedQueries, key, value);
788822
return this;
789823
}
790824

@@ -996,7 +1030,11 @@ protected static <TDocument> void setupHitDeserializer(
9961030
);
9971031
op.add(Builder::index, JsonpDeserializer.stringDeserializer(), "_index");
9981032
op.add(Builder::innerHits, JsonpDeserializer.stringMapDeserializer(InnerHitsResult._DESERIALIZER), "inner_hits");
999-
op.add(Builder::matchedQueries, JsonData._DESERIALIZER, "matched_queries");
1033+
op.add(
1034+
Builder::matchedQueries,
1035+
JsonpDeserializer.stringArrayOrMapDeserializer(JsonpDeserializer.doubleDeserializer()),
1036+
"matched_queries"
1037+
);
10001038
op.add(Builder::nested, NestedIdentity._DESERIALIZER, "_nested");
10011039
op.add(Builder::node, JsonpDeserializer.stringDeserializer(), "_node");
10021040
op.add(Builder::primaryTerm, JsonpDeserializer.longDeserializer(), "_primary_term");

java-client/src/main/java/org/opensearch/client/json/JsonpDeserializer.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,15 @@ static <T> JsonpDeserializer<Map<String, T>> stringMapDeserializer(JsonpDeserial
227227
return new JsonpDeserializerBase.StringMapDeserializer<T>(itemDeserializer);
228228
}
229229

230+
/**
231+
* Deserializer for a value that is serialized either as an array of string keys or as an object mapping those keys
232+
* to values. The array form is deserialized into a map whose values are {@code null}; the object form is
233+
* deserialized into a map of keys to values.
234+
*/
235+
static <T> JsonpDeserializer<Map<String, T>> stringArrayOrMapDeserializer(JsonpDeserializer<T> itemDeserializer) {
236+
return new JsonpDeserializerBase.StringArrayOrMapDeserializer<T>(itemDeserializer);
237+
}
238+
230239
static <K extends JsonEnum, V> JsonpDeserializer<Map<K, V>> enumMapDeserializer(
231240
JsonpDeserializer<K> keyDeserializer,
232241
JsonpDeserializer<V> valueDeserializer

java-client/src/main/java/org/opensearch/client/json/JsonpDeserializerBase.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import java.util.Collections;
4242
import java.util.EnumSet;
4343
import java.util.HashMap;
44+
import java.util.LinkedHashMap;
4445
import java.util.List;
4546
import java.util.Map;
4647

@@ -373,6 +374,33 @@ public Map<String, T> deserialize(JsonParser parser, JsonpMapper mapper, Event e
373374
}
374375
}
375376

377+
static class StringArrayOrMapDeserializer<T> extends JsonpDeserializerBase<Map<String, T>> {
378+
private final JsonpDeserializer<T> itemDeserializer;
379+
380+
protected StringArrayOrMapDeserializer(JsonpDeserializer<T> itemDeserializer) {
381+
super(EnumSet.of(Event.START_ARRAY, Event.START_OBJECT));
382+
this.itemDeserializer = itemDeserializer;
383+
}
384+
385+
@Override
386+
public Map<String, T> deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
387+
Map<String, T> result = new LinkedHashMap<>();
388+
if (event == Event.START_ARRAY) {
389+
while ((event = parser.next()) != Event.END_ARRAY) {
390+
JsonpUtils.expectEvent(parser, Event.VALUE_STRING, event);
391+
result.put(parser.getString(), null);
392+
}
393+
} else {
394+
while ((event = parser.next()) != Event.END_OBJECT) {
395+
JsonpUtils.expectEvent(parser, Event.KEY_NAME, event);
396+
String key = parser.getString();
397+
result.put(key, itemDeserializer.deserialize(parser, mapper));
398+
}
399+
}
400+
return result;
401+
}
402+
}
403+
376404
static class EnumMapDeserializer<K, V> extends JsonpDeserializerBase<Map<K, V>> {
377405
private final JsonpDeserializer<K> keyDeserializer;
378406
private final JsonpDeserializer<V> valueDeserializer;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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 static org.junit.Assert.assertEquals;
12+
import static org.junit.Assert.assertNull;
13+
import static org.junit.Assert.assertTrue;
14+
import static org.opensearch.client.opensearch.model.ModelTestCase.toJson;
15+
16+
import jakarta.json.stream.JsonParser;
17+
import java.io.StringReader;
18+
import java.util.Map;
19+
import org.junit.Test;
20+
import org.opensearch.client.json.JsonData;
21+
import org.opensearch.client.json.JsonpDeserializer;
22+
import org.opensearch.client.json.JsonpMapper;
23+
import org.opensearch.client.json.jsonb.JsonbJsonpMapper;
24+
25+
public class MatchedQueriesTest {
26+
private final JsonpMapper mapper = new JsonbJsonpMapper();
27+
private final JsonpDeserializer<Hit<JsonData>> hitDeserializer = Hit.createHitDeserializer(JsonData._DESERIALIZER);
28+
29+
private Hit<JsonData> parse(String json) {
30+
JsonParser parser = mapper.jsonProvider().createParser(new StringReader(json));
31+
return hitDeserializer.deserialize(parser, mapper);
32+
}
33+
34+
@Test
35+
public void deserializesArrayFormAsMapWithNullValues() {
36+
Hit<JsonData> hit = parse("{\"_index\":\"i\",\"matched_queries\":[\"a\",\"b\"]}");
37+
Map<String, Double> matchedQueries = hit.matchedQueries();
38+
assertEquals(2, matchedQueries.size());
39+
assertTrue(matchedQueries.containsKey("a"));
40+
assertNull(matchedQueries.get("a"));
41+
assertNull(matchedQueries.get("b"));
42+
}
43+
44+
@Test
45+
public void deserializesObjectFormAsMapWithScores() {
46+
Hit<JsonData> hit = parse("{\"_index\":\"i\",\"matched_queries\":{\"a\":1.5,\"b\":2.5}}");
47+
Map<String, Double> matchedQueries = hit.matchedQueries();
48+
assertEquals(2, matchedQueries.size());
49+
assertEquals(Double.valueOf(1.5), matchedQueries.get("a"));
50+
assertEquals(Double.valueOf(2.5), matchedQueries.get("b"));
51+
}
52+
53+
@Test
54+
public void returnsEmptyMapWhenAbsent() {
55+
Hit<JsonData> hit = parse("{\"_index\":\"i\"}");
56+
assertTrue(hit.matchedQueries().isEmpty());
57+
}
58+
59+
@Test
60+
public void serializesAsArrayWhenValuesAreNull() {
61+
Hit<JsonData> hit = parse("{\"_index\":\"i\",\"matched_queries\":[\"a\",\"b\"]}");
62+
assertTrue(toJson(hit, mapper).contains("\"matched_queries\":[\"a\",\"b\"]"));
63+
}
64+
65+
@Test
66+
public void serializesAsObjectWhenScoresPresent() {
67+
Hit<JsonData> hit = parse("{\"_index\":\"i\",\"matched_queries\":{\"a\":1.5}}");
68+
assertTrue(toJson(hit, mapper).contains("\"matched_queries\":{\"a\":1.5}"));
69+
}
70+
}

java-codegen/src/main/java/org/opensearch/client/codegen/model/Field.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public class Field {
3131
@Nonnull
3232
private final TypeRef type;
3333
private final boolean required;
34+
private final boolean arrayOrMapUnion;
3435
@Nullable
3536
private final String description;
3637
@Nullable
@@ -52,6 +53,7 @@ private Field(@Nonnull Builder builder) {
5253
}
5354
this.type = Objects.requireNonNull(builder.type, "type must not be null");
5455
this.required = builder.required;
56+
this.arrayOrMapUnion = builder.arrayOrMapUnion;
5557
this.description = builder.description;
5658
this.deprecation = builder.deprecation;
5759
this.aliases = builder.aliases;
@@ -76,6 +78,10 @@ public boolean isRequired() {
7678
return required;
7779
}
7880

81+
public boolean isArrayOrMapUnion() {
82+
return arrayOrMapUnion;
83+
}
84+
7985
@Nullable
8086
public String getDescription() {
8187
return description;
@@ -111,6 +117,7 @@ public static final class Builder extends ObjectBuilderBase<Field, Builder> {
111117
private boolean verbatimName;
112118
private TypeRef type;
113119
private boolean required;
120+
private boolean arrayOrMapUnion;
114121
private String description;
115122
private Deprecation deprecation;
116123
private Set<String> aliases;
@@ -122,6 +129,7 @@ private Builder(Field f) {
122129
this.name = f.name;
123130
this.type = f.type;
124131
this.required = f.required;
132+
this.arrayOrMapUnion = f.arrayOrMapUnion;
125133
this.description = f.description;
126134
this.deprecation = f.deprecation;
127135
this.aliases = f.aliases != null ? new HashSet<>(f.aliases) : null;
@@ -165,6 +173,12 @@ public Builder withRequired(boolean required) {
165173
return this;
166174
}
167175

176+
@Nonnull
177+
public Builder withArrayOrMapUnion(boolean arrayOrMapUnion) {
178+
this.arrayOrMapUnion = arrayOrMapUnion;
179+
return this;
180+
}
181+
168182
@Nonnull
169183
public Builder withDescription(@Nullable String description) {
170184
this.description = description != null ? Markdown.toJavaDocHtml(description) : null;

java-codegen/src/main/java/org/opensearch/client/codegen/transformer/SpecTransformer.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,9 @@ private void visitInto(OpenApiSchema schema, ObjectShapeBase shape) {
601601

602602
var propOverrides = overrides.map(so -> so.getProperty(k));
603603

604-
var type = propOverrides.flatMap(PropertyOverride::getMappedType).orElseGet(() -> typeMapper.mapType(v));
604+
var mappedType = propOverrides.flatMap(PropertyOverride::getMappedType);
605+
var arrayOrMapUnionType = mappedType.isPresent() ? Optional.<TypeRef>empty() : typeMapper.mapStringArrayOrValueMapUnion(v);
606+
var type = mappedType.or(() -> arrayOrMapUnionType).orElseGet(() -> typeMapper.mapType(v));
605607

606608
var isRequired = propOverrides.flatMap(PropertyOverride::getRequired).orElseGet(() -> required.contains(k));
607609

@@ -617,6 +619,7 @@ private void visitInto(OpenApiSchema schema, ObjectShapeBase shape) {
617619
.withName(propOverrides.flatMap(PropertyOverride::getName).orElse(null))
618620
.withType(type)
619621
.withRequired(!canBeNull)
622+
.withArrayOrMapUnion(arrayOrMapUnionType.isPresent())
620623
.withDescription(v.getDescription().orElse(null))
621624
.whenPresent(propOverrides.flatMap(PropertyOverride::getAliases), (b, aliases) -> b.withAliases(a -> a.with(aliases)))
622625
.build();

0 commit comments

Comments
 (0)