From cae34e50986ba3d1c58f7634da1e88202de409eb Mon Sep 17 00:00:00 2001 From: Rizwan Idrees Date: Fri, 22 Mar 2013 12:34:38 +0000 Subject: [PATCH] added support for dynamic mapping (https://github.com/BioMedCentralLtd/spring-data-elasticsearch/issues/5) --- .../data/elasticsearch/annotations/Field.java | 37 +++++ .../core/ElasticsearchOperations.java | 10 +- .../core/ElasticsearchTemplate.java | 18 ++- .../elasticsearch/core/MappingBuilder.java | 130 ++++++++++++++++++ .../SimpleElasticsearchRepository.java | 5 + .../elasticsearch/SampleMappingEntity.java | 71 ++++++++++ .../core/ElasticsearchTemplateTest.java | 11 ++ .../repositories/RepositoryTest.java | 1 - 8 files changed, 280 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/springframework/data/elasticsearch/annotations/Field.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/core/MappingBuilder.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/SampleMappingEntity.java diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java new file mode 100644 index 000000000..848946436 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java @@ -0,0 +1,37 @@ +/* + * Copyright 2013 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 + * + * http://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.springframework.data.elasticsearch.annotations; + +import java.lang.annotation.*; + + +/** + * @author Rizwan Idrees + * @author Mohsin Husen + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Documented +public @interface Field { + + String type() default ""; + String index() default ""; + boolean store() default true; + String searchAnalyzer() default ""; + String indexAnalyzer() default ""; + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java index dd0896238..2664e4a5e 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java @@ -37,12 +37,20 @@ public interface ElasticsearchOperations { ElasticsearchConverter getElasticsearchConverter(); /** - * Create an index + * Create an index for a class * @param clazz * @param */ boolean createIndex(Class clazz); + /** + * Create mapping for a class + * @param clazz + * @param + */ + boolean putMapping(Class clazz); + + /** * Execute the query against elasticsearch and return the first returned object * diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java index e6945928b..37d3cadfc 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java @@ -18,6 +18,7 @@ package org.springframework.data.elasticsearch.core; import org.codehaus.jackson.map.DeserializationConfig; import org.codehaus.jackson.map.ObjectMapper; +import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequestBuilder; import org.elasticsearch.action.bulk.BulkItemResponse; import org.elasticsearch.action.bulk.BulkRequestBuilder; import org.elasticsearch.action.bulk.BulkResponse; @@ -31,6 +32,7 @@ import org.elasticsearch.client.Client; import org.elasticsearch.client.Requests; import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.query.FilterBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.search.SearchHit; @@ -63,6 +65,7 @@ import static org.elasticsearch.action.search.SearchType.SCAN; import static org.elasticsearch.client.Requests.indicesExistsRequest; import static org.elasticsearch.client.Requests.refreshRequest; import static org.elasticsearch.index.VersionType.EXTERNAL; +import static org.springframework.data.elasticsearch.core.MappingBuilder.buildMapping; /** * ElasticsearchTemplate @@ -90,13 +93,26 @@ public class ElasticsearchTemplate implements ElasticsearchOperations { this.elasticsearchConverter = (elasticsearchConverter == null)? new MappingElasticsearchConverter(new SimpleElasticsearchMappingContext()) : elasticsearchConverter ; } - @Override public boolean createIndex(Class clazz) { ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); return createIndexIfNotCreated(persistentEntity.getIndexName()); } + @Override + public boolean putMapping(Class clazz) { + ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); + PutMappingRequestBuilder requestBuilder = client.admin().indices().preparePutMapping(persistentEntity.getIndexName()) + .setType(persistentEntity.getIndexType()); + + try { + XContentBuilder xContentBuilder = buildMapping(clazz, persistentEntity.getIndexType(), persistentEntity.getIdProperty().getFieldName()); + return requestBuilder.setSource(xContentBuilder).execute().actionGet().acknowledged(); + } catch (Exception e) { + throw new ElasticsearchException("Failed to build mapping for " + clazz.getSimpleName() , e); + } + } + @Override public ElasticsearchConverter getElasticsearchConverter() { return elasticsearchConverter; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/MappingBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/MappingBuilder.java new file mode 100644 index 000000000..8abfe9efc --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/MappingBuilder.java @@ -0,0 +1,130 @@ +/* + * Copyright 2013 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 + * + * http://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.springframework.data.elasticsearch.core; + + +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; + +import java.io.IOException; +import java.util.Map; + +import static org.apache.commons.lang.StringUtils.EMPTY; +import static org.apache.commons.lang.StringUtils.isNotBlank; +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; + +/** + * @author Rizwan Idrees + * @author Mohsin Husen + */ + +class MappingBuilder { + + private static SimpleTypeHolder SIMPLE_TYPE_HOLDER = new SimpleTypeHolder(); + + static XContentBuilder buildMapping(Class clazz, String indexType, String idFieldName) throws IOException { + XContentBuilder xContentBuilder = jsonBuilder() + .startObject().startObject(indexType).startObject("properties"); + + mapEntity(xContentBuilder, clazz, true, idFieldName, EMPTY); + + return xContentBuilder.endObject().endObject().endObject(); + } + + private static void mapEntity(XContentBuilder xContentBuilder, + Class clazz, + boolean isRootObject, + String idFieldName, + String nestedObjectFieldName) throws IOException{ + + java.lang.reflect.Field[] fields = clazz.getDeclaredFields(); + + if(!isRootObject && isAnyPropertyAnnotatedAsField(fields)){ + xContentBuilder.startObject(nestedObjectFieldName) + .field("type", "object") + .startObject("properties"); + } + + for(java.lang.reflect.Field field : fields){ + if(isEntity(field)){ + mapEntity(xContentBuilder, field.getType(), false, EMPTY, field.getName()); + } + Field fieldAnnotation = field.getAnnotation(Field.class); + if(isRootObject && fieldAnnotation != null && isIdField(field, idFieldName)){ + applyDefaultIdFieldMapping(xContentBuilder, field); + }else if(fieldAnnotation != null){ + applyFieldAnnotationMapping(xContentBuilder, field, fieldAnnotation); + } + } + + if(!isRootObject && isAnyPropertyAnnotatedAsField(fields)){ + xContentBuilder.endObject().endObject(); + } + + } + + private static void applyDefaultIdFieldMapping(XContentBuilder xContentBuilder, java.lang.reflect.Field field) throws IOException { + xContentBuilder.startObject(field.getName()) + .field("type", "string") + .field("index", "not_analyzed") + .endObject(); + } + + private static void applyFieldAnnotationMapping(XContentBuilder xContentBuilder, + java.lang.reflect.Field field, + Field fieldAnnotation) throws IOException { + xContentBuilder.startObject(field.getName()); + xContentBuilder.field("store", fieldAnnotation.store()); + if(isNotBlank(fieldAnnotation.type())){ + xContentBuilder.field("type", fieldAnnotation.type()); + } + if(isNotBlank(fieldAnnotation.index())){ + xContentBuilder.field("index", fieldAnnotation.index()); + } + if(isNotBlank(fieldAnnotation.searchAnalyzer())){ + xContentBuilder.field("search_analyzer", fieldAnnotation.searchAnalyzer()); + } + if(isNotBlank(fieldAnnotation.indexAnalyzer())){ + xContentBuilder.field("index_analyzer", fieldAnnotation.indexAnalyzer()); + } + xContentBuilder.endObject(); + } + + private static boolean isEntity(java.lang.reflect.Field field) { + TypeInformation typeInformation = ClassTypeInformation.from(field.getType()); + TypeInformation actualType = typeInformation.getActualType(); + boolean isComplexType = actualType == null ? false : !SIMPLE_TYPE_HOLDER.isSimpleType(actualType.getType()); + return isComplexType && !actualType.isCollectionLike() && !Map.class.isAssignableFrom(typeInformation.getType()); + } + + private static boolean isAnyPropertyAnnotatedAsField(java.lang.reflect.Field[] fields){ + if(fields != null){ + for(java.lang.reflect.Field field : fields){ + if (field.isAnnotationPresent(Field.class)){ + return true; + } + } + } + return false; + } + + private static boolean isIdField(java.lang.reflect.Field field, String idFieldName){ + return idFieldName.equals(field.getName()); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/SimpleElasticsearchRepository.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/SimpleElasticsearchRepository.java index 81091376e..f97cec75a 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/SimpleElasticsearchRepository.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/SimpleElasticsearchRepository.java @@ -63,12 +63,17 @@ public class SimpleElasticsearchRepository implements ElasticsearchRepository this.entityInformation = metadata; setEntityClass(this.entityInformation.getJavaType()); createIndex(); + putMapping(); } private void createIndex(){ elasticsearchOperations.createIndex(getEntityClass()); } + private void putMapping(){ + elasticsearchOperations.putMapping(getEntityClass()); + } + @Override public T findOne(String id) { GetQuery query = new GetQuery(); diff --git a/src/test/java/org/springframework/data/elasticsearch/SampleMappingEntity.java b/src/test/java/org/springframework/data/elasticsearch/SampleMappingEntity.java new file mode 100644 index 000000000..514ab7d5a --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/SampleMappingEntity.java @@ -0,0 +1,71 @@ +/* + * Copyright 2013 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 + * + * http://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.springframework.data.elasticsearch; + + +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; + +/** + * @author Rizwan Idrees + * @author Mohsin Husen + */ +@Document(indexName = "test-mapping", type = "mapping") +public class SampleMappingEntity { + + @Id + private String id; + @Field(type = "string",index = "not_analyzed", store = true, searchAnalyzer = "standard", indexAnalyzer = "standard") + private String message; + + private NestedEntity nested; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + + static class NestedEntity{ + @Field(type = "string") + private String someField; + + public String getSomeField() { + return someField; + } + + public void setSomeField(String someField) { + this.someField = someField; + } + } + +} + + + diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTest.java b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTest.java index 34f805d40..788c31763 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTest.java @@ -31,6 +31,7 @@ import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.elasticsearch.SampleEntity; +import org.springframework.data.elasticsearch.SampleMappingEntity; import org.springframework.data.elasticsearch.core.query.*; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -612,4 +613,14 @@ public class ElasticsearchTemplateTest { } assertThat(sampleEntities.size(), is(equalTo(2))); } + + @Test + public void shouldPutMappingForGivenEntity()throws Exception{ + //given + Class entity = SampleMappingEntity.class; + elasticsearchTemplate.createIndex(entity); + //when + assertThat(elasticsearchTemplate.putMapping(entity) , is(true)) ; + } + } diff --git a/src/test/java/org/springframework/data/elasticsearch/repositories/RepositoryTest.java b/src/test/java/org/springframework/data/elasticsearch/repositories/RepositoryTest.java index 793facb47..170fd0619 100644 --- a/src/test/java/org/springframework/data/elasticsearch/repositories/RepositoryTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/repositories/RepositoryTest.java @@ -417,5 +417,4 @@ public class RepositoryTest { } return sampleEntities; } - }