added support for dynamic mapping

(https://github.com/BioMedCentralLtd/spring-data-elasticsearch/issues/5)
This commit is contained in:
Rizwan Idrees 2013-03-22 12:34:38 +00:00
parent 33f506fb8c
commit cae34e5098
8 changed files with 280 additions and 3 deletions

View File

@ -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 "";
}

View File

@ -37,12 +37,20 @@ public interface ElasticsearchOperations {
ElasticsearchConverter getElasticsearchConverter(); ElasticsearchConverter getElasticsearchConverter();
/** /**
* Create an index * Create an index for a class
* @param clazz * @param clazz
* @param <T> * @param <T>
*/ */
<T> boolean createIndex(Class<T> clazz); <T> boolean createIndex(Class<T> clazz);
/**
* Create mapping for a class
* @param clazz
* @param <T>
*/
<T> boolean putMapping(Class<T> clazz);
/** /**
* Execute the query against elasticsearch and return the first returned object * Execute the query against elasticsearch and return the first returned object
* *

View File

@ -18,6 +18,7 @@ package org.springframework.data.elasticsearch.core;
import org.codehaus.jackson.map.DeserializationConfig; import org.codehaus.jackson.map.DeserializationConfig;
import org.codehaus.jackson.map.ObjectMapper; 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.BulkItemResponse;
import org.elasticsearch.action.bulk.BulkRequestBuilder; import org.elasticsearch.action.bulk.BulkRequestBuilder;
import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.bulk.BulkResponse;
@ -31,6 +32,7 @@ import org.elasticsearch.client.Client;
import org.elasticsearch.client.Requests; import org.elasticsearch.client.Requests;
import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.collect.MapBuilder;
import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.index.query.FilterBuilder; import org.elasticsearch.index.query.FilterBuilder;
import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.search.SearchHit; 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.indicesExistsRequest;
import static org.elasticsearch.client.Requests.refreshRequest; import static org.elasticsearch.client.Requests.refreshRequest;
import static org.elasticsearch.index.VersionType.EXTERNAL; import static org.elasticsearch.index.VersionType.EXTERNAL;
import static org.springframework.data.elasticsearch.core.MappingBuilder.buildMapping;
/** /**
* ElasticsearchTemplate * ElasticsearchTemplate
@ -90,13 +93,26 @@ public class ElasticsearchTemplate implements ElasticsearchOperations {
this.elasticsearchConverter = (elasticsearchConverter == null)? new MappingElasticsearchConverter(new SimpleElasticsearchMappingContext()) : elasticsearchConverter ; this.elasticsearchConverter = (elasticsearchConverter == null)? new MappingElasticsearchConverter(new SimpleElasticsearchMappingContext()) : elasticsearchConverter ;
} }
@Override @Override
public <T> boolean createIndex(Class<T> clazz) { public <T> boolean createIndex(Class<T> clazz) {
ElasticsearchPersistentEntity<T> persistentEntity = getPersistentEntityFor(clazz); ElasticsearchPersistentEntity<T> persistentEntity = getPersistentEntityFor(clazz);
return createIndexIfNotCreated(persistentEntity.getIndexName()); return createIndexIfNotCreated(persistentEntity.getIndexName());
} }
@Override
public <T> boolean putMapping(Class<T> clazz) {
ElasticsearchPersistentEntity<T> 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 @Override
public ElasticsearchConverter getElasticsearchConverter() { public ElasticsearchConverter getElasticsearchConverter() {
return elasticsearchConverter; return elasticsearchConverter;

View File

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

View File

@ -63,12 +63,17 @@ public class SimpleElasticsearchRepository<T> implements ElasticsearchRepository
this.entityInformation = metadata; this.entityInformation = metadata;
setEntityClass(this.entityInformation.getJavaType()); setEntityClass(this.entityInformation.getJavaType());
createIndex(); createIndex();
putMapping();
} }
private void createIndex(){ private void createIndex(){
elasticsearchOperations.createIndex(getEntityClass()); elasticsearchOperations.createIndex(getEntityClass());
} }
private void putMapping(){
elasticsearchOperations.putMapping(getEntityClass());
}
@Override @Override
public T findOne(String id) { public T findOne(String id) {
GetQuery query = new GetQuery(); GetQuery query = new GetQuery();

View File

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

View File

@ -31,6 +31,7 @@ import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.SampleEntity; import org.springframework.data.elasticsearch.SampleEntity;
import org.springframework.data.elasticsearch.SampleMappingEntity;
import org.springframework.data.elasticsearch.core.query.*; import org.springframework.data.elasticsearch.core.query.*;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@ -612,4 +613,14 @@ public class ElasticsearchTemplateTest {
} }
assertThat(sampleEntities.size(), is(equalTo(2))); 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)) ;
}
} }

View File

@ -417,5 +417,4 @@ public class RepositoryTest {
} }
return sampleEntities; return sampleEntities;
} }
} }