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.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
@ -550,13 +549,13 @@ public class MappingElasticsearchConverter
}
Class<?> entityType = ClassUtils.getUserClass(source.getClass());
TypeInformation<? extends Object> type = ClassTypeInformation.from(entityType);
TypeInformation<? extends Object> typeInformation = ClassTypeInformation.from(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 sink
* @param typeHint
* @param typeInformation
*/
@SuppressWarnings("unchecked")
protected void writeInternal(@Nullable Object source, Map<String, Object> sink,
@Nullable TypeInformation<?> typeHint) {
@Nullable TypeInformation<?> typeInformation) {
if (null == source) {
return;
@ -594,7 +593,7 @@ public class MappingElasticsearchConverter
}
ElasticsearchPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(entityType);
addCustomTypeKeyIfNecessary(typeHint, source, sink);
addCustomTypeKeyIfNecessary(source, sink, typeInformation);
writeInternal(source, sink, entity);
}
@ -603,7 +602,7 @@ public class MappingElasticsearchConverter
*
* @param source
* @param sink
* @param typeHint
* @param entity
*/
protected void writeInternal(@Nullable Object source, Map<String, Object> sink,
@Nullable ElasticsearchPersistentEntity<?> entity) {
@ -725,7 +724,7 @@ public class MappingElasticsearchConverter
Map<String, Object> document = existingValue instanceof Map ? (Map<String, Object>) existingValue
: Document.create();
addCustomTypeKeyIfNecessary(ClassTypeInformation.from(property.getRawType()), value, document);
addCustomTypeKeyIfNecessary(value, document, ClassTypeInformation.from(property.getRawType()));
writeInternal(value, document, entity);
sink.set(property, document);
}
@ -923,18 +922,18 @@ public class MappingElasticsearchConverter
// 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
* given. This is usually the case if you store a subtype of the actual declared type of the property.
* Adds custom typeInformation information to the given {@link Map} if necessary. That is if the value is not the same
* 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 value must not be {@literal null}.
* @param source must not be {@literal null}.
* @param sink must not be {@literal null}.
* @param type
*/
protected void addCustomTypeKeyIfNecessary(@Nullable TypeInformation<?> type, Object value,
Map<String, Object> sink) {
protected void addCustomTypeKeyIfNecessary(Object source, Map<String, Object> sink, @Nullable TypeInformation<?> type) {
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);
if (notTheSameClass) {
@ -948,7 +947,7 @@ public class MappingElasticsearchConverter
* @param type must not be {@literal null}.
* @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)
&& !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_CONTEXTS = "contexts";
private static final String TYPEHINT_PROPERTY = "_class";
private static final String TYPE_DYNAMIC = "dynamic";
private static final String TYPE_VALUE_KEYWORD = "keyword";
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,
boolean isRootObject, String nestedObjectFieldName, boolean nestedOrObjectField, FieldType fieldType,
@Nullable Field parentFieldAnnotation, @Nullable DynamicMapping dynamicMapping) throws IOException {
@ -162,6 +172,8 @@ public class MappingBuilder {
builder.startObject(FIELD_PROPERTIES);
writeTypeHintMapping(builder);
if (entity != null) {
entity.doWithProperties((PropertyHandler<ElasticsearchPersistentProperty>) property -> {
try {

View File

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

View File

@ -37,9 +37,7 @@ import java.time.LocalDate;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.elasticsearch.search.suggest.completion.context.ContextMapping;
import org.json.JSONException;
@ -420,7 +418,7 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
String mapping = getMappingBuilder().buildPropertyMapping(FieldMappingParameters.class);
// then
assertEquals(expected, mapping, true);
assertEquals(expected, mapping, false);
}
@Test
@ -439,7 +437,7 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
String mapping = getMappingBuilder().buildPropertyMapping(ConfigureDynamicMappingEntity.class);
assertEquals(expected, mapping, true);
assertEquals(expected, mapping, false);
}
@Test // DATAES-784
@ -454,7 +452,7 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
String mapping = getMappingBuilder().buildPropertyMapping(ValueDoc.class);
assertEquals(expected, mapping, true);
assertEquals(expected, mapping, false);
}
@Test // DATAES-788
@ -568,6 +566,54 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
.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
@Getter
@NoArgsConstructor
@ -862,21 +908,6 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
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")
static class FieldMappingParameters {
@Nullable @Field private String indexTrue;
@ -1008,4 +1039,25 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
@Field(type = Text) private String text;
@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;
import static org.assertj.core.api.Assertions.*;
import static org.skyscreamer.jsonassert.JSONAssert.*;
import java.util.HashMap;
import java.util.Map;
import org.json.JSONException;
import org.junit.jupiter.api.Test;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
@ -38,7 +39,7 @@ import org.springframework.lang.Nullable;
public class SimpleDynamicTemplatesMappingTests extends MappingContextBaseTests {
@Test // DATAES-568
public void testCorrectDynamicTemplatesMappings() {
public void testCorrectDynamicTemplatesMappings() throws JSONException {
String mapping = getMappingBuilder().buildPropertyMapping(SampleDynamicTemplatesEntity.class);
@ -46,11 +47,11 @@ public class SimpleDynamicTemplatesMappingTests extends MappingContextBaseTests
+ "\"mapping\":{\"type\":\"string\",\"analyzer\":\"standard_lowercase_asciifolding\"},"
+ "\"path_match\":\"names.*\"}}]," + "\"properties\":{\"names\":{\"type\":\"object\"}}}";
assertThat(mapping).isEqualTo(EXPECTED_MAPPING_ONE);
assertEquals(EXPECTED_MAPPING_ONE, mapping, false);
}
@Test // DATAES-568
public void testCorrectDynamicTemplatesMappingsTwo() {
public void testCorrectDynamicTemplatesMappingsTwo() throws JSONException {
String mapping = getMappingBuilder().buildPropertyMapping(SampleDynamicTemplatesEntityTwo.class);
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\"},"
+ "\"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;
import static org.assertj.core.api.Assertions.*;
import static org.skyscreamer.jsonassert.JSONAssert.*;
import static org.springframework.data.elasticsearch.annotations.FieldType.*;
import lombok.Data;
import java.time.LocalDateTime;
import org.json.JSONException;
import org.junit.jupiter.api.Test;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.DateFormat;
@ -43,11 +44,11 @@ public class SimpleElasticsearchDateMappingTests extends MappingContextBaseTests
+ "\"basicFormatDate\":{\"" + "type\":\"date\",\"format\":\"basic_date\"}}}";
@Test // DATAES-568, DATAES-828
public void testCorrectDateMappings() {
public void testCorrectDateMappings() throws JSONException {
String mapping = getMappingBuilder().buildPropertyMapping(SampleDateMappingEntity.class);
assertThat(mapping).isEqualTo(EXPECTED_MAPPING);
assertEquals(EXPECTED_MAPPING, mapping, false);
}
/**