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 0a9d4f4635..f1ba459a68 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java @@ -252,4 +252,11 @@ * @since 5.4 */ String mappedTypeName() default ""; + + /** + * Maps your data beyond the dynamic field mapping rules. + * + * @since 5.4 + */ + boolean dynamicTemplate() default false; } 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 c46f1da0ec..4b20ef1f9a 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 @@ -80,7 +80,6 @@ public IndicesTemplate(ElasticsearchIndicesClient client, ClusterTemplate cluste this.elasticsearchConverter = elasticsearchConverter; this.boundClass = boundClass; this.boundIndex = null; - } public IndicesTemplate(ElasticsearchIndicesClient client, ClusterTemplate clusterTemplate, @@ -95,7 +94,6 @@ public IndicesTemplate(ElasticsearchIndicesClient client, ClusterTemplate cluste this.elasticsearchConverter = elasticsearchConverter; this.boundClass = null; this.boundIndex = boundIndex; - } protected Class checkForBoundClass() { @@ -145,6 +143,8 @@ protected boolean doCreate(IndexCoordinates indexCoordinates, Map client.create(createIndexRequest)); + // refresh cached mappings + refreshMapping(); return Boolean.TRUE.equals(createIndexResponse.acknowledged()); } @@ -241,6 +241,28 @@ public Map getMapping() { return responseConverter.indicesGetMapping(getMappingResponse, indexCoordinates); } + /** + * Refreshes the mapping for the current entity. + *

+ * This method is responsible for retrieving and updating the metadata related to the current entity. + */ + private void refreshMapping() { + if (boundClass == null) { + return; + } + + ElasticsearchPersistentEntity entity = this.elasticsearchConverter.getMappingContext() + .getPersistentEntity(boundClass); + if (entity == null) { + return; + } + + Object dynamicTemplates = getMapping().get("dynamic_templates"); + if (dynamicTemplates instanceof List value) { + entity.buildDynamicTemplates(value); + } + } + @Override public Settings createSettings() { return createSettings(checkForBoundClass()); 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 d4c7a6e1b3..2bb2e8583b 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 @@ -25,6 +25,7 @@ import reactor.core.publisher.Mono; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -142,7 +143,12 @@ private Mono doCreate(IndexCoordinates indexCoordinates, Map createIndexResponse = Mono.from(execute(client -> client.create(createIndexRequest))); - return createIndexResponse.map(CreateIndexResponse::acknowledged); + return createIndexResponse + .doOnNext((result) -> { + // refresh cached mappings + refreshMapping(); + }) + .map(CreateIndexResponse::acknowledged); } @Override @@ -218,6 +224,30 @@ public Mono getMapping() { return getMappingResponse.map(response -> responseConverter.indicesGetMapping(response, indexCoordinates)); } + /** + * Refreshes the mapping for the current entity. + *

+ * This method is responsible for retrieving and updating the metadata related to the current entity. + */ + private void refreshMapping() { + if (boundClass == null) { + return; + } + + ElasticsearchPersistentEntity entity = this.elasticsearchConverter.getMappingContext() + .getPersistentEntity(boundClass); + if (entity == null) { + return; + } + + getMapping().subscribe((mappings) -> { + Object dynamicTemplates = mappings.get("dynamic_templates"); + if (dynamicTemplates instanceof List value) { + entity.buildDynamicTemplates(value); + } + }); + } + @Override public Mono createSettings() { return createSettings(checkForBoundClass()); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java index ccac971fd1..d59069e6fe 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java @@ -15,6 +15,8 @@ */ package org.springframework.data.elasticsearch.core.convert; +import static org.springframework.util.PatternMatchUtils.simpleMatch; + import java.time.temporal.TemporalAccessor; import java.util.*; import java.util.Map.Entry; @@ -42,6 +44,7 @@ import org.springframework.data.elasticsearch.annotations.ScriptedField; import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.document.SearchDocument; +import org.springframework.data.elasticsearch.core.mapping.DynamicTemplate; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; import org.springframework.data.elasticsearch.core.mapping.PropertyValueConverter; @@ -388,6 +391,10 @@ private R readEntity(ElasticsearchPersistentEntity entity, Map void populateScriptFields(ElasticsearchPersistentEntity entity, T }); } + private void populateFieldsUsingDynamicTemplates(ElasticsearchPersistentEntity targetEntity, R result, + Document document) { + for (Entry templateEntry : targetEntity.getDynamicTemplates().entrySet()) { + ElasticsearchPersistentProperty property = targetEntity + .getPersistentPropertyWithFieldName(templateEntry.getKey()); + if (property != null && property.isDynamicFieldMapping()) { + // prepare value + Map values = new HashMap<>(); + // TODO: Path match and unmatched + document.entrySet().stream() + .filter(fieldKey -> templateEntry.getValue().getMatch().stream() + .anyMatch(regex -> simpleMatch(regex, fieldKey.getKey())) + && templateEntry.getValue().getUnmatch().stream() + .noneMatch(regex -> simpleMatch(regex, fieldKey.getKey()))) + .forEach(entry -> values.put(entry.getKey(), entry.getValue())); + + // set property + targetEntity.getPropertyAccessor(result).setProperty(property, read(property.getType(), Document.from(values))); + } + } + } + /** * Compute the type to use by checking the given entity against the store type; */ @@ -1035,7 +1064,14 @@ protected void writeProperty(ElasticsearchPersistentProperty property, Object va if (valueType.isMap()) { Map mapDbObj = createMap((Map) value, property); - sink.set(property, mapDbObj); + if (property.isDynamicFieldMapping()) { + for (Entry entry : mapDbObj.entrySet()) { + sink.set(entry.getKey(), entry.getValue()); + } + } else { + sink.set(property, mapDbObj); + } + return; } @@ -1058,7 +1094,14 @@ protected void writeProperty(ElasticsearchPersistentProperty property, Object va addCustomTypeKeyIfNecessary(value, document, TypeInformation.of(property.getRawType())); writeInternal(value, document, entity); - sink.set(property, document); + if (property.isDynamicFieldMapping()) { + // flatten + for (Entry entry : document.entrySet()) { + sink.set(entry.getKey(), entry.getValue()); + } + } else { + sink.set(property, document); + } } /** @@ -1499,7 +1542,11 @@ public void set(ElasticsearchPersistentProperty property, @Nullable Object value } } - target.put(property.getFieldName(), value); + set(property.getFieldName(), value); + } + + public void set(String key, @Nullable Object value) { + target.put(key, value); } private Map getAsMap(Object result) { diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/DynamicTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/DynamicTemplate.java new file mode 100644 index 0000000000..0ee59fc72c --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/DynamicTemplate.java @@ -0,0 +1,190 @@ +package org.springframework.data.elasticsearch.core.mapping; + +import java.util.ArrayList; +import java.util.List; + +/** + * Immutable Value object encapsulating dynamic template(s). + * {@see Elastic + * docs} + * + * @author Youssef Aouichaoui + * @since 5.4 + */ +public class DynamicTemplate { + /** + * Patterns to match on the field name. + */ + private final List match; + + /** + * Path patterns for a nested type to match the field name. + */ + private final List pathMatch; + + /** + * Patterns that do not match the field name. + */ + private final List unmatch; + + /** + * Path patterns for a nested type that do not match the field name. + */ + private final List pathUnmatch; + + /** + * Data types that correspond to the field. + */ + private final List matchMappingType; + + /** + * Data types that do not match to the field. + */ + private final List unmatchMappingType; + + private DynamicTemplate(Builder builder) { + this.match = builder.match; + this.pathMatch = builder.pathMatch; + + this.unmatch = builder.unmatch; + this.pathUnmatch = builder.pathUnmatch; + + this.matchMappingType = builder.matchMappingType; + this.unmatchMappingType = builder.unmatchMappingType; + } + + public List getMatch() { + return match; + } + + public List getPathMatch() { + return pathMatch; + } + + public List getUnmatch() { + return unmatch; + } + + public List getPathUnmatch() { + return pathUnmatch; + } + + public List getMatchMappingType() { + return matchMappingType; + } + + public List getUnmatchMappingType() { + return unmatchMappingType; + } + + public boolean isRegexMatching() { + return false; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private final List match = new ArrayList<>(); + private final List pathMatch = new ArrayList<>(); + + private final List unmatch = new ArrayList<>(); + private final List pathUnmatch = new ArrayList<>(); + + private final List matchMappingType = new ArrayList<>(); + private final List unmatchMappingType = new ArrayList<>(); + + private Builder() {} + + /** + * Patterns to match on the field name. + */ + public Builder withMatch(String... match) { + for (String value : match) { + if (value != null) { + parseValues(value, this.match); + } + } + + return this; + } + + /** + * Path patterns for a nested type to match the field name. + */ + public Builder withPathMatch(String... pathMatch) { + for (String value : pathMatch) { + if (value != null) { + parseValues(value, this.pathMatch); + } + } + + return this; + } + + /** + * Patterns that do not match the field name. + */ + public Builder withUnmatch(String... unmatch) { + for (String value : unmatch) { + if (value != null) { + parseValues(value, this.unmatch); + } + } + + return this; + } + + /** + * Path patterns for a nested type that do not match the field name. + */ + public Builder withPathUnmatch(String... pathUnmatch) { + for (String value : pathUnmatch) { + if (value != null) { + parseValues(value, this.pathUnmatch); + } + } + + return this; + } + + /** + * Data types that correspond to the field. + */ + public Builder withMatchMappingType(String... matchMappingType) { + for (String value : matchMappingType) { + if (value != null) { + parseValues(value, this.matchMappingType); + } + } + + return this; + } + + /** + * Data types that do not match to the field. + */ + public Builder withUnmatchMappingType(String... unmatchMappingType) { + for (String value : unmatchMappingType) { + if (value != null) { + parseValues(value, this.unmatchMappingType); + } + } + + return this; + } + + private void parseValues(String source, List target) { + if (source.startsWith("[")) { + target.addAll(List.of(source.replace("[", "").replace("]", "").split(",", -1))); + } else { + target.add(source); + } + } + + public DynamicTemplate build() { + return new DynamicTemplate(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 252376bed1..bea0e8f39b 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 @@ -15,6 +15,8 @@ */ package org.springframework.data.elasticsearch.core.mapping; +import java.util.List; +import java.util.Map; import java.util.Set; import org.springframework.data.elasticsearch.annotations.Document; @@ -203,4 +205,25 @@ default ElasticsearchPersistentProperty getRequiredSeqNoPrimaryTermProperty() { * @since 5.2 */ boolean isAlwaysWriteMapping(); + + /** + * Retrieves the dynamic templates defined for the current document. + * + * @since 5.4 + */ + Map getDynamicTemplates(); + + /** + * if the mapping should be written to the index on repository bootstrap even if the index already exists. + * + * @since 5.4 + */ + boolean hasDynamicTemplates(); + + /** + * Building the dynamic templates for the current document. + * + * @since 5.4 + */ + void buildDynamicTemplates(List dynamicTemplates); } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentProperty.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentProperty.java index 2b9d6232e0..7575b88648 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentProperty.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentProperty.java @@ -110,6 +110,13 @@ public interface ElasticsearchPersistentProperty extends PersistentProperty extends BasicPersistentEntit private @Nullable String routing; private final ContextConfiguration contextConfiguration; private final Set aliases = new HashSet<>(); + private final Map dynamicTemplates = new HashMap<>(); private final ConcurrentHashMap indexNameExpressions = new ConcurrentHashMap<>(); private final Lazy indexNameEvaluationContext = Lazy.of(this::getIndexNameEvaluationContext); @@ -627,6 +633,48 @@ public Dynamic dynamic() { return dynamic; } + @Override + public Map getDynamicTemplates() { + return unmodifiableMap(dynamicTemplates); + } + + @Override + public boolean hasDynamicTemplates() { + return !dynamicTemplates.isEmpty(); + } + + @Override + public void buildDynamicTemplates(List dynamicTemplates) { + for (Object dynamicTemplate : dynamicTemplates) { + if (dynamicTemplate instanceof Map template) { + template.forEach((name, value) -> { + if (value instanceof Map settings) { + this.dynamicTemplates.put((String) name, + DynamicTemplate.builder() + .withMatch(parseMapValue(settings.get("match"))) + .withPathMatch(parseMapValue(settings.get("path_match"))) + .withUnmatch(parseMapValue(settings.get("unmatch"))) + .withPathUnmatch(parseMapValue(settings.get("path_unmatch"))) + .withMatchMappingType(parseMapValue(settings.get("match_mapping_type"))) + .withUnmatchMappingType(parseMapValue(settings.get("unmatch_mapping_type"))) + .build()); + } + }); + } + } + } + + /** + * Parses the provided value and converts it into an array of Strings. + */ + private String[] parseMapValue(@Nullable Object value) { + if (value instanceof List values) { + return values.toArray(new String[0]); + } + + return new String[0]; + } + /** * Building once the aliases for the current document. */ diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java index 8b32842b91..9abd71a820 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java @@ -86,6 +86,7 @@ public class SimpleElasticsearchPersistentProperty extends @Nullable private PropertyValueConverter propertyValueConverter; private final boolean storeNullValue; private final boolean storeEmptyValue; + private final boolean isDynamicFieldMapping; public SimpleElasticsearchPersistentProperty(Property property, PersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { @@ -114,6 +115,7 @@ public SimpleElasticsearchPersistentProperty(Property property, : isMultiField && getRequiredAnnotation(MultiField.class).mainField().storeNullValue(); storeEmptyValue = isField ? getRequiredAnnotation(Field.class).storeEmptyValue() : !isMultiField || getRequiredAnnotation(MultiField.class).mainField().storeEmptyValue(); + isDynamicFieldMapping = isField && getRequiredAnnotation(Field.class).dynamicTemplate(); } @Override @@ -393,4 +395,9 @@ public boolean isCompletionProperty() { public boolean isIndexedIndexNameProperty() { return isAnnotationPresent(IndexedIndexName.class); } + + @Override + public boolean isDynamicFieldMapping() { + return isDynamicFieldMapping; + } } 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 b4aaab0e95..dffd5b5145 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 @@ -16,19 +16,14 @@ package org.springframework.data.elasticsearch.core.index; +import static java.util.UUID.randomUUID; import static org.assertj.core.api.Assertions.*; import static org.springframework.data.elasticsearch.annotations.FieldType.*; +import static org.springframework.data.elasticsearch.core.query.StringQuery.MATCH_ALL; import java.time.Instant; import java.time.LocalDate; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -40,7 +35,10 @@ import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.IndexOperations; import org.springframework.data.elasticsearch.core.MappingContextBaseTests; +import org.springframework.data.elasticsearch.core.SearchHit; +import org.springframework.data.elasticsearch.core.SearchHits; 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; @@ -280,6 +278,35 @@ void shouldWriteMappingWithFieldAliases() { operations.indexOps(FieldAliasEntity.class).createWithMapping(); } + @Test + void shouldMapDynamicFields() { + // Given + IndexOperations documentOperations = operations.indexOps(DynamicFieldDocument.class); + documentOperations.createWithMapping(); + + DynamicFieldDocument document = new DynamicFieldDocument(); + document.dynamicFields = Map.of("a_str", randomUUID().toString(), "b_str", randomUUID().toString()); + document.value = new DynamicFieldDocument.Value(1L, new Date()); + operations.save(document); + + // When + SearchHits results = operations.search(new StringQuery(MATCH_ALL), + DynamicFieldDocument.class); + + // Then + assertThat(results.getTotalHits()).isEqualTo(1); + assertThat(results.getSearchHits()).first() + .extracting(SearchHit::getContent) + .extracting(doc -> doc.dynamicFields) + .isEqualTo(document.dynamicFields); + assertThat(results.getSearchHits()).first() + .extracting(SearchHit::getContent) + .extracting(doc -> doc.value) + .isEqualTo(document.value); + + documentOperations.delete(); + } + // region Entities @Document(indexName = "#{@indexNameProvider.indexName()}") static class Book { @@ -933,5 +960,47 @@ private static class FieldAliasEntity { @Field(type = Text) private String otherText; } + @SuppressWarnings("unused") + @Document(indexName = "#{@indexNameProvider.indexName()}-foo") + @DynamicTemplates(mappingPath = "/mappings/test-dynamic_templates_mappings_three.json") + private static class DynamicFieldDocument { + @Nullable + @Id String id; + + @Field(name = "_str", dynamicTemplate = true) private Map dynamicFields = new HashMap<>(); + + @Nullable + @Field(name = "obj", dynamicTemplate = true) private Value value; + + static class Value { + @Nullable + @Field(name = "value_sum", type = FieldType.Long) + private Long sum; + + @Nullable + @Field(name = "value_date", type = FieldType.Long) + private Date date; + + public Value() { + } + + public Value(Long sum, Date date) { + this.sum = sum; + this.date = date; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Value value)) return false; + return Objects.equals(sum, value.sum) && Objects.equals(date, value.date); + } + + @Override + public int hashCode() { + return Objects.hash(sum, date); + } + } + } // endregion } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexTemplateIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexTemplateIntegrationTests.java index e64ce7e104..adbad32236 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveIndexTemplateIntegrationTests.java @@ -18,12 +18,12 @@ import static org.assertj.core.api.Assertions.*; import static org.skyscreamer.jsonassert.JSONAssert.*; import static org.springframework.data.elasticsearch.core.IndexOperationsAdapter.*; +import static org.springframework.data.elasticsearch.core.query.StringQuery.MATCH_ALL; +import reactor.core.publisher.Flux; import reactor.test.StepVerifier; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; import org.assertj.core.api.SoftAssertions; import org.json.JSONException; @@ -34,16 +34,20 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.DynamicTemplates; 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.core.IndexOperations; +import org.springframework.data.elasticsearch.core.IndexOperationsAdapter; import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; import org.springframework.data.elasticsearch.core.ReactiveIndexOperations; +import org.springframework.data.elasticsearch.core.SearchHit; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.query.Criteria; import org.springframework.data.elasticsearch.core.query.CriteriaQuery; import org.springframework.data.elasticsearch.core.query.Query; +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; @@ -415,6 +419,40 @@ void shouldDeleteTemplate() { assertThat(exists).isFalse(); } + @Test + void shouldMapDynamicFields() { + // Given + IndexOperationsAdapter documentOperations = blocking(operations.indexOps(DynamicFieldDocument.class)); + documentOperations.createWithMapping(); + + DynamicFieldDocument document = new DynamicFieldDocument(); + document.dynamicFields = Map.of("a_str", UUID.randomUUID().toString(), "b_str", UUID.randomUUID().toString()); + document.value = new DynamicFieldDocument.Value(1L, new Date()); + operations.save(document).block(); + + // When + Flux> results = operations.search(new StringQuery(MATCH_ALL), + DynamicFieldDocument.class); + + // Then + results.as(StepVerifier::create) + .expectNextMatches((hits) -> { + assertThat(hits) + .extracting(SearchHit::getContent) + .extracting(doc -> doc.dynamicFields) + .isEqualTo(document.dynamicFields); + + assertThat(hits) + .extracting(SearchHit::getContent) + .extracting(doc -> doc.value) + .isEqualTo(document.value); + + return true; + }).verifyComplete(); + + documentOperations.delete(); + } + @Document(indexName = "#{@indexNameProvider.indexName()}") @Setting(refreshInterval = "5s") static class TemplateClass { @@ -442,4 +480,46 @@ public void setMessage(@Nullable String message) { } } + @SuppressWarnings("unused") + @Document(indexName = "#{@indexNameProvider.indexName()}-foo") + @DynamicTemplates(mappingPath = "/mappings/test-dynamic_templates_mappings_three.json") + private static class DynamicFieldDocument { + @Nullable + @Id String id; + + @Field(name = "_str", dynamicTemplate = true) private Map dynamicFields = new HashMap<>(); + + @Nullable + @Field(name = "obj", dynamicTemplate = true) private Value value; + + static class Value { + @Nullable + @Field(name = "value_sum", type = FieldType.Long) + private Long sum; + + @Nullable + @Field(name = "value_date", type = FieldType.Long) + private Date date; + + public Value() { + } + + public Value(Long sum, Date date) { + this.sum = sum; + this.date = date; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Value value)) return false; + return Objects.equals(sum, value.sum) && Objects.equals(date, value.date); + } + + @Override + public int hashCode() { + return Objects.hash(sum, date); + } + } + } } diff --git a/src/test/resources/mappings/test-dynamic_templates_mappings_three.json b/src/test/resources/mappings/test-dynamic_templates_mappings_three.json new file mode 100644 index 0000000000..01bf783983 --- /dev/null +++ b/src/test/resources/mappings/test-dynamic_templates_mappings_three.json @@ -0,0 +1,21 @@ +{ + "dynamic_templates": [ + { + "_str": { + "match": "*_str", + "mapping": { + "type": "keyword" + } + } + }, + { + "obj": { + "match": "value_*", + "mapping": { + "type": "text", + "index": false + } + } + } + ] +}