DATAES-50 - Added scripted field functionality

This commit is contained in:
Ryan Murfitt 2014-02-11 16:13:55 +10:00 committed by Artur Konczak
parent 727cb5eb09
commit d6603f8edf
9 changed files with 285 additions and 2 deletions

View File

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

View File

@ -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<T>(results, pageable, totalHits, facets);
}
private <T> T mapEntity(Collection<SearchHitField> values, Class<T> clazz) {
private <T> 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> T mapEntity(Collection<SearchHitField> values, Class<T> clazz) {
return mapEntity(buildJSONFromFields(values), clazz);
}

View File

@ -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) {

View File

@ -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<SortBuilder> sorts;
private final List<ScriptField> scriptFields = new ArrayList<ScriptField>();
private List<FacetRequest> facets;
private List<AbstractAggregationBuilder> aggregations;
private HighlightBuilder.Field[] highlightFields;
@ -81,6 +86,17 @@ public class NativeSearchQuery extends AbstractQuery implements SearchQuery {
return highlightFields;
}
@Override
public List<ScriptField> getScriptFields() { return scriptFields; }
public void setScriptFields(List<ScriptField> 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<FacetRequest>();

View File

@ -41,6 +41,7 @@ public class NativeSearchQueryBuilder {
private QueryBuilder queryBuilder;
private FilterBuilder filterBuilder;
private List<ScriptField> scriptFields = new ArrayList<ScriptField>();
private List<SortBuilder> sortBuilders = new ArrayList<SortBuilder>();
private List<FacetRequest> facetRequests = new ArrayList<FacetRequest>();
private List<AbstractAggregationBuilder> aggregationBuilders = new ArrayList<AbstractAggregationBuilder>();
@ -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);

View File

@ -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<String, Object> params;
public ScriptField(String fieldName, String script, Map<String, Object> params) {
this.fieldName = fieldName;
this.script = script;
this.params = params;
}
public String fieldName() {
return fieldName;
}
public String script() {
return script;
}
public Map<String, Object> params() {
return params;
}
}

View File

@ -44,4 +44,7 @@ public interface SearchQuery extends Query {
List<AbstractAggregationBuilder> getAggregations();
HighlightBuilder.Field[] getHighlightFields();
List<ScriptField> getScriptFields();
}

View File

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

View File

@ -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<String, Object> params = new HashMap<String, Object>();
params.put("factor", 2);
// when
SearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(matchAllQuery())
.withScriptField(new ScriptField("scriptedRate", "doc['rate'].value * factor", params))
.build();
Page<SampleEntity> 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