Add the type hint _class attribute to the index mapping.

Original Pull Request #1717 
Closes #1711
This commit is contained in:
Peter-Josef Meisch 2021-03-04 23:56:29 +01:00 committed by GitHub
parent 6634d0075a
commit e4c7b968e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 114 additions and 50 deletions

View File

@ -23,7 +23,6 @@ import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
@ -550,13 +549,13 @@ public class MappingElasticsearchConverter
} }
Class<?> entityType = ClassUtils.getUserClass(source.getClass()); Class<?> entityType = ClassUtils.getUserClass(source.getClass());
TypeInformation<? extends Object> type = ClassTypeInformation.from(entityType); TypeInformation<? extends Object> typeInformation = ClassTypeInformation.from(entityType);
if (requiresTypeHint(entityType)) { if (requiresTypeHint(entityType)) {
typeMapper.writeType(type, sink); typeMapper.writeType(typeInformation, sink);
} }
writeInternal(source, sink, type); writeInternal(source, sink, typeInformation);
} }
/** /**
@ -564,11 +563,11 @@ public class MappingElasticsearchConverter
* *
* @param source * @param source
* @param sink * @param sink
* @param typeHint * @param typeInformation
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
protected void writeInternal(@Nullable Object source, Map<String, Object> sink, protected void writeInternal(@Nullable Object source, Map<String, Object> sink,
@Nullable TypeInformation<?> typeHint) { @Nullable TypeInformation<?> typeInformation) {
if (null == source) { if (null == source) {
return; return;
@ -594,7 +593,7 @@ public class MappingElasticsearchConverter
} }
ElasticsearchPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(entityType); ElasticsearchPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(entityType);
addCustomTypeKeyIfNecessary(typeHint, source, sink); addCustomTypeKeyIfNecessary(source, sink, typeInformation);
writeInternal(source, sink, entity); writeInternal(source, sink, entity);
} }
@ -603,7 +602,7 @@ public class MappingElasticsearchConverter
* *
* @param source * @param source
* @param sink * @param sink
* @param typeHint * @param entity
*/ */
protected void writeInternal(@Nullable Object source, Map<String, Object> sink, protected void writeInternal(@Nullable Object source, Map<String, Object> sink,
@Nullable ElasticsearchPersistentEntity<?> entity) { @Nullable ElasticsearchPersistentEntity<?> entity) {
@ -725,7 +724,7 @@ public class MappingElasticsearchConverter
Map<String, Object> document = existingValue instanceof Map ? (Map<String, Object>) existingValue Map<String, Object> document = existingValue instanceof Map ? (Map<String, Object>) existingValue
: Document.create(); : Document.create();
addCustomTypeKeyIfNecessary(ClassTypeInformation.from(property.getRawType()), value, document); addCustomTypeKeyIfNecessary(value, document, ClassTypeInformation.from(property.getRawType()));
writeInternal(value, document, entity); writeInternal(value, document, entity);
sink.set(property, document); sink.set(property, document);
} }
@ -923,18 +922,18 @@ public class MappingElasticsearchConverter
// region helper methods // region helper methods
/** /**
* Adds custom type information to the given {@link Map} if necessary. That is if the value is not the same as the one * Adds custom typeInformation information to the given {@link Map} if necessary. That is if the value is not the same
* given. This is usually the case if you store a subtype of the actual declared type of the property. * as the one given. This is usually the case if you store a subtype of the actual declared typeInformation of the
* property.
* *
* @param type * @param source must not be {@literal null}.
* @param value must not be {@literal null}.
* @param sink must not be {@literal null}. * @param sink must not be {@literal null}.
* @param type
*/ */
protected void addCustomTypeKeyIfNecessary(@Nullable TypeInformation<?> type, Object value, protected void addCustomTypeKeyIfNecessary(Object source, Map<String, Object> sink, @Nullable TypeInformation<?> type) {
Map<String, Object> sink) {
Class<?> reference = type != null ? type.getActualType().getType() : Object.class; Class<?> reference = type != null ? type.getActualType().getType() : Object.class;
Class<?> valueType = ClassUtils.getUserClass(value.getClass()); Class<?> valueType = ClassUtils.getUserClass(source.getClass());
boolean notTheSameClass = !valueType.equals(reference); boolean notTheSameClass = !valueType.equals(reference);
if (notTheSameClass) { if (notTheSameClass) {
@ -948,7 +947,7 @@ public class MappingElasticsearchConverter
* @param type must not be {@literal null}. * @param type must not be {@literal null}.
* @return {@literal true} if not a simple type, {@link Collection} or type with custom write target. * @return {@literal true} if not a simple type, {@link Collection} or type with custom write target.
*/ */
private boolean requiresTypeHint(Class<?> type) { public boolean requiresTypeHint(Class<?> type) {
return !isSimpleType(type) && !ClassUtils.isAssignable(Collection.class, type) return !isSimpleType(type) && !ClassUtils.isAssignable(Collection.class, type)
&& !conversions.hasCustomWriteTarget(type, Document.class); && !conversions.hasCustomWriteTarget(type, Document.class);

View File

@ -81,6 +81,8 @@ public class MappingBuilder {
private static final String COMPLETION_MAX_INPUT_LENGTH = "max_input_length"; private static final String COMPLETION_MAX_INPUT_LENGTH = "max_input_length";
private static final String COMPLETION_CONTEXTS = "contexts"; private static final String COMPLETION_CONTEXTS = "contexts";
private static final String TYPEHINT_PROPERTY = "_class";
private static final String TYPE_DYNAMIC = "dynamic"; private static final String TYPE_DYNAMIC = "dynamic";
private static final String TYPE_VALUE_KEYWORD = "keyword"; private static final String TYPE_VALUE_KEYWORD = "keyword";
private static final String TYPE_VALUE_GEO_POINT = "geo_point"; private static final String TYPE_VALUE_GEO_POINT = "geo_point";
@ -131,6 +133,14 @@ public class MappingBuilder {
} }
} }
private void writeTypeHintMapping(XContentBuilder builder) throws IOException {
builder.startObject(TYPEHINT_PROPERTY) //
.field(FIELD_PARAM_TYPE, TYPE_VALUE_KEYWORD) //
.field(FIELD_PARAM_INDEX, false) //
.field(FIELD_PARAM_DOC_VALUES, false) //
.endObject();
}
private void mapEntity(XContentBuilder builder, @Nullable ElasticsearchPersistentEntity<?> entity, private void mapEntity(XContentBuilder builder, @Nullable ElasticsearchPersistentEntity<?> entity,
boolean isRootObject, String nestedObjectFieldName, boolean nestedOrObjectField, FieldType fieldType, boolean isRootObject, String nestedObjectFieldName, boolean nestedOrObjectField, FieldType fieldType,
@Nullable Field parentFieldAnnotation, @Nullable DynamicMapping dynamicMapping) throws IOException { @Nullable Field parentFieldAnnotation, @Nullable DynamicMapping dynamicMapping) throws IOException {
@ -162,6 +172,8 @@ public class MappingBuilder {
builder.startObject(FIELD_PROPERTIES); builder.startObject(FIELD_PROPERTIES);
writeTypeHintMapping(builder);
if (entity != null) { if (entity != null) {
entity.doWithProperties((PropertyHandler<ElasticsearchPersistentProperty>) property -> { entity.doWithProperties((PropertyHandler<ElasticsearchPersistentProperty>) property -> {
try { try {

View File

@ -31,7 +31,6 @@ import org.json.JSONException;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
@ -257,7 +256,7 @@ public class ReactiveIndexOperationsTest {
.as(StepVerifier::create) // .as(StepVerifier::create) //
.assertNext(document -> { .assertNext(document -> {
try { try {
assertEquals(expected, document.toJson(), JSONCompareMode.NON_EXTENSIBLE); assertEquals(expected, document.toJson(), false);
} catch (JSONException e) { } catch (JSONException e) {
fail("", e); fail("", e);
} }
@ -282,7 +281,7 @@ public class ReactiveIndexOperationsTest {
.as(StepVerifier::create) // .as(StepVerifier::create) //
.assertNext(document -> { .assertNext(document -> {
try { try {
assertEquals(expected, document.toJson(), JSONCompareMode.NON_EXTENSIBLE); assertEquals(expected, document.toJson(), false);
} catch (JSONException e) { } catch (JSONException e) {
fail("", e); fail("", e);
} }
@ -310,7 +309,7 @@ public class ReactiveIndexOperationsTest {
.as(StepVerifier::create) // .as(StepVerifier::create) //
.assertNext(document -> { .assertNext(document -> {
try { try {
assertEquals(expected, document.toJson(), JSONCompareMode.NON_EXTENSIBLE); assertEquals(expected, document.toJson(), false);
} catch (JSONException e) { } catch (JSONException e) {
fail("", e); fail("", e);
} }
@ -340,7 +339,7 @@ public class ReactiveIndexOperationsTest {
.as(StepVerifier::create) // .as(StepVerifier::create) //
.assertNext(document -> { .assertNext(document -> {
try { try {
assertEquals(expected, document.toJson(), JSONCompareMode.NON_EXTENSIBLE); assertEquals(expected, document.toJson(), false);
} catch (JSONException e) { } catch (JSONException e) {
fail("", e); fail("", e);
} }

View File

@ -37,9 +37,7 @@ import java.time.LocalDate;
import java.util.Collection; import java.util.Collection;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set;
import org.elasticsearch.search.suggest.completion.context.ContextMapping; import org.elasticsearch.search.suggest.completion.context.ContextMapping;
import org.json.JSONException; import org.json.JSONException;
@ -420,7 +418,7 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
String mapping = getMappingBuilder().buildPropertyMapping(FieldMappingParameters.class); String mapping = getMappingBuilder().buildPropertyMapping(FieldMappingParameters.class);
// then // then
assertEquals(expected, mapping, true); assertEquals(expected, mapping, false);
} }
@Test @Test
@ -439,7 +437,7 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
String mapping = getMappingBuilder().buildPropertyMapping(ConfigureDynamicMappingEntity.class); String mapping = getMappingBuilder().buildPropertyMapping(ConfigureDynamicMappingEntity.class);
assertEquals(expected, mapping, true); assertEquals(expected, mapping, false);
} }
@Test // DATAES-784 @Test // DATAES-784
@ -454,7 +452,7 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
String mapping = getMappingBuilder().buildPropertyMapping(ValueDoc.class); String mapping = getMappingBuilder().buildPropertyMapping(ValueDoc.class);
assertEquals(expected, mapping, true); assertEquals(expected, mapping, false);
} }
@Test // DATAES-788 @Test // DATAES-788
@ -568,6 +566,54 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
.isInstanceOf(MappingException.class); .isInstanceOf(MappingException.class);
} }
@Test // #1711
@DisplayName("should write typeHint entries")
void shouldWriteTypeHintEntries() throws JSONException {
String expected = "{\n" + //
" \"properties\": {\n" + //
" \"_class\": {\n" + //
" \"type\": \"keyword\",\n" + //
" \"index\": false,\n" + //
" \"doc_values\": false\n" + //
" },\n" + //
" \"id\": {\n" + //
" \"type\": \"keyword\"\n" + //
" },\n" + //
" \"nestedEntity\": {\n" + //
" \"type\": \"nested\",\n" + //
" \"properties\": {\n" + //
" \"_class\": {\n" + //
" \"type\": \"keyword\",\n" + //
" \"index\": false,\n" + //
" \"doc_values\": false\n" + //
" },\n" + //
" \"nestedField\": {\n" + //
" \"type\": \"text\"\n" + //
" }\n" + //
" }\n" + //
" },\n" + //
" \"objectEntity\": {\n" + //
" \"type\": \"object\",\n" + //
" \"properties\": {\n" + //
" \"_class\": {\n" + //
" \"type\": \"keyword\",\n" + //
" \"index\": false,\n" + //
" \"doc_values\": false\n" + //
" },\n" + //
" \"objectField\": {\n" + //
" \"type\": \"text\"\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
" }\n" + //
"}\n"; //
String mapping = getMappingBuilder().buildPropertyMapping(TypeHintEntity.class);
assertEquals(expected, mapping, false);
}
@Setter @Setter
@Getter @Getter
@NoArgsConstructor @NoArgsConstructor
@ -862,21 +908,6 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
orientation = GeoShapeField.Orientation.clockwise) private String shape2; orientation = GeoShapeField.Orientation.clockwise) private String shape2;
} }
@Document(indexName = "test-index-user-mapping-builder")
static class User {
@Nullable @Id private String id;
@Field(type = FieldType.Nested, ignoreFields = { "users" }) private Set<Group> groups = new HashSet<>();
}
@Document(indexName = "test-index-group-mapping-builder")
static class Group {
@Nullable @Id String id;
@Field(type = FieldType.Nested, ignoreFields = { "groups" }) private Set<User> users = new HashSet<>();
}
@Document(indexName = "test-index-field-mapping-parameters") @Document(indexName = "test-index-field-mapping-parameters")
static class FieldMappingParameters { static class FieldMappingParameters {
@Nullable @Field private String indexTrue; @Nullable @Field private String indexTrue;
@ -1008,4 +1039,25 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
@Field(type = Text) private String text; @Field(type = Text) private String text;
@Mapping(enabled = false) @Field(type = Object) private Object object; @Mapping(enabled = false) @Field(type = Object) private Object object;
} }
@Data
@AllArgsConstructor
@NoArgsConstructor
static class TypeHintEntity {
@Id @Field(type = Keyword) private String id;
@Field(type = Nested) private NestedEntity nestedEntity;
@Field(type = Object) private ObjectEntity objectEntity;
@Data
static class NestedEntity {
@Field(type = Text) private String nestedField;
}
@Data
static class ObjectEntity {
@Field(type = Text) private String objectField;
}
}
} }

View File

@ -15,11 +15,12 @@
*/ */
package org.springframework.data.elasticsearch.core.index; package org.springframework.data.elasticsearch.core.index;
import static org.assertj.core.api.Assertions.*; import static org.skyscreamer.jsonassert.JSONAssert.*;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.json.JSONException;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Document;
@ -38,7 +39,7 @@ import org.springframework.lang.Nullable;
public class SimpleDynamicTemplatesMappingTests extends MappingContextBaseTests { public class SimpleDynamicTemplatesMappingTests extends MappingContextBaseTests {
@Test // DATAES-568 @Test // DATAES-568
public void testCorrectDynamicTemplatesMappings() { public void testCorrectDynamicTemplatesMappings() throws JSONException {
String mapping = getMappingBuilder().buildPropertyMapping(SampleDynamicTemplatesEntity.class); String mapping = getMappingBuilder().buildPropertyMapping(SampleDynamicTemplatesEntity.class);
@ -46,11 +47,11 @@ public class SimpleDynamicTemplatesMappingTests extends MappingContextBaseTests
+ "\"mapping\":{\"type\":\"string\",\"analyzer\":\"standard_lowercase_asciifolding\"}," + "\"mapping\":{\"type\":\"string\",\"analyzer\":\"standard_lowercase_asciifolding\"},"
+ "\"path_match\":\"names.*\"}}]," + "\"properties\":{\"names\":{\"type\":\"object\"}}}"; + "\"path_match\":\"names.*\"}}]," + "\"properties\":{\"names\":{\"type\":\"object\"}}}";
assertThat(mapping).isEqualTo(EXPECTED_MAPPING_ONE); assertEquals(EXPECTED_MAPPING_ONE, mapping, false);
} }
@Test // DATAES-568 @Test // DATAES-568
public void testCorrectDynamicTemplatesMappingsTwo() { public void testCorrectDynamicTemplatesMappingsTwo() throws JSONException {
String mapping = getMappingBuilder().buildPropertyMapping(SampleDynamicTemplatesEntityTwo.class); String mapping = getMappingBuilder().buildPropertyMapping(SampleDynamicTemplatesEntityTwo.class);
String EXPECTED_MAPPING_TWO = "{\"dynamic_templates\":" + "[{\"with_custom_analyzer\":{" String EXPECTED_MAPPING_TWO = "{\"dynamic_templates\":" + "[{\"with_custom_analyzer\":{"
@ -59,7 +60,7 @@ public class SimpleDynamicTemplatesMappingTests extends MappingContextBaseTests
+ "\"mapping\":{\"type\":\"string\",\"analyzer\":\"standard_lowercase_asciifolding\"}," + "\"mapping\":{\"type\":\"string\",\"analyzer\":\"standard_lowercase_asciifolding\"},"
+ "\"path_match\":\"participantA1.*\"}}]," + "\"properties\":{\"names\":{\"type\":\"object\"}}}"; + "\"path_match\":\"participantA1.*\"}}]," + "\"properties\":{\"names\":{\"type\":\"object\"}}}";
assertThat(mapping).isEqualTo(EXPECTED_MAPPING_TWO); assertEquals(EXPECTED_MAPPING_TWO, mapping, false);
} }
/** /**

View File

@ -15,13 +15,14 @@
*/ */
package org.springframework.data.elasticsearch.core.index; package org.springframework.data.elasticsearch.core.index;
import static org.assertj.core.api.Assertions.*; import static org.skyscreamer.jsonassert.JSONAssert.*;
import static org.springframework.data.elasticsearch.annotations.FieldType.*; import static org.springframework.data.elasticsearch.annotations.FieldType.*;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.json.JSONException;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.DateFormat; import org.springframework.data.elasticsearch.annotations.DateFormat;
@ -43,11 +44,11 @@ public class SimpleElasticsearchDateMappingTests extends MappingContextBaseTests
+ "\"basicFormatDate\":{\"" + "type\":\"date\",\"format\":\"basic_date\"}}}"; + "\"basicFormatDate\":{\"" + "type\":\"date\",\"format\":\"basic_date\"}}}";
@Test // DATAES-568, DATAES-828 @Test // DATAES-568, DATAES-828
public void testCorrectDateMappings() { public void testCorrectDateMappings() throws JSONException {
String mapping = getMappingBuilder().buildPropertyMapping(SampleDateMappingEntity.class); String mapping = getMappingBuilder().buildPropertyMapping(SampleDateMappingEntity.class);
assertThat(mapping).isEqualTo(EXPECTED_MAPPING); assertEquals(EXPECTED_MAPPING, mapping, false);
} }
/** /**