From dc581cc0900c5840f0c011f507deed2b69533d5a Mon Sep 17 00:00:00 2001 From: Peter-Josef Meisch Date: Wed, 7 Feb 2024 20:24:51 +0100 Subject: [PATCH] Add support for field aliases in the index mapping. Closes #2845 --- .../elasticsearch/elasticsearch-new.adoc | 1 + .../elasticsearch/annotations/Mapping.java | 7 +++ .../annotations/MappingAlias.java | 45 +++++++++++++++ .../core/index/MappingBuilder.java | 15 ++++- .../core/index/MappingParameters.java | 2 + .../index/MappingBuilderIntegrationTests.java | 23 +++++++- .../core/index/MappingBuilderUnitTests.java | 57 ++++++++++++++++--- 7 files changed, 137 insertions(+), 13 deletions(-) create mode 100644 src/main/java/org/springframework/data/elasticsearch/annotations/MappingAlias.java diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc index 1678e6894..398af1e6b 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc @@ -9,6 +9,7 @@ * Add shard statistics to the `SearchHit` class. * Add support for multi search template API. * Add support for SpEL in @Query. +* Add support for field aliases in the index mapping. [[new-features.5-2-0]] == New in Spring Data Elasticsearch 5.2 diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Mapping.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Mapping.java index 125f1a767..0fd36357d 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Mapping.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Mapping.java @@ -74,6 +74,13 @@ */ String runtimeFieldsPath() default ""; + /** + * field alias definitions to be written to the index mapping + * + * @since 5.3 + */ + MappingAlias[] aliases() default {}; + enum Detection { DEFAULT, TRUE, FALSE; } diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/MappingAlias.java b/src/main/java/org/springframework/data/elasticsearch/annotations/MappingAlias.java new file mode 100644 index 000000000..f71e664ee --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/MappingAlias.java @@ -0,0 +1,45 @@ +/* + * 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.Documented; +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; + +/** + * Defines a field alias in the index mapping. + * + * @author Peter-Josef Meisch + * @since 5.3 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Documented +@Inherited +public @interface MappingAlias { + /** + * the name of the alias. + */ + String name(); + + /** + * the path of the alias. + */ + String path(); +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java index 693111975..321486770 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java @@ -214,8 +214,9 @@ private void mapEntity(ObjectNode objectNode, @Nullable ElasticsearchPersistentE @Nullable Field parentFieldAnnotation, @Nullable Dynamic dynamicMapping, @Nullable Document runtimeFields) throws IOException { - if (entity != null && entity.isAnnotationPresent(Mapping.class)) { - Mapping mappingAnnotation = entity.getRequiredAnnotation(Mapping.class); + var mappingAnnotation = entity != null ? entity.findAnnotation(Mapping.class) : null; + + if (mappingAnnotation != null) { if (!mappingAnnotation.enabled()) { objectNode.put(MAPPING_ENABLED, false); @@ -289,6 +290,16 @@ private void mapEntity(ObjectNode objectNode, @Nullable ElasticsearchPersistentE LOGGER.warn(String.format("error mapping property with name %s", property.getName()), e); } }); + + } + + // write the alias entries after the properties + if (mappingAnnotation != null) { + for (MappingAlias mappingAlias : mappingAnnotation.aliases()) { + var aliasNode = propertiesNode.putObject(mappingAlias.name()); + aliasNode.put(FIELD_PARAM_TYPE, FIELD_PARAM_TYPE_ALIAS); + aliasNode.put(FIELD_PARAM_PATH, mappingAlias.path()); + } } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/MappingParameters.java b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingParameters.java index a3c1578ee..72fa129bc 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/MappingParameters.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingParameters.java @@ -84,6 +84,8 @@ public final class MappingParameters { static final String FIELD_PARAM_SIMILARITY = "similarity"; static final String FIELD_PARAM_TERM_VECTOR = "term_vector"; static final String FIELD_PARAM_TYPE = "type"; + static final String FIELD_PARAM_PATH = "path"; + static final String FIELD_PARAM_TYPE_ALIAS = "alias"; private final String analyzer; private final boolean coerce; diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderIntegrationTests.java index 4da1e9831..3b8ea9442 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderIntegrationTests.java @@ -18,10 +18,7 @@ import static org.assertj.core.api.Assertions.*; import static org.springframework.data.elasticsearch.annotations.FieldType.*; -import static org.springframework.data.elasticsearch.annotations.FieldType.Object; -import java.lang.Integer; -import java.lang.Object; import java.time.Instant; import java.time.LocalDate; import java.util.Collection; @@ -269,6 +266,12 @@ void shouldWriteCorrectMappingForDenseVectorProperty() { operations.indexOps(SimilarityEntity.class).createWithMapping(); } + @Test // #2845 + @DisplayName("should write mapping with field aliases") + void shouldWriteMappingWithFieldAliases() { + operations.indexOps(FieldAliasEntity.class).createWithMapping(); + } + // region Entities @Document(indexName = "#{@indexNameProvider.indexName()}") static class Book { @@ -908,5 +911,19 @@ static class SimilarityEntity { @Field(type = FieldType.Dense_Vector, dims = 42, similarity = "cosine") private double[] denseVector; } + @Mapping(aliases = { + @MappingAlias(name = "someAlly", path = "someText"), + @MappingAlias(name = "otherAlly", path = "otherText") + }) + @Document(indexName = "#{@indexNameProvider.indexName()}") + private static class FieldAliasEntity { + @Id + @Nullable private String id; + @Nullable + @Field(type = Text) private String someText; + @Nullable + @Field(type = Text) private String otherText; + } + // endregion } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java index 56656235d..1acab61de 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java @@ -19,12 +19,7 @@ import static org.assertj.core.api.Assertions.*; import static org.skyscreamer.jsonassert.JSONAssert.*; import static org.springframework.data.elasticsearch.annotations.FieldType.*; -import static org.springframework.data.elasticsearch.annotations.FieldType.Object; -import java.lang.Boolean; -import java.lang.Double; -import java.lang.Integer; -import java.lang.Object; import java.math.BigDecimal; import java.time.Instant; import java.time.LocalDate; @@ -1179,6 +1174,39 @@ void shouldUseCustomNameWithDots() throws JSONException { assertEquals(expected, mapping, true); } + @Test // #2845 + @DisplayName("should write field aliases to the mapping") + void shouldWriteFieldAliasesToTheMapping() throws JSONException { + + var expected = """ + { + "properties": { + "_class": { + "type": "keyword", + "index": false, + "doc_values": false + }, + "someText": { + "type": "text" + }, + "otherText": { + "type": "text" + }, + "someAlly": { + "type": "alias", + "path": "someText" + }, + "otherAlly": { + "type": "alias", + "path": "otherText" + } + } + } + """; + String mapping = getMappingBuilder().buildPropertyMapping(FieldAliasEntity.class); + + assertEquals(expected, mapping, true); + } // region entities @Document(indexName = "ignore-above-index") @@ -1298,8 +1326,8 @@ static class MultiFieldEntity { @Nullable @MultiField(mainField = @Field(name = "alternate-description", type = FieldType.Text, analyzer = "whitespace"), - otherFields = { - @InnerField(suffix = "suff-ix", type = FieldType.Text, analyzer = "stop", searchAnalyzer = "standard") }) // + otherFields = { + @InnerField(suffix = "suff-ix", type = FieldType.Text, analyzer = "stop", searchAnalyzer = "standard") }) // public String getAlternateDescription() { return alternateDescription; } @@ -1662,7 +1690,7 @@ static class GeoEntity { @GeoPointField private String pointC; @Nullable @GeoPointField private double[] pointD; - // geo shape, until e have the classes for this, us a strng + @Nullable @GeoShapeField private String shape1; @Nullable @@ -2390,5 +2418,18 @@ private static class FieldNameDotsEntity { @Nullable @Field(name = "dotted.field", type = Text) private String dottedField; } + + @Mapping(aliases = { + @MappingAlias(name = "someAlly", path = "someText"), + @MappingAlias(name = "otherAlly", path = "otherText") + }) + private static class FieldAliasEntity { + @Id + @Nullable private String id; + @Nullable + @Field(type = Text) private String someText; + @Nullable + @Field(type = Text) private String otherText; + } // endregion }