From a9d2aaa93dc1c80a74638755ca83b98048e02dbb Mon Sep 17 00:00:00 2001 From: Peter-Josef Meisch Date: Thu, 29 May 2025 18:10:48 +0200 Subject: [PATCH] Polishing. Signed-off-by: Peter-Josef Meisch --- src/main/antora/modules/ROOT/nav.adoc | 1 + .../elasticsearch/elasticsearch-new.adoc | 8 + .../ROOT/pages/elasticsearch/versions.adoc | 2 +- .../migration-guide-5.5-6.0.adoc | 21 ++ .../client/elc/DocumentAdapters.java | 308 +++++++++--------- .../elasticsearch/core/query/ScriptData.java | 138 ++++---- .../elasticsearch/core/script/Script.java | 3 + 7 files changed, 269 insertions(+), 212 deletions(-) create mode 100644 src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.5-6.0.adoc diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc index 53580343a..fa1ee8110 100644 --- a/src/main/antora/modules/ROOT/nav.adoc +++ b/src/main/antora/modules/ROOT/nav.adoc @@ -12,6 +12,7 @@ *** xref:migration-guides/migration-guide-5.2-5.3.adoc[] *** xref:migration-guides/migration-guide-5.3-5.4.adoc[] *** xref:migration-guides/migration-guide-5.4-5.5.adoc[] +*** xref:migration-guides/migration-guide-5.5-6.0.adoc[] * xref:elasticsearch.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc index 5dca4fab6..f1e07dd19 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc @@ -1,6 +1,14 @@ [[new-features]] = What's new +[[new-features.6-0-0]] +== New in Spring Data Elasticsearch 6.6 + +* Upgarde to Spring 7 +* Switch to jspecify nullability annotations +* Upgrade to Elasticsearch 9.0.1 + + [[new-features.5-5-0]] == New in Spring Data Elasticsearch 5.5 diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/versions.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/versions.adoc index 23990e2a7..8fb6d7261 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/versions.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/versions.adoc @@ -6,7 +6,7 @@ The following table shows the Elasticsearch and Spring versions that are used by [cols="^,^,^,^",options="header"] |=== | Spring Data Release Train | Spring Data Elasticsearch | Elasticsearch | Spring Framework -| 2025.1 (in development) | 6.0.x | 8.18.1 | 7.0.x +| 2025.1 (in development) | 6.0.x | 9.0.1 | 7.0.x | 2025.0 | 5.5.x | 8.18.1 | 6.2.x | 2024.1 | 5.4.x | 8.15.5 | 6.1.x | 2024.0 | 5.3.xfootnote:oom[Out of maintenance] | 8.13.4 | 6.1.x diff --git a/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.5-6.0.adoc b/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.5-6.0.adoc new file mode 100644 index 000000000..7667701a1 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/migration-guides/migration-guide-5.5-6.0.adoc @@ -0,0 +1,21 @@ +[[elasticsearch-migration-guide-5.5-6.0]] += Upgrading from 5.5.x to 6.0.x + +This section describes breaking changes from version 5.5.x to 6.0.x and how removed features can be replaced by new introduced features. + +[[elasticsearch-migration-guide-5.5-6.0.breaking-changes]] +== Breaking Changes + +[[elasticsearch-migration-guide-5.5-6.0.deprecations]] +== Deprecations + + +=== Removals + +The `org.springframework.data.elasticsearch.core.query.ScriptType` enum has been removed. To distinguish between an inline and a stored script set the appropriate values in the `org.springframework.data.elasticsearch.core.query.ScriptData` record. + +These methods have been removed because the Elasticsearch Client 9 does not support them anymore: +``` +org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchIndicesClient.unfreeze(UnfreezeRequest) +org.springframework.data.elasticsearch.client.elc.ReactiveElasticsearchIndicesClient.unfreeze(Function>) +``` diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/DocumentAdapters.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/DocumentAdapters.java index ea38dfe58..53e8cefa7 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/DocumentAdapters.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/DocumentAdapters.java @@ -24,14 +24,6 @@ import co.elastic.clients.elasticsearch.core.search.Hit; import co.elastic.clients.elasticsearch.core.search.NestedIdentity; import co.elastic.clients.json.JsonData; import co.elastic.clients.json.JsonpMapper; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; @@ -44,6 +36,13 @@ import org.springframework.data.elasticsearch.core.document.SearchDocumentAdapte import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse; import org.springframework.util.Assert; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + /** * Utility class to adapt different Elasticsearch responses to a * {@link org.springframework.data.elasticsearch.core.document.Document} @@ -55,187 +54,188 @@ import org.springframework.util.Assert; */ final class DocumentAdapters { - private static final Log LOGGER = LogFactory.getLog(DocumentAdapters.class); + private static final Log LOGGER = LogFactory.getLog(DocumentAdapters.class); - private DocumentAdapters() {} + private DocumentAdapters() { + } - /** - * Creates a {@link SearchDocument} from a {@link Hit} returned by the Elasticsearch client. - * - * @param hit the hit object - * @param jsonpMapper to map JsonData objects - * @return the created {@link SearchDocument} - */ - public static SearchDocument from(Hit hit, JsonpMapper jsonpMapper) { + /** + * Creates a {@link SearchDocument} from a {@link Hit} returned by the Elasticsearch client. + * + * @param hit the hit object + * @param jsonpMapper to map JsonData objects + * @return the created {@link SearchDocument} + */ + public static SearchDocument from(Hit hit, JsonpMapper jsonpMapper) { - Assert.notNull(hit, "hit must not be null"); + Assert.notNull(hit, "hit must not be null"); - Map> highlightFields = hit.highlight(); + Map> highlightFields = hit.highlight(); - Map innerHits = new LinkedHashMap<>(); - hit.innerHits().forEach((name, innerHitsResult) -> { - // noinspection ReturnOfNull - innerHits.put(name, SearchDocumentResponseBuilder.from(innerHitsResult.hits(), null, null, null, 0, null, null, - searchDocument -> null, jsonpMapper)); - }); + Map innerHits = new LinkedHashMap<>(); + hit.innerHits().forEach((name, innerHitsResult) -> { + // noinspection ReturnOfNull + innerHits.put(name, SearchDocumentResponseBuilder.from(innerHitsResult.hits(), null, null, null, 0, null, null, + searchDocument -> null, jsonpMapper)); + }); - NestedMetaData nestedMetaData = from(hit.nested()); + NestedMetaData nestedMetaData = from(hit.nested()); - Explanation explanation = from(hit.explanation()); + Explanation explanation = from(hit.explanation()); - Map matchedQueries = hit.matchedQueries(); + Map matchedQueries = hit.matchedQueries(); - Function, EntityAsMap> fromFields = fields -> { - StringBuilder sb = new StringBuilder("{"); - final boolean[] firstField = { true }; - hit.fields().forEach((key, jsonData) -> { - if (!firstField[0]) { - sb.append(','); - } - sb.append('"').append(key).append("\":") // - .append(jsonData.toJson(jsonpMapper).toString()); - firstField[0] = false; - }); - sb.append('}'); - return new EntityAsMap().fromJson(sb.toString()); - }; + Function, EntityAsMap> fromFields = fields -> { + StringBuilder sb = new StringBuilder("{"); + final boolean[] firstField = {true}; + hit.fields().forEach((key, jsonData) -> { + if (!firstField[0]) { + sb.append(','); + } + sb.append('"').append(key).append("\":") // + .append(jsonData.toJson(jsonpMapper).toString()); + firstField[0] = false; + }); + sb.append('}'); + return new EntityAsMap().fromJson(sb.toString()); + }; - EntityAsMap hitFieldsAsMap = fromFields.apply(hit.fields()); + EntityAsMap hitFieldsAsMap = fromFields.apply(hit.fields()); - Map> documentFields = new LinkedHashMap<>(); - hitFieldsAsMap.forEach((key, value) -> { - if (value instanceof List) { - // noinspection unchecked - documentFields.put(key, (List) value); - } else { - documentFields.put(key, Collections.singletonList(value)); - } - }); + Map> documentFields = new LinkedHashMap<>(); + hitFieldsAsMap.forEach((key, value) -> { + if (value instanceof List) { + // noinspection unchecked + documentFields.put(key, (List) value); + } else { + documentFields.put(key, Collections.singletonList(value)); + } + }); - Document document; - Object source = hit.source(); - if (source == null) { - document = Document.from(hitFieldsAsMap); - } else { - if (source instanceof EntityAsMap entityAsMap) { - document = Document.from(entityAsMap); - } else if (source instanceof JsonData jsonData) { - document = Document.from(jsonData.to(EntityAsMap.class)); - } else { + Document document; + Object source = hit.source(); + if (source == null) { + document = Document.from(hitFieldsAsMap); + } else { + if (source instanceof EntityAsMap entityAsMap) { + document = Document.from(entityAsMap); + } else if (source instanceof JsonData jsonData) { + document = Document.from(jsonData.to(EntityAsMap.class)); + } else { - if (LOGGER.isWarnEnabled()) { - LOGGER.warn(String.format("Cannot map from type " + source.getClass().getName())); - } - document = Document.create(); - } - } - document.setIndex(hit.index()); - document.setId(hit.id()); + if (LOGGER.isWarnEnabled()) { + LOGGER.warn(String.format("Cannot map from type " + source.getClass().getName())); + } + document = Document.create(); + } + } + document.setIndex(hit.index()); + document.setId(hit.id()); - if (hit.version() != null) { - document.setVersion(hit.version()); - } - document.setSeqNo(hit.seqNo() != null && hit.seqNo() >= 0 ? hit.seqNo() : -2); // -2 was the default value in the - // old client - document.setPrimaryTerm(hit.primaryTerm() != null && hit.primaryTerm() > 0 ? hit.primaryTerm() : 0); + if (hit.version() != null) { + document.setVersion(hit.version()); + } + document.setSeqNo(hit.seqNo() != null && hit.seqNo() >= 0 ? hit.seqNo() : -2); // -2 was the default value in the + // old client + document.setPrimaryTerm(hit.primaryTerm() != null && hit.primaryTerm() > 0 ? hit.primaryTerm() : 0); - float score = hit.score() != null ? hit.score().floatValue() : Float.NaN; - return new SearchDocumentAdapter(document, score, hit.sort().stream().map(TypeUtils::toObject).toArray(), - documentFields, highlightFields, innerHits, nestedMetaData, explanation, matchedQueries, hit.routing()); - } + float score = hit.score() != null ? hit.score().floatValue() : Float.NaN; + return new SearchDocumentAdapter(document, score, hit.sort().stream().map(TypeUtils::toObject).toArray(), + documentFields, highlightFields, innerHits, nestedMetaData, explanation, matchedQueries, hit.routing()); + } - public static SearchDocument from(CompletionSuggestOption completionSuggestOption) { + public static SearchDocument from(CompletionSuggestOption completionSuggestOption) { - Document document = completionSuggestOption.source() != null ? Document.from(completionSuggestOption.source()) - : Document.create(); - document.setIndex(completionSuggestOption.index()); + Document document = completionSuggestOption.source() != null ? Document.from(completionSuggestOption.source()) + : Document.create(); + document.setIndex(completionSuggestOption.index()); - if (completionSuggestOption.id() != null) { - document.setId(completionSuggestOption.id()); - } + if (completionSuggestOption.id() != null) { + document.setId(completionSuggestOption.id()); + } - float score = completionSuggestOption.score() != null ? completionSuggestOption.score().floatValue() : Float.NaN; - return new SearchDocumentAdapter(document, score, new Object[] {}, Collections.emptyMap(), Collections.emptyMap(), - Collections.emptyMap(), null, null, null, completionSuggestOption.routing()); - } + float score = completionSuggestOption.score() != null ? completionSuggestOption.score().floatValue() : Float.NaN; + return new SearchDocumentAdapter(document, score, new Object[]{}, Collections.emptyMap(), Collections.emptyMap(), + Collections.emptyMap(), null, null, null, completionSuggestOption.routing()); + } - @Nullable - private static Explanation from(co.elastic.clients.elasticsearch.core.explain.@Nullable Explanation explanation) { + @Nullable + private static Explanation from(co.elastic.clients.elasticsearch.core.explain.@Nullable Explanation explanation) { - if (explanation == null) { - return null; - } - List details = explanation.details().stream().map(DocumentAdapters::from).collect(Collectors.toList()); - return new Explanation(true, (double) explanation.value(), explanation.description(), details); - } + if (explanation == null) { + return null; + } + List details = explanation.details().stream().map(DocumentAdapters::from).collect(Collectors.toList()); + return new Explanation(true, (double) explanation.value(), explanation.description(), details); + } - private static Explanation from(ExplanationDetail explanationDetail) { + private static Explanation from(ExplanationDetail explanationDetail) { - List details = explanationDetail.details().stream().map(DocumentAdapters::from) - .collect(Collectors.toList()); - return new Explanation(null, (double) explanationDetail.value(), explanationDetail.description(), details); - } + List details = explanationDetail.details().stream().map(DocumentAdapters::from) + .collect(Collectors.toList()); + return new Explanation(null, (double) explanationDetail.value(), explanationDetail.description(), details); + } - @Nullable - private static NestedMetaData from(@Nullable NestedIdentity nestedIdentity) { + @Nullable + private static NestedMetaData from(@Nullable NestedIdentity nestedIdentity) { - if (nestedIdentity == null) { - return null; - } + if (nestedIdentity == null) { + return null; + } - NestedMetaData child = from(nestedIdentity.nested()); - return NestedMetaData.of(nestedIdentity.field(), nestedIdentity.offset(), child); - } + NestedMetaData child = from(nestedIdentity.nested()); + return NestedMetaData.of(nestedIdentity.field(), nestedIdentity.offset(), child); + } - /** - * Creates a {@link Document} from a {@link GetResponse} where the found document is contained as {@link EntityAsMap}. - * - * @param getResponse the response instance - * @return the Document - */ - @Nullable - public static Document from(GetResult getResponse) { + /** + * Creates a {@link Document} from a {@link GetResponse} where the found document is contained as {@link EntityAsMap}. + * + * @param getResponse the response instance + * @return the Document + */ + @Nullable + public static Document from(GetResult getResponse) { - Assert.notNull(getResponse, "getResponse must not be null"); + Assert.notNull(getResponse, "getResponse must not be null"); - if (!getResponse.found()) { - return null; - } + if (!getResponse.found()) { + return null; + } - Document document = getResponse.source() != null ? Document.from(getResponse.source()) : Document.create(); - document.setIndex(getResponse.index()); - document.setId(getResponse.id()); + Document document = getResponse.source() != null ? Document.from(getResponse.source()) : Document.create(); + document.setIndex(getResponse.index()); + document.setId(getResponse.id()); - if (getResponse.version() != null) { - document.setVersion(getResponse.version()); - } + if (getResponse.version() != null) { + document.setVersion(getResponse.version()); + } - if (getResponse.seqNo() != null) { - document.setSeqNo(getResponse.seqNo()); - } + if (getResponse.seqNo() != null) { + document.setSeqNo(getResponse.seqNo()); + } - if (getResponse.primaryTerm() != null) { - document.setPrimaryTerm(getResponse.primaryTerm()); - } + if (getResponse.primaryTerm() != null) { + document.setPrimaryTerm(getResponse.primaryTerm()); + } - return document; - } + return document; + } - /** - * Creates a list of {@link MultiGetItem}s from a {@link MgetResponse} where the data is contained as - * {@link EntityAsMap} instances. - * - * @param mgetResponse the response instance - * @return list of multiget items - */ - public static List> from(MgetResponse mgetResponse) { + /** + * Creates a list of {@link MultiGetItem}s from a {@link MgetResponse} where the data is contained as + * {@link EntityAsMap} instances. + * + * @param mgetResponse the response instance + * @return list of multiget items + */ + public static List> from(MgetResponse mgetResponse) { - Assert.notNull(mgetResponse, "mgetResponse must not be null"); + Assert.notNull(mgetResponse, "mgetResponse must not be null"); - return mgetResponse.docs().stream() // - .map(itemResponse -> MultiGetItem.of( // - itemResponse.isFailure() ? null : from(itemResponse.result()), // - ResponseConverter.getFailure(itemResponse))) - .collect(Collectors.toList()); - } + return mgetResponse.docs().stream() // + .map(itemResponse -> MultiGetItem.of( // + itemResponse.isFailure() ? null : from(itemResponse.result()), // + ResponseConverter.getFailure(itemResponse))) + .collect(Collectors.toList()); + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptData.java b/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptData.java index 1597f064f..fc81b95d4 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptData.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptData.java @@ -15,86 +15,110 @@ */ package org.springframework.data.elasticsearch.core.query; -import java.util.Map; -import java.util.function.Function; - import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; +import java.util.Map; +import java.util.function.Function; + /** * value class combining script information. + *

+ * A script is either an inline script, then the script parameters must be set + * or it refers to a stored script, then the name parameter is required. * + * @param language the language when the script is passed in the script parameter + * @param script the script to use as inline script + * @param scriptName the name when using a stored script + * @param params the script parameters * @author Peter-Josef Meisch * @since 4.4 */ public record ScriptData(@Nullable String language, @Nullable String script, - @Nullable String scriptName, @Nullable Map params) { + @Nullable String scriptName, @Nullable Map params) { - public ScriptData(@Nullable String language, @Nullable String script, @Nullable String scriptName, - @Nullable Map params) { + /* + * constructor overload to check the parameters + */ + public ScriptData(@Nullable String language, @Nullable String script, @Nullable String scriptName, + @Nullable Map params) { - this.language = language; - this.script = script; - this.scriptName = scriptName; - this.params = params; - } + Assert.isTrue(script != null || scriptName != null, "script or scriptName is required"); - /** - * @since 5.2 - */ - public static ScriptData of(@Nullable String language, @Nullable String script, - @Nullable String scriptName, @Nullable Map params) { - return new ScriptData(language, script, scriptName, params); - } + this.language = language; + this.script = script; + this.scriptName = scriptName; + this.params = params; + } - public static ScriptData of(Function builderFunction) { + /** + * factory method to create a ScriptData object. + * + * @since 5.2 + */ + public static ScriptData of(@Nullable String language, @Nullable String script, + @Nullable String scriptName, @Nullable Map params) { + return new ScriptData(language, script, scriptName, params); + } - Assert.notNull(builderFunction, "f must not be null"); + /** + * factory method to create a ScriptData object using a ScriptBuilder callback. + * + * @param builderFunction function called to populate the builder + * @return + */ + public static ScriptData of(Function builderFunction) { - return builderFunction.apply(new Builder()).build(); - } + Assert.notNull(builderFunction, "builderFunction must not be null"); - /** - * @since 5.2 - */ - public static Builder builder() { - return new Builder(); - } + return builderFunction.apply(new Builder()).build(); + } - /** - * @since 5.2 - */ - public static final class Builder { - @Nullable private String language; - @Nullable private String script; - @Nullable private String scriptName; - @Nullable private Map params; + /** + * @since 5.2 + */ + public static Builder builder() { + return new Builder(); + } - private Builder() {} + /** + * @since 5.2 + */ + public static final class Builder { + @Nullable + private String language; + @Nullable + private String script; + @Nullable + private String scriptName; + @Nullable + private Map params; - public Builder withLanguage(@Nullable String language) { - this.language = language; - return this; - } + private Builder() { + } - public Builder withScript(@Nullable String script) { - this.script = script; - return this; - } + public Builder withLanguage(@Nullable String language) { + this.language = language; + return this; + } - public Builder withScriptName(@Nullable String scriptName) { - this.scriptName = scriptName; - return this; - } + public Builder withScript(@Nullable String script) { + this.script = script; + return this; + } - public Builder withParams(@Nullable Map params) { - this.params = params; - return this; - } + public Builder withScriptName(@Nullable String scriptName) { + this.scriptName = scriptName; + return this; + } - public ScriptData build() { + public Builder withParams(@Nullable Map params) { + this.params = params; + return this; + } - return new ScriptData(language, script, scriptName, params); - } - } + public ScriptData build() { + return new ScriptData(language, script, scriptName, params); + } + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/script/Script.java b/src/main/java/org/springframework/data/elasticsearch/core/script/Script.java index b1f35896b..1f29c6c22 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/script/Script.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/script/Script.java @@ -28,6 +28,7 @@ public record Script(String id, String language, String source) { Assert.notNull(id, "id must not be null"); Assert.notNull(language, "language must not be null"); + Assert.notNull(source, "source must not be null"); } public static ScriptBuilder builder() { @@ -60,6 +61,8 @@ public record Script(String id, String language, String source) { public ScriptBuilder withSource(String source) { + Assert.notNull(source, "source must not be null"); + this.source = source; return this; }