diff --git a/src/main/asciidoc/reference/elasticsearch-misc.adoc b/src/main/asciidoc/reference/elasticsearch-misc.adoc index 372dd04d3..8853ed66e 100644 --- a/src/main/asciidoc/reference/elasticsearch-misc.adoc +++ b/src/main/asciidoc/reference/elasticsearch-misc.adoc @@ -51,10 +51,24 @@ class Entity { When Spring Data Elasticsearch creates the index mapping with the `IndexOperations.createMapping()` methods, it uses the annotations described in <>, especially the `@Field` annotation. In addition to that it is possible to add the `@Mapping` annotation to a class. This annotation has the following properties: -* `mappingPath` a classpath resource in JSON format which is used as the mapping, no other mapping processing is done. +* `mappingPath` a classpath resource in JSON format; if this is not empty it is used as the mapping, no other mapping processing is done. * `enabled` when set to false, this flag is written to the mapping and no further processing is done. * `dateDetection` and `numericDetection` set the corresponding properties in the mapping when not set to `DEFAULT`. * `dynamicDateFormats` when this String array is not empty, it defines the date formats used for automatic date detection. +* `runtimeFieldsPath` a classpath resource in JSON format containing the definition of runtime fields which is written to the index mappings, for example: +==== +[source,json] +---- +{ + "day_of_week": { + "type": "keyword", + "script": { + "source": "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))" + } + } +} +---- +==== [[elasticsearch.misc.filter]] == Filter Builder 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 c41e1300e..d2827e55b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Mapping.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Mapping.java @@ -27,6 +27,7 @@ * Elasticsearch Mapping * * @author Mohsin Husen + * @author Peter-Josef Meisch */ @Persistent @Inherited @@ -42,6 +43,7 @@ * @since 4.2 */ boolean enabled() default true; + /** * whether date_detection is enabled * @@ -58,10 +60,20 @@ /** * custom dynamic date formats + * * @since 4.3 */ String[] dynamicDateFormats() default {}; + /** + * classpath to a JSON file containing the values for a runtime mapping definition. The file must contain the JSON + * object that is written as the value of the runtime property. {@see elasticsearch doc} + * + * @since 4.3 + */ + String runtimeFieldsPath() default ""; + enum Detection { DEFAULT, TRUE, FALSE; } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/AbstractDefaultIndexOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/AbstractDefaultIndexOperations.java index f7ef23b1a..54ea56bb2 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/AbstractDefaultIndexOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/AbstractDefaultIndexOperations.java @@ -219,8 +219,6 @@ protected Document buildMapping(Class clazz) { if (hasText(mappings)) { return Document.parse(mappings); } - } else { - LOGGER.info("mappingPath in @Mapping has to be defined. Building mappings using @Field"); } } 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 43b4bb4c5..1530b1352 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 @@ -95,6 +95,7 @@ public class MappingBuilder { private static final String DATE_DETECTION = "date_detection"; private static final String NUMERIC_DETECTION = "numeric_detection"; private static final String DYNAMIC_DATE_FORMATS = "dynamic_date_formats"; + private static final String RUNTIME = "runtime"; private final ElasticsearchConverter elasticsearchConverter; @@ -168,6 +169,10 @@ private void mapEntity(XContentBuilder builder, @Nullable ElasticsearchPersisten if (mappingAnnotation.dynamicDateFormats().length > 0) { builder.field(DYNAMIC_DATE_FORMATS, mappingAnnotation.dynamicDateFormats()); } + + if (StringUtils.hasText(mappingAnnotation.runtimeFieldsPath())) { + addRuntimeFields(builder, mappingAnnotation.runtimeFieldsPath()); + } } boolean writeNestedProperties = !isRootObject && (isAnyPropertyAnnotatedWithField(entity) || nestedOrObjectField); @@ -222,6 +227,15 @@ private void mapEntity(XContentBuilder builder, @Nullable ElasticsearchPersisten } + private void addRuntimeFields(XContentBuilder builder, String runtimeFieldsPath) throws IOException { + + ClassPathResource runtimeFields = new ClassPathResource(runtimeFieldsPath); + + if (runtimeFields.exists()) { + builder.rawField(RUNTIME, runtimeFields.getInputStream(), XContentType.JSON); + } + } + private void buildPropertyMapping(XContentBuilder builder, boolean isRootObject, ElasticsearchPersistentProperty property) throws IOException { 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 de0e9e4bb..98f6a264b 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 @@ -25,6 +25,7 @@ import java.lang.Integer; import java.lang.Object; import java.math.BigDecimal; +import java.time.Instant; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -316,6 +317,16 @@ void shouldWriteDynamicDetectionValues() { indexOps.delete(); } + @Test // #1816 + @DisplayName("should write runtime fields") + void shouldWriteRuntimeFields() { + + IndexOperations indexOps = operations.indexOps(RuntimeFieldEntity.class); + indexOps.create(); + indexOps.putMapping(); + indexOps.delete(); + } + // region entities @Document(indexName = "ignore-above-index") static class IgnoreAboveEntity { @@ -1130,6 +1141,14 @@ public void setAuthor(Author author) { private static class DynamicDetectionMapping { @Id @Nullable private String id; } + + @Document(indexName = "runtime-fields") + @Mapping(runtimeFieldsPath = "/mappings/runtime-fields.json") + private static class RuntimeFieldEntity { + @Id @Nullable private String id; + @Field(type = Date, format = DateFormat.epoch_millis, name = "@timestamp") @Nullable private Instant timestamp; + } + // 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 7fbaca7ff..7ed2405c0 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 @@ -26,6 +26,7 @@ import java.lang.Integer; import java.lang.Object; import java.math.BigDecimal; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Collection; @@ -855,7 +856,38 @@ void shouldWriteDynamicDateFormats() throws JSONException { assertEquals(expected, mapping, true); } + @Test // #1816 + @DisplayName("should write runtime fields") + void shouldWriteRuntimeFields() throws JSONException { + + String expected = "{\n" + // + " \"runtime\": {\n" + // + " \"day_of_week\": {\n" + // + " \"type\": \"keyword\",\n" + // + " \"script\": {\n" + // + " \"source\": \"emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))\"\n" + // + " }\n" + // + " }\n" + // + " },\n" + // + " \"properties\": {\n" + // + " \"_class\": {\n" + // + " \"type\": \"keyword\",\n" + // + " \"index\": false,\n" + // + " \"doc_values\": false\n" + // + " },\n" + // + " \"@timestamp\": {\n" + // + " \"type\": \"date\",\n" + // + " \"format\": \"epoch_millis\"\n" + // + " }\n" + // + " }\n" + // + "}\n"; // + + String mapping = getMappingBuilder().buildPropertyMapping(RuntimeFieldEntity.class); + + assertEquals(expected, mapping, true); + } // region entities + @Document(indexName = "ignore-above-index") static class IgnoreAboveEntity { @Nullable @Id private String id; @@ -1778,7 +1810,7 @@ private static class DynamicDetectionMappingDefault { } @Document(indexName = "dynamic-dateformats-mapping") - @Mapping(dynamicDateFormats = {"date1", "date2"}) + @Mapping(dynamicDateFormats = { "date1", "date2" }) private static class DynamicDateFormatsMapping { @Id @Nullable private String id; } @@ -1794,5 +1826,12 @@ private static class DynamicDetectionMappingTrue { private static class DynamicDetectionMappingFalse { @Id @Nullable private String id; } + + @Document(indexName = "runtime-fields") + @Mapping(runtimeFieldsPath = "/mappings/runtime-fields.json") + private static class RuntimeFieldEntity { + @Id @Nullable private String id; + @Field(type = Date, format = DateFormat.epoch_millis, name = "@timestamp") @Nullable private Instant timestamp; + } // endregion } diff --git a/src/test/resources/mappings/runtime-fields.json b/src/test/resources/mappings/runtime-fields.json new file mode 100644 index 000000000..97fe9c5d4 --- /dev/null +++ b/src/test/resources/mappings/runtime-fields.json @@ -0,0 +1,8 @@ +{ + "day_of_week": { + "type": "keyword", + "script": { + "source": "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))" + } + } +}