Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a2a23820fa | |||
| 8c9dbc90ec | |||
| 6d01b5824f | |||
| 142e385cf2 | |||
| b89acfb2bd | |||
| 854ba1f8a8 | |||
| 073bdd3751 | |||
| 20b049abed | |||
| c08dd4bff0 | |||
| 8cc6da4b98 | |||
| 29821baed6 | |||
| 56eb92e090 | |||
| fd557bc45c | |||
| 63f3a355d9 | |||
| becfe765f5 | |||
| 82be8ffefc | |||
| f01f797296 | |||
| 8b067983ed | |||
| eb98fa0bb5 | |||
| c64780be71 | |||
| 3b21a1882b | |||
| 7e70435f48 | |||
| 14209bf730 | |||
| 31eee9fae5 | |||
| 977b7b23cc |
@@ -3,7 +3,7 @@ name: CI Build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ 6.1.x, 'issue/6.1.x/**' ]
|
||||
branches: [ main, 'issue/**' ]
|
||||
|
||||
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@4.1.x
|
||||
uses: spring-projects/spring-data-build/actions/setup-maven@main
|
||||
with:
|
||||
java-version: ${{ matrix.java-version }}
|
||||
develocity-access-key: '${{ secrets.DEVELOCITY_ACCESS_KEY }}'
|
||||
- name: Build
|
||||
uses: spring-projects/spring-data-build/actions/maven-build@4.1.x
|
||||
uses: spring-projects/spring-data-build/actions/maven-build@main
|
||||
env:
|
||||
OSS_ARTIFACTORY_USR: '${{ secrets.ARTIFACTORY_USERNAME }}'
|
||||
OSS_ARTIFACTORY_PSW: '${{ secrets.ARTIFACTORY_PASSWORD }}'
|
||||
|
||||
@@ -4,7 +4,7 @@ name: "CodeQL Advanced"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 6.1.x ]
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Snapshots
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ 6.1.x, 'issue/6.1.x/**', 'release/**' ]
|
||||
branches: [ main, 'issue/**', '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@4.1.x
|
||||
uses: spring-projects/spring-data-build/actions/setup-maven@main
|
||||
with:
|
||||
develocity-access-key: '${{ secrets.DEVELOCITY_ACCESS_KEY }}'
|
||||
- name: Deploy to Artifactory
|
||||
uses: spring-projects/spring-data-build/actions/maven-artifactory-deploy@4.1.x
|
||||
uses: spring-projects/spring-data-build/actions/maven-artifactory-deploy@main
|
||||
env:
|
||||
OSS_ARTIFACTORY_USR: '${{ secrets.ARTIFACTORY_USERNAME }}'
|
||||
OSS_ARTIFACTORY_PSW: '${{ secrets.ARTIFACTORY_PASSWORD }}'
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
= 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
@@ -0,0 +1,15 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,11 @@
|
||||
= 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.
|
||||
@@ -5,12 +5,12 @@
|
||||
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-elasticsearch</artifactId>
|
||||
<version>6.1.2-SNAPSHOT</version>
|
||||
<version>6.2.0-M1</version>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.data.build</groupId>
|
||||
<artifactId>spring-data-parent</artifactId>
|
||||
<version>4.1.2-SNAPSHOT</version>
|
||||
<version>4.2.0-M1</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.1.2-SNAPSHOT</springdata.commons>
|
||||
<springdata.commons>4.2.0-M1</springdata.commons>
|
||||
|
||||
<!-- version of the ElasticsearchClient -->
|
||||
<elasticsearch-java>9.4.6</elasticsearch-java>
|
||||
<elasticsearch-rest-client>9.4.5</elasticsearch-rest-client>
|
||||
<elasticsearch-java>9.5.0</elasticsearch-java>
|
||||
<elasticsearch-rest-client>9.5.0</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 or a cluster.
|
||||
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.
|
||||
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,6 +454,22 @@ 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,16 +1,13 @@
|
||||
[[new-features]]
|
||||
= What's new
|
||||
|
||||
[[new-features.6-1-2]]
|
||||
== New in Spring Data Elasticsearch 6.1.2
|
||||
[[new-features.6-2-0]]
|
||||
== New in Spring Data Elasticsearch 6.2
|
||||
|
||||
* Upgrade to Elasticsearch client 9.4.6
|
||||
* Fix formatting of DateFormat.strict_date_optional_time_nanos (https://github.com/spring-projects/spring-data-elasticsearch/issues/3334[GH #3334])
|
||||
|
||||
[[new-features.6-1-1]]
|
||||
== New in Spring Data Elasticsearch 6.1.1
|
||||
|
||||
* Upgrade to Elasticsearch 9.4.5
|
||||
* Upgrade to Elasticsearch 9.5.0
|
||||
* 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])
|
||||
|
||||
[[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()}"`
|
||||
This can contain a SpEL template expression like `"log-#{T(java.time.LocalDate).now().toString()}"` or a property expression like `{index.name}`
|
||||
** `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,7 +6,8 @@ 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.0 | 6.1.x | 9.4.6 | 7.0.x
|
||||
| 2026.1 | 6.2.x | 9.5.0 | 7.0.x
|
||||
| 2026.0 | 6.1.x | 9.4.2 | 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
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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,4 +252,11 @@ 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,4 +179,11 @@ 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,12 +22,14 @@ 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
|
||||
@@ -59,9 +61,10 @@ public @interface Setting {
|
||||
short replicas() default 1;
|
||||
|
||||
/**
|
||||
* Refresh interval for the index. Used for index creation.
|
||||
* Refresh interval for the index. Used for index creation. If no value, defaults are server type dependant and set in
|
||||
* {@link SimpleElasticsearchPersistentEntity}
|
||||
*/
|
||||
String refreshInterval() default "1s";
|
||||
String refreshInterval() default "";
|
||||
|
||||
/**
|
||||
* Index storage type for the index. Used for index creation.
|
||||
|
||||
+64
-17
@@ -24,12 +24,18 @@ 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;
|
||||
@@ -85,22 +91,8 @@ final class DocumentAdapters {
|
||||
|
||||
Map<String, Double> matchedQueries = hit.matchedQueries();
|
||||
|
||||
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());
|
||||
EntityAsMap hitFieldsAsMap = new EntityAsMap();
|
||||
hit.fields().forEach((key, jsonData) -> hitFieldsAsMap.put(key, toJavaObject(jsonData, jsonpMapper)));
|
||||
|
||||
Map<String, List<@Nullable Object>> documentFields = new LinkedHashMap<>();
|
||||
hitFieldsAsMap.forEach((key, value) -> {
|
||||
@@ -144,6 +136,61 @@ 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())
|
||||
|
||||
+27
-7
@@ -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));
|
||||
}
|
||||
|
||||
+14
-1
@@ -42,6 +42,7 @@ import org.springframework.util.StringUtils;
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Steven Pearce
|
||||
* @since 3.2
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@@ -73,6 +74,7 @@ public class ElasticsearchConfigurationSupport {
|
||||
mappingContext.setSimpleTypeHolder(elasticsearchCustomConversions.getSimpleTypeHolder());
|
||||
mappingContext.setFieldNamingStrategy(fieldNamingStrategy());
|
||||
mappingContext.setWriteTypeHints(writeTypeHints());
|
||||
mappingContext.setServerType(serverType());
|
||||
|
||||
return mappingContext;
|
||||
}
|
||||
@@ -174,7 +176,7 @@ public class ElasticsearchConfigurationSupport {
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag specifiying if type hints (_class fields) should be written in the index. It is strongly advised to keep the
|
||||
* Flag specifying 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.
|
||||
@@ -185,4 +187,15 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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,
|
||||
}
|
||||
+1
-2
@@ -104,9 +104,8 @@ 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(
|
||||
expressionParser, this, spELContext);
|
||||
new SpelExpressionParser(), this, spELContext);
|
||||
|
||||
private final EntityInstantiators instantiators = new EntityInstantiators();
|
||||
private final ElasticsearchTypeMapper typeMapper;
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
+301
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -27,6 +27,7 @@ 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;
|
||||
@@ -120,6 +121,7 @@ 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.
|
||||
@@ -185,6 +187,7 @@ 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) {
|
||||
@@ -231,6 +234,7 @@ public class MappingParameters {
|
||||
knnIndexOptions = field.knnIndexOptions().length > 0 ? field.knnIndexOptions()[0] : null;
|
||||
enabled = true;
|
||||
eagerGlobalOrdinals = field.eagerGlobalOrdinals();
|
||||
customIndexOptions = field.customIndexOptions();
|
||||
}
|
||||
|
||||
public boolean isStore() {
|
||||
@@ -419,6 +423,12 @@ 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() {
|
||||
@@ -548,4 +558,8 @@ public class MappingParameters {
|
||||
protected String mappedTypeName() {
|
||||
return mappedTypeName;
|
||||
}
|
||||
|
||||
protected CustomIndexOption[] customIndexOptions() {
|
||||
return customIndexOptions;
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -21,6 +21,7 @@ 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;
|
||||
@@ -39,6 +40,7 @@ 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> {
|
||||
|
||||
@@ -179,6 +181,12 @@ 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
|
||||
|
||||
+16
-1
@@ -18,6 +18,7 @@ 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;
|
||||
@@ -31,6 +32,7 @@ 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> {
|
||||
@@ -39,6 +41,7 @@ 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.
|
||||
@@ -61,6 +64,18 @@ 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());
|
||||
@@ -69,7 +84,7 @@ public class SimpleElasticsearchMappingContext
|
||||
@Override
|
||||
protected <T> SimpleElasticsearchPersistentEntity<?> createPersistentEntity(TypeInformation<T> typeInformation) {
|
||||
return new SimpleElasticsearchPersistentEntity<>(typeInformation,
|
||||
new SimpleElasticsearchPersistentEntity.ContextConfiguration(fieldNamingStrategy, writeTypeHints));
|
||||
new SimpleElasticsearchPersistentEntity.ContextConfiguration(fieldNamingStrategy, writeTypeHints, serverType));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+101
-58
@@ -33,24 +33,26 @@ 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
|
||||
@@ -64,15 +66,15 @@ import org.springframework.util.Assert;
|
||||
* @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 @Nullable String indexName;
|
||||
private final String unresolvedIndexName;
|
||||
private final Lazy<SettingsParameter> settingsParameter;
|
||||
private @Nullable ElasticsearchPersistentProperty seqNoPrimaryTermProperty;
|
||||
private @Nullable ElasticsearchPersistentProperty joinFieldProperty;
|
||||
@@ -82,14 +84,9 @@ 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;
|
||||
|
||||
@@ -108,10 +105,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.indexName = document.indexName();
|
||||
|
||||
this.unresolvedIndexName = document.indexName();
|
||||
this.versionType = document.versionType();
|
||||
this.createIndexAndMapping = document.createIndex();
|
||||
this.alwaysWriteMapping = document.alwaysWriteMapping();
|
||||
@@ -120,6 +117,7 @@ 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;
|
||||
@@ -136,13 +134,9 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
}
|
||||
}
|
||||
|
||||
private String getIndexName() {
|
||||
return indexName != null ? indexName : getTypeInformation().getType().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IndexCoordinates getIndexCoordinates() {
|
||||
return resolve(IndexCoordinates.of(getIndexName()));
|
||||
return resolve(IndexCoordinates.of(unresolvedIndexName));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -174,7 +168,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
@Nullable
|
||||
@Override
|
||||
public String getRefreshInterval() {
|
||||
return settingsParameter.get().refreshIntervall;
|
||||
return settingsParameter.get().refreshInterval;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -196,6 +190,11 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
return contextConfiguration.getFieldNamingStrategy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElasticsearchServerType getServerType() {
|
||||
return contextConfiguration.getServerType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean writeTypeHints() {
|
||||
|
||||
@@ -368,52 +367,55 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
|
||||
Assert.notNull(name, "name must not be null");
|
||||
|
||||
Expression expression = getExpressionForIndexName(name);
|
||||
ValueExpression expression = ExpressionUtils.detectExpression(name);
|
||||
|
||||
String resolvedName = expression != null ? expression.getValue(indexNameEvaluationContext.get(), String.class)
|
||||
: null;
|
||||
return resolvedName != null ? resolvedName : 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns an {@link Expression} for #name if name contains a {@link ParserContext#TEMPLATE_EXPRESSION} otherwise
|
||||
* returns {@literal null}.
|
||||
* build the {@link ValueEvaluationContext} from the expression.
|
||||
*
|
||||
* @param name the name to get the expression for
|
||||
* @return Expression may be null
|
||||
* @return ValueEvaluationContext
|
||||
* @param expression
|
||||
*/
|
||||
@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;
|
||||
});
|
||||
private ValueEvaluationContext getValueEvaluationContext(@Nullable String expression) {
|
||||
|
||||
ValueExpression valueExpression = ExpressionUtils.detectExpression(expression);
|
||||
|
||||
return getValueEvaluationContext(valueExpression);
|
||||
}
|
||||
|
||||
/**
|
||||
* build the {@link EvaluationContext} considering {@link ExpressionDependencies} from the name returned by
|
||||
* {@link #getIndexName()}.
|
||||
* build the {@link ValueEvaluationContext} considering {@link ExpressionDependencies} from the expression.
|
||||
*
|
||||
* @return EvaluationContext
|
||||
* @return ValueEvaluationContext
|
||||
* @param expression
|
||||
*/
|
||||
private EvaluationContext getIndexNameEvaluationContext() {
|
||||
private ValueEvaluationContext getValueEvaluationContext(@Nullable ValueExpression expression) {
|
||||
|
||||
Expression expression = getExpressionForIndexName(getIndexName());
|
||||
ExpressionDependencies expressionDependencies = expression != null ? ExpressionDependencies.discover(expression)
|
||||
: ExpressionDependencies.none();
|
||||
var expressionDependencies = expression != null ? expression.getExpressionDependencies() : null;
|
||||
|
||||
// noinspection ConstantConditions
|
||||
return getEvaluationContext(null, expressionDependencies);
|
||||
return expressionDependencies != null ? getValueEvaluationContext(null, expressionDependencies)
|
||||
: getValueEvaluationContext((Object) null);
|
||||
}
|
||||
|
||||
@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) {
|
||||
@@ -422,14 +424,29 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
return propertyValue != null ? propertyValue.toString() : null;
|
||||
}
|
||||
|
||||
// try to resolve the annotations value
|
||||
try {
|
||||
Expression expression = routingExpressions.computeIfAbsent(routing, PARSER::parseExpression);
|
||||
ExpressionDependencies expressionDependencies = ExpressionDependencies.discover(expression);
|
||||
var routingValue = routing;
|
||||
ValueExpression expression = ExpressionUtils.detectExpression(routingValue);
|
||||
|
||||
EvaluationContext context = getEvaluationContext(null, expressionDependencies);
|
||||
context.setVariable("entity", bean);
|
||||
if (expression == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return expression.getValue(context, String.class);
|
||||
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));
|
||||
} catch (EvaluationException e) {
|
||||
throw new InvalidDataAccessApiUsageException(
|
||||
"Could not resolve expression: " + routing + " for object of class " + bean.getClass().getCanonicalName(), e);
|
||||
@@ -447,9 +464,13 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
}
|
||||
|
||||
try {
|
||||
Expression expression = PARSER.parseExpression(settingPathFromParameter, ParserContext.TEMPLATE_EXPRESSION);
|
||||
return (expression instanceof LiteralExpression) ? settingPathFromParameter
|
||||
: expression.getValue(getEvaluationContext(null, ExpressionDependencies.discover(expression)), String.class);
|
||||
ValueExpression expression = ExpressionUtils.detectExpression(settingPathFromParameter);
|
||||
|
||||
if (expression == null || expression instanceof LiteralExpression) {
|
||||
return settingPathFromParameter;
|
||||
}
|
||||
|
||||
return ObjectUtils.nullSafeToString(expression.evaluate(getValueEvaluationContext(settingPathFromParameter)));
|
||||
} catch (ExpressionException e) {
|
||||
throw new InvalidDataAccessApiUsageException(
|
||||
"Could not resolve expression: " + settingPathFromParameter + " for @Setting.settingPath ", e);
|
||||
@@ -468,9 +489,13 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
|
||||
// default values
|
||||
settingsParameter.useServerConfiguration = false;
|
||||
settingsParameter.serverType = contextConfiguration.serverType;
|
||||
settingsParameter.shards = 1;
|
||||
settingsParameter.replicas = 1;
|
||||
settingsParameter.refreshIntervall = "1s";
|
||||
settingsParameter.refreshInterval = switch (contextConfiguration.serverType) {
|
||||
case DEFAULT -> "1s";
|
||||
case SERVERLESS -> "5s";
|
||||
};
|
||||
|
||||
if (settingAnnotation != null) {
|
||||
processSettingAnnotation(settingAnnotation, settingsParameter);
|
||||
@@ -484,7 +509,9 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
settingsParameter.settingPath = settingAnnotation.settingPath();
|
||||
settingsParameter.shards = settingAnnotation.shards();
|
||||
settingsParameter.replicas = settingAnnotation.replicas();
|
||||
settingsParameter.refreshIntervall = settingAnnotation.refreshInterval();
|
||||
if (StringUtils.hasText(settingAnnotation.refreshInterval())) {
|
||||
settingsParameter.refreshInterval = settingAnnotation.refreshInterval();
|
||||
}
|
||||
settingsParameter.indexStoreType = settingAnnotation.indexStoreType();
|
||||
|
||||
String[] sortFields = settingAnnotation.sortFields();
|
||||
@@ -560,10 +587,11 @@ 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 refreshIntervall;
|
||||
@Nullable String refreshInterval;
|
||||
@Nullable String indexStoreType;
|
||||
private String @Nullable [] sortFields;
|
||||
private Setting.@Nullable SortOrder @Nullable [] sortOrders;
|
||||
@@ -576,12 +604,16 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
return new Settings();
|
||||
}
|
||||
|
||||
var index = new Settings() //
|
||||
.append("number_of_shards", String.valueOf(shards)) //
|
||||
.append("number_of_replicas", String.valueOf(replicas));
|
||||
var index = new Settings();
|
||||
|
||||
if (refreshIntervall != null) {
|
||||
index.append("refresh_interval", refreshIntervall);
|
||||
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 (indexStoreType != null && !"fs".equals(indexStoreType)) {
|
||||
@@ -619,10 +651,17 @@ 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() {
|
||||
@@ -632,6 +671,10 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
|
||||
public boolean getWriteTypeHints() {
|
||||
return writeTypeHints;
|
||||
}
|
||||
|
||||
public ElasticsearchServerType getServerType() {
|
||||
return serverType;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+1
-1
@@ -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 the method name in the repository definition. Was originally named
|
||||
* A repository query that is built from the method name in the repository definition. Was originally named
|
||||
* ElasticsearchPartQuery.
|
||||
*
|
||||
* @author Rizwan Idrees
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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,4 +1,4 @@
|
||||
Spring Data Elasticsearch 6.1.1 (2026.0.1)
|
||||
Spring Data Elasticsearch 6.2.0-M1 (2026.1.0)
|
||||
Copyright (c) [2013-2026] Pivotal Software, Inc.
|
||||
|
||||
This product is licensed to you under the Apache License, Version 2.0 (the "License").
|
||||
|
||||
+44
@@ -19,6 +19,7 @@ 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;
|
||||
@@ -77,6 +78,49 @@ 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() {
|
||||
|
||||
+1
-22
@@ -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());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
-10
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+40
@@ -41,6 +41,9 @@ 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;
|
||||
@@ -273,6 +276,12 @@ 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")
|
||||
@@ -280,6 +289,12 @@ 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 {
|
||||
@@ -908,6 +923,22 @@ 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
|
||||
@@ -931,5 +962,14 @@ 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
|
||||
}
|
||||
|
||||
+73
@@ -41,6 +41,9 @@ 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;
|
||||
@@ -1371,6 +1374,37 @@ 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")
|
||||
@@ -2248,6 +2282,45 @@ 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
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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 {}
|
||||
|
||||
}
|
||||
+160
@@ -36,7 +36,12 @@ 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;
|
||||
@@ -53,6 +58,7 @@ import org.springframework.util.ReflectionUtils;
|
||||
* @author Oliver Gierke
|
||||
* @author Peter-Josef Meisch
|
||||
* @author Roman Puchkovskiy
|
||||
* @author Steven Pearce
|
||||
*/
|
||||
public class SimpleElasticsearchPersistentEntityTests extends MappingContextBaseTests {
|
||||
|
||||
@@ -229,6 +235,34 @@ 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() {
|
||||
@@ -248,6 +282,61 @@ 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 {
|
||||
@@ -310,6 +399,57 @@ 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
|
||||
@@ -446,5 +586,25 @@ 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
|
||||
}
|
||||
|
||||
+48
-2
@@ -89,6 +89,17 @@ 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 {
|
||||
@@ -151,6 +162,37 @@ 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 {
|
||||
@@ -187,8 +229,12 @@ class DefaultRoutingResolverUnitTest {
|
||||
@Nullable
|
||||
public String getRouting(Object o) {
|
||||
|
||||
if (o instanceof ValidSpelRoutingEntity) {
|
||||
return ((ValidSpelRoutingEntity) o).getTheRouting();
|
||||
if (o instanceof ValidSpelRoutingEntity e) {
|
||||
return e.getTheRouting();
|
||||
}
|
||||
|
||||
if (o instanceof ValidValueRoutingEntity e) {
|
||||
return e.getTheRouting();
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
#
|
||||
#
|
||||
sde.testcontainers.image-name=docker.elastic.co/elasticsearch/elasticsearch
|
||||
sde.testcontainers.image-version=9.4.5
|
||||
sde.testcontainers.image-version=9.5.0
|
||||
#
|
||||
#
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user