diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java index f4cec2b38..c3a54e6f7 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java @@ -53,4 +53,7 @@ String[] ignoreFields() default {}; boolean includeInParent() default false; + + boolean excludeFromSource() default false; + } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/MappingBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/MappingBuilder.java index 91c8dfb49..74ed722ba 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/MappingBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/MappingBuilder.java @@ -47,6 +47,8 @@ class MappingBuilder { + public static final String ARRAY_EXCLUDES = "excludes"; + public static final String FIELD_STORE = "store"; public static final String FIELD_TYPE = "type"; public static final String FIELD_INDEX = "index"; @@ -55,6 +57,7 @@ class MappingBuilder { public static final String FIELD_INDEX_ANALYZER = "index_analyzer"; public static final String FIELD_PROPERTIES = "properties"; public static final String FIELD_PARENT = "_parent"; + public static final String FIELD_SOURCE = "_source"; public static final String COMPLETION_PAYLOADS = "payloads"; public static final String COMPLETION_PRESERVE_SEPARATORS = "preserve_separators"; @@ -76,6 +79,9 @@ static XContentBuilder buildMapping(Class clazz, String indexType, String idFiel mapping.startObject(FIELD_PARENT).field(FIELD_TYPE, parentType).endObject(); } + // Field level source exclusions + mapFieldExclusions(mapping, clazz); + // Properties XContentBuilder xContentBuilder = mapping.startObject(FIELD_PROPERTIES); @@ -84,6 +90,28 @@ static XContentBuilder buildMapping(Class clazz, String indexType, String idFiel return xContentBuilder.endObject().endObject().endObject(); } + private static void mapFieldExclusions(XContentBuilder mapping, Class clazz) throws IOException { + List exclusions = new ArrayList(); + java.lang.reflect.Field[] fields = retrieveFields(clazz); + for (java.lang.reflect.Field field: fields) { + Field singleField = field.getAnnotation(Field.class); + if (singleField == null) { + continue; + } + if (singleField.excludeFromSource()) { + exclusions.add(field.getName()); + } + } + + if (exclusions.isEmpty()) { + return; + } + + XContentBuilder sourceFieldBuilder = mapping.startObject(FIELD_SOURCE); + sourceFieldBuilder.array(ARRAY_EXCLUDES, exclusions.toArray()); + sourceFieldBuilder.endObject(); + } + private static void mapEntity(XContentBuilder xContentBuilder, Class clazz, boolean isRootObject, String idFieldName, String nestedObjectFieldName, boolean nestedOrObjectField, FieldType fieldType, Field fieldAnnotation) throws IOException { diff --git a/src/test/java/org/springframework/data/elasticsearch/builder/SourceExcludedEntityBuilder.java b/src/test/java/org/springframework/data/elasticsearch/builder/SourceExcludedEntityBuilder.java new file mode 100644 index 000000000..1023af222 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/builder/SourceExcludedEntityBuilder.java @@ -0,0 +1,36 @@ +package org.springframework.data.elasticsearch.builder; + +import org.springframework.data.elasticsearch.core.query.IndexQuery; +import org.springframework.data.elasticsearch.entities.SourceExcludedEntity; + +public class SourceExcludedEntityBuilder { + + private SourceExcludedEntity result; + + public SourceExcludedEntityBuilder(String id) { + this.result = new SourceExcludedEntity(); + this.result.setId(id); + } + + public SourceExcludedEntityBuilder indexOnlyField(String value) { + this.result.setIndexOnlyField(value); + return this; + } + + public SourceExcludedEntityBuilder simpleField(String value) { + this.result.setSimpleField(value); + return this; + } + + public SourceExcludedEntity build() { + return result; + } + + public IndexQuery buildIndex() { + IndexQuery indexQuery = new IndexQuery(); + indexQuery.setId(result.getId()); + indexQuery.setObject(result); + return indexQuery; + } + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/MappingBuilderTests.java b/src/test/java/org/springframework/data/elasticsearch/core/MappingBuilderTests.java index 6dfc845fc..2086f4037 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/MappingBuilderTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/MappingBuilderTests.java @@ -16,6 +16,8 @@ package org.springframework.data.elasticsearch.core; +import static org.elasticsearch.index.query.FilterBuilders.boolFilter; +import static org.elasticsearch.index.query.FilterBuilders.termFilter; import static org.elasticsearch.index.query.QueryBuilders.*; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; @@ -30,6 +32,7 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.elasticsearch.builder.SampleInheritedEntityBuilder; +import org.springframework.data.elasticsearch.builder.SourceExcludedEntityBuilder; import org.springframework.data.elasticsearch.builder.StockPriceBuilder; import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder; import org.springframework.data.elasticsearch.core.query.SearchQuery; @@ -122,6 +125,16 @@ public void shouldBuildMappingWithSuperclass() throws IOException { assertThat(xContentBuilder.string(), is(expected)); } + @Test + public void shouldBuildMappingWithExcludedFields() throws IOException { + final String expected = "{\"mapping\":{\"_source\":{\"excludes\":[\"indexOnlyField\"]}," + + "\"properties\":{\"indexOnlyField\":{\"store\":false,\"type\":\"string\"}," + + "\"simpleField\":{\"store\":false,\"type\":\"string\"}}}}"; + + XContentBuilder xContentBuilder = MappingBuilder.buildMapping(SourceExcludedEntity.class, "mapping", "id", null); + assertThat(xContentBuilder.string(), is(expected)); + } + /* * DATAES-76 */ @@ -147,4 +160,28 @@ public void shouldAddSampleInheritedEntityDocumentToIndex() throws IOException { assertThat(entry.getCreatedDate(), is(createdDate)); assertThat(entry.getMessage(), is(message)); } + + @Test + public void shouldNotStoreInSourceButSuccessfullySearchInExcludedField() throws IOException { + elasticsearchTemplate.deleteIndex(SourceExcludedEntity.class); + elasticsearchTemplate.createIndex(SourceExcludedEntity.class); + elasticsearchTemplate.putMapping(SourceExcludedEntity.class); + + String id = "abc"; + String indexOnlyField = "xyz"; + String simpleField = "someContent"; + + elasticsearchTemplate.index(new SourceExcludedEntityBuilder(id).indexOnlyField(indexOnlyField).simpleField(simpleField).buildIndex()); + elasticsearchTemplate.refresh(SourceExcludedEntity.class, true); + + SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()) + .withFilter(boolFilter().must(termFilter("indexOnlyField", indexOnlyField))) + .build(); + List result = elasticsearchTemplate.queryForList(searchQuery, SourceExcludedEntity.class); + assertThat(result.size(), is(1)); + SourceExcludedEntity entry = result.get(0); + assertThat(entry.getSimpleField(), is(simpleField)); + assertNull(entry.getIndexOnlyField()); + } + } diff --git a/src/test/java/org/springframework/data/elasticsearch/entities/SourceExcludedEntity.java b/src/test/java/org/springframework/data/elasticsearch/entities/SourceExcludedEntity.java new file mode 100644 index 000000000..4d24dafc4 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/entities/SourceExcludedEntity.java @@ -0,0 +1,44 @@ +package org.springframework.data.elasticsearch.entities; + +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; + +@Document(indexName = "test-source-exclusion", type = "mapping", indexStoreType = "memory", shards = 1, replicas = 0, refreshInterval = "-1") +public class SourceExcludedEntity { + + @Id + private String id; + + @Field(type = FieldType.String, excludeFromSource = true) + private String indexOnlyField; + + @Field(type = FieldType.String) + private String simpleField; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getIndexOnlyField() { + return indexOnlyField; + } + + public void setIndexOnlyField(String indexOnlyField) { + this.indexOnlyField = indexOnlyField; + } + + public String getSimpleField() { + return simpleField; + } + + public void setSimpleField(String simpleField) { + this.simpleField = simpleField; + } + +}