CustomMappingBuilder.java
/*
* Copyright 2014-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gringlobal.custom.elasticsearch;
import static org.elasticsearch.common.xcontent.XContentFactory.*;
import static org.springframework.util.StringUtils.*;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.time.Instant;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.UUID;
import javax.persistence.Column;
import javax.persistence.Lob;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentType;
import org.gringlobal.custom.validation.javax.CodeValueField;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.elasticsearch.annotations.CompletionContext;
import org.springframework.data.elasticsearch.annotations.CompletionField;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.GeoPointField;
import org.springframework.data.elasticsearch.annotations.InnerField;
import org.springframework.data.elasticsearch.annotations.Mapping;
import org.springframework.data.elasticsearch.annotations.MultiField;
import org.springframework.data.elasticsearch.core.completion.Completion;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import com.fasterxml.jackson.annotation.JsonIdentityReference;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
/**
* Modified MappingBuilder that includes all simple (non-entity) types and
* forces index to not use dynamic mappings.
*
* @author Rizwan Idrees
* @author Mohsin Husen
* @author Artur Konczak
* @author Kevin Leturc
* @author Alexander Volz
* @author Dennis Maaß
* @author Pavel Luhin
* @author Mark Paluch
* @author Sascha Woo
* @author Nordine Bittich
* @author Robert Gruendler
* @author Petr Kukral
* @author Peter-Josef Meisch
* @author Matija Obreza
* @author Maxym Borodenko
*/
@Slf4j
class CustomMappingBuilder {
private static final String FIELD_DATA = "fielddata";
private static final String FIELD_STORE = "store";
private static final String FIELD_TYPE = "type";
private static final String FIELD_INDEX = "index";
private static final String FIELD_FORMAT = "format";
private static final String FIELD_SEARCH_ANALYZER = "search_analyzer";
private static final String FIELD_INDEX_ANALYZER = "analyzer";
private static final String FIELD_NORMALIZER = "normalizer";
private static final String FIELD_PROPERTIES = "properties";
private static final String FIELD_PARENT = "_parent";
private static final String FIELD_COPY_TO = "copy_to";
private static final String FIELD_CONTEXT_NAME = "name";
private static final String FIELD_CONTEXT_TYPE = "type";
private static final String FIELD_CONTEXT_PRECISION = "precision";
private static final String FIELD_DYNAMIC = "dynamic";
// private static final String FIELD_DYNAMIC_TEMPLATES = "dynamic_templates";
private static final String COMPLETION_PRESERVE_SEPARATORS = "preserve_separators";
private static final String COMPLETION_PRESERVE_POSITION_INCREMENTS = "preserve_position_increments";
private static final String COMPLETION_MAX_INPUT_LENGTH = "max_input_length";
private static final String COMPLETION_CONTEXTS = "contexts";
private static final String TYPE_VALUE_KEYWORD = "keyword";
private static final String TYPE_VALUE_GEO_POINT = "geo_point";
private static final String TYPE_VALUE_COMPLETION = "completion";
private static final String ALL_TEXT_FIELD = "_texts";
private static final String[] COPYTO_ALL_TEXT = { ALL_TEXT_FIELD };
private final ElasticsearchConverter elasticsearchConverter;
CustomMappingBuilder(ElasticsearchConverter elasticsearchConverter) {
this.elasticsearchConverter = elasticsearchConverter;
}
/**
* builds the Elasticsearch mapping for the given clazz.
* @param object
* @param indexType
*
* @return JSON string
* @throws IOException
*/
XContentBuilder buildPropertyMapping(Class<?> clazz, String indexType, String parentType) throws IOException {
ElasticsearchPersistentEntity<?> entity = elasticsearchConverter.getMappingContext()
.getRequiredPersistentEntity(clazz);
XContentBuilder builder = jsonBuilder().startObject().startObject(indexType);
// Parent
if (hasText(parentType)) {
builder.startObject(FIELD_PARENT).field(FIELD_TYPE, parentType).endObject();
}
// Dynamic
builder.field(FIELD_DYNAMIC, false);
// We don't want the source stored in the index
XContentBuilder ignoreSource = builder.startObject("_source");
ignoreSource.field("enabled", false);
ignoreSource.endObject();
// Properties
builder.startObject(FIELD_PROPERTIES);
builder.startObject(ALL_TEXT_FIELD).field(FIELD_TYPE, "text").endObject();
mapEntity(new HashSet<>(), builder, entity, true, "", false, FieldType.Auto, null, null);
builder.endObject() // FIELD_PROPERTIES
.endObject() // indexType
.endObject() // root object
.close();
return builder;
}
private void mapEntity(Set<Class<?>> circularReferences, XContentBuilder builder, @Nullable ElasticsearchPersistentEntity<?> entity, boolean isRootObject,
String nestedObjectFieldName, boolean nestedOrObjectField, FieldType fieldType,
@Nullable Field parentFieldAnnotation, JsonIgnoreProperties jsonIgnoreProperties) throws IOException {
if (entity != null && circularReferences.contains(entity.getTypeInformation().getType())) {
log.info("Circular reference detected class={} in {}", entity.getIndexName(), circularReferences);
return;
} else if (entity != null) {
circularReferences.add(entity.getTypeInformation().getType());
}
boolean writeNestedProperties = !isRootObject && (isAnyPropertyAnnotatedWithField(entity) || nestedOrObjectField);
if (writeNestedProperties) {
String type = nestedOrObjectField ? fieldType.toString().toLowerCase()
: FieldType.Object.toString().toLowerCase();
builder.startObject(nestedObjectFieldName).field(FIELD_TYPE, type);
if (nestedOrObjectField && FieldType.Nested == fieldType && parentFieldAnnotation != null
&& parentFieldAnnotation.includeInParent()) {
builder.field("include_in_parent", parentFieldAnnotation.includeInParent());
}
builder.startObject(FIELD_PROPERTIES);
}
if (entity != null) {
entity.doWithProperties((PropertyHandler<ElasticsearchPersistentProperty>) property -> {
try {
// if (property.isAnnotationPresent(Transient.class) || isInIgnoreFields(property, parentFieldAnnotation, jsonIgnoreProperties)) {
if (isInIgnoreFields(property, parentFieldAnnotation, jsonIgnoreProperties)) {
return;
}
buildPropertyMapping(circularReferences, builder, isRootObject, property);
CodeValueField codeValueField = property.findAnnotation(CodeValueField.class);
if (codeValueField != null && codeValueField.indexed()) {
builder.startObject(property.getFieldName() + "_cv");
builder.field(FIELD_TYPE, FieldType.Text.name().toLowerCase());
builder.field(FIELD_STORE, false);
builder.field(FIELD_COPY_TO, ALL_TEXT_FIELD);
builder.endObject();
}
} catch (IOException e) {
log.warn("error mapping property with name {}", property.getName(), e);
}
});
// Add _class field to entities, don't store
builder.startObject("_class").field(FIELD_TYPE, TYPE_VALUE_KEYWORD).field(FIELD_STORE, false).endObject();
circularReferences.remove(entity.getTypeInformation().getType());
}
if (writeNestedProperties) {
builder.endObject().endObject();
}
}
private void buildPropertyMapping(Set<Class<?>> circularReferences, XContentBuilder builder, boolean isRootObject,
ElasticsearchPersistentProperty property) throws IOException {
if (property.isAnnotationPresent(Mapping.class)) {
String mappingPath = property.getRequiredAnnotation(Mapping.class).mappingPath();
if (!StringUtils.isEmpty(mappingPath)) {
ClassPathResource mappings = new ClassPathResource(mappingPath);
if (mappings.exists()) {
builder.rawField(property.getFieldName(), mappings.getInputStream(), XContentType.JSON);
return;
}
}
}
boolean isGeoPointProperty = isGeoPointProperty(property);
boolean isCompletionProperty = isCompletionProperty(property);
boolean isNestedOrObjectProperty = isNestedOrObjectProperty(property);
Field fieldAnnotation = property.findAnnotation(Field.class);
if (!isGeoPointProperty && !isCompletionProperty && hasRelevantAnnotation(property)) {
JsonIdentityReference idRef = property.findAnnotation(JsonIdentityReference.class);
if (idRef != null && idRef.alwaysAsId()) {
log.debug("@JsonIdentityReference for {}#{}", property.getActualType(), property.getFieldName());
applyDefaultIdFieldMapping(builder, property);
return;
}
if (fieldAnnotation == null) {
log.info("Skipping {}#{}", property.getActualType(), property.getFieldName());
return;
}
Iterator<? extends TypeInformation<?>> iterator = property.getPersistentEntityTypeInformation().iterator();
ElasticsearchPersistentEntity<?> persistentEntity = iterator.hasNext()
? elasticsearchConverter.getMappingContext().getPersistentEntity(iterator.next())
: null;
JsonIgnoreProperties jsonIgnoreProperties = property.findAnnotation(JsonIgnoreProperties.class);
mapEntity(circularReferences, builder, persistentEntity, false, property.getFieldName(), isNestedOrObjectProperty,
fieldAnnotation.type(), fieldAnnotation, jsonIgnoreProperties);
if (isNestedOrObjectProperty) {
return;
}
}
MultiField multiField = property.findAnnotation(MultiField.class);
if (isGeoPointProperty) {
applyGeoPointFieldMapping(builder, property);
return;
}
if (isCompletionProperty) {
CompletionField completionField = property.findAnnotation(CompletionField.class);
applyCompletionFieldMapping(builder, property, completionField);
}
if (isRootObject && fieldAnnotation != null && property.isIdProperty()) {
applyDefaultIdFieldMapping(builder, property);
} else if (multiField != null) {
addMultiFieldMapping(builder, property, multiField, isNestedOrObjectProperty);
} else if (fieldAnnotation != null) {
addSingleFieldMapping(builder, property, fieldAnnotation, isNestedOrObjectProperty);
} else if (!property.isEntity() && !property.isMap() && !isConstant(property.getField())) {
// This includes all simple property
addSingleFieldMapping(builder, property, null, false);
} else {
Iterator<? extends TypeInformation<?>> entityTypes = property.getPersistentEntityTypeInformation().iterator();
if (entityTypes.hasNext()) {
TypeInformation<?> entityType = entityTypes.next();
Class<?> type1 = entityType.getType();
if (UUID.class.isAssignableFrom(type1)) {
log.info("Force adding {}.{} {}", property.getOwner().getName(), property.getFieldName(), property.getActualType());
addSingleFieldMapping(builder, property, null, false);
} else {
log.info("Skipping {}.{} {}", property.getOwner().getName(), property.getFieldName(), property.getActualType());
}
} else {
log.info("Skipping {}.{} {}", property.getOwner().getName(), property.getFieldName(), property.getActualType());
}
}
}
/// Things that generally don't change much
private boolean isConstant(java.lang.reflect.Field field) {
return field != null && (Modifier.isStatic(field.getModifiers()) || Modifier.isFinal(field.getModifiers()));
}
private boolean hasRelevantAnnotation(ElasticsearchPersistentProperty property) {
return property.findAnnotation(Field.class) != null || property.findAnnotation(MultiField.class) != null
|| property.findAnnotation(GeoPointField.class) != null
|| property.findAnnotation(CompletionField.class) != null;
}
private void applyGeoPointFieldMapping(XContentBuilder builder, ElasticsearchPersistentProperty property)
throws IOException {
builder.startObject(property.getFieldName()).field(FIELD_TYPE, TYPE_VALUE_GEO_POINT).endObject();
}
private void applyCompletionFieldMapping(XContentBuilder builder, ElasticsearchPersistentProperty property,
@Nullable CompletionField annotation) throws IOException {
builder.startObject(property.getFieldName());
builder.field(FIELD_TYPE, TYPE_VALUE_COMPLETION);
if (annotation != null) {
builder.field(COMPLETION_MAX_INPUT_LENGTH, annotation.maxInputLength());
builder.field(COMPLETION_PRESERVE_POSITION_INCREMENTS, annotation.preservePositionIncrements());
builder.field(COMPLETION_PRESERVE_SEPARATORS, annotation.preserveSeparators());
if (!StringUtils.isEmpty(annotation.searchAnalyzer())) {
builder.field(FIELD_SEARCH_ANALYZER, annotation.searchAnalyzer());
}
if (!StringUtils.isEmpty(annotation.analyzer())) {
builder.field(FIELD_INDEX_ANALYZER, annotation.analyzer());
}
if (annotation.contexts().length > 0) {
builder.startArray(COMPLETION_CONTEXTS);
for (CompletionContext context : annotation.contexts()) {
builder.startObject();
builder.field(FIELD_CONTEXT_NAME, context.name());
builder.field(FIELD_CONTEXT_TYPE, context.type().name().toLowerCase());
if (context.precision().length() > 0) {
builder.field(FIELD_CONTEXT_PRECISION, context.precision());
}
builder.endObject();
}
builder.endArray();
}
}
builder.endObject();
}
private void applyDefaultIdFieldMapping(XContentBuilder builder, ElasticsearchPersistentProperty property)
throws IOException {
builder.startObject(property.getFieldName()).field(FIELD_TYPE, TYPE_VALUE_KEYWORD).field(FIELD_INDEX, true)
.endObject();
}
/**
* Add mapping for @Field annotation
*
* @throws IOException
*/
private void addSingleFieldMapping(XContentBuilder builder, ElasticsearchPersistentProperty property, Field annotation, boolean nestedOrObjectField) throws IOException {
log.trace("addSingleFieldMapping {}#{}", property.getActualType(), property.getFieldName());
builder.startObject(property.getFieldName());
addFieldMappingParameters(builder, property, annotation, nestedOrObjectField);
builder.endObject();
}
/**
* Add mapping for @MultiField annotation
*
* @throws IOException
*/
private void addMultiFieldMapping(XContentBuilder builder, ElasticsearchPersistentProperty property,
MultiField annotation, boolean nestedOrObjectField) throws IOException {
// main field
builder.startObject(property.getFieldName());
addFieldMappingParameters(builder, property, annotation.mainField(), nestedOrObjectField);
// inner fields
builder.startObject("fields");
for (InnerField innerField : annotation.otherFields()) {
builder.startObject(innerField.suffix());
addFieldMappingParameters(builder, property, innerField, false);
builder.endObject();
}
builder.endObject();
builder.endObject();
}
private void addFieldMappingParameters(XContentBuilder builder, ElasticsearchPersistentProperty property, Object annotation, boolean nestedOrObjectField) throws IOException {
boolean index;
boolean store = false;
boolean fielddata = false;
FieldType type;
DateFormat dateFormat;
String datePattern = null;
String analyzer = null;
String searchAnalyzer = null;
String normalizer = null;
String[] copyTo = null;
if (annotation == null) {
// Auto-detect
type = typeForField(property);
index = true; // indexForField(property);
// What fields need fielddata? We are using for indexed text for now.
// if (index && type.equals(FieldType.Text)) {
// fielddata = Boolean.TRUE;
// }
dateFormat = DateFormat.none;
} else if (annotation instanceof Field) {
// @Field
Field fieldAnnotation = (Field) annotation;
index = fieldAnnotation.index();
store = fieldAnnotation.store();
fielddata = fieldAnnotation.fielddata();
type = fieldAnnotation.type();
dateFormat = fieldAnnotation.format();
datePattern = fieldAnnotation.pattern();
analyzer = fieldAnnotation.analyzer();
searchAnalyzer = fieldAnnotation.searchAnalyzer();
normalizer = fieldAnnotation.normalizer();
copyTo = fieldAnnotation.copyTo();
} else if (annotation instanceof InnerField) {
// @InnerField
InnerField fieldAnnotation = (InnerField) annotation;
index = fieldAnnotation.index();
store = fieldAnnotation.store();
fielddata = fieldAnnotation.fielddata();
type = fieldAnnotation.type();
dateFormat = fieldAnnotation.format();
datePattern = fieldAnnotation.pattern();
analyzer = fieldAnnotation.analyzer();
searchAnalyzer = fieldAnnotation.searchAnalyzer();
normalizer = fieldAnnotation.normalizer();
} else {
throw new IllegalArgumentException("annotation must be an instance of @Field or @InnerField");
}
if (!nestedOrObjectField && store) {
builder.field(FIELD_STORE, store);
}
if (fielddata) {
builder.field(FIELD_DATA, fielddata);
}
if (type != FieldType.Auto) {
builder.field(FIELD_TYPE, type.name().toLowerCase());
if (type == FieldType.Date && dateFormat != DateFormat.none) {
builder.field(FIELD_FORMAT, dateFormat == DateFormat.custom ? datePattern : dateFormat.toString());
}
// Add indexed version of keywords for full-text search
if (type == FieldType.Keyword && annotation == null) {
builder.startObject("fields");
builder.startObject("i"); // for indexed
builder.field(FIELD_TYPE, FieldType.Text.name().toLowerCase());
builder.endObject();
builder.endObject();
}
} else {
builder.field(FIELD_TYPE, typeForField(property).name().toLowerCase());
}
if (!index) {
builder.field(FIELD_INDEX, index);
}
if (!StringUtils.isEmpty(analyzer)) {
builder.field(FIELD_INDEX_ANALYZER, analyzer);
}
if (!StringUtils.isEmpty(searchAnalyzer)) {
builder.field(FIELD_SEARCH_ANALYZER, searchAnalyzer);
}
if (!StringUtils.isEmpty(normalizer)) {
builder.field(FIELD_NORMALIZER, normalizer);
}
SearchField searchField = property.getField().getAnnotation(SearchField.class);
if (searchField != null) {
if (copyTo == null) {
copyTo = COPYTO_ALL_TEXT;
} else {
copyTo = ArrayUtils.add(copyTo, ALL_TEXT_FIELD);
}
}
if (copyTo != null && copyTo.length > 0) {
builder.field(FIELD_COPY_TO, copyTo);
}
}
private FieldType typeForField(@NonNull ElasticsearchPersistentProperty property) {
if (property.isCollectionLike()) {
var field = property.getField();
if (field.getType().equals(byte[].class)) {
return FieldType.Text;
}
var type = field.getGenericType();
// if (type instanceof ParameterizedType) {
ParameterizedType paramType = (ParameterizedType) type;
Class<?> paramClass = (Class<?>) paramType.getActualTypeArguments()[0];
return typeForClass(paramClass, property.getField());
// }
} else {
return typeForClass(property.getActualType(), property.getField());
}
}
private FieldType typeForClass(Class<?> clazz, java.lang.reflect.Field field) {
final int targetTypeIndex = 1;
// Handle @JsonSerialize(converter)
JsonSerialize jsonSerialize = field.getAnnotation(JsonSerialize.class);
if (jsonSerialize != null) {
Class<?> converter = jsonSerialize.converter();
if (converter != null) {
ParameterizedType paramType = (ParameterizedType) converter.getGenericSuperclass();
clazz = (Class<?>) paramType.getActualTypeArguments()[targetTypeIndex];
log.info("Field {}.{} is serialized using {} to {}", field.getDeclaringClass(), field.getName(), converter.getName(), clazz);
}
}
if (String.class.equals(clazz)) {
return isAnalyzed(clazz, field) ? FieldType.Text : FieldType.Keyword;
} else if (Boolean.TYPE.equals(clazz) || Boolean.class.equals(clazz)) {
return FieldType.Boolean;
} else if (Long.TYPE.equals(clazz) || Long.class.equals(clazz)) {
return FieldType.Long;
} else if (Double.TYPE.equals(clazz) || Double.class.equals(clazz)) {
return FieldType.Double;
} else if (Float.TYPE.equals(clazz) || Float.class.equals(clazz)) {
return FieldType.Float;
} else if (Integer.TYPE.equals(clazz) || Integer.class.equals(clazz)) {
return FieldType.Integer;
} else if (Date.class.isAssignableFrom(clazz)) {
return FieldType.Date;
} else if (Instant.class.isAssignableFrom(clazz)) {
return FieldType.Date;
} else if (LocalDate.class.isAssignableFrom(clazz)) {
return FieldType.Date;
} else if (Number.class.isAssignableFrom(clazz)) {
return FieldType.Double;
} else if (UUID.class.isAssignableFrom(clazz)) {
return FieldType.Keyword;
} else if (clazz.isEnum()) {
return FieldType.Keyword;
} else {
log.warn("Default mapping for class={} as {} for field={}#{}", clazz, FieldType.Text, field.getDeclaringClass(), field.getName());
return FieldType.Text;
}
}
// private Boolean indexForField(@NonNull ElasticsearchPersistentProperty property) {
// if (property.isCollectionLike()) {
// ParameterizedType paramType = (ParameterizedType) property.getField().getGenericType();
// Class<?> paramClass = (Class<?>) paramType.getActualTypeArguments()[0];
// return isAnalyzed(paramClass, property.getField());
// } else {
// return isAnalyzed(property.getActualType(), property.getField());
// }
// }
private Boolean isAnalyzed(Class<?> clazz, java.lang.reflect.Field field) {
if (String.class.equals(clazz)) {
Lob jpaLob = field.getAnnotation(Lob.class);
Column jpaColumn = field.getAnnotation(Column.class);
boolean analyzed = false;
if (jpaLob != null) {
analyzed = true;
} else if (jpaColumn != null) {
if (jpaColumn.length() > 100) {
analyzed = true;
}
}
return analyzed ? Boolean.TRUE : Boolean.FALSE;
}
else if (clazz.isEnum()) {
return Boolean.FALSE;
} else {
log.debug("Using default indexing for class={}", clazz);
return Boolean.TRUE;
}
}
private boolean isAnyPropertyAnnotatedWithField(@Nullable ElasticsearchPersistentEntity<?> entity) {
return entity != null && entity.getPersistentProperty(Field.class) != null;
}
private boolean isInIgnoreFields(ElasticsearchPersistentProperty property, @Nullable Field parentFieldAnnotation, JsonIgnoreProperties jsonIgnoreProperties) {
if (property.getFieldName().equals("serialVersionUID")) {
return true;
}
IgnoreField ignoreFieldAnnotation = property.findAnnotation(IgnoreField.class);
if (ignoreFieldAnnotation != null) {
return ignoreFieldAnnotation.value();
}
JsonIgnore ignoreAnnotation = property.findAnnotation(JsonIgnore.class);
if (ignoreAnnotation != null) {
return ignoreAnnotation.value();
}
if (null != parentFieldAnnotation) {
String[] ignoreFields = parentFieldAnnotation.ignoreFields();
if (Arrays.asList(ignoreFields).contains(property.getFieldName())) {
return true;
}
}
if (null != jsonIgnoreProperties) {
String[] ignoreFields = jsonIgnoreProperties.value();
if (Arrays.asList(ignoreFields).contains(property.getFieldName())) {
return true;
}
}
return false;
}
private boolean isNestedOrObjectProperty(ElasticsearchPersistentProperty property) {
Field fieldAnnotation = property.findAnnotation(Field.class);
return fieldAnnotation != null
&& (FieldType.Nested == fieldAnnotation.type() || FieldType.Object == fieldAnnotation.type());
}
private boolean isGeoPointProperty(ElasticsearchPersistentProperty property) {
return property.getActualType() == GeoPoint.class || property.isAnnotationPresent(GeoPointField.class);
}
private boolean isCompletionProperty(ElasticsearchPersistentProperty property) {
return property.getActualType() == Completion.class;
}
}