Skip to content

Add the type hint _class attribute to the index mapping. #1717

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
Expand Down Expand Up @@ -550,25 +549,25 @@ public void write(Object source, Document sink) {
}

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);
}

/**
* Internal write conversion method which should be used for nested invocations.
*
* @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;
Expand All @@ -594,7 +593,7 @@ protected void writeInternal(@Nullable Object source, Map<String, Object> sink,
}

ElasticsearchPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(entityType);
addCustomTypeKeyIfNecessary(typeHint, source, sink);
addCustomTypeKeyIfNecessary(source, sink, typeInformation);
writeInternal(source, sink, entity);
}

Expand All @@ -603,7 +602,7 @@ protected void writeInternal(@Nullable Object source, Map<String, Object> sink,
*
* @param source
* @param sink
* @param typeHint
* @param entity
*/
protected void writeInternal(@Nullable Object source, Map<String, Object> sink,
@Nullable ElasticsearchPersistentEntity<?> entity) {
Expand Down Expand Up @@ -725,7 +724,7 @@ protected void writeProperty(ElasticsearchPersistentProperty property, Object va
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);
}
Expand Down Expand Up @@ -923,18 +922,18 @@ protected Object getWriteComplexValue(ElasticsearchPersistentProperty property,
// 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) {
Expand All @@ -948,7 +947,7 @@ protected void addCustomTypeKeyIfNecessary(@Nullable TypeInformation<?> type, Ob
* @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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -131,6 +133,14 @@ public String buildPropertyMapping(Class<?> clazz) throws MappingException {
}
}

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 {
Expand Down Expand Up @@ -162,6 +172,8 @@ private void mapEntity(XContentBuilder builder, @Nullable ElasticsearchPersisten

builder.startObject(FIELD_PROPERTIES);

writeTypeHintMapping(builder);

if (entity != null) {
entity.doWithProperties((PropertyHandler<ElasticsearchPersistentProperty>) property -> {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
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;
Expand Down Expand Up @@ -257,7 +256,7 @@ void shouldCreateMappingForEntityFromProperties() {
.as(StepVerifier::create) //
.assertNext(document -> {
try {
assertEquals(expected, document.toJson(), JSONCompareMode.NON_EXTENSIBLE);
assertEquals(expected, document.toJson(), false);
} catch (JSONException e) {
fail("", e);
}
Expand All @@ -282,7 +281,7 @@ void shouldCreateMappingForEntityFromMappingAnnotation() {
.as(StepVerifier::create) //
.assertNext(document -> {
try {
assertEquals(expected, document.toJson(), JSONCompareMode.NON_EXTENSIBLE);
assertEquals(expected, document.toJson(), false);
} catch (JSONException e) {
fail("", e);
}
Expand Down Expand Up @@ -310,7 +309,7 @@ void shouldCreateMappingBoundEntity() {
.as(StepVerifier::create) //
.assertNext(document -> {
try {
assertEquals(expected, document.toJson(), JSONCompareMode.NON_EXTENSIBLE);
assertEquals(expected, document.toJson(), false);
} catch (JSONException e) {
fail("", e);
}
Expand Down Expand Up @@ -340,7 +339,7 @@ void shouldPutAndGetMapping() {
.as(StepVerifier::create) //
.assertNext(document -> {
try {
assertEquals(expected, document.toJson(), JSONCompareMode.NON_EXTENSIBLE);
assertEquals(expected, document.toJson(), false);
} catch (JSONException e) {
fail("", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@
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;
Expand Down Expand Up @@ -420,7 +418,7 @@ public void shouldSetFieldMappingProperties() throws JSONException {
String mapping = getMappingBuilder().buildPropertyMapping(FieldMappingParameters.class);

// then
assertEquals(expected, mapping, true);
assertEquals(expected, mapping, false);
}

@Test
Expand All @@ -439,7 +437,7 @@ void shouldWriteDynamicMappingSettings() throws JSONException {

String mapping = getMappingBuilder().buildPropertyMapping(ConfigureDynamicMappingEntity.class);

assertEquals(expected, mapping, true);
assertEquals(expected, mapping, false);
}

@Test // DATAES-784
Expand All @@ -454,7 +452,7 @@ void shouldMapPropertyObjectsToFieldDefinition() throws JSONException {

String mapping = getMappingBuilder().buildPropertyMapping(ValueDoc.class);

assertEquals(expected, mapping, true);
assertEquals(expected, mapping, false);
}

@Test // DATAES-788
Expand Down Expand Up @@ -568,6 +566,54 @@ void shouldOnlyAllowDisabledPropertiesOnTypeObject() {
.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
Expand Down Expand Up @@ -862,21 +908,6 @@ static class GeoEntity {
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;
Expand Down Expand Up @@ -1008,4 +1039,25 @@ static class DisabledMappingProperty {
@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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,19 +39,19 @@
public class SimpleDynamicTemplatesMappingTests extends MappingContextBaseTests {

@Test // DATAES-568
public void testCorrectDynamicTemplatesMappings() {
public void testCorrectDynamicTemplatesMappings() throws JSONException {

String mapping = getMappingBuilder().buildPropertyMapping(SampleDynamicTemplatesEntity.class);

String EXPECTED_MAPPING_ONE = "{\"dynamic_templates\":" + "[{\"with_custom_analyzer\":{"
+ "\"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\":{"
Expand All @@ -59,7 +60,7 @@ public void testCorrectDynamicTemplatesMappingsTwo() {
+ "\"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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}

/**
Expand Down