Skip to content

Commit 233cf99

Browse files
author
Luc Boutier
committed
Allow the mapping of maps as objects of key and values.
Note that such mapping requires specific json serializer to take that in account. Alien 4 Cloud provide such in it's common project.
1 parent 6907290 commit 233cf99

File tree

7 files changed

+285
-28
lines changed

7 files changed

+285
-28
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package org.elasticsearch.annotation;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
import org.elasticsearch.mapping.*;
9+
10+
/**
11+
* Annotation to tell the mapper to process the mapping for a Map using key and value fields (support requires a specific JSON serializer).
12+
*
13+
* @author luc boutier
14+
*/
15+
@Retention(RetentionPolicy.RUNTIME)
16+
@Target({ ElementType.FIELD, ElementType.METHOD })
17+
public @interface MapKeyValue {
18+
String NULL_VALUE_NOT_SPECIFIED = "org.elasticsearch.annotation.StringField.NullValue.NotSpecified";
19+
20+
/**
21+
* Set to yes to store actual field in the index, no to not store it. Defaults to no (note, the JSON document itself
22+
* is stored, and it can be retrieved from it).
23+
*
24+
* @return Yes or no (default is no).
25+
*/
26+
boolean store() default false;
27+
28+
/**
29+
* Set to analyzed for the field to be indexed and searchable after being broken down into token using an analyzer.
30+
* not_analyzed means that its still searchable, but does not go through any analysis process or broken down into
31+
* tokens. no means that it won���t be searchable at all (as an individual field; it may still be included in _all).
32+
*
33+
* @return Defaults to analyzed, any other value to change setting.
34+
*/
35+
IndexType indexType() default IndexType.analyzed;
36+
37+
/**
38+
* Possible values are no, yes, with_offsets, with_positions, with_positions_offsets.
39+
*
40+
* @return Defaults to no.
41+
*/
42+
TermVector termVector() default TermVector.no;
43+
44+
/**
45+
* The boost value. Defaults to 1.0.
46+
*
47+
* @return The new boost value.
48+
*/
49+
float boost() default 1f;
50+
51+
/**
52+
* When there is a (JSON) null value for the field, use the null_value as the field value. Defaults to not adding the field at all.
53+
*
54+
* @return
55+
*/
56+
String nullValue() default NULL_VALUE_NOT_SPECIFIED;
57+
58+
/**
59+
* Boolean value if norms should be enabled or not. Defaults to true for analyzed fields, and to false for not_analyzed fields.
60+
*
61+
* @return True to omit norms, false to not omit norms.
62+
*/
63+
NormEnabled normsEnabled() default NormEnabled.DEFAULT;
64+
65+
/**
66+
* Describes how norms should be loaded, possible values are eager and lazy (default). It is possible to change the default value to eager for all fields by
67+
* configuring the index setting index.norms.loading to eager.
68+
*
69+
* @return
70+
*/
71+
NormLoading normsLoading() default NormLoading.DEFAULT;
72+
73+
/**
74+
* Allows to set the indexing options, possible values are docs (only doc numbers are indexed), freqs (doc numbers and term frequencies), and positions (doc
75+
* numbers, term frequencies and positions). Defaults to positions for analyzed fields, and to docs for not_analyzed fields. It is also possible to set it
76+
* to offsets (doc numbers, term frequencies, positions and offsets).
77+
*
78+
* @return
79+
*/
80+
IndexOptions indexOptions() default IndexOptions.DEFAULT;
81+
82+
/**
83+
* The analyzer used to analyze the text contents when analyzed during indexing and when searching using a query string. Defaults to the globally configured
84+
* analyzer.
85+
*
86+
* @return
87+
*/
88+
String analyser() default "";
89+
90+
/**
91+
* The analyzer used to analyze the text contents when analyzed during indexing.
92+
*
93+
* @return
94+
*/
95+
String indexAnalyzer() default "";
96+
97+
/**
98+
* The analyzer used to analyze the field when part of a query string. Can be updated on an existing field.
99+
*
100+
* @return
101+
*/
102+
String searchAnalyzer() default "";
103+
104+
/**
105+
* Should the field be included in the _all field (if enabled). Defaults to true or to the parent object type
106+
* setting.
107+
*
108+
* @return True if the field should be included in the global _all field (if enabled), false if not.
109+
*/
110+
boolean includeInAll() default true;
111+
112+
/**
113+
* Set to a size where above the mentioned size the string will be ignored. Handly for generic not_analyzed fields
114+
* that should ignore long text.
115+
*
116+
* @return The size above which to ignore the field.
117+
*/
118+
int ignoreAbove() default -1;
119+
120+
/**
121+
* Position increment gap between field instances with the same field name. Defaults to 0.
122+
*
123+
* @return
124+
*/
125+
int positionOffsetGap() default 0;
126+
}

elasticsearch-mapping/src/main/java/org/elasticsearch/mapping/FieldsMappingBuilder.java

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import org.elasticsearch.annotation.*;
1212
import org.elasticsearch.annotation.query.*;
13+
import org.elasticsearch.common.collect.Maps;
1314
import org.elasticsearch.common.logging.ESLogger;
1415
import org.elasticsearch.common.logging.Loggers;
1516
import org.elasticsearch.mapping.parser.*;
@@ -81,26 +82,41 @@ private void parseFieldMappings(Class<?> clazz, Map<String, Object> classDefinit
8182
if (annotation != null) {
8283
processStringOrPrimitive(clazz, propertiesDefinitionMap, pathPrefix, indexable);
8384
}
85+
} else if (Map.class.isAssignableFrom(indexable.getType())) {
86+
MapKeyValue annotation = indexable.getAnnotation(MapKeyValue.class);
87+
if (annotation != null) {
88+
// Create an object mapping with key and value
89+
processFieldAnnotation(MapKeyValue.class, new MapKeyValueAnnotationParser(this, filteredFields, facetFields), propertiesDefinitionMap,
90+
pathPrefix, indexable);
91+
} else {
92+
processComplexOrArray(clazz, facetFields, filteredFields, pathPrefix, propertiesDefinitionMap, indexable);
93+
}
8494
} else {
85-
Class<?> arrayType = indexable.getComponentType();
86-
// mapping of a complex field
87-
if (arrayType != null) {
88-
// process the array type.
89-
if (ClassUtils.isPrimitiveOrWrapper(arrayType) || arrayType == String.class || indexable.getType() == Date.class) {
95+
processComplexOrArray(clazz, facetFields, filteredFields, pathPrefix, propertiesDefinitionMap, indexable);
96+
}
97+
}
98+
99+
private void processComplexOrArray(Class<?> clazz, List<IFacetBuilderHelper> facetFields, List<IFilterBuilderHelper> filteredFields, String pathPrefix,
100+
Map<String, Object> propertiesDefinitionMap, Indexable indexable) {
101+
Class<?> arrayType = indexable.getComponentType();
102+
Class<?> mapValueType = indexable.getComponentType(1);
103+
// mapping of a complex field
104+
if (arrayType != null && mapValueType == null) {
105+
// process the array type.
106+
if (ClassUtils.isPrimitiveOrWrapper(arrayType) || arrayType == String.class || indexable.getType() == Date.class) {
107+
processStringOrPrimitive(clazz, propertiesDefinitionMap, pathPrefix, indexable);
108+
} else if (arrayType.isEnum()) {
109+
// if this is an enum and there is a String
110+
StringField annotation = indexable.getAnnotation(StringField.class);
111+
if (annotation != null) {
90112
processStringOrPrimitive(clazz, propertiesDefinitionMap, pathPrefix, indexable);
91-
} else if (arrayType.isEnum()) {
92-
// if this is an enum and there is a String
93-
StringField annotation = indexable.getAnnotation(StringField.class);
94-
if (annotation != null) {
95-
processStringOrPrimitive(clazz, propertiesDefinitionMap, pathPrefix, indexable);
96-
}
97-
} else {
98-
processComplexType(clazz, propertiesDefinitionMap, pathPrefix, indexable, filteredFields, facetFields);
99113
}
100114
} else {
101-
// process the type
102115
processComplexType(clazz, propertiesDefinitionMap, pathPrefix, indexable, filteredFields, facetFields);
103116
}
117+
} else {
118+
// process the type
119+
processComplexType(clazz, propertiesDefinitionMap, pathPrefix, indexable, filteredFields, facetFields);
104120
}
105121
}
106122

elasticsearch-mapping/src/main/java/org/elasticsearch/mapping/Indexable.java

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.lang.reflect.ParameterizedType;
77
import java.lang.reflect.Type;
88
import java.util.Collection;
9+
import java.util.Map;
910

1011
public class Indexable {
1112

@@ -54,10 +55,16 @@ public Class<?> getType() {
5455
}
5556

5657
public Class<?> getComponentType() {
58+
return getComponentType(0);
59+
}
60+
61+
public Class<?> getComponentType(int index) {
5762
Class<?> type = getType();
5863
if (type.isArray()) {
59-
return type.getComponentType();
60-
} else if (Collection.class.isAssignableFrom(type)) {
64+
if(index == 0) {
65+
return type.getComponentType();
66+
}
67+
} else if (Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type)) {
6168
ParameterizedType pt = null;
6269
if (field != null && field.getGenericType() instanceof ParameterizedType) {
6370
pt = (ParameterizedType) field.getGenericType();
@@ -67,15 +74,14 @@ public Class<?> getComponentType() {
6774
return null;
6875
}
6976
Type[] types = pt.getActualTypeArguments();
70-
if (types.length > 0) {
71-
Class<?> valueClass = (Class<?>) types[0];
72-
return valueClass;
73-
} else {
74-
return null;
77+
if (types.length > index) {
78+
if (types[index] instanceof Class) {
79+
Class<?> valueClass = (Class<?>) types[index];
80+
return valueClass;
81+
}
7582
}
76-
} else {
77-
return null;
7883
}
84+
return null;
7985
}
8086

8187
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package org.elasticsearch.mapping.parser;
2+
3+
import java.beans.IntrospectionException;
4+
import java.util.HashMap;
5+
import java.util.List;
6+
import java.util.Map;
7+
8+
import org.elasticsearch.annotation.MapKeyValue;
9+
import org.elasticsearch.annotation.StringField;
10+
import org.elasticsearch.common.collect.Maps;
11+
import org.elasticsearch.common.logging.ESLogger;
12+
import org.elasticsearch.common.logging.Loggers;
13+
import org.elasticsearch.mapping.*;
14+
15+
/**
16+
* Created by lucboutier on 15/06/2016.
17+
*/
18+
public class MapKeyValueAnnotationParser implements IPropertyAnnotationParser<MapKeyValue> {
19+
private static final ESLogger LOGGER = Loggers.getLogger(MappingBuilder.class);
20+
21+
private final FieldsMappingBuilder fieldsMappingBuilder;
22+
private final List<IFilterBuilderHelper> filters;
23+
private final List<IFacetBuilderHelper> facets;
24+
25+
public MapKeyValueAnnotationParser(FieldsMappingBuilder fieldsMappingBuilder, List<IFilterBuilderHelper> filters, List<IFacetBuilderHelper> facets) {
26+
this.fieldsMappingBuilder = fieldsMappingBuilder;
27+
this.filters = filters;
28+
this.facets = facets;
29+
}
30+
31+
@Override
32+
public void parseAnnotation(MapKeyValue annotation, Map<String, Object> fieldDefinition, String pathPrefix, Indexable indexable) {
33+
if (fieldDefinition.get("type") != null) {
34+
LOGGER.info("Overriding mapping for field {} for class {} was defined as type {}", indexable.getName(), indexable.getDeclaringClassName(),
35+
fieldDefinition.get("type"));
36+
fieldDefinition.clear();
37+
}
38+
// the key of the map is a spring mapping.
39+
Map<String, Object> properties = Maps.newHashMap();
40+
fieldDefinition.put("type", "object");
41+
fieldDefinition.put("enabled", "true");
42+
fieldDefinition.put("properties", properties);
43+
44+
Map<String, Object> keyFieldDefinition = Maps.newHashMap();
45+
properties.put("key", keyFieldDefinition);
46+
47+
keyFieldDefinition.put("type", "string");
48+
keyFieldDefinition.put("store", annotation.store());
49+
keyFieldDefinition.put("index", annotation.indexType());
50+
// TODO doc_values
51+
keyFieldDefinition.put("term_vector", annotation.termVector());
52+
keyFieldDefinition.put("boost", annotation.boost());
53+
if (!StringField.NULL_VALUE_NOT_SPECIFIED.equals(annotation.nullValue())) {
54+
keyFieldDefinition.put("null_value", annotation.nullValue());
55+
}
56+
if (!NormEnabled.DEFAULT.equals(annotation.normsEnabled())) {
57+
Map<String, Object> norms = new HashMap<String, Object>();
58+
norms.put("enabled", annotation.normsEnabled().name().toLowerCase());
59+
if (!NormLoading.DEFAULT.equals(annotation.normsLoading())) {
60+
norms.put("loading", annotation.normsLoading());
61+
}
62+
keyFieldDefinition.put("norms", norms);
63+
}
64+
if (!IndexOptions.DEFAULT.equals(annotation.indexOptions())) {
65+
keyFieldDefinition.put("index_options", annotation.indexOptions());
66+
}
67+
if (!annotation.analyser().isEmpty()) {
68+
keyFieldDefinition.put("analyzer", annotation.analyser());
69+
}
70+
if (!annotation.indexAnalyzer().isEmpty()) {
71+
keyFieldDefinition.put("index_analyzer", annotation.indexAnalyzer());
72+
}
73+
if (!annotation.searchAnalyzer().isEmpty()) {
74+
keyFieldDefinition.put("search_analyzer", annotation.searchAnalyzer());
75+
}
76+
77+
keyFieldDefinition.put("include_in_all", annotation.includeInAll());
78+
if (annotation.ignoreAbove() > 0) {
79+
keyFieldDefinition.put("ignore_above", annotation.ignoreAbove());
80+
}
81+
82+
Map<String, Object> valueFieldDefinition = Maps.newHashMap();
83+
properties.put("value", valueFieldDefinition);
84+
85+
// we need to process the map value type recursively
86+
Class<?> mapValueType = indexable.getComponentType(1);
87+
if (mapValueType != null) {
88+
Map<String, SourceFetchContext> fetchContext = Maps.newHashMap();
89+
try {
90+
this.fieldsMappingBuilder.parseFieldMappings(mapValueType, valueFieldDefinition, facets, filters, fetchContext,
91+
indexable.getName() + ".value.");
92+
} catch (IntrospectionException e) {
93+
LOGGER.error("Fail to parse object class <" + mapValueType.getName() + ">", e);
94+
}
95+
} else {
96+
LOGGER.warn("Cannot find value class for map with annotation MapKeyValue");
97+
}
98+
}
99+
}

elasticsearch-mapping/src/main/java/org/elasticsearch/mapping/parser/ObjectFieldAnnotationParser.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,6 @@ public void parseAnnotation(ObjectField annotation, Map<String, Object> fieldDef
4444
// nested types can provide replacement class to be managed. This can be usefull to override map default type for example.
4545
Class<?> replaceClass = objectClass.equals(ObjectField.class) ? indexable.getType() : objectClass;
4646
try {
47-
if(replaceClass.getName().equals("alien4cloud.model.components.PropertyDefinition")) {
48-
LOGGER.info("Process class {}", replaceClass.getName());
49-
}
50-
5147
this.fieldsMappingBuilder.parseFieldMappings(replaceClass, fieldDefinition, facets, filters, fetchContext, indexable.getName() + ".");
5248
} catch (IntrospectionException e) {
5349
LOGGER.error("Fail to parse object class <" + replaceClass.getName() + ">", e);

elasticsearch-mapping/src/test/java/org/elasticsearch/mapping/model/Person.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import org.elasticsearch.annotation.query.TermFilter;
55
import org.elasticsearch.mapping.IndexType;
66

7+
import java.util.Map;
8+
79
/**
810
*
911
* @author luc boutier
@@ -23,6 +25,10 @@ public class Person {
2325

2426
private Address alternateAddress;
2527

28+
// index this by specifying key and value as fields, do not index the key.
29+
@MapKeyValue(indexType = IndexType.no)
30+
private Map<String, Address> addressMap;
31+
2632
@NumberField(index = IndexType.not_analyzed, includeInAll = false)
2733
private long alienScore = 1;
2834

@@ -73,4 +79,12 @@ public Address getAlternateAddress() {
7379
public void setAlternateAddress(Address alternateAddress) {
7480
this.alternateAddress = alternateAddress;
7581
}
82+
83+
public Map<String, Address> getAddressMap() {
84+
return addressMap;
85+
}
86+
87+
public void setAddressMap(Map<String, Address> addressMap) {
88+
this.addressMap = addressMap;
89+
}
7690
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"person":{"_type":{"index":"not_analyzed","store":false},"_source":{"enabled":true},"_id":{"path":"id","index":"no","store":false},"_all":{"analyzer":"simple","store":false,"enabled":true},"properties":{"firstname":{"include_in_all":false,"term_vector":"no","index":"no","boost":1.0,"store":false,"type":"string"},"address":{"type":"nested","properties":{"city":{"include_in_all":false,"term_vector":"no","index":"not_analyzed","boost":1.0,"store":false,"type":"string"}}},"alienScore":{"include_in_all":false,"precision_step":4,"index":"not_analyzed","boost":1.0,"store":false,"ignore_malformed":false,"type":"long"},"alternateAddress":{"type":"object","enabled":true,"properties":{"city":{"include_in_all":false,"term_vector":"no","index":"not_analyzed","boost":1.0,"store":false,"type":"string"}}},"lastname":{"include_in_all":true,"term_vector":"no","index":"analyzed","boost":1.0,"store":false,"type":"string"}}}}
1+
{"person":{"_type":{"index":"not_analyzed","store":false},"_source":{"enabled":true},"_id":{"path":"id","index":"no","store":false},"_all":{"analyzer":"simple","store":false,"enabled":true},"properties":{"firstname":{"include_in_all":false,"term_vector":"no","index":"no","boost":1.0,"store":false,"type":"string"},"address":{"type":"nested","properties":{"city":{"include_in_all":false,"term_vector":"no","index":"not_analyzed","boost":1.0,"store":false,"type":"string"}}},"alienScore":{"include_in_all":false,"precision_step":4,"index":"not_analyzed","boost":1.0,"store":false,"ignore_malformed":false,"type":"long"},"addressMap":{"type":"object","enabled":"true","properties":{"value":{"properties":{"city":{"include_in_all":false,"term_vector":"no","index":"not_analyzed","boost":1.0,"store":false,"type":"string"}}},"key":{"include_in_all":true,"term_vector":"no","index":"no","boost":1.0,"store":false,"type":"string"}}},"alternateAddress":{"type":"object","enabled":true,"properties":{"city":{"include_in_all":false,"term_vector":"no","index":"not_analyzed","boost":1.0,"store":false,"type":"string"}}},"lastname":{"include_in_all":true,"term_vector":"no","index":"analyzed","boost":1.0,"store":false,"type":"string"}}}}

0 commit comments

Comments
 (0)