Allow to customize the mapped type name for @InnerField and @Field annotations.

Original Pull request: #2950
Closes #2942
This commit is contained in:
Andriy Redko 2024-08-06 12:04:37 -04:00 committed by GitHub
parent eba8eec6c3
commit 06de217ceb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 110 additions and 6 deletions

View File

@ -38,6 +38,7 @@ import org.springframework.core.annotation.AliasFor;
* @author Morgan Lutz * @author Morgan Lutz
* @author Sascha Woo * @author Sascha Woo
* @author Haibo Liu * @author Haibo Liu
* @author Andriy Redko
*/ */
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.METHOD })
@ -240,4 +241,11 @@ public @interface Field {
* @since 5.1 * @since 5.1
*/ */
boolean storeEmptyValue() default true; 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 "";
} }

View File

@ -30,6 +30,7 @@ import java.lang.annotation.Target;
* @author Brian Kimmig * @author Brian Kimmig
* @author Morgan Lutz * @author Morgan Lutz
* @author Haibo Liu * @author Haibo Liu
* @author Andriy Redko
*/ */
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE) @Target(ElementType.ANNOTATION_TYPE)
@ -171,4 +172,11 @@ public @interface InnerField {
* @since 5.4 * @since 5.4
*/ */
KnnIndexOptions[] knnIndexOptions() default {}; 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 "";
} }

View File

@ -69,6 +69,7 @@ import com.fasterxml.jackson.databind.util.RawValue;
* @author Peter-Josef Meisch * @author Peter-Josef Meisch
* @author Xiao Yu * @author Xiao Yu
* @author Subhobrata Dey * @author Subhobrata Dey
* @author Andriy Redko
*/ */
public class MappingBuilder { public class MappingBuilder {
@ -175,7 +176,8 @@ public class MappingBuilder {
.findAnnotation(org.springframework.data.elasticsearch.annotations.Document.class); .findAnnotation(org.springframework.data.elasticsearch.annotations.Document.class);
var dynamicMapping = docAnnotation != null ? docAnnotation.dynamic() : null; 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()) { if (!excludeFromSource.isEmpty()) {
ObjectNode sourceNode = objectNode.putObject(SOURCE); ObjectNode sourceNode = objectNode.putObject(SOURCE);
@ -210,7 +212,7 @@ public class MappingBuilder {
} }
private void mapEntity(ObjectNode objectNode, @Nullable ElasticsearchPersistentEntity<?> entity, 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) @Nullable Field parentFieldAnnotation, @Nullable Dynamic dynamicMapping, @Nullable Document runtimeFields)
throws IOException { throws IOException {
@ -244,7 +246,7 @@ public class MappingBuilder {
boolean writeNestedProperties = !isRootObject && (isAnyPropertyAnnotatedWithField(entity) || nestedOrObjectField); boolean writeNestedProperties = !isRootObject && (isAnyPropertyAnnotatedWithField(entity) || nestedOrObjectField);
if (writeNestedProperties) { if (writeNestedProperties) {
String type = nestedOrObjectField ? fieldType.getMappedName() : FieldType.Object.getMappedName(); String type = nestedOrObjectField ? fieldTypeMappedName : FieldType.Object.getMappedName();
ObjectNode nestedObjectNode = objectMapper.createObjectNode(); ObjectNode nestedObjectNode = objectMapper.createObjectNode();
nestedObjectNode.put(FIELD_PARAM_TYPE, type); nestedObjectNode.put(FIELD_PARAM_TYPE, type);
@ -370,7 +372,7 @@ public class MappingBuilder {
nestedPropertyPrefix = nestedPropertyPath; nestedPropertyPrefix = nestedPropertyPath;
mapEntity(propertiesNode, persistentEntity, false, property.getFieldName(), true, fieldAnnotation.type(), mapEntity(propertiesNode, persistentEntity, false, property.getFieldName(), true, fieldAnnotation.type(),
fieldAnnotation, dynamicMapping, null); getMappedTypeName(fieldAnnotation), fieldAnnotation, dynamicMapping, null);
nestedPropertyPrefix = currentNestedPropertyPrefix; nestedPropertyPrefix = currentNestedPropertyPrefix;
return; return;
@ -473,7 +475,7 @@ public class MappingBuilder {
} }
propertiesNode.set(property.getFieldName(), objectMapper.createObjectNode() // propertiesNode.set(property.getFieldName(), objectMapper.createObjectNode() //
.put(FIELD_PARAM_TYPE, field.type().getMappedName()) // .put(FIELD_PARAM_TYPE, getMappedTypeName(field)) //
.put(MAPPING_ENABLED, false) // .put(MAPPING_ENABLED, false) //
); );
@ -482,6 +484,15 @@ public class MappingBuilder {
} }
} }
/**
* 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 * Add mapping for @Field annotation
* *

View File

@ -116,6 +116,7 @@ public final class MappingParameters {
private final boolean store; private final boolean store;
private final TermVector termVector; private final TermVector termVector;
private final FieldType type; private final FieldType type;
private final String mappedTypeName;
/** /**
* extracts the mapping parameters from the relevant annotations. * extracts the mapping parameters from the relevant annotations.
@ -141,6 +142,7 @@ public final class MappingParameters {
store = field.store(); store = field.store();
fielddata = field.fielddata(); fielddata = field.fielddata();
type = field.type(); type = field.type();
mappedTypeName = StringUtils.hasText(field.mappedTypeName()) ? field.mappedTypeName() : type.getMappedName();
dateFormats = field.format(); dateFormats = field.format();
dateFormatPatterns = field.pattern(); dateFormatPatterns = field.pattern();
analyzer = field.analyzer(); analyzer = field.analyzer();
@ -187,6 +189,7 @@ public final class MappingParameters {
store = field.store(); store = field.store();
fielddata = field.fielddata(); fielddata = field.fielddata();
type = field.type(); type = field.type();
mappedTypeName = StringUtils.hasText(field.mappedTypeName()) ? field.mappedTypeName() : type.getMappedName();
dateFormats = field.format(); dateFormats = field.format();
dateFormatPatterns = field.pattern(); dateFormatPatterns = field.pattern();
analyzer = field.analyzer(); analyzer = field.analyzer();
@ -245,7 +248,7 @@ public final class MappingParameters {
} }
if (type != FieldType.Auto) { 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) { if (type == FieldType.Date || type == FieldType.Date_Nanos || type == FieldType.Date_Range) {
List<String> formats = new ArrayList<>(); List<String> formats = new ArrayList<>();

View File

@ -59,6 +59,7 @@ import org.springframework.lang.Nullable;
* @author Brian Kimmig * @author Brian Kimmig
* @author Morgan Lutz * @author Morgan Lutz
* @author Haibo Liu * @author Haibo Liu
* @author Andriy Redko
*/ */
@SpringIntegrationTest @SpringIntegrationTest
public abstract class MappingBuilderIntegrationTests extends MappingContextBaseTests { public abstract class MappingBuilderIntegrationTests extends MappingContextBaseTests {
@ -77,6 +78,12 @@ public abstract class MappingBuilderIntegrationTests extends MappingContextBaseT
operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete(); operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete();
} }
@Test
public void shouldSupportAllTypes() {
IndexOperations indexOperations = operations.indexOps(EntityWithAllTypes.class);
indexOperations.createWithMapping();
}
@Test @Test
public void shouldNotFailOnCircularReference() { public void shouldNotFailOnCircularReference() {

View File

@ -63,6 +63,7 @@ import org.springframework.lang.Nullable;
* @author Brian Kimmig * @author Brian Kimmig
* @author Morgan Lutz * @author Morgan Lutz
* @author Haibo Liu * @author Haibo Liu
* @author Andriy Redko
*/ */
public class MappingBuilderUnitTests extends MappingContextBaseTests { public class MappingBuilderUnitTests extends MappingContextBaseTests {
@ -1242,6 +1243,59 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
assertEquals(expected, mapping, true); 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 // region entities
@Document(indexName = "ignore-above-index") @Document(indexName = "ignore-above-index")
@ -2503,5 +2557,18 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
@Nullable @Nullable
@Field(type = Text) private String otherText; @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 // endregion
} }