1
0
mirror of synced 2026-08-05 08:26:55 +00:00

Support Elasticsearch Serverless

Closes: #3306
Original Pull Request: #3312

Signed-off-by: Steven <steven.pearce@nowyoyo.com>
This commit is contained in:
Steven
2026-07-25 14:00:49 +01:00
committed by GitHub
parent f01f797296
commit 82be8ffefc
7 changed files with 275 additions and 13 deletions
@@ -22,10 +22,12 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.data.annotation.Persistent;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchPersistentEntity;
/**
* Elasticsearch Setting
*
* @author Steven Pearce
* @author Mohsin Husen
* @author Peter-Josef Meisch
*/
@@ -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.
@@ -40,6 +40,7 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* @author Steven Pearce
* @author Christoph Strobl
* @author Peter-Josef Meisch
* @since 3.2
@@ -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,13 @@ 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; }
}
@@ -0,0 +1,36 @@
/*
* 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
*/
public enum ElasticsearchServerType {
/**
* Normal installations of Elasticsearch including cloud-hosted Elasticsearch
*/
DEFAULT,
/**
* New Flavour of Elasticsearch, offered by Elastic
*/
SERVERLESS,
}
@@ -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;
@@ -179,6 +180,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
@@ -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;
@@ -27,6 +28,7 @@ import org.springframework.data.mapping.model.SimpleTypeHolder;
/**
* SimpleElasticsearchMappingContext
*
* @author Steven Pearce
* @author Rizwan Idrees
* @author Mohsin Husen
* @author Mark Paluch
@@ -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
@@ -33,6 +33,7 @@ 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;
@@ -51,11 +52,14 @@ 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.StringUtils;
/**
* Elasticsearch specific {@link org.springframework.data.mapping.PersistentEntity} implementation holding
*
* @param <T>
*
* @author Steven Pearce
* @author Rizwan Idrees
* @author Mohsin Husen
* @author Mark Paluch
@@ -174,7 +178,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
@Nullable
@Override
public String getRefreshInterval() {
return settingsParameter.get().refreshIntervall;
return settingsParameter.get().refreshInterval;
}
@Override
@@ -196,6 +200,11 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
return contextConfiguration.getFieldNamingStrategy();
}
@Override
public ElasticsearchServerType getServerType() {
return contextConfiguration.getServerType();
}
@Override
public boolean writeTypeHints() {
@@ -468,9 +477,14 @@ 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 +498,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 +576,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 +593,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 +640,16 @@ 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 +659,10 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
public boolean getWriteTypeHints() {
return writeTypeHints;
}
public ElasticsearchServerType getServerType() {
return serverType;
}
}
@Override
@@ -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;
@@ -47,6 +52,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.util.ReflectionUtils;
/**
* @author Steven Pearce
* @author Rizwan Idrees
* @author Mohsin Husen
* @author Mark Paluch
@@ -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,56 @@ 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 +585,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
}