diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Alias.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Alias.java new file mode 100644 index 000000000..0fd1e3694 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Alias.java @@ -0,0 +1,80 @@ +/* + * Copyright 2024 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.core.annotation.AliasFor; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Identifies an alias for the index. + * + * @author Youssef Aouichaoui + * @since 5.4 + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@Repeatable(Aliases.class) +public @interface Alias { + /** + * @return Index alias name. Alias for {@link #alias}. + */ + @AliasFor("alias") + String value() default ""; + + /** + * @return Index alias name. Alias for {@link #value}. + */ + @AliasFor("value") + String alias() default ""; + + /** + * @return Query used to limit documents the alias can access. + */ + Filter filter() default @Filter; + + /** + * @return Used to route indexing operations to a specific shard. + */ + String indexRouting() default ""; + + /** + * @return Used to route indexing and search operations to a specific shard. + */ + String routing() default ""; + + /** + * @return Used to route search operations to a specific shard. + */ + String searchRouting() default ""; + + /** + * @return Is the alias hidden? + */ + boolean isHidden() default false; + + /** + * @return Is it the 'write index' for the alias? + */ + boolean isWriteIndex() default false; +} diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Aliases.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Aliases.java new file mode 100644 index 000000000..b72108210 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Aliases.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 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 java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Container annotation that aggregates several {@link Alias} annotations. + * + * @author Youssef Aouichaoui + * @see Alias + * @since 5.4 + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface Aliases { + Alias[] value(); +} diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Document.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Document.java index 391e303e0..fbe761e4c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Document.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Document.java @@ -100,6 +100,13 @@ */ boolean storeVersionInSource() default true; + /** + * Aliases for the index. + * + * @since 5.4 + */ + Alias[] aliases() default {}; + /** * @since 4.3 */ diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Filter.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Filter.java new file mode 100644 index 000000000..6d68e3813 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Filter.java @@ -0,0 +1,39 @@ +/* + * Copyright 2024 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.core.annotation.AliasFor; + +/** + * Query used to limit documents. + * + * @author Youssef Aouichaoui + * @since 5.4 + */ +public @interface Filter { + /** + * @return Query used to limit documents. Alias for {@link #query}. + */ + @AliasFor("query") + String value() default ""; + + /** + * @return Query used to limit documents. Alias for {@link #value}. + */ + @AliasFor("value") + String query() default ""; +} diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/IndicesTemplate.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/IndicesTemplate.java index ec6ea3914..c5a2d029c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/IndicesTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/IndicesTemplate.java @@ -21,6 +21,7 @@ import co.elastic.clients.transport.ElasticsearchTransport; import co.elastic.clients.transport.endpoints.BooleanResponse; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -46,6 +47,8 @@ import org.springframework.data.elasticsearch.core.index.GetTemplateRequest; import org.springframework.data.elasticsearch.core.index.PutIndexTemplateRequest; import org.springframework.data.elasticsearch.core.index.PutTemplateRequest; +import org.springframework.data.elasticsearch.core.mapping.Alias; +import org.springframework.data.elasticsearch.core.mapping.CreateIndexSettings; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.lang.Nullable; @@ -137,11 +140,14 @@ public boolean createWithMapping() { protected boolean doCreate(IndexCoordinates indexCoordinates, Map settings, @Nullable Document mapping) { - - Assert.notNull(indexCoordinates, "indexCoordinates must not be null"); - Assert.notNull(settings, "settings must not be null"); - - CreateIndexRequest createIndexRequest = requestConverter.indicesCreateRequest(indexCoordinates, settings, mapping); + Set aliases = (boundClass != null) ? getAliasesFor(boundClass) : new HashSet<>(); + CreateIndexSettings indexSettings = CreateIndexSettings.builder(indexCoordinates) + .withAliases(aliases) + .withSettings(settings) + .withMapping(mapping) + .build(); + + CreateIndexRequest createIndexRequest = requestConverter.indicesCreateRequest(indexSettings); CreateIndexResponse createIndexResponse = execute(client -> client.create(createIndexRequest)); return Boolean.TRUE.equals(createIndexResponse.acknowledged()); } @@ -449,5 +455,14 @@ public IndexCoordinates getIndexCoordinates() { public IndexCoordinates getIndexCoordinatesFor(Class clazz) { return getRequiredPersistentEntity(clazz).getIndexCoordinates(); } + + /** + * Get the {@link Alias} of the provided class. + * + * @param clazz provided class that can be used to extract aliases. + */ + public Set getAliasesFor(Class clazz) { + return getRequiredPersistentEntity(clazz).getAliases(); + } // endregion } diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveIndicesTemplate.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveIndicesTemplate.java index 82f21f107..1c90c7a83 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveIndicesTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveIndicesTemplate.java @@ -21,9 +21,12 @@ import co.elastic.clients.elasticsearch.indices.*; import co.elastic.clients.transport.ElasticsearchTransport; import co.elastic.clients.transport.endpoints.BooleanResponse; +import org.springframework.data.elasticsearch.core.mapping.Alias; +import org.springframework.data.elasticsearch.core.mapping.CreateIndexSettings; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -130,8 +133,14 @@ public Mono createWithMapping() { private Mono doCreate(IndexCoordinates indexCoordinates, Map settings, @Nullable Document mapping) { - - CreateIndexRequest createIndexRequest = requestConverter.indicesCreateRequest(indexCoordinates, settings, mapping); + Set aliases = (boundClass != null) ? getAliasesFor(boundClass) : new HashSet<>(); + CreateIndexSettings indexSettings = CreateIndexSettings.builder(indexCoordinates) + .withAliases(aliases) + .withSettings(settings) + .withMapping(mapping) + .build(); + + CreateIndexRequest createIndexRequest = requestConverter.indicesCreateRequest(indexSettings); Mono createIndexResponse = Mono.from(execute(client -> client.create(createIndexRequest))); return createIndexResponse.map(CreateIndexResponse::acknowledged); } @@ -435,6 +444,15 @@ private IndexCoordinates getIndexCoordinatesFor(Class clazz) { return elasticsearchConverter.getMappingContext().getRequiredPersistentEntity(clazz).getIndexCoordinates(); } + /** + * Get the {@link Alias} of the provided class. + * + * @param clazz provided class that can be used to extract aliases. + */ + private Set getAliasesFor(Class clazz) { + return elasticsearchConverter.getMappingContext().getRequiredPersistentEntity(clazz).getAliases(); + } + private Class checkForBoundClass() { if (boundClass == null) { throw new InvalidDataAccessApiUsageException("IndexOperations are not bound"); diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java index c0aafd60e..aef836ad2 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java @@ -88,6 +88,8 @@ import org.springframework.data.elasticsearch.core.index.GetTemplateRequest; import org.springframework.data.elasticsearch.core.index.PutIndexTemplateRequest; import org.springframework.data.elasticsearch.core.index.PutTemplateRequest; +import org.springframework.data.elasticsearch.core.mapping.Alias; +import org.springframework.data.elasticsearch.core.mapping.CreateIndexSettings; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; @@ -170,7 +172,7 @@ public co.elastic.clients.elasticsearch.cluster.PutComponentTemplateRequest clus })); } - private Alias.Builder buildAlias(AliasActionParameters parameters, Alias.Builder aliasBuilder) { + private co.elastic.clients.elasticsearch.indices.Alias.Builder buildAlias(AliasActionParameters parameters, co.elastic.clients.elasticsearch.indices.Alias.Builder aliasBuilder) { if (parameters.getRouting() != null) { aliasBuilder.routing(parameters.getRouting()); @@ -234,17 +236,25 @@ public ExistsRequest indicesExistsRequest(IndexCoordinates indexCoordinates) { return new ExistsRequest.Builder().index(Arrays.asList(indexCoordinates.getIndexNames())).build(); } - public CreateIndexRequest indicesCreateRequest(IndexCoordinates indexCoordinates, Map settings, - @Nullable Document mapping) { - - Assert.notNull(indexCoordinates, "indexCoordinates must not be null"); - Assert.notNull(settings, "settings must not be null"); + public CreateIndexRequest indicesCreateRequest(CreateIndexSettings indexSettings) { + Map aliases = new HashMap<>(); + for (Alias alias : indexSettings.getAliases()) { + co.elastic.clients.elasticsearch.indices.Alias esAlias = co.elastic.clients.elasticsearch.indices.Alias.of(ab -> ab.filter(getQuery(alias.getFilter(), null)) + .routing(alias.getRouting()) + .indexRouting(alias.getIndexRouting()) + .searchRouting(alias.getSearchRouting()) + .isHidden(alias.getHidden()) + .isWriteIndex(alias.getWriteIndex()) + ); + aliases.put(alias.getAlias(), esAlias); + } // note: the new client does not support the index.storeType anymore return new CreateIndexRequest.Builder() // - .index(indexCoordinates.getIndexName()) // - .settings(indexSettings(settings)) // - .mappings(typeMapping(mapping)) // + .index(indexSettings.getIndexCoordinates().getIndexName()) // + .aliases(aliases) + .settings(indexSettings(indexSettings.getSettings())) // + .mappings(typeMapping(indexSettings.getMapping())) // .build(); } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/Alias.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/Alias.java new file mode 100644 index 000000000..bbaa62443 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/Alias.java @@ -0,0 +1,232 @@ +/* + * Copyright 2024 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.data.elasticsearch.core.query.Query; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +import java.util.Objects; + +/** + * Immutable Value object encapsulating index alias(es). + * + * @author Youssef Aouichaoui + * @since 5.4 + */ +public class Alias { + /** + * Alias name for the index. + */ + private final String alias; + + /** + * Query used to limit documents the alias can access. + */ + @Nullable + private final Query filter; + + /** + * Used to route indexing operations to a specific shard. + */ + @Nullable + private final String indexRouting; + + /** + * Used to route search operations to a specific shard. + */ + @Nullable + private final String searchRouting; + + /** + * Used to route indexing and search operations to a specific shard. + */ + @Nullable + private final String routing; + + /** + * The alias is hidden? + * By default, this is set to {@code false}. + */ + @Nullable + private final Boolean isHidden; + + /** + * The index is the 'write index' for the alias? + * By default, this is set to {@code false}. + */ + @Nullable + private final Boolean isWriteIndex; + + private Alias(Builder builder) { + this.alias = builder.alias; + + this.filter = builder.filter; + + this.indexRouting = builder.indexRouting; + this.searchRouting = builder.searchRouting; + this.routing = builder.routing; + + this.isHidden = builder.isHidden; + this.isWriteIndex = builder.isWriteIndex; + } + + public String getAlias() { + return alias; + } + + @Nullable + public Query getFilter() { + return filter; + } + + @Nullable + public String getIndexRouting() { + return indexRouting; + } + + @Nullable + public String getSearchRouting() { + return searchRouting; + } + + @Nullable + public String getRouting() { + return routing; + } + + @Nullable + public Boolean getHidden() { + return isHidden; + } + + @Nullable + public Boolean getWriteIndex() { + return isWriteIndex; + } + + public static Builder builder(String alias) { + return new Builder(alias); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Alias that)) return false; + + return Objects.equals(alias, that.alias) && Objects.equals(filter, that.filter) + && Objects.equals(indexRouting, that.indexRouting) + && Objects.equals(searchRouting, that.searchRouting) + && Objects.equals(routing, that.routing) + && Objects.equals(isHidden, that.isHidden) + && Objects.equals(isWriteIndex, that.isWriteIndex); + } + + @Override + public int hashCode() { + return Objects.hash(alias, filter, indexRouting, searchRouting, routing, isHidden, isWriteIndex); + } + + public static class Builder { + private final String alias; + + @Nullable + private Query filter; + + @Nullable + private String indexRouting; + @Nullable + private String searchRouting; + @Nullable + private String routing; + + @Nullable + private Boolean isHidden; + @Nullable + private Boolean isWriteIndex; + + public Builder(String alias) { + Assert.notNull(alias, "alias must not be null"); + this.alias = alias; + } + + /** + * Query used to limit documents the alias can access. + */ + public Builder withFilter(@Nullable Query filter) { + this.filter = filter; + + return this; + } + + /** + * Used to route indexing operations to a specific shard. + */ + public Builder withIndexRouting(@Nullable String indexRouting) { + if (indexRouting != null && !indexRouting.trim().isEmpty()) { + this.indexRouting = indexRouting; + } + + return this; + } + + /** + * Used to route search operations to a specific shard. + */ + public Builder withSearchRouting(@Nullable String searchRouting) { + if (searchRouting != null && !searchRouting.trim().isEmpty()) { + this.searchRouting = searchRouting; + } + + return this; + } + + /** + * Used to route indexing and search operations to a specific shard. + */ + public Builder withRouting(@Nullable String routing) { + if (routing != null && !routing.trim().isEmpty()) { + this.routing = routing; + } + + return this; + } + + /** + * The alias is hidden? + * By default, this is set to {@code false}. + */ + public Builder withHidden(@Nullable Boolean hidden) { + isHidden = hidden; + + return this; + } + + /** + * The index is the 'write index' for the alias? + * By default, this is set to {@code false}. + */ + public Builder withWriteIndex(@Nullable Boolean writeIndex) { + isWriteIndex = writeIndex; + + return this; + } + + public Alias build() { + return new Alias(this); + } + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/CreateIndexSettings.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/CreateIndexSettings.java new file mode 100644 index 000000000..1406626dd --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/CreateIndexSettings.java @@ -0,0 +1,117 @@ +/* + * Copyright 2024 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.data.elasticsearch.core.document.Document; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Encapsulating index mapping fields, settings, and index alias(es). + * + * @author Youssef Aouichaoui + * @since 5.3 + */ +public class CreateIndexSettings { + private final IndexCoordinates indexCoordinates; + private final Set aliases; + + @Nullable + private final Map settings; + + @Nullable + private final Document mapping; + + private CreateIndexSettings(Builder builder) { + this.indexCoordinates = builder.indexCoordinates; + this.aliases = builder.aliases; + + this.settings = builder.settings; + this.mapping = builder.mapping; + } + + public static Builder builder(IndexCoordinates indexCoordinates) { + return new Builder(indexCoordinates); + } + + public IndexCoordinates getIndexCoordinates() { + return indexCoordinates; + } + + public Alias[] getAliases() { + return aliases.toArray(Alias[]::new); + } + + public Map getSettings() { + return settings; + } + + @Nullable + public Document getMapping() { + return mapping; + } + + public static class Builder { + private IndexCoordinates indexCoordinates; + private final Set aliases = new HashSet<>(); + + @Nullable + private Map settings; + + @Nullable + private Document mapping; + + public Builder(IndexCoordinates indexCoordinates) { + Assert.notNull(indexCoordinates, "indexCoordinates must not be null"); + this.indexCoordinates = indexCoordinates; + } + + public Builder withAlias(Alias alias) { + Assert.notNull(alias, "alias must not be null"); + this.aliases.add(alias); + + return this; + } + + public Builder withAliases(Set aliases) { + Assert.notNull(aliases, "aliases must not be null"); + this.aliases.addAll(aliases); + + return this; + } + + public Builder withSettings(Map settings) { + Assert.notNull(settings, "settings must not be null"); + this.settings = settings; + + return this; + } + + public Builder withMapping(@Nullable Document mapping) { + this.mapping = mapping; + + return this; + } + + public CreateIndexSettings build() { + return new CreateIndexSettings(this); + } + } +} 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 6d13d7eb6..e9d3d0c7a 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 @@ -25,6 +25,8 @@ import org.springframework.data.mapping.model.FieldNamingStrategy; import org.springframework.lang.Nullable; +import java.util.Set; + /** * ElasticsearchPersistentEntity * @@ -42,6 +44,14 @@ public interface ElasticsearchPersistentEntity extends PersistentEntity getAliases(); + short getShards(); short getReplicas(); 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 f265fa13d..4ff39850e 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 @@ -15,7 +15,9 @@ */ package org.springframework.data.elasticsearch.core.mapping; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; @@ -31,6 +33,8 @@ import org.springframework.data.elasticsearch.annotations.Setting; 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.mapping.MappingException; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.model.BasicPersistentEntity; @@ -80,6 +84,7 @@ public class SimpleElasticsearchPersistentEntity extends BasicPersistentEntit private final ConcurrentHashMap routingExpressions = new ConcurrentHashMap<>(); private @Nullable String routing; private final ContextConfiguration contextConfiguration; + private final Set aliases = new HashSet<>(); private final ConcurrentHashMap indexNameExpressions = new ConcurrentHashMap<>(); private final Lazy indexNameEvaluationContext = Lazy.of(this::getIndexNameEvaluationContext); @@ -112,6 +117,7 @@ public SimpleElasticsearchPersistentEntity(TypeInformation typeInformation, this.dynamic = document.dynamic(); this.storeIdInSource = document.storeIdInSource(); this.storeVersionInSource = document.storeVersionInSource(); + buildAliases(); } else { this.dynamic = Dynamic.INHERIT; this.storeIdInSource = true; @@ -138,6 +144,11 @@ public IndexCoordinates getIndexCoordinates() { return resolve(IndexCoordinates.of(getIndexName())); } + @Override + public Set getAliases() { + return aliases; + } + @Nullable @Override public String getIndexStoreType() { @@ -615,4 +626,36 @@ public boolean getWriteTypeHints() { public Dynamic dynamic() { return dynamic; } + + /** + * Building once the aliases for the current document. + */ + private void buildAliases() { + // Clear the existing aliases. + aliases.clear(); + + if (document != null) { + for (org.springframework.data.elasticsearch.annotations.Alias alias : document.aliases()) { + if (alias.value().isEmpty()) { + continue; + } + + Query query = null; + if (!alias.filter().value().isEmpty()) { + query = new StringQuery(alias.filter().value()); + } + + aliases.add( + Alias.builder(alias.value()) + .withFilter(query) + .withIndexRouting(alias.indexRouting()) + .withSearchRouting(alias.searchRouting()) + .withRouting(alias.routing()) + .withHidden(alias.isHidden()) + .withWriteIndex(alias.isWriteIndex()) + .build() + ); + } + } + } } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/IndexOperationsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/IndexOperationsIntegrationTests.java index eb65d0ac9..d308eb8f0 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/IndexOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/IndexOperationsIntegrationTests.java @@ -16,11 +16,14 @@ package org.springframework.data.elasticsearch.core.index; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.data.elasticsearch.annotations.FieldType.Text; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import org.assertj.core.api.InstanceOfAssertFactories; import org.assertj.core.api.SoftAssertions; import org.json.JSONException; import org.junit.jupiter.api.BeforeEach; @@ -30,13 +33,18 @@ import org.skyscreamer.jsonassert.JSONAssert; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Alias; import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.Filter; import org.springframework.data.elasticsearch.annotations.Mapping; import org.springframework.data.elasticsearch.annotations.Setting; +import org.springframework.data.elasticsearch.client.elc.Queries; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.IndexInformation; import org.springframework.data.elasticsearch.core.IndexOperations; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.data.elasticsearch.core.query.StringQuery; import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; import org.springframework.data.elasticsearch.utils.IndexNameProvider; import org.springframework.lang.Nullable; @@ -171,6 +179,29 @@ void shouldReturnAliasDataWithGetAliasesForIndexMethod() { softly.assertAll(); } + @Test + void shouldCreateIndexWithAliases() { + // Given + indexNameProvider.increment(); + String indexName = indexNameProvider.indexName(); + indexOperations = operations.indexOps(EntityWithAliases.class); + indexOperations.createWithMapping(); + + // When + Map> aliases = indexOperations.getAliasesForIndex(indexName); + + // Then + AliasData result = aliases.values().stream().findFirst().orElse(new HashSet<>()).stream().findFirst().orElse(null); + assertThat(result).isNotNull(); + assertThat(result.getAlias()).isEqualTo("first_alias"); + assertThat(result.getFilterQuery()).asInstanceOf(InstanceOfAssertFactories.type(StringQuery.class)) + .extracting(StringQuery::getSource) + .asString() + .contains(Queries.wrapperQuery(""" + {"bool" : {"must" : {"term" : {"type" : "abc"}}}} + """).query()); + } + @Document(indexName = "#{@indexNameProvider.indexName()}") @Setting(settingPath = "settings/test-settings.json") @Mapping(mappingPath = "mappings/test-mappings.json") @@ -186,4 +217,31 @@ public void setId(@Nullable String id) { this.id = id; } } + + @Document(indexName = "#{@indexNameProvider.indexName()}", aliases = { + @Alias(value = "first_alias", filter =@Filter(""" + {"bool" : {"must" : {"term" : {"type" : "abc"}}}} + """)) + }) + private static class EntityWithAliases { + @Nullable private @Id String id; + @Field(type = Text) private String type; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + } } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexOperationsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexOperationsIntegrationTests.java index b37ba3c53..3a56d505c 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexOperationsIntegrationTests.java @@ -17,12 +17,19 @@ import static org.assertj.core.api.Assertions.*; import static org.skyscreamer.jsonassert.JSONAssert.*; +import static org.springframework.data.elasticsearch.annotations.FieldType.Text; import static org.springframework.data.elasticsearch.core.IndexOperationsAdapter.*; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.springframework.data.elasticsearch.annotations.Alias; +import org.springframework.data.elasticsearch.annotations.Filter; +import org.springframework.data.elasticsearch.client.elc.Queries; +import org.springframework.data.elasticsearch.core.query.StringQuery; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.time.LocalDate; +import java.util.HashSet; import java.util.Set; import org.json.JSONException; @@ -346,6 +353,33 @@ void shouldGetAliasData() { .verifyComplete(); } + @Test + void shouldCreateIndexWithAliases() { + // Given + indexNameProvider.increment(); + String indexName = indexNameProvider.indexName(); + indexOperations = operations.indexOps(EntityWithAliases.class); + blocking(indexOperations).createWithMapping(); + + // When + + // Then + indexOperations.getAliasesForIndex(indexName) + .as(StepVerifier::create) + .assertNext(aliases -> { + AliasData result = aliases.values().stream().findFirst().orElse(new HashSet<>()).stream().findFirst().orElse(null); + + assertThat(result).isNotNull(); + assertThat(result.getAlias()).isEqualTo("first_alias"); + assertThat(result.getFilterQuery()).asInstanceOf(InstanceOfAssertFactories.type(StringQuery.class)) + .extracting(StringQuery::getSource) + .asString() + .contains(Queries.wrapperQuery(""" + {"bool" : {"must" : {"term" : {"type" : "abc"}}}} + """).query()); + }).verifyComplete(); + } + @Document(indexName = "#{@indexNameProvider.indexName()}") @Setting(shards = 3, replicas = 2, refreshInterval = "4s") static class Entity { @@ -401,4 +435,31 @@ public void setId(@Nullable String id) { this.id = id; } } + + @Document(indexName = "#{@indexNameProvider.indexName()}", aliases = { + @Alias(value = "first_alias", filter =@Filter(""" + {"bool" : {"must" : {"term" : {"type" : "abc"}}}} + """)) + }) + private static class EntityWithAliases { + @Nullable private @Id String id; + @Field(type = Text) private String type; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + } }