diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/ScriptedField.java b/src/main/java/org/springframework/data/elasticsearch/annotations/ScriptedField.java new file mode 100644 index 000000000..f1eb3e06a --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/ScriptedField.java @@ -0,0 +1,19 @@ +package org.springframework.data.elasticsearch.annotations; + +import java.lang.annotation.*; + +/** + * @author Ryan Murfitt + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Documented +public @interface ScriptedField { + + /** + * (Optional) The name of the scripted field. Defaults to + * the field name. + */ + String name() default ""; + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/DefaultResultMapper.java b/src/main/java/org/springframework/data/elasticsearch/core/DefaultResultMapper.java index 7f66140a7..127e8531e 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/DefaultResultMapper.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/DefaultResultMapper.java @@ -37,7 +37,9 @@ import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHitField; import org.elasticsearch.search.facet.Facet; import org.springframework.data.domain.Pageable; +import org.springframework.data.elasticsearch.ElasticsearchException; import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.ScriptedField; import org.springframework.data.elasticsearch.core.facet.DefaultFacetMapper; import org.springframework.data.elasticsearch.core.facet.FacetResult; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; @@ -85,6 +87,7 @@ public class DefaultResultMapper extends AbstractResultMapper { result = mapEntity(hit.getFields().values(), clazz); } setPersistentEntityId(result, hit.getId(), clazz); + populateScriptFields(result, hit); results.add(result); } } @@ -101,7 +104,31 @@ public class DefaultResultMapper extends AbstractResultMapper { return new FacetedPageImpl(results, pageable, totalHits, facets); } - private T mapEntity(Collection values, Class clazz) { + private void populateScriptFields(T result, SearchHit hit) { + if (hit.getFields() != null && !hit.getFields().isEmpty() && result != null) { + for (java.lang.reflect.Field field : result.getClass().getDeclaredFields()) { + ScriptedField scriptedField = field.getAnnotation(ScriptedField.class); + if (scriptedField != null) { + String name = scriptedField.name().isEmpty() ? field.getName() : scriptedField.name(); + SearchHitField searchHitField = hit.getFields().get(name); + if (searchHitField != null) { + field.setAccessible(true); + try { + field.set(result, searchHitField.getValue()); + } catch (IllegalArgumentException e) { + throw new ElasticsearchException("failed to set scripted field: " + name + " with value: " + + searchHitField.getValue(), e); + } catch (IllegalAccessException e) { + throw new ElasticsearchException("failed to access scripted field: " + name, e); + } + } + } + } + } + } + + + private T mapEntity(Collection values, Class clazz) { return mapEntity(buildJSONFromFields(values), clazz); } 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 20924956c..7d65305c8 100755 --- a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java @@ -87,6 +87,7 @@ import org.springframework.data.elasticsearch.ElasticsearchException; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Mapping; import org.springframework.data.elasticsearch.annotations.Setting; +import org.springframework.data.elasticsearch.annotations.ScriptedField; import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter; import org.springframework.data.elasticsearch.core.facet.FacetRequest; @@ -841,7 +842,15 @@ public class ElasticsearchTemplate implements ElasticsearchOperations, Applicati } } - if (CollectionUtils.isNotEmpty(searchQuery.getFacets())) { + if (!searchQuery.getScriptFields().isEmpty()) { + searchRequest.addField("_source"); + } + + for (ScriptField scriptedField : searchQuery.getScriptFields()) { + searchRequest.addScriptField(scriptedField.fieldName(), scriptedField.script(), scriptedField.params()); + } + + if (CollectionUtils.isNotEmpty(searchQuery.getFacets())) { for (FacetRequest facetRequest : searchQuery.getFacets()) { FacetBuilder facet = facetRequest.getFacet(); if (facetRequest.applyQueryFilter() && searchQuery.getFilter() != null) { diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQuery.java index 0477f9494..ed16f734c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQuery.java @@ -15,6 +15,9 @@ */ package org.springframework.data.elasticsearch.core.query; +import java.util.ArrayList; +import java.util.List; + import org.elasticsearch.index.query.FilterBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.search.aggregations.AbstractAggregationBuilder; @@ -23,6 +26,7 @@ import org.elasticsearch.search.sort.SortBuilder; import org.springframework.data.elasticsearch.core.facet.FacetRequest; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** @@ -37,6 +41,7 @@ public class NativeSearchQuery extends AbstractQuery implements SearchQuery { private QueryBuilder query; private FilterBuilder filter; private List sorts; + private final List scriptFields = new ArrayList(); private List facets; private List aggregations; private HighlightBuilder.Field[] highlightFields; @@ -81,6 +86,17 @@ public class NativeSearchQuery extends AbstractQuery implements SearchQuery { return highlightFields; } + @Override + public List getScriptFields() { return scriptFields; } + + public void setScriptFields(List scriptFields) { + this.scriptFields.addAll(scriptFields); + } + + public void addScriptField(ScriptField... scriptField) { + scriptFields.addAll(Arrays.asList(scriptField)); + } + public void addFacet(FacetRequest facetRequest) { if (facets == null) { facets = new ArrayList(); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQueryBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQueryBuilder.java index 6a632c9ef..2b4884c8f 100755 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQueryBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQueryBuilder.java @@ -41,6 +41,7 @@ public class NativeSearchQueryBuilder { private QueryBuilder queryBuilder; private FilterBuilder filterBuilder; + private List scriptFields = new ArrayList(); private List sortBuilders = new ArrayList(); private List facetRequests = new ArrayList(); private List aggregationBuilders = new ArrayList(); @@ -69,6 +70,11 @@ public class NativeSearchQueryBuilder { return this; } + public NativeSearchQueryBuilder withScriptField(ScriptField scriptField) { + this.scriptFields.add(scriptField); + return this; + } + public NativeSearchQueryBuilder addAggregation(AbstractAggregationBuilder aggregationBuilder) { this.aggregationBuilders.add(aggregationBuilder); return this; @@ -141,6 +147,9 @@ public class NativeSearchQueryBuilder { if (fields != null) { nativeSearchQuery.addFields(fields); } + if (CollectionUtils.isNotEmpty(scriptFields)) { + nativeSearchQuery.setScriptFields(scriptFields); + } if (CollectionUtils.isNotEmpty(facetRequests)) { nativeSearchQuery.setFacets(facetRequests); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptField.java b/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptField.java new file mode 100644 index 000000000..a9c56b5d3 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/ScriptField.java @@ -0,0 +1,31 @@ +package org.springframework.data.elasticsearch.core.query; + +import java.util.Map; + +/** + * @author Ryan Murfitt + */ +public class ScriptField { + private final String fieldName; + private final String script; + private final Map params; + + public ScriptField(String fieldName, String script, Map params) { + this.fieldName = fieldName; + this.script = script; + this.params = params; + } + + public String fieldName() { + return fieldName; + } + + public String script() { + return script; + } + + public Map params() { + return params; + } +} + diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/SearchQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/SearchQuery.java index 9c21761c9..283d02897 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/SearchQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/SearchQuery.java @@ -44,4 +44,7 @@ public interface SearchQuery extends Query { List getAggregations(); HighlightBuilder.Field[] getHighlightFields(); + + List getScriptFields(); + } diff --git a/src/test/java/org/springframework/data/elasticsearch/SampleEntity.java b/src/test/java/org/springframework/data/elasticsearch/SampleEntity.java new file mode 100644 index 000000000..9057c8b49 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/SampleEntity.java @@ -0,0 +1,139 @@ +/* + * 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.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.ScriptedField; + +/** + * @author Rizwan Idrees + * @author Mohsin Husen + */ +@Document(indexName = "test-index", type = "test-type", indexStoreType = "memory", shards = 1, replicas = 0, refreshInterval = "-1") +public class SampleEntity { + + @Id + private String id; + private String type; + private String message; + private int rate; + @ScriptedField + private Long scriptedRate; + private boolean available; + private String highlightedMessage; + @Version + private Long version; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public int getRate() { + return rate; + } + + public void setRate(int rate) { + this.rate = rate; + } + + public Long getScriptedRate() { + return scriptedRate; + } + + public void setScriptedRate(Long scriptedRate) { + this.scriptedRate = scriptedRate; + } + + public boolean isAvailable() { + return available; + } + + public void setAvailable(boolean available) { + this.available = available; + } + + public String getHighlightedMessage() { + return highlightedMessage; + } + + public void setHighlightedMessage(String highlightedMessage) { + this.highlightedMessage = highlightedMessage; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SampleEntity)) { + return false; + } + if (this == obj) { + return true; + } + SampleEntity rhs = (SampleEntity) obj; + return new EqualsBuilder().append(this.id, rhs.id).append(this.type, rhs.type).append(this.message, rhs.message) + .append(this.rate, rhs.rate).append(this.available, rhs.available).append(this.version, rhs.version).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder().append(id).append(type).append(message).append(rate).append(available).append(version) + .toHashCode(); + } + + @Override + public String toString() { + return "SampleEntity{" + + "id='" + id + '\'' + + ", type='" + type + '\'' + + ", message='" + message + '\'' + + ", rate=" + rate + + ", available=" + available + + ", highlightedMessage='" + highlightedMessage + '\'' + + ", version=" + version + + '}'; + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java index 492518fe5..41928f75f 100755 --- a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java @@ -466,6 +466,36 @@ public class ElasticsearchTemplateTests { assertThat(sampleEntities.getTotalElements(), equalTo(1L)); } + @Test + public void shouldUseScriptedFields() { + // given + String documentId = randomNumeric(5); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setId(documentId); + sampleEntity.setRate(2); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + + IndexQuery indexQuery = new IndexQuery(); + indexQuery.setId(documentId); + indexQuery.setObject(sampleEntity); + + elasticsearchTemplate.index(indexQuery); + elasticsearchTemplate.refresh(SampleEntity.class, true); + + Map params = new HashMap(); + params.put("factor", 2); + // when + SearchQuery searchQuery = new NativeSearchQueryBuilder() + .withQuery(matchAllQuery()) + .withScriptField(new ScriptField("scriptedRate", "doc['rate'].value * factor", params)) + .build(); + Page sampleEntities = elasticsearchTemplate.queryForPage(searchQuery, SampleEntity.class); + // then + assertThat(sampleEntities.getTotalElements(), equalTo(1L)); + assertThat(sampleEntities.getContent().get(0).getScriptedRate(), equalTo(4L)); + } + @Test public void shouldReturnPageableResultsGivenStringQuery() { // given