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 c1a7b8ff1..4378f0bdf 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Document.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Document.java @@ -33,6 +33,7 @@ * @author Ivan Greene * @author Mark Paluch * @author Peter-Josef Meisch + * @author Sascha Woo */ @Persistent @Inherited @@ -112,4 +113,12 @@ * @since 4.3 */ WriteTypeHint writeTypeHint() default WriteTypeHint.DEFAULT; + + /** + * Controls how Elasticsearch dynamically adds fields to the document. + * + * @since 4.3 + */ + Dynamic dynamic() default Dynamic.INHERIT; + } diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Dynamic.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Dynamic.java new file mode 100644 index 000000000..a0bd128f9 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Dynamic.java @@ -0,0 +1,50 @@ +/* + * Copyright 2021 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; + +/** + * Values for the {@code dynamic} mapping parameter. + * + * @author Sascha Woo + * @since 4.3 + */ +public enum Dynamic { + /** + * New fields are added to the mapping. + */ + TRUE, + /** + * New fields are added to the mapping as + * runtime fields. These + * fields are not indexed, and are loaded from {@code _source} at query time. + */ + RUNTIME, + /** + * New fields are ignored. These fields will not be indexed or searchable, but will still appear in the + * {@code _source} field of returned hits. These fields will not be added to the mapping, and new fields must be added + * explicitly. + */ + FALSE, + /** + * If new fields are detected, an exception is thrown and the document is rejected. New fields must be explicitly + * added to the mapping. + */ + STRICT, + /** + * Inherit the dynamic setting from their parent object or from the mapping type. + */ + INHERIT +} diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/DynamicMapping.java b/src/main/java/org/springframework/data/elasticsearch/annotations/DynamicMapping.java index d06aed12b..513faf7a5 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/DynamicMapping.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/DynamicMapping.java @@ -26,11 +26,14 @@ * {@see elasticsearch doc} * * @author Peter-Josef Meisch + * @author Sascha Woo * @since 4.0 + * @deprecated since 4.3, use {@link Document#dynamic()} or {@link Field#dynamic()} instead. */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.FIELD }) @Documented +@Deprecated public @interface DynamicMapping { DynamicMappingValue value() default DynamicMappingValue.True; diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/DynamicMappingValue.java b/src/main/java/org/springframework/data/elasticsearch/annotations/DynamicMappingValue.java index b2110637e..85fe3b8e8 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/DynamicMappingValue.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/DynamicMappingValue.java @@ -19,8 +19,11 @@ * values for the {@link DynamicMapping annotation} * * @author Peter-Josef Meisch + * @author Sascha Woo * @since 4.0 + * @deprecated since 4.3, use {@link Document#dynamic()} or {@link Field#dynamic()} instead. */ +@Deprecated public enum DynamicMappingValue { True, False, Strict } diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java index be5ae13f5..75d7bdedf 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java @@ -195,4 +195,12 @@ * @since 4.2 */ int dims() default -1; + + /** + * Controls how Elasticsearch dynamically adds fields to the inner object within the document.
+ * To be used in combination with {@link FieldType#Object} or {@link FieldType#Nested} + * + * @since 4.3 + */ + Dynamic dynamic() default Dynamic.INHERIT; } 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 a5153bb63..981b7ec52 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 @@ -197,7 +197,9 @@ private void mapEntity(XContentBuilder builder, @Nullable ElasticsearchPersisten } } - if (dynamicMapping != null) { + if (entity != null && entity.dynamic() != Dynamic.INHERIT) { + builder.field(TYPE_DYNAMIC, entity.dynamic().name().toLowerCase()); + } else if (dynamicMapping != null) { builder.field(TYPE_DYNAMIC, dynamicMapping.value().name().toLowerCase()); } @@ -440,8 +442,12 @@ private void addSingleFieldMapping(XContentBuilder builder, ElasticsearchPersist builder.startObject(property.getFieldName()); - if (nestedOrObjectField && dynamicMapping != null) { - builder.field(TYPE_DYNAMIC, dynamicMapping.value().name().toLowerCase()); + if (nestedOrObjectField) { + if (annotation.dynamic() != Dynamic.INHERIT) { + builder.field(TYPE_DYNAMIC, annotation.dynamic().name().toLowerCase()); + } else if (dynamicMapping != null) { + builder.field(TYPE_DYNAMIC, dynamicMapping.value().name().toLowerCase()); + } } addFieldMappingParameters(builder, annotation, nestedOrObjectField); @@ -489,8 +495,12 @@ private void addMultiFieldMapping(XContentBuilder builder, ElasticsearchPersiste // main field builder.startObject(property.getFieldName()); - if (nestedOrObjectField && dynamicMapping != null) { - builder.field(TYPE_DYNAMIC, dynamicMapping.value().name().toLowerCase()); + if (nestedOrObjectField) { + if (annotation.mainField().dynamic() != Dynamic.INHERIT) { + builder.field(TYPE_DYNAMIC, annotation.mainField().dynamic().name().toLowerCase()); + } else if (dynamicMapping != null) { + builder.field(TYPE_DYNAMIC, dynamicMapping.value().name().toLowerCase()); + } } addFieldMappingParameters(builder, annotation.mainField(), nestedOrObjectField); 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 3a53b4b1c..2bb134cbb 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 @@ -16,6 +16,7 @@ package org.springframework.data.elasticsearch.core.mapping; import org.elasticsearch.index.VersionType; +import org.springframework.data.elasticsearch.annotations.Dynamic; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.core.index.Settings; import org.springframework.data.elasticsearch.core.join.JoinField; @@ -160,4 +161,10 @@ default ElasticsearchPersistentProperty getRequiredSeqNoPrimaryTermProperty() { * @since 4.3 */ boolean writeTypeHints(); + + /** + * @return the {@code dynamic} mapping parameter value. + * @since 4.3 + */ + Dynamic dynamic(); } 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 9b4822a6e..0cec9b4d7 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 @@ -25,6 +25,7 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.dao.InvalidDataAccessApiUsageException; 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.annotations.FieldType; import org.springframework.data.elasticsearch.annotations.Routing; @@ -73,6 +74,7 @@ public class SimpleElasticsearchPersistentEntity extends BasicPersistentEntit private @Nullable ElasticsearchPersistentProperty joinFieldProperty; private @Nullable VersionType versionType; private boolean createIndexAndMapping; + private final Dynamic dynamic; private final Map fieldNamePropertyCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap routingExpressions = new ConcurrentHashMap<>(); private @Nullable String routing; @@ -102,8 +104,10 @@ public SimpleElasticsearchPersistentEntity(TypeInformation typeInformation, this.indexName = document.indexName(); this.versionType = document.versionType(); this.createIndexAndMapping = document.createIndex(); + this.dynamic = document.dynamic(); + } else { + this.dynamic = Dynamic.INHERIT; } - Routing routingAnnotation = AnnotatedElementUtils.findMergedAnnotation(clazz, Routing.class); if (routingAnnotation != null) { @@ -559,4 +563,9 @@ public FieldNamingStrategy getFieldNamingStrategy() { return fieldNamingStrategy; } } + + @Override + public Dynamic dynamic() { + return dynamic; + } } 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 67a9a6099..d5e3d6da3 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 @@ -287,8 +287,18 @@ void shouldWriteMappingForDisabledProperty() { } @Test // #1767 - @DisplayName("should write dynamic mapping entries") - void shouldWriteDynamicMappingEntries() { + @DisplayName("should write dynamic mapping annotations") + void shouldWriteDynamicMappingAnnotations() { + + IndexOperations indexOps = operations.indexOps(DynamicMappingAnnotationEntity.class); + indexOps.create(); + indexOps.putMapping(); + + } + + @Test // #1871 + @DisplayName("should write dynamic mapping") + void shouldWriteDynamicMapping() { IndexOperations indexOps = operations.indexOps(DynamicMappingEntity.class); indexOps.create(); @@ -1104,9 +1114,9 @@ public void setDense_vector(@Nullable float[] dense_vector) { } } - @Document(indexName = "dynamic-mapping") + @Document(indexName = "dynamic-mapping-annotation") @DynamicMapping(DynamicMappingValue.False) - static class DynamicMappingEntity { + static class DynamicMappingAnnotationEntity { @Nullable @DynamicMapping(DynamicMappingValue.Strict) @Field(type = FieldType.Object) private Author author; @Nullable @DynamicMapping(DynamicMappingValue.False) @Field( @@ -1124,6 +1134,31 @@ public void setAuthor(Author author) { } } + @Document(indexName = "dynamic-mapping", dynamic = Dynamic.FALSE) + static class DynamicMappingEntity { + + @Nullable @Field(type = FieldType.Object) // + private Map objectInherit; + @Nullable @Field(type = FieldType.Object, dynamic = Dynamic.FALSE) // + private Map objectFalse; + @Nullable @Field(type = FieldType.Object, dynamic = Dynamic.TRUE) // + private Map objectTrue; + @Nullable @Field(type = FieldType.Object, dynamic = Dynamic.STRICT) // + private Map objectStrict; + @Nullable @Field(type = FieldType.Object, dynamic = Dynamic.RUNTIME) // + private Map objectRuntime; + @Nullable @Field(type = FieldType.Nested) // + private List> nestedObjectInherit; + @Nullable @Field(type = FieldType.Nested, dynamic = Dynamic.FALSE) // + private List> nestedObjectFalse; + @Nullable @Field(type = FieldType.Nested, dynamic = Dynamic.TRUE) // + private List> nestedObjectTrue; + @Nullable @Field(type = FieldType.Nested, dynamic = Dynamic.STRICT) // + private List> nestedObjectStrict; + @Nullable @Field(type = FieldType.Nested, dynamic = Dynamic.RUNTIME) // + private List> nestedObjectRuntime; + } + @Document(indexName = "dynamic-detection-mapping-true") @Mapping(dateDetection = Mapping.Detection.TRUE, numericDetection = Mapping.Detection.TRUE, dynamicDateFormats = { "MM/dd/yyyy" }) 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 7ed2405c0..bb27ea215 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 @@ -419,7 +419,7 @@ public void shouldSetFieldMappingProperties() throws JSONException { } @Test // DATAES-148, #1767 - void shouldWriteDynamicMappingSettings() throws JSONException { + void shouldWriteDynamicMappingFromAnnotation() throws JSONException { String expected = "{\n" + // " \"dynamic\": \"false\",\n" + // @@ -451,7 +451,65 @@ void shouldWriteDynamicMappingSettings() throws JSONException { " }\n" + // "}"; // - String mapping = getMappingBuilder().buildPropertyMapping(ConfigureDynamicMappingEntity.class); + String mapping = getMappingBuilder().buildPropertyMapping(DynamicMappingAnnotationEntity.class); + + assertEquals(expected, mapping, true); + } + + @Test // #1871 + void shouldWriteDynamicMapping() throws JSONException { + + String expected = "{\n" // + + " \"dynamic\": \"false\",\n" // + + " \"properties\": {\n" // + + " \"_class\": {\n" // + + " \"type\": \"keyword\",\n" // + + " \"index\": false,\n" // + + " \"doc_values\": false\n" // + + " },\n" // + + " \"objectInherit\": {\n" // + + " \"type\": \"object\"\n" // + + " },\n" // + + " \"objectFalse\": {\n" // + + " \"dynamic\": \"false\",\n" // + + " \"type\": \"object\"\n" // + + " },\n" // + + " \"objectTrue\": {\n" // + + " \"dynamic\": \"true\",\n" // + + " \"type\": \"object\"\n" // + + " },\n" // + + " \"objectStrict\": {\n" // + + " \"dynamic\": \"strict\",\n" // + + " \"type\": \"object\"\n" // + + " },\n" // + + " \"objectRuntime\": {\n" // + + " \"dynamic\": \"runtime\",\n" // + + " \"type\": \"object\"\n" // + + " },\n" // + + " \"nestedObjectInherit\": {\n" // + + " \"type\": \"nested\"\n" // + + " },\n" // + + " \"nestedObjectFalse\": {\n" // + + " \"dynamic\": \"false\",\n" // + + " \"type\": \"nested\"\n" // + + " },\n" // + + " \"nestedObjectTrue\": {\n" // + + " \"dynamic\": \"true\",\n" // + + " \"type\": \"nested\"\n" // + + " },\n" // + + " \"nestedObjectStrict\": {\n" // + + " \"dynamic\": \"strict\",\n" // + + " \"type\": \"nested\"\n" // + + " },\n" // + + " \"nestedObjectRuntime\": {\n" // + + " \"dynamic\": \"runtime\",\n" // + + " \"type\": \"nested\"\n" // + + " }\n" // + + " }\n" // + + "}\n" // + + ""; + + String mapping = getMappingBuilder().buildPropertyMapping(DynamicMappingEntity.class); assertEquals(expected, mapping, true); } @@ -865,7 +923,8 @@ void shouldWriteRuntimeFields() throws JSONException { " \"day_of_week\": {\n" + // " \"type\": \"keyword\",\n" + // " \"script\": {\n" + // - " \"source\": \"emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))\"\n" + // + " \"source\": \"emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))\"\n" + + // " }\n" + // " }\n" + // " },\n" + // @@ -1441,7 +1500,7 @@ static class FieldMappingParameters { @Document(indexName = "test-index-configure-dynamic-mapping") @DynamicMapping(DynamicMappingValue.False) - static class ConfigureDynamicMappingEntity { + static class DynamicMappingAnnotationEntity { @Nullable @DynamicMapping(DynamicMappingValue.Strict) @Field(type = FieldType.Object) private Author author; @Nullable @DynamicMapping(DynamicMappingValue.False) @Field( @@ -1459,6 +1518,32 @@ public void setAuthor(Author author) { } } + @Document(indexName = "test-index-configure-dynamic-mapping", dynamic = Dynamic.FALSE) + static class DynamicMappingEntity { + + @Nullable @Field(type = FieldType.Object) // + private Map objectInherit; + @Nullable @Field(type = FieldType.Object, dynamic = Dynamic.FALSE) // + private Map objectFalse; + @Nullable @Field(type = FieldType.Object, dynamic = Dynamic.TRUE) // + private Map objectTrue; + @Nullable @Field(type = FieldType.Object, dynamic = Dynamic.STRICT) // + private Map objectStrict; + @Nullable @Field(type = FieldType.Object, dynamic = Dynamic.RUNTIME) // + private Map objectRuntime; + @Nullable @Field(type = FieldType.Nested) // + private List> nestedObjectInherit; + @Nullable @Field(type = FieldType.Nested, dynamic = Dynamic.FALSE) // + private List> nestedObjectFalse; + @Nullable @Field(type = FieldType.Nested, dynamic = Dynamic.TRUE) // + private List> nestedObjectTrue; + @Nullable @Field(type = FieldType.Nested, dynamic = Dynamic.STRICT) // + private List> nestedObjectStrict; + @Nullable @Field(type = FieldType.Nested, dynamic = Dynamic.RUNTIME) // + private List> nestedObjectRuntime; + + } + static class ValueObject { private final String value;