Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `Map<String, Double>`, where the array form yields a map with `null` values and the object form yields query name to score entries ([#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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,8 @@ public final Boolean ignoreUnavailable() {
}

/**
* Whether to return scores with named queries. Default is false.
* Indicates whether <code>hit.matched_queries</code> 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)
* <p>
* API name: {@code include_named_queries_score}
* </p>
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -1954,7 +1950,8 @@ public final Builder ignoreUnavailable(@Nullable Boolean value) {
}

/**
* Whether to return scores with named queries. Default is false.
* Indicates whether <code>hit.matched_queries</code> 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)
* <p>
* API name: {@code include_named_queries_score}
* </p>
Expand Down Expand Up @@ -2728,7 +2725,6 @@ protected static void setupSearchRequestDeserializer(ObjectDeserializer<SearchRe
op.add(Builder::fields, JsonpDeserializer.arrayDeserializer(FieldAndFormat._DESERIALIZER), "fields");
op.add(Builder::from, JsonpDeserializer.integerDeserializer(), "from");
op.add(Builder::highlight, Highlight._DESERIALIZER, "highlight");
op.add(Builder::includeNamedQueriesScore, JsonpDeserializer.booleanDeserializer(), "include_named_queries_score");
op.add(
Builder::indicesBoost,
JsonpDeserializer.arrayDeserializer(JsonpDeserializer.stringMapDeserializer(JsonpDeserializer.floatDeserializer())),
Expand Down Expand Up @@ -2801,6 +2797,9 @@ protected void applyQueryParameters(@Nonnull Map<String, String> 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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ public class Hit<TDocument> implements PlainJsonSerializable, ToCopyableBuilder<
@Nonnull
private final Map<String, InnerHitsResult> innerHits;

@Nullable
private final JsonData matchedQueries;
@Nonnull
private final Map<String, Double> matchedQueries;

@Nonnull
private final Map<String, JsonData> metaFields;
Expand Down Expand Up @@ -140,7 +140,7 @@ private Hit(Builder<TDocument> builder) {
this.ignoredFieldValues = ApiTypeHelper.unmodifiable(builder.ignoredFieldValues);
this.index = builder.index;
this.innerHits = ApiTypeHelper.unmodifiable(builder.innerHits);
this.matchedQueries = builder.matchedQueries;
this.matchedQueries = ApiTypeHelper.unmodifiable(builder.matchedQueries);
this.metaFields = ApiTypeHelper.unmodifiable(builder.metaFields);
this.nested = builder.nested;
this.node = builder.node;
Expand Down Expand Up @@ -230,8 +230,8 @@ public final Map<String, InnerHitsResult> innerHits() {
* API name: {@code matched_queries}
* </p>
*/
@Nullable
public final JsonData matchedQueries() {
@Nonnull
public final Map<String, Double> matchedQueries() {
return this.matchedQueries;
}

Expand Down Expand Up @@ -422,9 +422,22 @@ protected void serializeInternal(JsonGenerator generator, JsonpMapper mapper) {
generator.writeEnd();
}

if (this.matchedQueries != null) {
if (ApiTypeHelper.isDefined(this.matchedQueries)) {
generator.writeKey("matched_queries");
this.matchedQueries.serialize(generator, mapper);
if (this.matchedQueries.values().stream().allMatch(Objects::isNull)) {
generator.writeStartArray();
for (String item : this.matchedQueries.keySet()) {
generator.write(item);
}
generator.writeEnd();
} else {
generator.writeStartObject();
for (Map.Entry<String, Double> item0 : this.matchedQueries.entrySet()) {
generator.writeKey(item0.getKey());
generator.write(item0.getValue());
}
generator.writeEnd();
}
}

if (this.nested != null) {
Expand Down Expand Up @@ -516,7 +529,7 @@ public static class Builder<TDocument> extends ObjectBuilderBase implements Copy
@Nullable
private Map<String, InnerHitsResult> innerHits;
@Nullable
private JsonData matchedQueries;
private Map<String, Double> matchedQueries;
Comment thread
reta marked this conversation as resolved.
Outdated
@Nullable
private Map<String, JsonData> metaFields;
@Nullable
Expand Down Expand Up @@ -553,7 +566,7 @@ private Builder(Hit<TDocument> o) {
this.ignoredFieldValues = _mapCopy(o.ignoredFieldValues);
this.index = o.index;
this.innerHits = _mapCopy(o.innerHits);
this.matchedQueries = o.matchedQueries;
this.matchedQueries = _mapCopy(o.matchedQueries);
this.metaFields = _mapCopy(o.metaFields);
this.nested = o.nested;
this.node = o.node;
Expand All @@ -577,7 +590,7 @@ private Builder(Builder<TDocument> o) {
this.ignoredFieldValues = _mapCopy(o.ignoredFieldValues);
this.index = o.index;
this.innerHits = _mapCopy(o.innerHits);
this.matchedQueries = o.matchedQueries;
this.matchedQueries = _mapCopy(o.matchedQueries);
this.metaFields = _mapCopy(o.metaFields);
this.nested = o.nested;
this.node = o.node;
Expand Down Expand Up @@ -781,10 +794,31 @@ public final Builder<TDocument> innerHits(String key, Function<InnerHitsResult.B
* <p>
* API name: {@code matched_queries}
* </p>
*
* <p>
* Adds all elements of <code>map</code> to <code>matchedQueries</code>.
* </p>
*/
@Nonnull
public final Builder<TDocument> matchedQueries(@Nullable JsonData value) {
this.matchedQueries = value;
public final Builder<TDocument> matchedQueries(Map<String, Double> map) {
this.matchedQueries = _mapPutAll(this.matchedQueries, map);
return this;
}

/**
* The names of queries that matched the document. When <code>include_named_queries_score</code> is false (default), returns an
* array of query names. When true, returns an object mapping query names to their scores.
* <p>
* API name: {@code matched_queries}
* </p>
*
* <p>
* Adds an entry to <code>matchedQueries</code>.
* </p>
*/
@Nonnull
public final Builder<TDocument> matchedQueries(String key, Double value) {
this.matchedQueries = _mapPut(this.matchedQueries, key, value);
return this;
}

Expand Down Expand Up @@ -996,7 +1030,11 @@ protected static <TDocument> 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,
JsonpDeserializer.stringArrayOrMapDeserializer(JsonpDeserializer.doubleDeserializer()),
"matched_queries"
);
op.add(Builder::nested, NestedIdentity._DESERIALIZER, "_nested");
op.add(Builder::node, JsonpDeserializer.stringDeserializer(), "_node");
op.add(Builder::primaryTerm, JsonpDeserializer.longDeserializer(), "_primary_term");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,15 @@ static <T> JsonpDeserializer<Map<String, T>> stringMapDeserializer(JsonpDeserial
return new JsonpDeserializerBase.StringMapDeserializer<T>(itemDeserializer);
}

/**
* Deserializer for a value that is serialized either as an array of string keys or as an object mapping those keys
* to values. The array form is deserialized into a map whose values are {@code null}; the object form is
* deserialized into a map of keys to values.
*/
static <T> JsonpDeserializer<Map<String, T>> stringArrayOrMapDeserializer(JsonpDeserializer<T> itemDeserializer) {
return new JsonpDeserializerBase.StringArrayOrMapDeserializer<T>(itemDeserializer);
}

static <K extends JsonEnum, V> JsonpDeserializer<Map<K, V>> enumMapDeserializer(
JsonpDeserializer<K> keyDeserializer,
JsonpDeserializer<V> valueDeserializer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -373,6 +374,33 @@ public Map<String, T> deserialize(JsonParser parser, JsonpMapper mapper, Event e
}
}

static class StringArrayOrMapDeserializer<T> extends JsonpDeserializerBase<Map<String, T>> {
private final JsonpDeserializer<T> itemDeserializer;

protected StringArrayOrMapDeserializer(JsonpDeserializer<T> itemDeserializer) {
super(EnumSet.of(Event.START_ARRAY, Event.START_OBJECT));
this.itemDeserializer = itemDeserializer;
}

@Override
public Map<String, T> deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
Map<String, T> result = new LinkedHashMap<>();
if (event == Event.START_ARRAY) {
while ((event = parser.next()) != Event.END_ARRAY) {
JsonpUtils.expectEvent(parser, Event.VALUE_STRING, event);
result.put(parser.getString(), null);
}
} else {
while ((event = parser.next()) != Event.END_OBJECT) {
JsonpUtils.expectEvent(parser, Event.KEY_NAME, event);
String key = parser.getString();
result.put(key, itemDeserializer.deserialize(parser, mapper));
}
}
return result;
}
}

static class EnumMapDeserializer<K, V> extends JsonpDeserializerBase<Map<K, V>> {
private final JsonpDeserializer<K> keyDeserializer;
private final JsonpDeserializer<V> valueDeserializer;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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.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.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<Hit<JsonData>> hitDeserializer = Hit.createHitDeserializer(JsonData._DESERIALIZER);

private Hit<JsonData> parse(String json) {
JsonParser parser = mapper.jsonProvider().createParser(new StringReader(json));
return hitDeserializer.deserialize(parser, mapper);
}

@Test
public void deserializesArrayFormAsMapWithNullValues() {
Hit<JsonData> hit = parse("{\"_index\":\"i\",\"matched_queries\":[\"a\",\"b\"]}");
Map<String, Double> matchedQueries = hit.matchedQueries();
assertEquals(2, matchedQueries.size());
assertTrue(matchedQueries.containsKey("a"));
assertNull(matchedQueries.get("a"));
assertNull(matchedQueries.get("b"));
}

@Test
public void deserializesObjectFormAsMapWithScores() {
Hit<JsonData> hit = parse("{\"_index\":\"i\",\"matched_queries\":{\"a\":1.5,\"b\":2.5}}");
Map<String, Double> matchedQueries = hit.matchedQueries();
assertEquals(2, matchedQueries.size());
assertEquals(Double.valueOf(1.5), matchedQueries.get("a"));
assertEquals(Double.valueOf(2.5), matchedQueries.get("b"));
}

@Test
public void returnsEmptyMapWhenAbsent() {
Hit<JsonData> hit = parse("{\"_index\":\"i\"}");
assertTrue(hit.matchedQueries().isEmpty());
}

@Test
public void serializesAsArrayWhenValuesAreNull() {
Hit<JsonData> hit = parse("{\"_index\":\"i\",\"matched_queries\":[\"a\",\"b\"]}");
assertTrue(toJson(hit, mapper).contains("\"matched_queries\":[\"a\",\"b\"]"));
}

@Test
public void serializesAsObjectWhenScoresPresent() {
Hit<JsonData> hit = parse("{\"_index\":\"i\",\"matched_queries\":{\"a\":1.5}}");
assertTrue(toJson(hit, mapper).contains("\"matched_queries\":{\"a\":1.5}"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class Field {
@Nonnull
private final TypeRef type;
private final boolean required;
private final boolean arrayOrMapUnion;
@Nullable
private final String description;
@Nullable
Expand All @@ -52,6 +53,7 @@ private Field(@Nonnull Builder builder) {
}
this.type = Objects.requireNonNull(builder.type, "type must not be null");
this.required = builder.required;
this.arrayOrMapUnion = builder.arrayOrMapUnion;
this.description = builder.description;
this.deprecation = builder.deprecation;
this.aliases = builder.aliases;
Expand All @@ -76,6 +78,10 @@ public boolean isRequired() {
return required;
}

public boolean isArrayOrMapUnion() {
return arrayOrMapUnion;
}

@Nullable
public String getDescription() {
return description;
Expand Down Expand Up @@ -111,6 +117,7 @@ public static final class Builder extends ObjectBuilderBase<Field, Builder> {
private boolean verbatimName;
private TypeRef type;
private boolean required;
private boolean arrayOrMapUnion;
private String description;
private Deprecation deprecation;
private Set<String> aliases;
Expand All @@ -122,6 +129,7 @@ private Builder(Field f) {
this.name = f.name;
this.type = f.type;
this.required = f.required;
this.arrayOrMapUnion = f.arrayOrMapUnion;
this.description = f.description;
this.deprecation = f.deprecation;
this.aliases = f.aliases != null ? new HashSet<>(f.aliases) : null;
Expand Down Expand Up @@ -165,6 +173,12 @@ public Builder withRequired(boolean required) {
return this;
}

@Nonnull
public Builder withArrayOrMapUnion(boolean arrayOrMapUnion) {
this.arrayOrMapUnion = arrayOrMapUnion;
return this;
}

@Nonnull
public Builder withDescription(@Nullable String description) {
this.description = description != null ? Markdown.toJavaDocHtml(description) : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,9 @@ private void visitInto(OpenApiSchema schema, ObjectShapeBase shape) {

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

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

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

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