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..70ce5a867 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 field type in the mapping which otherwise will be taken from corresponding {@link FieldType} + * + * @since 5.4 + */ + 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 35bccd968..49d83d016 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 field type in the mapping which otherwise will be taken from corresponding {@link FieldType} + * + * @since 5.4 + */ + 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 bcf25c01b..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 @@ -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 fieldTypeMappedName, @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 ? fieldTypeMappedName : 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); + getMappedTypeName(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, getMappedTypeName(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 getMappedTypeName(Field field) { + return StringUtils.hasText(field.mappedTypeName()) ? field.mappedTypeName() : 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..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,6 +116,7 @@ public final class MappingParameters { private final boolean store; private final TermVector termVector; private final FieldType type; + private final String mappedTypeName; /** * 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(); + mappedTypeName = StringUtils.hasText(field.mappedTypeName()) ? field.mappedTypeName() : 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(); + mappedTypeName = StringUtils.hasText(field.mappedTypeName()) ? field.mappedTypeName() : 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, 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/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..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 @@ -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, mappedTypeName = "match_only_text") private String someText; + } + + @SuppressWarnings("unused") + private static class MultiFieldMappedNameEntity { + @Nullable + @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 }