1
0
mirror of synced 2026-09-01 05:34:27 +00:00

Compare commits

..

13 Commits

Author SHA1 Message Date
Christoph Strobl 318de63a4c Release version 6.1.1 (2026.0.1).
See #3295
2026-08-14 10:51:49 +02:00
Christoph Strobl a5ff84e50f Enter rampdown phase for 6.1.1.
See #3295
2026-08-13 09:05:27 +02:00
Peter-Josef Meisch 213aea4c8b Polishing
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-08-12 16:38:15 +02:00
Mark Paluch 03da2d4b2f Upgrade to Elasticsearch Java Client 9.4.5.
See #3326
2026-08-11 15:34:19 +02:00
Mark Paluch b133a3bded Update build config for correct notice and license.txt inclusion.
Closes spring-projects/spring-data-commons#3524
2026-08-10 12:10:18 +02:00
Mark Paluch ce1b0a3379 Add release train automation.
See: spring-projects/spring-data-build#2891
2026-08-10 07:12:44 +02:00
Peter-Josef Meisch d3248adee1 Upgrade ELasticsearch to 9.4.4
Closes: #3316
Original Pull Request: #3318

Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-07-25 15:51:33 +02:00
Peter-Josef Meisch 08ca770465 Polishing
(cherry picked from commit 3b21a1882b)
2026-07-13 18:46:25 +02:00
林桉 25bdc281e5 Fix async Rest5 client withHeaders() not seeing ThreadLocal auth
Closes #3300

Signed-off-by: 014-code <2402143478@qq.com>
(cherry picked from commit 7e70435f48)
2026-07-13 18:46:25 +02:00
Peter-Josef Meisch 06d57b8a55 Update Elasticsearch to 9.4.3.
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
2026-07-03 22:06:04 +02:00
Mark Paluch 17cb07b0c0 Use property to easier configure Spring Framework reference docs location.
See spring-projects/spring-data-build#2886
2026-06-17 16:40:41 +02:00
Christoph Strobl f29c836e33 Prepare next development iteration.
See #3277
2026-06-09 10:50:10 +02:00
Christoph Strobl cea2c282fe After release cleanups.
See #3277
2026-06-09 10:50:10 +02:00
40 changed files with 154 additions and 1358 deletions
+3 -3
View File
@@ -3,7 +3,7 @@ name: CI Build
on:
workflow_dispatch:
push:
branches: [ main, 'issue/**' ]
branches: [ 6.1.x, 'issue/6.1.x/**' ]
permissions: read-all
@@ -17,12 +17,12 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Setup Java and Maven
uses: spring-projects/spring-data-build/actions/setup-maven@main
uses: spring-projects/spring-data-build/actions/setup-maven@4.1.x
with:
java-version: ${{ matrix.java-version }}
develocity-access-key: '${{ secrets.DEVELOCITY_ACCESS_KEY }}'
- name: Build
uses: spring-projects/spring-data-build/actions/maven-build@main
uses: spring-projects/spring-data-build/actions/maven-build@4.1.x
env:
OSS_ARTIFACTORY_USR: '${{ secrets.ARTIFACTORY_USERNAME }}'
OSS_ARTIFACTORY_PSW: '${{ secrets.ARTIFACTORY_PASSWORD }}'
+1 -1
View File
@@ -4,7 +4,7 @@ name: "CodeQL Advanced"
on:
push:
branches: [ main ]
branches: [ 6.1.x ]
pull_request:
workflow_dispatch:
schedule:
+3 -3
View File
@@ -3,7 +3,7 @@ name: Snapshots
on:
workflow_dispatch:
push:
branches: [ main, 'issue/**', 'release/**' ]
branches: [ 6.1.x, 'issue/6.1.x/**', 'release/**' ]
permissions: read-all
@@ -15,11 +15,11 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Setup Java and Maven
uses: spring-projects/spring-data-build/actions/setup-maven@main
uses: spring-projects/spring-data-build/actions/setup-maven@4.1.x
with:
develocity-access-key: '${{ secrets.DEVELOCITY_ACCESS_KEY }}'
- name: Deploy to Artifactory
uses: spring-projects/spring-data-build/actions/maven-artifactory-deploy@main
uses: spring-projects/spring-data-build/actions/maven-artifactory-deploy@4.1.x
env:
OSS_ARTIFACTORY_USR: '${{ secrets.ARTIFACTORY_USERNAME }}'
OSS_ARTIFACTORY_PSW: '${{ secrets.ARTIFACTORY_PASSWORD }}'
+15
View File
@@ -0,0 +1,15 @@
= Security Policy
== Reporting a Vulnerability
Please, https://github.com/spring-projects/security-advisories/security/advisories/new[open a draft security advisory] if you need to disclose and discuss a security issue in private with the Spring Data team.
Note that we only accept reports against https://spring.io/projects/spring-data#support[supported versions].
For more details, check out our https://spring.io/security-policy[security policy].
== JAR signing
Spring Data JARs released on Maven Central are signed.
You'll find more information about the key here: https://spring.io/GPG-KEY-spring.txt
Versions released prior to 2023 may be signed with a different key.
-15
View File
@@ -1,15 +0,0 @@
# Security Policy
## Reporting a Vulnerability
Please [open a draft security advisory](https://github.com/spring-projects/spring-data-elasticsearch/security/advisories/new) if you need to disclose and discuss a security issue in private with the Spring Data team.
Note that we only accept reports against [supported versions](https://spring.io/projects/spring-data#support).
For more details, check out our [security policy](https://spring.io/security-policy).
## JAR signing
Spring Data JARs released on Maven Central are signed.
You'll find more information about the key here: https://spring.io/GPG-KEY-spring.txt
Versions released prior to 2023 may be signed with a different key.
-11
View File
@@ -1,11 +0,0 @@
= Getting support for Spring Data Elasticsearch
Spring Data Elasticsearch is a community-maintained Spring Data project. It is released as part of the Spring Data Release train.
== GitHub issues
We choose not to use GitHub issues for general usage questions and support, preferring to use issues solely for the tracking of bugs and enhancements.
If you have a general usage question, please do not open a GitHub issue but use one of the other channels https://github.com/spring-projects/spring-data-elasticsearch#getting-help[described in the readme file].
If you are reporting a bug, please help to speed up problem diagnosis by providing as much information as possible.
Ideally, that would include a small sample project that reproduces the problem.
+7 -19
View File
@@ -5,12 +5,12 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>6.2.0-SNAPSHOT</version>
<version>6.1.1</version>
<parent>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId>
<version>4.2.0-SNAPSHOT</version>
<version>4.1.1</version>
</parent>
<name>Spring Data Elasticsearch</name>
@@ -18,11 +18,11 @@
<url>https://github.com/spring-projects/spring-data-elasticsearch</url>
<properties>
<springdata.commons>4.2.0-SNAPSHOT</springdata.commons>
<springdata.commons>4.1.1</springdata.commons>
<!-- version of the ElasticsearchClient -->
<elasticsearch-java>9.5.2</elasticsearch-java>
<elasticsearch-rest-client>9.5.2</elasticsearch-rest-client>
<elasticsearch-java>9.4.5</elasticsearch-java>
<elasticsearch-rest-client>9.4.5</elasticsearch-rest-client>
<hoverfly>0.20.2</hoverfly>
<log4j>2.25.4</log4j>
@@ -508,20 +508,8 @@
</profiles>
<repositories>
<repository>
<id>spring-snapshot</id>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<id>spring-milestone</id>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
</project>
@@ -3,7 +3,7 @@
This chapter illustrates configuration and usage of supported Elasticsearch client implementations.
Spring Data Elasticsearch operates upon an Elasticsearch client (provided by Elasticsearch client libraries) that is connected to a single Elasticsearch node, a cluster or Elasticsearch serverless.
Spring Data Elasticsearch operates upon an Elasticsearch client (provided by Elasticsearch client libraries) that is connected to a single Elasticsearch node or a cluster.
Although the Elasticsearch Client can be used directly to work with the cluster, applications using Spring Data Elasticsearch normally use the higher level abstractions of xref:elasticsearch/template.adoc[Elasticsearch Operations] and xref:elasticsearch/repositories/elasticsearch-repositories.adoc[Elasticsearch Repositories].
[[elasticsearch.clients.rest5client]]
@@ -454,22 +454,6 @@ ClientConfiguration.builder()
----
====
[[elasticsearch.clients.servertype]]
== Server Type
There are now 2 types of server that a client could connect to. They are `DEFAULT` which represent all single nodes and clusters; and `SERVERLESS` which represents Elasticsearch new flavour of providing cloud hosted elasticsearch.
If you are going to use Elasticsearch Serverless then you need to specific this so that certain settings are changed and indexes are created correctly.
The javadoc:org.springframework.data.elasticsearch.config.ElasticsearchConfigurationSupport[] class (extended by javadoc:org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration[]) allows the overriding of `serverType()`.
====
[source,java]
----
@Override
ElasticsearchServerType serverType() {
return ElasticsearchServerType.SERVERLESS;
}
----
====
[[elasticsearch.clients.logging]]
== Client Logging
@@ -1,15 +1,10 @@
[[new-features]]
= What's new
[[new-features.6-2-0]]
== New in Spring Data Elasticsearch 6.2
[[new-features.6-1-1]]
== New in Spring Data Elasticsearch 6.1.1
* Upgrade to Elasticsearch 9.5.2
* Allow to provide non-standard (custom) index options for `@Field` / `@InnerField`
* Support Elasticsearch Serverless
* Support propert resolution in index name expressions (https://github.com/spring-projects/spring-data-elasticsearch/issues/3310[GH #3310])
* Fix script handling when building an UpdateQuery (https://github.com/spring-projects/spring-data-elasticsearch/issues/3324[GH #3324])
* Fix formatting of DateFormat.strict_date_optional_time_nanos (https://github.com/spring-projects/spring-data-elasticsearch/issues/3334[GH #3334])
* Upgrade to Elasticsearch 9.4.5
[[new-features.6-1-0]]
== New in Spring Data Elasticsearch 6.1
@@ -22,7 +22,7 @@ The following annotations are available:
* `@Document`: Applied at the class level to indicate this class is a candidate for mapping to the database.
The most important attributes are (check the API documentation for the complete list of attributes):
** `indexName`: the name of the index to store this entity in.
This can contain a SpEL template expression like `"log-#{T(java.time.LocalDate).now().toString()}"` or a property expression like `{index.name}`
This can contain a SpEL template expression like `"log-#{T(java.time.LocalDate).now().toString()}"`
** `createIndex`: flag whether to create an index on repository bootstrapping.
Default value is _true_.
See xref:elasticsearch/repositories/elasticsearch-repositories.adoc#elasticsearch.repositories.autocreation[Automatic creation of indices with the corresponding mapping]
@@ -6,8 +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
| 2026.1 | 6.2.x | 9.5.2 | 7.0.x
| 2026.0 | 6.1.x | 9.4.2 | 7.0.x
| 2026.0 | 6.1.x | 9.4.5 | 7.0.x
| 2025.1 | 6.0.x | 9.2.2 | 7.0.x
| 2025.0 | 5.5.xfootnote:oom[Out of maintenance] | 8.18.1 | 6.2.x
| 2024.1 | 5.4.xfootnote:oom[] | 8.15.5 | 6.1.x
@@ -1,48 +0,0 @@
/*
* Copyright 2026-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.annotations;
import org.springframework.data.elasticsearch.core.index.IndexOptionMapper;
/**
* Represents the custom index option, either not provided by the core (fe, custom plugin) or deviates across different
* engines.
*
* @author Andriy Redko
* @since 6.2
*/
public @interface CustomIndexOption {
/**
* The name of the custom index option
*/
String name();
/**
* The value(s) of the custom index option
*/
String[] values() default {};
/**
* Should the index property be overridden if already present or not
*/
boolean overrideIfPresent() default false;
/**
* The index option mapper that will be used to populate this custom index option
*/
Class<? extends IndexOptionMapper> mapper();
}
@@ -88,7 +88,7 @@ public enum DateFormat {
* @since 5.3
*/
strict_date_optional_time("uuuu-MM-dd['T'HH:mm:ss.SSSXXX]"), //
strict_date_optional_time_nanos("uuuu-MM-dd['T'HH:mm:ss.SSSSSSSSSXXX]"), //
strict_date_optional_time_nanos("uuuu-MM-dd['T'HH:mm:ss.SSSSSSXXX]"), //
date_time("uuuu-MM-dd'T'HH:mm:ss.SSSXXX"), //
/**
* @since 5.3
@@ -252,11 +252,4 @@ public @interface Field {
* @since 5.4
*/
String mappedTypeName() default "";
/**
* adds the custom index options for a particular field
*
* @since 6.2
*/
CustomIndexOption[] customIndexOptions() default {};
}
@@ -179,11 +179,4 @@ public @interface InnerField {
* @since 5.4
*/
String mappedTypeName() default "";
/**
* adds the custom index options for a particular field
*
* @since 6.2
*/
CustomIndexOption[] customIndexOptions() default {};
}
@@ -22,14 +22,12 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.data.annotation.Persistent;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchPersistentEntity;
/**
* Elasticsearch Setting
*
* @author Mohsin Husen
* @author Peter-Josef Meisch
* @author Steven Pearce
*/
@Persistent
@@ -61,10 +59,9 @@ public @interface Setting {
short replicas() default 1;
/**
* Refresh interval for the index. Used for index creation. If no value, defaults are server type dependant and set in
* {@link SimpleElasticsearchPersistentEntity}
* Refresh interval for the index. Used for index creation.
*/
String refreshInterval() default "";
String refreshInterval() default "1s";
/**
* Index storage type for the index. Used for index creation.
@@ -24,18 +24,12 @@ 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 jakarta.json.JsonArray;
import jakarta.json.JsonNumber;
import jakarta.json.JsonObject;
import jakarta.json.JsonString;
import jakarta.json.JsonValue;
import java.math.BigInteger;
import java.util.ArrayList;
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;
@@ -91,8 +85,22 @@ final class DocumentAdapters {
Map<String, Double> matchedQueries = hit.matchedQueries();
EntityAsMap hitFieldsAsMap = new EntityAsMap();
hit.fields().forEach((key, jsonData) -> hitFieldsAsMap.put(key, toJavaObject(jsonData, jsonpMapper)));
Function<Map<String, JsonData>, 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());
Map<String, List<@Nullable Object>> documentFields = new LinkedHashMap<>();
hitFieldsAsMap.forEach((key, value) -> {
@@ -136,61 +144,6 @@ final class DocumentAdapters {
documentFields, highlightFields, innerHits, nestedMetaData, explanation, matchedQueries, hit.routing());
}
@Nullable
private static Object toJavaObject(JsonData jsonData, JsonpMapper jsonpMapper) {
return toJavaObject(jsonData.toJson(jsonpMapper));
}
@Nullable
private static Object toJavaObject(JsonValue jsonValue) {
return switch (jsonValue.getValueType()) {
case OBJECT -> toMap(jsonValue.asJsonObject());
case ARRAY -> toList(jsonValue.asJsonArray());
case STRING -> ((JsonString) jsonValue).getString();
case NUMBER -> toNumber((JsonNumber) jsonValue);
case TRUE -> Boolean.TRUE;
case FALSE -> Boolean.FALSE;
case NULL -> null;
};
}
private static Map<String, @Nullable Object> toMap(JsonObject jsonObject) {
Map<String, @Nullable Object> result = new LinkedHashMap<>();
jsonObject.forEach((key, value) -> result.put(key, toJavaObject(value)));
return result;
}
private static List<@Nullable Object> toList(JsonArray jsonArray) {
List<@Nullable Object> result = new ArrayList<>(jsonArray.size());
jsonArray.forEach(value -> result.add(toJavaObject(value)));
return result;
}
private static Number toNumber(JsonNumber jsonNumber) {
if (!jsonNumber.isIntegral()) {
return jsonNumber.doubleValue();
}
try {
return jsonNumber.intValueExact();
} catch (ArithmeticException ignored) {
// continue with a wider numeric type
}
try {
return jsonNumber.longValueExact();
} catch (ArithmeticException ignored) {
// continue with a wider numeric type
}
BigInteger value = jsonNumber.bigIntegerValue();
return value;
}
public static SearchDocument from(CompletionSuggestOption<EntityAsMap> completionSuggestOption) {
Document document = completionSuggestOption.source() != null ? Document.from(completionSuggestOption.source())
@@ -70,11 +70,11 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -234,8 +234,7 @@ class RequestConverter extends AbstractQueryProcessor {
for (Alias alias : indexSettings.getAliases()) {
co.elastic.clients.elasticsearch.indices.Alias esAlias = co.elastic.clients.elasticsearch.indices.Alias
.of(ab -> {
co.elastic.clients.elasticsearch.indices.Alias.Builder aliasBuilder = ab
.filter(getQuery(alias.getFilter(), null))
co.elastic.clients.elasticsearch.indices.Alias.Builder aliasBuilder = ab.filter(getQuery(alias.getFilter(), null))
.isHidden(alias.getHidden())
.isWriteIndex(alias.getWriteIndex());
getRouting(alias.getRouting()).ifPresent(aliasBuilder::routing);
@@ -808,7 +807,7 @@ class RequestConverter extends AbstractQueryProcessor {
Assert.notNull(indexCoordinates, "indexCoordinates must not be null");
return GetRequest.of(grb -> {
GetRequest.Builder builder = grb //
GetRequest.Builder builder = grb //
.index(indexCoordinates.getIndexName()) //
.id(id); //
getRouting(routing).ifPresent(builder::routing);
@@ -847,7 +846,7 @@ class RequestConverter extends AbstractQueryProcessor {
List<MultiGetOperation> multiGetOperations = query.getIdsWithRouting().stream()
.map(idWithRouting -> MultiGetOperation.of(mgo -> {
MultiGetOperation.Builder builder = mgo //
MultiGetOperation.Builder builder = mgo //
.index(index.getIndexName()) //
.id(idWithRouting.id()) //
.source(sourceConfig);
@@ -1072,7 +1071,29 @@ class RequestConverter extends AbstractQueryProcessor {
return UpdateRequest.of(uqb -> {
uqb.index(indexName).id(query.getId());
uqb.script(getScript(query.getScriptData()));
var scriptData = query.getScriptData();
var script = scriptData != null ? scriptData.script() : null;
if (script != null) {
Map<String, JsonData> params = new HashMap<>();
if (query.getParams() != null) {
query.getParams().forEach((key, value) -> params.put(key, JsonData.of(value, jsonpMapper)));
}
uqb.script(sb -> {
sb
.lang(scriptData.language())
.params(params);
if (script != null) {
sb.source(s -> s.scriptString(script));
}
sb.id(scriptData.scriptName());
return sb;
});
}
uqb
.doc(query.getDocument())
@@ -1959,7 +1980,6 @@ class RequestConverter extends AbstractQueryProcessor {
}
return Optional.empty();
}
Optional<String> getRouting(@Nullable String routing1, @Nullable String routing2) {
return getRouting(routing1).or(() -> getRouting(routing2));
}
@@ -42,7 +42,6 @@ import org.springframework.util.StringUtils;
/**
* @author Christoph Strobl
* @author Peter-Josef Meisch
* @author Steven Pearce
* @since 3.2
*/
@Configuration(proxyBeanMethods = false)
@@ -74,7 +73,6 @@ public class ElasticsearchConfigurationSupport {
mappingContext.setSimpleTypeHolder(elasticsearchCustomConversions.getSimpleTypeHolder());
mappingContext.setFieldNamingStrategy(fieldNamingStrategy());
mappingContext.setWriteTypeHints(writeTypeHints());
mappingContext.setServerType(serverType());
return mappingContext;
}
@@ -176,7 +174,7 @@ public class ElasticsearchConfigurationSupport {
}
/**
* Flag specifying if type hints (_class fields) should be written in the index. It is strongly advised to keep the
* Flag specifiying if type hints (_class fields) should be written in the index. It is strongly advised to keep the
* default value of {@literal true}. If you need to write to an existing index that does not have a mapping defined
* for these fields and that has a strict mapping set, then it might be necessary to disable type hints. But notice
* that in this case reading polymorphic types may fail.
@@ -187,15 +185,4 @@ public class ElasticsearchConfigurationSupport {
protected boolean writeTypeHints() {
return true;
}
/**
* Configures the {@link ElasticsearchServerType} to use when creating indexes. The Default value will support the
* standard configurations, and SERVERLESS will support ElasticSearch Serverless
*
* @return the {@link ElasticsearchServerType} to use
* @since 6.2
*/
protected ElasticsearchServerType serverType() {
return ElasticsearchServerType.DEFAULT;
}
}
@@ -1,37 +0,0 @@
/*
* Copyright 2026-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.config;
/**
* ElasticsearchServerType defines the type of Elasticsearch server you are connecting to.
* @see #DEFAULT
* @see #SERVERLESS
*
* @author Steven Pearce
* @since 6.2
*/
public enum ElasticsearchServerType {
/**
* Normal installations of Elasticsearch including cloud-hosted Elasticsearch
*/
DEFAULT,
/**
* New Flavour of Elasticsearch, offered by Elastic
*/
SERVERLESS,
}
@@ -104,8 +104,9 @@ public class MappingElasticsearchConverter
protected @Nullable Environment environment;
private final SpELContext spELContext = new SpELContext(new MapAccessor());
private final SpelExpressionParser expressionParser = new SpelExpressionParser();
private final CachingValueExpressionEvaluatorFactory expressionEvaluatorFactory = new CachingValueExpressionEvaluatorFactory(
new SpelExpressionParser(), this, spELContext);
expressionParser, this, spELContext);
private final EntityInstantiators instantiators = new EntityInstantiators();
private final ElasticsearchTypeMapper typeMapper;
@@ -1,31 +0,0 @@
/*
* Copyright 2026-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.index;
import tools.jackson.databind.node.ObjectNode;
import org.springframework.data.elasticsearch.annotations.CustomIndexOption;
/**
* Writes the particular instance of the {@code CustomIndexOption} into the mapping, represented by objectNode.
*
* @author Andriy Redko
* @since 6.2
*/
public interface IndexOptionMapper {
void writeIndexOptionTo(CustomIndexOption indexOption, ObjectNode objectNode);
}
@@ -1,301 +0,0 @@
/*
* Copyright 2026-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.index;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.BigIntegerNode;
import tools.jackson.databind.node.BooleanNode;
import tools.jackson.databind.node.DecimalNode;
import tools.jackson.databind.node.NullNode;
import tools.jackson.databind.node.ObjectNode;
import tools.jackson.databind.node.StringNode;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Objects;
import org.jspecify.annotations.Nullable;
import org.springframework.data.elasticsearch.annotations.CustomIndexOption;
/**
* The collection of predefined {@link IndexOptionMapper}s
*
* @author Andriy Redko
* @since 6.2
*/
public final class IndexOptionMappers {
private IndexOptionMappers() {}
/**
* Writes the {@link CustomIndexOption} instance into mapping as JSON string (or array of strings) property
*/
public static final class StringMapper implements IndexOptionMapper {
@Override
public void writeIndexOptionTo(CustomIndexOption indexOption, ObjectNode objectNode) {
final String[] values = Objects.requireNonNull(indexOption.values(), "Values are required");
if (values.length == 1) {
writePropertyAsString(indexOption.name(), indexOption.overrideIfPresent(), values[0], objectNode);
} else if (values.length > 1) {
writePropertyAsArray(indexOption.name(), indexOption.overrideIfPresent(), values, objectNode);
} else {
removeProperty(indexOption.name(), objectNode);
}
}
}
/**
* Writes the {@link CustomIndexOption} instance into mapping as JSON number (or array of numbers) property
*/
public static final class NumberMapper implements IndexOptionMapper {
@Override
public void writeIndexOptionTo(CustomIndexOption indexOption, ObjectNode objectNode) {
final @Nullable Number[] values = toNumbers(Objects.requireNonNull(indexOption.values(), "Values are required"));
if (values.length == 1) {
writePropertyAsNumber(indexOption.name(), indexOption.overrideIfPresent(), values[0], objectNode);
} else if (values.length > 1) {
writePropertyAsArray(indexOption.name(), indexOption.overrideIfPresent(), values, objectNode);
} else {
removeProperty(indexOption.name(), objectNode);
}
}
}
/**
* Writes the {@link CustomIndexOption} instance into mapping as JSON boolean (or array of booleans) property
*/
public static final class BooleanMapper implements IndexOptionMapper {
@Override
public void writeIndexOptionTo(CustomIndexOption indexOption, ObjectNode objectNode) {
final @Nullable Boolean[] values = toBoolean(Objects.requireNonNull(indexOption.values(), "Values are required"));
if (values.length == 1) {
writePropertyAsBoolean(indexOption.name(), indexOption.overrideIfPresent(), values[0], objectNode);
} else if (values.length > 1) {
writePropertyAsArray(indexOption.name(), indexOption.overrideIfPresent(), values, objectNode);
} else {
removeProperty(indexOption.name(), objectNode);
}
}
}
/**
* Convert the array of strings to array of booleans.
*
* @param values array of strings
* @return array of booleans
*/
private static @Nullable Boolean[] toBoolean(@Nullable String[] values) {
return Arrays.stream(values).map(Boolean::valueOf).toArray(Boolean[]::new);
}
/**
* Convert the array of strings to array of numbers, throwing {@link NumberFormatException} if the conversion is not
* possible.
*
* @param values array of strings
* @return array of numbers
* @throws NumberFormatException
*/
private static @Nullable Number[] toNumbers(@Nullable String[] values) {
final @Nullable Number[] numbers = new Number[values.length];
for (int j = 0; j < values.length; ++j) {
Number number = null;
var value = values[j];
if (value != null) {
try {
number = new BigInteger(value);
} catch (NumberFormatException e) {
number = new BigDecimal(value);
}
}
numbers[j] = number;
}
return numbers;
}
/**
* Writes a property as a JSON boolean value.
*
* @param name property name
* @param override override if present
* @param value property value
* @param objectNode JSON object node
*/
private static void writePropertyAsBoolean(String name, boolean override, @Nullable Boolean value,
ObjectNode objectNode) {
final JsonNode node = BooleanNode.valueOf(value);
if (objectNode.has(name) && override) {
objectNode.replace(name, node);
} else {
objectNode.putIfAbsent(name, node);
}
}
/**
* Writes a property as a JSON number value.
*
* @param name property name
* @param override override if present
* @param value property value
* @param objectNode JSON object node
*/
private static void writePropertyAsNumber(String name, boolean override, @Nullable Number value,
ObjectNode objectNode) {
final JsonNode node = toJsonNode(value);
if (objectNode.has(name) && override) {
objectNode.replace(name, node);
} else {
objectNode.putIfAbsent(name, node);
}
}
/**
* Writes a property as a JSON string value.
*
* @param name property name
* @param override override if present
* @param value property value
* @param objectNode JSON object node
*/
private static void writePropertyAsString(String name, boolean override, String value, ObjectNode objectNode) {
final StringNode node = StringNode.valueOf(value);
if (objectNode.has(name) && override) {
objectNode.replace(name, node);
} else {
objectNode.putIfAbsent(name, node);
}
}
/**
* Removes the property if present.
*
* @param name property name
* @param objectNode JSON object node
*/
private static void removeProperty(String name, ObjectNode objectNode) {
if (objectNode.has(name)) {
objectNode.remove(name);
}
}
/**
* Writes a property as a JSON array of boolean values.
*
* @param name property name
* @param override override if present
* @param values property values
* @param objectNode JSON object node
*/
private static void writePropertyAsArray(String name, boolean override, @Nullable Boolean[] values,
ObjectNode objectNode) {
writePropertyAsArray(
name,
override,
objectNode
.arrayNode()
.addAll(
Arrays.stream(values).map(b -> b != null ? BooleanNode.valueOf(b) : NullNode.getInstance()).toList()),
objectNode);
}
/**
* Writes a property as a JSON array of number values.
*
* @param name property name
* @param override override if present
* @param values property values
* @param objectNode JSON object node
*/
private static void writePropertyAsArray(String name, boolean override, @Nullable Number[] values,
ObjectNode objectNode) {
writePropertyAsArray(
name,
override,
objectNode
.arrayNode()
.addAll(Arrays.stream(values).map(IndexOptionMappers::toJsonNode).toList()),
objectNode);
}
/**
* Writes a property as a JSON array of string values.
*
* @param name property name
* @param override override if present
* @param values property values
* @param objectNode JSON object node
*/
private static void writePropertyAsArray(String name, boolean override, String[] values, ObjectNode objectNode) {
writePropertyAsArray(
name,
override,
objectNode
.arrayNode()
.addAll(Arrays.stream(values).map(StringNode::valueOf).toList()),
objectNode);
}
/**
* Writes a property as a JSON array.
*
* @param name property name
* @param override override if present
* @param arrayNode JSON array
* @param objectNode JSON object node
*/
private static void writePropertyAsArray(String name, boolean override, ArrayNode arrayNode, ObjectNode objectNode) {
if (objectNode.has(name) && override) {
objectNode.replace(name, arrayNode);
} else {
objectNode.putIfAbsent(name, arrayNode);
}
}
/**
* Converts a number into appropriate JSON node. Only {@link DecimalNode} and {@link BigInteger} types are supported.
*
* @param value {@link Number} instance to convert
* @throws IllegalArgumentException
*/
private static JsonNode toJsonNode(@Nullable Number value) {
if (value == null) {
return NullNode.getInstance();
}
if (value instanceof BigDecimal d) {
return DecimalNode.valueOf(d);
} else if (value instanceof BigInteger i) {
return BigIntegerNode.valueOf(i);
} else {
// We only support BigDecimal or BigInteger numeric values, should never happen since the
// conversion is constrained within this class only.
throw new IllegalArgumentException("Only BigDecimal or BigInteger numbers are supported");
}
}
}
@@ -27,7 +27,6 @@ import java.util.List;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.BeanUtils;
import org.springframework.data.elasticsearch.annotations.*;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -121,7 +120,6 @@ public class MappingParameters {
private final TermVector termVector;
private final FieldType type;
private final String mappedTypeName;
private final CustomIndexOption[] customIndexOptions;
/**
* extracts the mapping parameters from the relevant annotations.
@@ -187,7 +185,6 @@ public class MappingParameters {
Assert.isTrue(field.enabled() || type == FieldType.Object, "enabled false is only allowed for field type object");
enabled = field.enabled();
eagerGlobalOrdinals = field.eagerGlobalOrdinals();
customIndexOptions = field.customIndexOptions();
}
protected MappingParameters(InnerField field) {
@@ -234,7 +231,6 @@ public class MappingParameters {
knnIndexOptions = field.knnIndexOptions().length > 0 ? field.knnIndexOptions()[0] : null;
enabled = true;
eagerGlobalOrdinals = field.eagerGlobalOrdinals();
customIndexOptions = field.customIndexOptions();
}
public boolean isStore() {
@@ -423,12 +419,6 @@ public class MappingParameters {
if (eagerGlobalOrdinals) {
objectNode.put(FIELD_PARAM_EAGER_GLOBAL_ORDINALS, eagerGlobalOrdinals);
}
// At last, check the custom index options
for (CustomIndexOption customIndexOption: customIndexOptions) {
final IndexOptionMapper mapper = BeanUtils.instantiateClass(customIndexOption.mapper());
mapper.writeIndexOptionTo(customIndexOption, objectNode);
}
}
protected String analyzer() {
@@ -558,8 +548,4 @@ public class MappingParameters {
protected String mappedTypeName() {
return mappedTypeName;
}
protected CustomIndexOption[] customIndexOptions() {
return customIndexOptions;
}
}
@@ -21,7 +21,6 @@ import org.jspecify.annotations.Nullable;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Dynamic;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.config.ElasticsearchServerType;
import org.springframework.data.elasticsearch.core.index.Settings;
import org.springframework.data.elasticsearch.core.join.JoinField;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
@@ -40,7 +39,6 @@ import org.springframework.data.mapping.model.FieldNamingStrategy;
* @author Peter-Josef Meisch
* @author Roman Puchkovskiy
* @author Subhobrata Dey
* @author Steven Pearce
*/
public interface ElasticsearchPersistentEntity<T> extends PersistentEntity<T, ElasticsearchPersistentProperty> {
@@ -181,12 +179,6 @@ public interface ElasticsearchPersistentEntity<T> extends PersistentEntity<T, El
*/
boolean writeTypeHints();
/**
* @return the {@link ElasticsearchServerType} for the server that stores this entity
* @since 6.2
*/
ElasticsearchServerType getServerType();
/**
* @return the {@code dynamic} mapping parameter value.
* @since 4.3
@@ -18,7 +18,6 @@ package org.springframework.data.elasticsearch.core.mapping;
import org.jspecify.annotations.Nullable;
import org.springframework.data.core.TypeInformation;
import org.springframework.data.elasticsearch.config.ElasticsearchServerType;
import org.springframework.data.mapping.context.AbstractMappingContext;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.Property;
@@ -32,7 +31,6 @@ import org.springframework.data.mapping.model.SimpleTypeHolder;
* @author Mohsin Husen
* @author Mark Paluch
* @author Peter-Josef Meisch
* @author Steven Pearce
*/
public class SimpleElasticsearchMappingContext
extends AbstractMappingContext<SimpleElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> {
@@ -41,7 +39,6 @@ public class SimpleElasticsearchMappingContext
private FieldNamingStrategy fieldNamingStrategy = DEFAULT_NAMING_STRATEGY;
private boolean writeTypeHints = true;
private ElasticsearchServerType serverType = ElasticsearchServerType.DEFAULT;
/**
* Configures the {@link FieldNamingStrategy} to be used to determine the field name if no manual mapping is applied.
@@ -64,18 +61,6 @@ public class SimpleElasticsearchMappingContext
this.writeTypeHints = writeTypeHints;
}
/**
* Sets the ElasticSearch Server Type
*
* @param serverType the {@link ElasticsearchServerType} to be used to set the server Type. The Default value will
* support the standard configurations, and SERVERLESS will support ElasticSearch Serverless
*
* @since 6.2
*/
public void setServerType(@Nullable ElasticsearchServerType serverType) {
this.serverType = serverType == null ? ElasticsearchServerType.DEFAULT : serverType;
}
@Override
protected boolean shouldCreatePersistentEntityFor(TypeInformation<?> type) {
return !ElasticsearchSimpleTypes.HOLDER.isSimpleType(type.getType());
@@ -84,7 +69,7 @@ public class SimpleElasticsearchMappingContext
@Override
protected <T> SimpleElasticsearchPersistentEntity<?> createPersistentEntity(TypeInformation<T> typeInformation) {
return new SimpleElasticsearchPersistentEntity<>(typeInformation,
new SimpleElasticsearchPersistentEntity.ContextConfiguration(fieldNamingStrategy, writeTypeHints, serverType));
new SimpleElasticsearchPersistentEntity.ContextConfiguration(fieldNamingStrategy, writeTypeHints));
}
@Override
@@ -33,26 +33,24 @@ import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.Routing;
import org.springframework.data.elasticsearch.annotations.Setting;
import org.springframework.data.elasticsearch.config.ElasticsearchServerType;
import org.springframework.data.elasticsearch.core.index.Settings;
import org.springframework.data.elasticsearch.core.join.JoinField;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.utils.spel.ExpressionUtils;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.data.util.Lazy;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionException;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Elasticsearch specific {@link org.springframework.data.mapping.PersistentEntity} implementation holding
@@ -66,15 +64,15 @@ import org.springframework.util.StringUtils;
* @author Peter-Josef Meisch
* @author Roman Puchkovskiy
* @author Subhobrata Dey
* @author Steven Pearce
*/
public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntity<T, ElasticsearchPersistentProperty>
implements ElasticsearchPersistentEntity<T> {
private static final Log LOGGER = LogFactory.getLog(SimpleElasticsearchPersistentEntity.class);
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
private @Nullable final Document document;
private final String unresolvedIndexName;
private @Nullable String indexName;
private final Lazy<SettingsParameter> settingsParameter;
private @Nullable ElasticsearchPersistentProperty seqNoPrimaryTermProperty;
private @Nullable ElasticsearchPersistentProperty joinFieldProperty;
@@ -84,9 +82,14 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
private final boolean alwaysWriteMapping;
private final Dynamic dynamic;
private final Map<String, ElasticsearchPersistentProperty> fieldNamePropertyCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Expression> routingExpressions = new ConcurrentHashMap<>();
private @Nullable String routing;
private final ContextConfiguration contextConfiguration;
private final Set<Alias> aliases = new HashSet<>();
private final ConcurrentHashMap<String, Expression> indexNameExpressions = new ConcurrentHashMap<>();
private final Lazy<EvaluationContext> indexNameEvaluationContext = Lazy.of(this::getIndexNameEvaluationContext);
private final boolean storeIdInSource;
private final boolean storeVersionInSource;
@@ -105,10 +108,10 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
this.settingsParameter = Lazy.of(() -> buildSettingsParameter(clazz));
if (document != null) {
Assert.hasText(document.indexName(),
" Unknown indexName. Make sure the indexName is defined. e.g @Document(indexName=\"foo\")");
this.unresolvedIndexName = document.indexName();
this.indexName = document.indexName();
this.versionType = document.versionType();
this.createIndexAndMapping = document.createIndex();
this.alwaysWriteMapping = document.alwaysWriteMapping();
@@ -117,7 +120,6 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
this.storeVersionInSource = document.storeVersionInSource();
buildAliases();
} else {
this.unresolvedIndexName = getTypeInformation().getType().getSimpleName();
this.dynamic = Dynamic.INHERIT;
this.storeIdInSource = true;
this.storeVersionInSource = true;
@@ -134,9 +136,13 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
}
}
private String getIndexName() {
return indexName != null ? indexName : getTypeInformation().getType().getSimpleName();
}
@Override
public IndexCoordinates getIndexCoordinates() {
return resolve(IndexCoordinates.of(unresolvedIndexName));
return resolve(IndexCoordinates.of(getIndexName()));
}
@Override
@@ -168,7 +174,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
@Nullable
@Override
public String getRefreshInterval() {
return settingsParameter.get().refreshInterval;
return settingsParameter.get().refreshIntervall;
}
@Override
@@ -190,11 +196,6 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
return contextConfiguration.getFieldNamingStrategy();
}
@Override
public ElasticsearchServerType getServerType() {
return contextConfiguration.getServerType();
}
@Override
public boolean writeTypeHints() {
@@ -367,55 +368,52 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
Assert.notNull(name, "name must not be null");
ValueExpression expression = ExpressionUtils.detectExpression(name);
Expression expression = getExpressionForIndexName(name);
Object resolvedName;
if (expression != null && !expression.isLiteral()) {
var valueEvaluationContext = getValueEvaluationContext(name);
resolvedName = expression.evaluate(valueEvaluationContext);
} else {
resolvedName = null;
}
return resolvedName != null ? ObjectUtils.nullSafeToString(resolvedName) : name;
String resolvedName = expression != null ? expression.getValue(indexNameEvaluationContext.get(), String.class)
: null;
return resolvedName != null ? resolvedName : name;
}
/**
* build the {@link ValueEvaluationContext} from the expression.
* returns an {@link Expression} for #name if name contains a {@link ParserContext#TEMPLATE_EXPRESSION} otherwise
* returns {@literal null}.
*
* @return ValueEvaluationContext
* @param expression
* @param name the name to get the expression for
* @return Expression may be null
*/
private ValueEvaluationContext getValueEvaluationContext(@Nullable String expression) {
ValueExpression valueExpression = ExpressionUtils.detectExpression(expression);
return getValueEvaluationContext(valueExpression);
@Nullable
private Expression getExpressionForIndexName(String name) {
return indexNameExpressions.computeIfAbsent(name, s -> {
Expression expr = PARSER.parseExpression(s, ParserContext.TEMPLATE_EXPRESSION);
return expr instanceof LiteralExpression ? null : expr;
});
}
/**
* build the {@link ValueEvaluationContext} considering {@link ExpressionDependencies} from the expression.
* build the {@link EvaluationContext} considering {@link ExpressionDependencies} from the name returned by
* {@link #getIndexName()}.
*
* @return ValueEvaluationContext
* @param expression
* @return EvaluationContext
*/
private ValueEvaluationContext getValueEvaluationContext(@Nullable ValueExpression expression) {
private EvaluationContext getIndexNameEvaluationContext() {
var expressionDependencies = expression != null ? expression.getExpressionDependencies() : null;
Expression expression = getExpressionForIndexName(getIndexName());
ExpressionDependencies expressionDependencies = expression != null ? ExpressionDependencies.discover(expression)
: ExpressionDependencies.none();
return expressionDependencies != null ? getValueEvaluationContext(null, expressionDependencies)
: getValueEvaluationContext((Object) null);
// noinspection ConstantConditions
return getEvaluationContext(null, expressionDependencies);
}
@Override
@Nullable
public String resolveRouting(T bean) {
// we need a @Routing annotation on the entity
if (routing == null) {
return null;
}
// if there is a property with name of of the @Routing annotation value, return the property's value
ElasticsearchPersistentProperty persistentProperty = getPersistentProperty(routing);
if (persistentProperty != null) {
@@ -424,29 +422,14 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
return propertyValue != null ? propertyValue.toString() : null;
}
// try to resolve the annotations value
try {
var routingValue = routing;
ValueExpression expression = ExpressionUtils.detectExpression(routingValue);
Expression expression = routingExpressions.computeIfAbsent(routing, PARSER::parseExpression);
ExpressionDependencies expressionDependencies = ExpressionDependencies.discover(expression);
if (expression == null) {
return null;
}
EvaluationContext context = getEvaluationContext(null, expressionDependencies);
context.setVariable("entity", bean);
if (expression.isLiteral()) {
// before using ValueExpressions, we had SpEL expressions without the "#{...},
// so try if we get a valid expression after wrapping that
routingValue = "#{" + routing + "}";
expression = ExpressionUtils.detectExpression(routingValue);
if (expression == null) {
return null;
}
}
ValueEvaluationContext context = getValueEvaluationContext(routingValue);
context.getEvaluationContext().setVariable("entity", bean);
return ObjectUtils.nullSafeToString(expression.evaluate(context));
return expression.getValue(context, String.class);
} catch (EvaluationException e) {
throw new InvalidDataAccessApiUsageException(
"Could not resolve expression: " + routing + " for object of class " + bean.getClass().getCanonicalName(), e);
@@ -464,13 +447,9 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
}
try {
ValueExpression expression = ExpressionUtils.detectExpression(settingPathFromParameter);
if (expression == null || expression instanceof LiteralExpression) {
return settingPathFromParameter;
}
return ObjectUtils.nullSafeToString(expression.evaluate(getValueEvaluationContext(settingPathFromParameter)));
Expression expression = PARSER.parseExpression(settingPathFromParameter, ParserContext.TEMPLATE_EXPRESSION);
return (expression instanceof LiteralExpression) ? settingPathFromParameter
: expression.getValue(getEvaluationContext(null, ExpressionDependencies.discover(expression)), String.class);
} catch (ExpressionException e) {
throw new InvalidDataAccessApiUsageException(
"Could not resolve expression: " + settingPathFromParameter + " for @Setting.settingPath ", e);
@@ -489,13 +468,9 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
// default values
settingsParameter.useServerConfiguration = false;
settingsParameter.serverType = contextConfiguration.serverType;
settingsParameter.shards = 1;
settingsParameter.replicas = 1;
settingsParameter.refreshInterval = switch (contextConfiguration.serverType) {
case DEFAULT -> "1s";
case SERVERLESS -> "5s";
};
settingsParameter.refreshIntervall = "1s";
if (settingAnnotation != null) {
processSettingAnnotation(settingAnnotation, settingsParameter);
@@ -509,9 +484,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
settingsParameter.settingPath = settingAnnotation.settingPath();
settingsParameter.shards = settingAnnotation.shards();
settingsParameter.replicas = settingAnnotation.replicas();
if (StringUtils.hasText(settingAnnotation.refreshInterval())) {
settingsParameter.refreshInterval = settingAnnotation.refreshInterval();
}
settingsParameter.refreshIntervall = settingAnnotation.refreshInterval();
settingsParameter.indexStoreType = settingAnnotation.indexStoreType();
String[] sortFields = settingAnnotation.sortFields();
@@ -587,11 +560,10 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
*/
private static class SettingsParameter {
boolean useServerConfiguration = false;
ElasticsearchServerType serverType = ElasticsearchServerType.DEFAULT;
@Nullable String settingPath;
short shards;
short replicas;
@Nullable String refreshInterval;
@Nullable String refreshIntervall;
@Nullable String indexStoreType;
private String @Nullable [] sortFields;
private Setting.@Nullable SortOrder @Nullable [] sortOrders;
@@ -604,16 +576,12 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
return new Settings();
}
var index = new Settings();
var index = new Settings() //
.append("number_of_shards", String.valueOf(shards)) //
.append("number_of_replicas", String.valueOf(replicas));
if (ElasticsearchServerType.DEFAULT.equals(serverType)) {
index
.append("number_of_shards", String.valueOf(shards))
.append("number_of_replicas", String.valueOf(replicas));
}
if (refreshInterval != null) {
index.append("refresh_interval", refreshInterval);
if (refreshIntervall != null) {
index.append("refresh_interval", refreshIntervall);
}
if (indexStoreType != null && !"fs".equals(indexStoreType)) {
@@ -651,17 +619,10 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
private final FieldNamingStrategy fieldNamingStrategy;
private final boolean writeTypeHints;
private final ElasticsearchServerType serverType;
ContextConfiguration(FieldNamingStrategy fieldNamingStrategy, boolean writeTypeHints) {
this(fieldNamingStrategy, writeTypeHints, ElasticsearchServerType.DEFAULT);
}
ContextConfiguration(FieldNamingStrategy fieldNamingStrategy, boolean writeTypeHints,
ElasticsearchServerType serverType) {
this.fieldNamingStrategy = fieldNamingStrategy;
this.writeTypeHints = writeTypeHints;
this.serverType = serverType;
}
public FieldNamingStrategy getFieldNamingStrategy() {
@@ -671,10 +632,6 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
public boolean getWriteTypeHints() {
return writeTypeHints;
}
public ElasticsearchServerType getServerType() {
return serverType;
}
}
@Override
@@ -24,7 +24,7 @@ import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.repository.query.parser.PartTree;
/**
* A repository query that is built from the method name in the repository definition. Was originally named
* A repository query that is built from the the method name in the repository definition. Was originally named
* ElasticsearchPartQuery.
*
* @author Rizwan Idrees
@@ -1,83 +0,0 @@
/*
* Copyright 2021-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.utils.spel;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Contract;
import org.springframework.util.ConcurrentLruCache;
import org.springframework.util.StringUtils;
/**
* Internal utility class for dealing with {@link ValueExpression} and potential ones. Shamelessly copied from Spring
* Data MongoDB. Adapted afterwards to our needs.
*
* @author Christoph Strobl
* @author Peter-Josef Meisch
* @since 6.2
*/
public final class ExpressionUtils {
private static final ValueExpressionParser PARSER = ValueExpressionParser.create(SpelExpressionParser::new);
public static final int CAPACITY = 256;
private static final ConcurrentLruCache<String, ValueExpression> expressionCache = new ConcurrentLruCache<>(
CAPACITY, PARSER::parse);
/**
* Returns a SpEL {@link ValueExpression} if the given {@link String} is not empty. ValueExpressions are stored in a
* cash.
*
* @param potentialExpression can be {@literal null}
* @return {@link ValueExpression} or null when input is empty
*/
@Contract("null -> null")
public static @Nullable ValueExpression detectExpression(@Nullable String potentialExpression) {
if (!StringUtils.hasText(potentialExpression)) {
return null;
}
return expressionCache.get(potentialExpression);
}
/**
* evbaluates value against the context provided by valueEvaluationContextSupplier. If value cannot be transformed
* into a {@link ValueExpression}, the defaultValue is returned.
*
* @param value the value to evaluate
* @param valueEvaluationContextSupplier supplies the context to use for evaluation
* @param defaultValue
* @return
*/
public static @Nullable Object evaluate(String value, Supplier<ValueEvaluationContext> valueEvaluationContextSupplier,
@Nullable Object defaultValue) {
ValueExpression expression = detectExpression(value);
if (expression == null) {
return defaultValue;
}
var evaluated = expression.evaluate(valueEvaluationContextSupplier.get());
return evaluated == null ? defaultValue : evaluated;
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
Spring Data Elasticsearch 6.2.0-M1 (2026.1.0)
Spring Data Elasticsearch 6.1.1 (2026.0.1)
Copyright (c) [2013-2026] Pivotal Software, Inc.
This product is licensed to you under the Apache License, Version 2.0 (the "License").
@@ -19,7 +19,6 @@ import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import jakarta.json.JsonValue;
import java.util.Arrays;
import java.util.Collections;
@@ -78,49 +77,6 @@ class DocumentAdaptersUnitTests {
softly.assertAll();
}
@Test // #3178
@DisplayName("should adapt parser backed search Hit fields to Java types")
void shouldAdaptParserBackedSearchHitFieldsToJavaTypes() {
Hit<EntityAsMap> searchHit = new Hit.Builder<EntityAsMap>() //
.index("index") //
.id("my-id") //
.fields("objectField", JsonData.fromJson(
"{\"string\":\"value\",\"integer\":2,\"decimal\":1.5,\"nested\":{\"flag\":true}}")) //
.fields("listField", JsonData.fromJson("[\"listValue\",{\"nested\":3},true,null]")) //
.fields("stringField", JsonData.fromJson("\"stringValue\"")) //
.build(); //
SearchDocument document = DocumentAdapters.from(searchHit, jsonpMapper);
SoftAssertions softly = new SoftAssertions();
Object objectFieldValue = document.get("objectField");
Object listFieldValue = document.get("listField");
Object objectFieldFirstValue = document.getFieldValue("objectField");
Object listFieldFirstValue = document.getFieldValue("listField");
softly.assertThat(objectFieldValue).isInstanceOf(Map.class).isNotInstanceOf(JsonValue.class);
softly.assertThat(listFieldValue).isInstanceOf(List.class).isNotInstanceOf(JsonValue.class);
softly.assertThat(document.get("stringField")).isEqualTo("stringValue");
softly.assertThat(objectFieldFirstValue).isInstanceOf(Map.class).isNotInstanceOf(JsonValue.class);
softly.assertThat(listFieldFirstValue).isEqualTo("listValue");
// noinspection unchecked
Map<String, Object> objectField = (Map<String, Object>) objectFieldValue;
softly.assertThat(objectField.get("string")).isEqualTo("value");
softly.assertThat(objectField.get("integer")).isEqualTo(2);
softly.assertThat(objectField.get("decimal")).isEqualTo(1.5d);
softly.assertThat(objectField.get("nested")).isInstanceOf(Map.class).isNotInstanceOf(JsonValue.class);
// noinspection unchecked
List<Object> listField = (List<Object>) listFieldValue;
softly.assertThat(listField).containsExactly("listValue", Collections.singletonMap("nested", 3), true, null);
softly.assertThat(listField.get(1)).isNotInstanceOf(JsonValue.class);
softly.assertAll();
}
@Test // #1973
@DisplayName("should adapt search Hit from source")
void shouldAdaptSearchHitFromSource() {
@@ -16,10 +16,6 @@
package org.springframework.data.elasticsearch.client.elc;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assertions.fail;
import static org.assertj.core.api.Assertions.fail;
import static org.assertj.core.api.Assertions.fail;
import static org.assertj.core.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -135,28 +131,10 @@ class RequestConverterTest {
.documentUpdateRequest(updateQuery, IndexCoordinates.of("foo"), null, null);
assertThat(updateRequest.id()).isEqualTo("queryId");
assertThat(updateRequest.script()).isNotNull();
assertThat(updateRequest.script().source().scriptString()).isEqualTo("script");
assertThat(updateRequest.script().id()).isEqualTo("scriptName");
}
@Test // #3324
@DisplayName("should allow update script with only an id and no script code")
void shouldAllowUpdateScriptWithOnlyAnIdAndNoScriptCode() {
var updateQuery = UpdateQuery
.builder("queryId")
.withScriptName("scriptName")
.build();
UpdateRequest<org.springframework.data.elasticsearch.core.document.Document, ?> updateRequest = requestConverter
.documentUpdateRequest(updateQuery, IndexCoordinates.of("foo"), null, null);
assertThat(updateRequest.id()).isEqualTo("queryId");
assertThat(updateRequest.script()).isNotNull();
assertThat(updateRequest.script().source()).isNull();
assertThat(updateRequest.script().id()).isEqualTo("scriptName");
}
@Test
void getRouting() {
@@ -170,4 +148,5 @@ class RequestConverterTest {
assertEquals("5", requestConverter.getRouting(null, "5").get());
}
}
@@ -27,8 +27,7 @@ import org.springframework.data.elasticsearch.annotations.DateFormat;
*/
class ElasticsearchDateConverterUnitTests {
public static final ZoneId zoneEuropeBerlin = ZoneId.of("Europe/Berlin");
private final ZonedDateTime zdt = ZonedDateTime.now(zoneEuropeBerlin);
private final ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Europe/Berlin"));
@ParameterizedTest // DATAES-716
@EnumSource(DateFormat.class)
@@ -275,14 +274,10 @@ class ElasticsearchDateConverterUnitTests {
check(ElasticsearchDateConverter.of(DateFormat.date_optional_time), LocalDateTime.class);
}
@Test // #2676, #3334
@Test // #2676
@DisplayName("should convert strict_date_optional_time_nanos")
void shouldConvertStrictDateOptionalTime() {
LocalDateTime parsed = check(ElasticsearchDateConverter.of(DateFormat.strict_date_optional_time_nanos),
LocalDateTime.class);
// additional check that all nanoseconds are re-read
var zoned = ZonedDateTime.of(parsed, zoneEuropeBerlin);
assertThat(zoned).isEqualTo(zdt);
check(ElasticsearchDateConverter.of(DateFormat.strict_date_optional_time_nanos), LocalDateTime.class);
}
@Test // #1647
@@ -423,12 +418,11 @@ class ElasticsearchDateConverterUnitTests {
check(ElasticsearchDateConverter.of("basic_date_time ||invalid-pattern"), LocalDateTime.class);
}
private <T extends TemporalAccessor> T check(ElasticsearchDateConverter converter, Class<T> type) {
private <T extends TemporalAccessor> void check(ElasticsearchDateConverter converter, Class<T> type) {
String formatted = converter.format(zdt);
T parsed = converter.parse(formatted, type);
assertThat(parsed).isNotNull();
return parsed;
}
}
@@ -41,9 +41,6 @@ import org.springframework.data.elasticsearch.annotations.*;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.IndexOperations;
import org.springframework.data.elasticsearch.core.MappingContextBaseTests;
import org.springframework.data.elasticsearch.core.index.IndexOptionMappers.BooleanMapper;
import org.springframework.data.elasticsearch.core.index.IndexOptionMappers.NumberMapper;
import org.springframework.data.elasticsearch.core.index.IndexOptionMappers.StringMapper;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
@@ -276,12 +273,6 @@ public abstract class MappingBuilderIntegrationTests extends MappingContextBaseT
void shouldWriteCorrectMappingForDenseVectorProperty() {
operations.indexOps(SimilarityEntity.class).createWithMapping();
}
@Test // #3299
@DisplayName("should write correct mapping for dense vector property with custom index options")
void shouldWriteCorrectMappingForDenseVectorPropertyWithCustomIndexOptions() {
operations.indexOps(SimilarityCustomIndexOptionEntity.class).createWithMapping();
}
@Test // #2845
@DisplayName("should write mapping with field aliases")
@@ -289,12 +280,6 @@ public abstract class MappingBuilderIntegrationTests extends MappingContextBaseT
operations.indexOps(FieldAliasEntity.class).createWithMapping();
}
@Test // #3298
@DisplayName("should write index prefixes with default value")
void shouldWriteIndexPrefixesWithDefaultValue() {
operations.indexOps(TextIndexPrefixesEntity.class).createWithMapping();
}
// region Entities
@Document(indexName = "#{@indexNameProvider.indexName()}")
static class Book {
@@ -923,22 +908,6 @@ public abstract class MappingBuilderIntegrationTests extends MappingContextBaseT
@Field(name = "dotted.field", type = Text) private String dottedField;
}
@Document(indexName = "#{@indexNameProvider.indexName()}")
static class SimilarityCustomIndexOptionEntity {
@Nullable
@Id private String id;
@Field(type = FieldType.Dense_Vector, dims = 42,
knnSimilarity = KnnSimilarity.COSINE,
customIndexOptions = {
@CustomIndexOption(name = "similarity", values = "l2_norm", overrideIfPresent = true, mapper = StringMapper.class),
@CustomIndexOption(name = "element_type", values = "bit", overrideIfPresent = false, mapper = StringMapper.class),
@CustomIndexOption(name = "dims", values = "64", overrideIfPresent = true, mapper = NumberMapper.class),
@CustomIndexOption(name = "index", values = "true", overrideIfPresent = true, mapper = BooleanMapper.class)
}) private double @Nullable [] denseVector;
}
@Document(indexName = "#{@indexNameProvider.indexName()}")
static class SimilarityEntity {
@Nullable
@@ -962,14 +931,5 @@ public abstract class MappingBuilderIntegrationTests extends MappingContextBaseT
@Field(type = Text) private String otherText;
}
@Document(indexName = "#{@indexNameProvider.indexName()}")
private static class TextIndexPrefixesEntity {
@Id
@Nullable private String id;
@Nullable
// the min value is set to the default
@Field(type = Text, indexPrefixes = @IndexPrefixes(maxChars = 10)) private String someText;
}
// endregion
}
@@ -41,9 +41,6 @@ import org.springframework.data.domain.Range;
import org.springframework.data.elasticsearch.annotations.*;
import org.springframework.data.elasticsearch.core.MappingContextBaseTests;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.elasticsearch.core.index.IndexOptionMappers.BooleanMapper;
import org.springframework.data.elasticsearch.core.index.IndexOptionMappers.NumberMapper;
import org.springframework.data.elasticsearch.core.index.IndexOptionMappers.StringMapper;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
import org.springframework.data.elasticsearch.core.suggest.Completion;
@@ -1374,37 +1371,6 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
assertEquals(expected, mapping, false);
}
@Test // #3299
@DisplayName("should write dense_vector properties for knn search with custom index options")
void shouldWriteDenseVectorPropertiesWithKnnSearchCustomIndexOptions() throws JSONException {
String expected = """
{
"properties":{
"my_vector":{
"dims":32,
"element_type":"bit",
"similarity":"dot_product",
"index": true,
"floats": [0.5644, 0.4445, 0.95442],
"ints": [-1, 0, 1],
"strings": ["s1", "", "s2"],
"bools": [true, false],
"index_options":{
"type":"hnsw",
"m":16,
"ef_construction":100
}
}
}
}
""";
String mapping = getMappingBuilder().buildPropertyMapping(DenseVectorEntityWithKnnSearchCustomIndexOptions.class);
assertEquals(expected, mapping, false);
}
// region entities
@Document(indexName = "ignore-above-index")
@@ -2282,45 +2248,6 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
}
}
@SuppressWarnings("unused")
static class DenseVectorEntityWithKnnSearchCustomIndexOptions {
@Nullable
@Id private String id;
@Field(type = FieldType.Dense_Vector, dims = 16,
knnIndexOptions = @KnnIndexOptions(type = KnnAlgorithmType.HNSW, m = 16, efConstruction = 100),
knnSimilarity = KnnSimilarity.DOT_PRODUCT,
customIndexOptions = {
@CustomIndexOption(name = "similarity", values = "l2_norm", mapper = StringMapper.class),
@CustomIndexOption(name = "element_type", values = "bit", mapper = StringMapper.class),
@CustomIndexOption(name = "dims", values = "32", overrideIfPresent = true, mapper = NumberMapper.class),
@CustomIndexOption(name = "index", values = "true", mapper = BooleanMapper.class),
@CustomIndexOption(name = "floats", values = { "0.5644", "0.4445", "0.95442" }, mapper = NumberMapper.class),
@CustomIndexOption(name = "ints", values = { "-1", "0", "1" }, mapper = NumberMapper.class),
@CustomIndexOption(name = "strings", values = { "s1", "", "s2" }, mapper = StringMapper.class),
@CustomIndexOption(name = "bools", values = { "false", "true" }, mapper = BooleanMapper.class),
@CustomIndexOption(name = "type", mapper = StringMapper.class)
}
) private float @Nullable [] my_vector;
@Nullable
public String getId() {
return id;
}
public void setId(@Nullable String id) {
this.id = id;
}
public float @Nullable [] getMy_vector() {
return my_vector;
}
public void setMy_vector(float @Nullable [] my_vector) {
this.my_vector = my_vector;
}
}
@Mapping(enabled = false)
static class DisabledMappingEntity {
@Nullable
@@ -1,33 +0,0 @@
/*
* Copyright 2024-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.mapping;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
public class SimpleElasticsearchPersistentEntityELCIntegrationTests extends SimpleElasticsearchPersistentEntityIntegrationTests {
@Configuration
static class Config extends ElasticsearchTemplateConfiguration {
@Bean
IndexNameProvider indexNameProvider() {
return new IndexNameProvider("persistent-entity");
}
}
}
@@ -1,100 +0,0 @@
/*
* Copyright 2021-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.elasticsearch.core.mapping;
/*
* Copyright 2024-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.elasticsearch.utils.IndexNameProvider;
import org.springframework.test.context.TestPropertySource;
/**
* @author Peter-Josef Meisch
*/
@SpringIntegrationTest
@TestPropertySource(properties = { "entity.indexName = index-property" })
public abstract class SimpleElasticsearchPersistentEntityIntegrationTests {
@Autowired private ElasticsearchOperations operations;
@Autowired private IndexNameProvider indexNameProvider;
@BeforeEach
void setUp() {
indexNameProvider.increment();
}
@Test // #3310
@DisplayName("should evaluate static index name")
void shouldEvaluateStaticIndexName() {
var persistentEntity = getRequiredPersistentEntity(EntityWithStaticName.class);
assertThat(persistentEntity.getIndexCoordinates().getIndexName()).isEqualTo("static-name");
}
@Test // #3310
@DisplayName("should evaluate SpEL index name")
void shouldEvaluateSpELIndexName() {
var persistentEntity = getRequiredPersistentEntity(EntityWithSpel.class);
assertThat(persistentEntity.getIndexCoordinates().getIndexName()).isEqualTo(indexNameProvider.indexName());
}
@Test // #3310
@DisplayName("should evaluate property index name")
void shouldEvaluatePropertyIndexName() {
var persistentEntity = getRequiredPersistentEntity(EntityWithProperty.class);
assertThat(persistentEntity.getIndexCoordinates().getIndexName()).isEqualTo("index-property");
}
private ElasticsearchPersistentEntity<?> getRequiredPersistentEntity(Class<?> clazz) {
return operations.getElasticsearchConverter().getMappingContext().getRequiredPersistentEntity(clazz);
}
@Document(indexName = "static-name")
static class EntityWithStaticName {}
@Document(indexName = "#{@indexNameProvider.indexName()}")
static class EntityWithSpel {}
@Document(indexName = "${entity.indexName}")
static class EntityWithProperty {}
}
@@ -36,12 +36,7 @@ import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.Setting;
import org.springframework.data.elasticsearch.annotations.WriteTypeHint;
import org.springframework.data.elasticsearch.config.ElasticsearchConfigurationSupport;
import org.springframework.data.elasticsearch.config.ElasticsearchServerType;
import org.springframework.data.elasticsearch.core.MappingContextBaseTests;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
import org.springframework.data.elasticsearch.core.index.Settings;
import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.model.FieldNamingStrategy;
@@ -58,7 +53,6 @@ import org.springframework.util.ReflectionUtils;
* @author Oliver Gierke
* @author Peter-Josef Meisch
* @author Roman Puchkovskiy
* @author Steven Pearce
*/
public class SimpleElasticsearchPersistentEntityTests extends MappingContextBaseTests {
@@ -235,34 +229,6 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
assertEquals(expected, json, false);
}
@Test
@DisplayName("should write Default parameters to Settings object")
void shouldWriteDefaultParametersToSettingsObject() {
ElasticsearchPersistentEntity<?> entity = elasticsearchConverter.get().getMappingContext()
.getRequiredPersistentEntity(SettingDefaults.class);
Settings settings = entity.getDefaultSettings().flatten();
assertThat(settings).containsEntry("index.number_of_shards", "1");
assertThat(settings).containsEntry("index.number_of_replicas", "1");
assertThat(settings).containsEntry("index.refresh_interval", "1s");
}
@Test
@DisplayName("should write Shard and Replica parameters to Settings object")
void shouldWriteShardReplicaToSettingsObject() {
ElasticsearchPersistentEntity<?> entity = elasticsearchConverter.get().getMappingContext()
.getRequiredPersistentEntity(SettingWithShardReplicas.class);
Settings settings = entity.getDefaultSettings().flatten();
assertThat(settings).containsEntry("index.number_of_shards", "4");
assertThat(settings).containsEntry("index.number_of_replicas", "5");
assertThat(settings).containsEntry("index.refresh_interval", "1s");
}
@Test // #3187
@DisplayName("should evaluate SpEL expression in settingPath")
void shouldEvaluateSpElExpressionInSettingPath() {
@@ -282,61 +248,6 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
}
}
@Nested
@DisplayName("serverless index settings")
@SpringJUnitConfig({ ServerlessSettingsTests.Config.class })
class ServerlessSettingsTests {
@Autowired private ElasticsearchConverter elasticsearchServerlessConverter;
@Configuration
static class Config {
@Bean
ElasticsearchConverter setupElasticsearchServerlessConverter() {
return new MappingElasticsearchConverter(setupMappingContext());
}
private SimpleElasticsearchMappingContext setupMappingContext() {
ElasticsearchConfigurationSupport configurationSupport = new ElasticsearchConfigurationSupport();
SimpleElasticsearchMappingContext mappingContext = configurationSupport
.elasticsearchMappingContext(configurationSupport.elasticsearchCustomConversions());
mappingContext.setServerType(ElasticsearchServerType.SERVERLESS);
mappingContext.initialize();
return mappingContext;
}
}
@Test
@DisplayName("should write Default parameters to Settings object")
void shouldWriteDefaultParametersToSettingsObject() {
ElasticsearchPersistentEntity<?> entity = elasticsearchServerlessConverter.getMappingContext()
.getRequiredPersistentEntity(SettingDefaults.class);
Settings settings = entity.getDefaultSettings().flatten();
assertThat(settings).doesNotContainKey("index.number_of_shards");
assertThat(settings).doesNotContainKey("index.number_of_replicas");
assertThat(settings).containsEntry("index.refresh_interval", "5s");
}
@Test
@DisplayName("should not write Shard and Replica parameters to Settings object")
void shouldNotWriteShardReplicaToSettingsObject() {
ElasticsearchPersistentEntity<?> entity = elasticsearchServerlessConverter.getMappingContext()
.getRequiredPersistentEntity(SettingWithShardReplicas.class);
Settings settings = entity.getDefaultSettings().flatten();
assertThat(settings).doesNotContainKey("index.number_of_shards");
assertThat(settings).doesNotContainKey("index.number_of_replicas");
assertThat(settings).containsEntry("index.refresh_interval", "5s");
}
}
@Nested
@DisplayName("configuration")
class ConfigurationTests {
@@ -399,57 +310,6 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
assertThat(entity.writeTypeHints()).isTrue();
}
@Test
@DisplayName("should return Default ElasticsearchServerType from context configuration")
void shouldReturnDefaultElasticsearchServerTypeFromContextConfiguration() {
SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext();
SimpleElasticsearchPersistentEntity<?> persistentEntity = context
.getRequiredPersistentEntity(FieldNameEntity.class);
assertThat(persistentEntity.getServerType()).isEqualTo(ElasticsearchServerType.DEFAULT);
assertThat(persistentEntity.getRefreshInterval()).isEqualTo("1s");
}
@Test
@DisplayName("should return ElasticsearchServerType from context configuration")
void shouldReturnElasticsearchServerTypeFromContextConfiguration() {
SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext();
context.setServerType(ElasticsearchServerType.SERVERLESS);
SimpleElasticsearchPersistentEntity<?> persistentEntity = context
.getRequiredPersistentEntity(FieldNameEntity.class);
assertThat(persistentEntity.getServerType()).isEqualTo(ElasticsearchServerType.SERVERLESS);
assertThat(persistentEntity.getRefreshInterval()).isEqualTo("5s");
}
@Test
@DisplayName("should return OverriddenRefreshInterval from DEFAULT context configuration")
void shouldReturnOverriddenRefreshIntervalFromDEFAULTContextConfiguration() {
SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext();
context.setServerType(ElasticsearchServerType.SERVERLESS);
SimpleElasticsearchPersistentEntity<?> persistentEntity = context
.getRequiredPersistentEntity(SettingWithRefreshInterval.class);
assertThat(persistentEntity.getServerType()).isEqualTo(ElasticsearchServerType.SERVERLESS);
assertThat(persistentEntity.getRefreshInterval()).isEqualTo("9s");
}
@Test
@DisplayName("should return OverriddenRefreshInterval from SERVERLESS context configuration")
void shouldReturnOverriddenRefreshIntervalFromSERVERLESSContextConfiguration() {
SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext();
context.setServerType(ElasticsearchServerType.SERVERLESS);
SimpleElasticsearchPersistentEntity<?> persistentEntity = context
.getRequiredPersistentEntity(SettingWithRefreshInterval.class);
assertThat(persistentEntity.getServerType()).isEqualTo(ElasticsearchServerType.SERVERLESS);
assertThat(persistentEntity.getRefreshInterval()).isEqualTo("9s");
}
}
// region helper
@@ -586,25 +446,5 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
@Nullable
@Id String id;
}
@Document(indexName = "foo")
private static class SettingDefaults {
@Nullable
@Id String id;
}
@Document(indexName = "foo")
@Setting(refreshInterval = "9s")
private static class SettingWithRefreshInterval {
@Nullable
@Id String id;
}
@Document(indexName = "foo")
@Setting(shards = 4, replicas = 5)
private static class SettingWithShardReplicas {
@Nullable
@Id String id;
}
// endregion
}
@@ -89,17 +89,6 @@ class DefaultRoutingResolverUnitTest {
assertThat(routing).isEqualTo("route 42");
}
@Test // #3321
@DisplayName("should return routing from Value expression")
void shouldReturnRoutingFromValueExpression() {
ValidValueRoutingEntity entity = new ValidValueRoutingEntity("42", "route 42");
String routing = routingResolver.getRouting(entity);
assertThat(routing).isEqualTo("route 42");
}
@Document(indexName = "routing-resolver-test")
@Routing("theRouting")
static class ValidRoutingEntity {
@@ -162,37 +151,6 @@ class DefaultRoutingResolverUnitTest {
}
}
@Document(indexName = "routing-resolver-test")
@Routing(value = "#{@spelRouting.getRouting(#entity)}")
static class ValidValueRoutingEntity {
@Nullable
@Id private String id;
@Nullable private String theRouting;
public ValidValueRoutingEntity(@Nullable String id, @Nullable String theRouting) {
this.id = id;
this.theRouting = theRouting;
}
@Nullable
public String getId() {
return id;
}
public void setId(@Nullable String id) {
this.id = id;
}
@Nullable
public String getTheRouting() {
return theRouting;
}
public void setTheRouting(@Nullable String theRouting) {
this.theRouting = theRouting;
}
}
@Document(indexName = "routing-resolver-test")
@Routing("unknownProperty")
static class InvalidRoutingEntity {
@@ -229,12 +187,8 @@ class DefaultRoutingResolverUnitTest {
@Nullable
public String getRouting(Object o) {
if (o instanceof ValidSpelRoutingEntity e) {
return e.getTheRouting();
}
if (o instanceof ValidValueRoutingEntity e) {
return e.getTheRouting();
if (o instanceof ValidSpelRoutingEntity) {
return ((ValidSpelRoutingEntity) o).getTheRouting();
}
return null;
@@ -15,7 +15,7 @@
#
#
sde.testcontainers.image-name=docker.elastic.co/elasticsearch/elasticsearch
sde.testcontainers.image-version=9.5.2
sde.testcontainers.image-version=9.4.5
#
#
# needed as we do a DELETE /* at the end of the tests, will be required from 8.0 on, produces a warning since 7.13