From be9a6029e40a25d34e53cd289ef2bf113ab0cf73 Mon Sep 17 00:00:00 2001 From: Andriy Redko Date: Wed, 31 Jul 2024 13:59:55 -0400 Subject: [PATCH 1/2] Allow to customize the mapped type name for @InnerField and @Field annotations Signed-off-by: Andriy Redko --- .../data/elasticsearch/annotations/Field.java | 8 +++ .../elasticsearch/annotations/InnerField.java | 8 +++ .../core/index/MappingBuilder.java | 21 ++++-- .../core/index/MappingParameters.java | 5 +- .../index/MappingBuilderIntegrationTests.java | 7 ++ .../core/index/MappingBuilderUnitTests.java | 67 +++++++++++++++++++ 6 files changed, 110 insertions(+), 6 deletions(-) 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 63f74716b..09423144c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java @@ -38,6 +38,7 @@ * @author Morgan Lutz * @author Sascha Woo * @author Haibo Liu + * @author Andriy Redko */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @@ -240,4 +241,11 @@ * @since 5.1 */ boolean storeEmptyValue() default true; + + /** + * overrides the mapping field type which otherwise will be taken from corresponding {@link FieldType} + * + * @since 5.4 + */ + String mappedName() default ""; } diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/InnerField.java b/src/main/java/org/springframework/data/elasticsearch/annotations/InnerField.java index 35bccd968..08c9b6115 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/InnerField.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/InnerField.java @@ -30,6 +30,7 @@ * @author Brian Kimmig * @author Morgan Lutz * @author Haibo Liu + * @author Andriy Redko */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) @@ -171,4 +172,11 @@ * @since 5.4 */ KnnIndexOptions[] knnIndexOptions() default {}; + + /** + * overrides the mapping field type which otherwise will be taken from corresponding {@link FieldType} + * + * @since 5.4 + */ + String mappedName() default ""; } 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 bcf25c01b..9d40a3a6e 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 @@ -69,6 +69,7 @@ * @author Peter-Josef Meisch * @author Xiao Yu * @author Subhobrata Dey + * @author Andriy Redko */ public class MappingBuilder { @@ -175,7 +176,8 @@ protected String buildPropertyMapping(ElasticsearchPersistentEntity entity, .findAnnotation(org.springframework.data.elasticsearch.annotations.Document.class); var dynamicMapping = docAnnotation != null ? docAnnotation.dynamic() : null; - mapEntity(objectNode, entity, true, "", false, FieldType.Auto, null, dynamicMapping, runtimeFields); + final FieldType fieldType = FieldType.Auto; + mapEntity(objectNode, entity, true, "", false, fieldType, fieldType.getMappedName(), null, dynamicMapping, runtimeFields); if (!excludeFromSource.isEmpty()) { ObjectNode sourceNode = objectNode.putObject(SOURCE); @@ -210,7 +212,7 @@ private void writeTypeHintMapping(ObjectNode propertiesNode) throws IOException } private void mapEntity(ObjectNode objectNode, @Nullable ElasticsearchPersistentEntity entity, - boolean isRootObject, String nestedObjectFieldName, boolean nestedOrObjectField, FieldType fieldType, + boolean isRootObject, String nestedObjectFieldName, boolean nestedOrObjectField, FieldType fieldType, String mappedName, @Nullable Field parentFieldAnnotation, @Nullable Dynamic dynamicMapping, @Nullable Document runtimeFields) throws IOException { @@ -244,7 +246,7 @@ private void mapEntity(ObjectNode objectNode, @Nullable ElasticsearchPersistentE boolean writeNestedProperties = !isRootObject && (isAnyPropertyAnnotatedWithField(entity) || nestedOrObjectField); if (writeNestedProperties) { - String type = nestedOrObjectField ? fieldType.getMappedName() : FieldType.Object.getMappedName(); + String type = nestedOrObjectField ? mappedName : FieldType.Object.getMappedName(); ObjectNode nestedObjectNode = objectMapper.createObjectNode(); nestedObjectNode.put(FIELD_PARAM_TYPE, type); @@ -370,7 +372,7 @@ private void buildPropertyMapping(ObjectNode propertiesNode, boolean isRootObjec nestedPropertyPrefix = nestedPropertyPath; mapEntity(propertiesNode, persistentEntity, false, property.getFieldName(), true, fieldAnnotation.type(), - fieldAnnotation, dynamicMapping, null); + getMappedName(fieldAnnotation), fieldAnnotation, dynamicMapping, null); nestedPropertyPrefix = currentNestedPropertyPrefix; return; @@ -473,7 +475,7 @@ private void applyDisabledPropertyMapping(ObjectNode propertiesNode, Elasticsear } propertiesNode.set(property.getFieldName(), objectMapper.createObjectNode() // - .put(FIELD_PARAM_TYPE, field.type().getMappedName()) // + .put(FIELD_PARAM_TYPE, getMappedName(field)) // .put(MAPPING_ENABLED, false) // ); @@ -482,6 +484,15 @@ private void applyDisabledPropertyMapping(ObjectNode propertiesNode, Elasticsear } } + /** + * Return the mapping type name to be used for the {@link Field} + * @param field field to return the mapping type name for + * @return the mapping type name + */ + private String getMappedName(Field field) { + return StringUtils.hasText(field.mappedName()) ? field.mappedName() : field.type().getMappedName(); + } + /** * Add mapping for @Field annotation * 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 cd3903376..509df3126 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 @@ -116,6 +116,7 @@ public final class MappingParameters { private final boolean store; private final TermVector termVector; private final FieldType type; + private final String mappedName; /** * extracts the mapping parameters from the relevant annotations. @@ -141,6 +142,7 @@ private MappingParameters(Field field) { store = field.store(); fielddata = field.fielddata(); type = field.type(); + mappedName = StringUtils.hasText(field.mappedName()) ? field.mappedName() : type.getMappedName(); dateFormats = field.format(); dateFormatPatterns = field.pattern(); analyzer = field.analyzer(); @@ -187,6 +189,7 @@ private MappingParameters(InnerField field) { store = field.store(); fielddata = field.fielddata(); type = field.type(); + mappedName = StringUtils.hasText(field.mappedName()) ? field.mappedName() : type.getMappedName(); dateFormats = field.format(); dateFormatPatterns = field.pattern(); analyzer = field.analyzer(); @@ -245,7 +248,7 @@ public void writeTypeAndParametersTo(ObjectNode objectNode) throws IOException { } if (type != FieldType.Auto) { - objectNode.put(FIELD_PARAM_TYPE, type.getMappedName()); + objectNode.put(FIELD_PARAM_TYPE, mappedName); if (type == FieldType.Date || type == FieldType.Date_Nanos || type == FieldType.Date_Range) { List formats = new ArrayList<>(); 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 e4bf6e48d..b4aaab0e9 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 @@ -59,6 +59,7 @@ * @author Brian Kimmig * @author Morgan Lutz * @author Haibo Liu + * @author Andriy Redko */ @SpringIntegrationTest public abstract class MappingBuilderIntegrationTests extends MappingContextBaseTests { @@ -77,6 +78,12 @@ void cleanup() { operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete(); } + @Test + public void shouldSupportAllTypes() { + IndexOperations indexOperations = operations.indexOps(EntityWithAllTypes.class); + indexOperations.createWithMapping(); + } + @Test public void shouldNotFailOnCircularReference() { 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 cf5ac6f3b..eb29976fb 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 @@ -63,6 +63,7 @@ * @author Brian Kimmig * @author Morgan Lutz * @author Haibo Liu + * @author Andriy Redko */ public class MappingBuilderUnitTests extends MappingContextBaseTests { @@ -1242,6 +1243,59 @@ void shouldWriteFieldAliasesToTheMapping() throws JSONException { assertEquals(expected, mapping, true); } + + @Test // #2942 + @DisplayName("should use custom mapped name") + void shouldUseCustomMappedName() throws JSONException { + + var expected = """ + { + "properties": { + "_class": { + "type": "keyword", + "index": false, + "doc_values": false + }, + "someText": { + "type": "match_only_text" + } + } + } + """; + String mapping = getMappingBuilder().buildPropertyMapping(FieldMappedNameEntity.class); + + assertEquals(expected, mapping, true); + } + + @Test // #2942 + @DisplayName("should use custom mapped name for multifield") + void shouldUseCustomMappedNameMultiField() throws JSONException { + + var expected = """ + { + "properties": { + "_class": { + "type": "keyword", + "index": false, + "doc_values": false + }, + "description": { + "type": "match_only_text", + "fields": { + "lower_case": { + "type": "constant_keyword", + "normalizer": "lower_case_normalizer" + } + } + } + } + } + """; + String mapping = getMappingBuilder().buildPropertyMapping(MultiFieldMappedNameEntity.class); + + assertEquals(expected, mapping, true); + } + // region entities @Document(indexName = "ignore-above-index") @@ -2503,5 +2557,18 @@ private static class FieldAliasEntity { @Nullable @Field(type = Text) private String otherText; } + + @SuppressWarnings("unused") + private static class FieldMappedNameEntity { + @Nullable + @Field(type = Text, mappedName = "match_only_text") private String someText; + } + + @SuppressWarnings("unused") + private static class MultiFieldMappedNameEntity { + @Nullable + @MultiField(mainField = @Field(type = FieldType.Text, mappedName = "match_only_text"), otherFields = { @InnerField(suffix = "lower_case", + type = FieldType.Keyword, normalizer = "lower_case_normalizer", mappedName = "constant_keyword") }) private String description; + } // endregion } From 8ae857f321139ff9c70632c8aeb79e42464179ee Mon Sep 17 00:00:00 2001 From: Andriy Redko Date: Mon, 5 Aug 2024 10:04:42 -0400 Subject: [PATCH 2/2] Address code review comments Signed-off-by: Andriy Redko --- .../data/elasticsearch/annotations/Field.java | 4 ++-- .../data/elasticsearch/annotations/InnerField.java | 4 ++-- .../elasticsearch/core/index/MappingBuilder.java | 12 ++++++------ .../elasticsearch/core/index/MappingParameters.java | 8 ++++---- .../core/index/MappingBuilderUnitTests.java | 6 +++--- 5 files changed, 17 insertions(+), 17 deletions(-) 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 09423144c..70ce5a867 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java @@ -243,9 +243,9 @@ boolean storeEmptyValue() default true; /** - * overrides the mapping field type which otherwise will be taken from corresponding {@link FieldType} + * overrides the field type in the mapping which otherwise will be taken from corresponding {@link FieldType} * * @since 5.4 */ - String mappedName() default ""; + String mappedTypeName() default ""; } diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/InnerField.java b/src/main/java/org/springframework/data/elasticsearch/annotations/InnerField.java index 08c9b6115..49d83d016 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/InnerField.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/InnerField.java @@ -174,9 +174,9 @@ KnnIndexOptions[] knnIndexOptions() default {}; /** - * overrides the mapping field type which otherwise will be taken from corresponding {@link FieldType} + * overrides the field type in the mapping which otherwise will be taken from corresponding {@link FieldType} * * @since 5.4 */ - String mappedName() default ""; + String mappedTypeName() default ""; } 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 9d40a3a6e..86f7769aa 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 @@ -212,7 +212,7 @@ private void writeTypeHintMapping(ObjectNode propertiesNode) throws IOException } private void mapEntity(ObjectNode objectNode, @Nullable ElasticsearchPersistentEntity entity, - boolean isRootObject, String nestedObjectFieldName, boolean nestedOrObjectField, FieldType fieldType, String mappedName, + boolean isRootObject, String nestedObjectFieldName, boolean nestedOrObjectField, FieldType fieldType, String fieldTypeMappedName, @Nullable Field parentFieldAnnotation, @Nullable Dynamic dynamicMapping, @Nullable Document runtimeFields) throws IOException { @@ -246,7 +246,7 @@ private void mapEntity(ObjectNode objectNode, @Nullable ElasticsearchPersistentE boolean writeNestedProperties = !isRootObject && (isAnyPropertyAnnotatedWithField(entity) || nestedOrObjectField); if (writeNestedProperties) { - String type = nestedOrObjectField ? mappedName : FieldType.Object.getMappedName(); + String type = nestedOrObjectField ? fieldTypeMappedName : FieldType.Object.getMappedName(); ObjectNode nestedObjectNode = objectMapper.createObjectNode(); nestedObjectNode.put(FIELD_PARAM_TYPE, type); @@ -372,7 +372,7 @@ private void buildPropertyMapping(ObjectNode propertiesNode, boolean isRootObjec nestedPropertyPrefix = nestedPropertyPath; mapEntity(propertiesNode, persistentEntity, false, property.getFieldName(), true, fieldAnnotation.type(), - getMappedName(fieldAnnotation), fieldAnnotation, dynamicMapping, null); + getMappedTypeName(fieldAnnotation), fieldAnnotation, dynamicMapping, null); nestedPropertyPrefix = currentNestedPropertyPrefix; return; @@ -475,7 +475,7 @@ private void applyDisabledPropertyMapping(ObjectNode propertiesNode, Elasticsear } propertiesNode.set(property.getFieldName(), objectMapper.createObjectNode() // - .put(FIELD_PARAM_TYPE, getMappedName(field)) // + .put(FIELD_PARAM_TYPE, getMappedTypeName(field)) // .put(MAPPING_ENABLED, false) // ); @@ -489,8 +489,8 @@ private void applyDisabledPropertyMapping(ObjectNode propertiesNode, Elasticsear * @param field field to return the mapping type name for * @return the mapping type name */ - private String getMappedName(Field field) { - return StringUtils.hasText(field.mappedName()) ? field.mappedName() : field.type().getMappedName(); + private String getMappedTypeName(Field field) { + return StringUtils.hasText(field.mappedTypeName()) ? field.mappedTypeName() : field.type().getMappedName(); } /** 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 509df3126..7875a7d5e 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 @@ -116,7 +116,7 @@ public final class MappingParameters { private final boolean store; private final TermVector termVector; private final FieldType type; - private final String mappedName; + private final String mappedTypeName; /** * extracts the mapping parameters from the relevant annotations. @@ -142,7 +142,7 @@ private MappingParameters(Field field) { store = field.store(); fielddata = field.fielddata(); type = field.type(); - mappedName = StringUtils.hasText(field.mappedName()) ? field.mappedName() : type.getMappedName(); + mappedTypeName = StringUtils.hasText(field.mappedTypeName()) ? field.mappedTypeName() : type.getMappedName(); dateFormats = field.format(); dateFormatPatterns = field.pattern(); analyzer = field.analyzer(); @@ -189,7 +189,7 @@ private MappingParameters(InnerField field) { store = field.store(); fielddata = field.fielddata(); type = field.type(); - mappedName = StringUtils.hasText(field.mappedName()) ? field.mappedName() : type.getMappedName(); + mappedTypeName = StringUtils.hasText(field.mappedTypeName()) ? field.mappedTypeName() : type.getMappedName(); dateFormats = field.format(); dateFormatPatterns = field.pattern(); analyzer = field.analyzer(); @@ -248,7 +248,7 @@ public void writeTypeAndParametersTo(ObjectNode objectNode) throws IOException { } if (type != FieldType.Auto) { - objectNode.put(FIELD_PARAM_TYPE, mappedName); + objectNode.put(FIELD_PARAM_TYPE, mappedTypeName); if (type == FieldType.Date || type == FieldType.Date_Nanos || type == FieldType.Date_Range) { List formats = new ArrayList<>(); 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 eb29976fb..2335d2c61 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 @@ -2561,14 +2561,14 @@ private static class FieldAliasEntity { @SuppressWarnings("unused") private static class FieldMappedNameEntity { @Nullable - @Field(type = Text, mappedName = "match_only_text") private String someText; + @Field(type = Text, mappedTypeName = "match_only_text") private String someText; } @SuppressWarnings("unused") private static class MultiFieldMappedNameEntity { @Nullable - @MultiField(mainField = @Field(type = FieldType.Text, mappedName = "match_only_text"), otherFields = { @InnerField(suffix = "lower_case", - type = FieldType.Keyword, normalizer = "lower_case_normalizer", mappedName = "constant_keyword") }) private String description; + @MultiField(mainField = @Field(type = FieldType.Text, mappedTypeName = "match_only_text"), otherFields = { @InnerField(suffix = "lower_case", + type = FieldType.Keyword, normalizer = "lower_case_normalizer", mappedTypeName = "constant_keyword") }) private String description; } // endregion }