From 82be8ffefc7b3df932dfa9b6a95ec3d76dc1258e Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 25 Jul 2026 14:00:49 +0100 Subject: [PATCH] Support Elasticsearch Serverless Closes: #3306 Original Pull Request: #3312 Signed-off-by: Steven --- .../elasticsearch/annotations/Setting.java | 7 +- .../ElasticsearchConfigurationSupport.java | 13 +- .../config/ElasticsearchServerType.java | 36 ++++ .../ElasticsearchPersistentEntity.java | 7 + .../SimpleElasticsearchMappingContext.java | 17 +- .../SimpleElasticsearchPersistentEntity.java | 49 +++++- ...pleElasticsearchPersistentEntityTests.java | 159 ++++++++++++++++++ 7 files changed, 275 insertions(+), 13 deletions(-) create mode 100644 src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchServerType.java diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Setting.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Setting.java index 88fb1d03d..67d20bb6a 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Setting.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Setting.java @@ -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. diff --git a/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchConfigurationSupport.java b/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchConfigurationSupport.java index 8af56c4e2..58d673831 100644 --- a/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchConfigurationSupport.java +++ b/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchConfigurationSupport.java @@ -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; } } diff --git a/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchServerType.java b/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchServerType.java new file mode 100644 index 000000000..a52b4766c --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchServerType.java @@ -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, +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentEntity.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentEntity.java index a17d3d702..9a8933494 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentEntity.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentEntity.java @@ -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 extends PersistentEntity type) { return !ElasticsearchSimpleTypes.HOLDER.isSimpleType(type.getType()); @@ -69,7 +84,7 @@ public class SimpleElasticsearchMappingContext @Override protected SimpleElasticsearchPersistentEntity createPersistentEntity(TypeInformation typeInformation) { return new SimpleElasticsearchPersistentEntity<>(typeInformation, - new SimpleElasticsearchPersistentEntity.ContextConfiguration(fieldNamingStrategy, writeTypeHints)); + new SimpleElasticsearchPersistentEntity.ContextConfiguration(fieldNamingStrategy, writeTypeHints, serverType)); } @Override diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntity.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntity.java index 8a05a69e1..3a97e4cf7 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntity.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntity.java @@ -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 + * + * @author Steven Pearce * @author Rizwan Idrees * @author Mohsin Husen * @author Mark Paluch @@ -174,7 +178,7 @@ public class SimpleElasticsearchPersistentEntity extends BasicPersistentEntit @Nullable @Override public String getRefreshInterval() { - return settingsParameter.get().refreshIntervall; + return settingsParameter.get().refreshInterval; } @Override @@ -196,6 +200,11 @@ public class SimpleElasticsearchPersistentEntity extends BasicPersistentEntit return contextConfiguration.getFieldNamingStrategy(); } + @Override + public ElasticsearchServerType getServerType() { + return contextConfiguration.getServerType(); + } + @Override public boolean writeTypeHints() { @@ -468,9 +477,14 @@ public class SimpleElasticsearchPersistentEntity 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 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 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 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 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 extends BasicPersistentEntit public boolean getWriteTypeHints() { return writeTypeHints; } + + public ElasticsearchServerType getServerType() { + return serverType; + } } @Override diff --git a/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntityTests.java b/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntityTests.java index 33a967e9f..6a38ccb1c 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntityTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntityTests.java @@ -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 }