From 95134e28e26e95dcdc97d425c23e60e0aa53152f Mon Sep 17 00:00:00 2001 From: Guillaume Hiron Date: Thu, 29 Jan 2015 10:33:36 +0100 Subject: [PATCH] DATAES-149 : add ES query Explain support --- .../core/DefaultResultMapper.java | 5 + .../core/ElasticsearchTemplate.java | 33 ++-- .../core/query/AbstractQuery.java | 10 ++ .../core/query/NativeSearchQueryBuilder.java | 12 +- .../data/elasticsearch/core/query/Query.java | 3 + .../core/ExplanationResultMapper.java | 20 +++ .../core/query/ExplanationQueryTests.java | 78 +++++++++ .../entities/SampleExplanableEntity.java | 154 ++++++++++++++++++ .../elasticsearch-explanation-test.xml | 15 ++ 9 files changed, 314 insertions(+), 16 deletions(-) create mode 100644 src/test/java/org/springframework/data/elasticsearch/core/ExplanationResultMapper.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/core/query/ExplanationQueryTests.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/entities/SampleExplanableEntity.java create mode 100644 src/test/resources/elasticsearch-explanation-test.xml 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 bf6020006..6266c12d9 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/DefaultResultMapper.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/DefaultResultMapper.java @@ -25,6 +25,7 @@ import java.util.LinkedList; import java.util.List; +import org.apache.commons.collections.CollectionUtils; import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.get.MultiGetItemResponse; import org.elasticsearch.action.get.MultiGetResponse; @@ -85,6 +86,7 @@ public FacetedPage mapResults(SearchResponse response, Class clazz, Pa result = mapEntity(hit.getFields().values(), clazz); } setPersistentEntityId(result, hit.getId(), clazz); + mapExplanation(result, hit); results.add(result); } } @@ -167,4 +169,7 @@ private void setPersistentEntityId(T result, String id, Class clazz) { } } } + + public void mapExplanation(T result, SearchHit hit) { + } } 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 4638edaa5..d63274be2 100755 --- a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java @@ -51,9 +51,9 @@ import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchType; -import org.elasticsearch.action.suggest.SuggestRequest; -import org.elasticsearch.action.suggest.SuggestRequestBuilder; -import org.elasticsearch.action.suggest.SuggestResponse; +import org.elasticsearch.action.suggest.SuggestRequest; +import org.elasticsearch.action.suggest.SuggestRequestBuilder; +import org.elasticsearch.action.suggest.SuggestResponse; import org.elasticsearch.action.update.UpdateRequestBuilder; import org.elasticsearch.action.update.UpdateResponse; import org.elasticsearch.client.Client; @@ -63,7 +63,7 @@ import org.elasticsearch.common.collect.ImmutableOpenMap; import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.query.FilterBuilder; import org.elasticsearch.index.query.QueryBuilder; @@ -74,7 +74,7 @@ import org.elasticsearch.search.highlight.HighlightBuilder; import org.elasticsearch.search.sort.SortBuilder; import org.elasticsearch.search.sort.SortOrder; -import org.elasticsearch.search.suggest.SuggestBuilder; +import org.elasticsearch.search.suggest.SuggestBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; @@ -779,6 +779,9 @@ private SearchRequestBuilder prepareSearch(Query query) { if (query.getMinScore() > 0) { searchRequestBuilder.setMinScore(query.getMinScore()); } + + searchRequestBuilder.setExplain(query.isExplain()); + return searchRequestBuilder; } @@ -981,14 +984,14 @@ public static String readFileFromClasspath(String url) { return stringBuilder.toString(); } - - public SuggestResponse suggest(SuggestBuilder.SuggestionBuilder suggestion, String... indices) { - SuggestRequestBuilder suggestRequestBuilder = client.prepareSuggest(indices); - suggestRequestBuilder.addSuggestion(suggestion); - return suggestRequestBuilder.execute().actionGet(); - } - - public SuggestResponse suggest(SuggestBuilder.SuggestionBuilder suggestion, Class clazz) { - return suggest(suggestion, retrieveIndexNameFromPersistentEntity(clazz)); - } + + public SuggestResponse suggest(SuggestBuilder.SuggestionBuilder suggestion, String... indices) { + SuggestRequestBuilder suggestRequestBuilder = client.prepareSuggest(indices); + suggestRequestBuilder.addSuggestion(suggestion); + return suggestRequestBuilder.execute().actionGet(); + } + + public SuggestResponse suggest(SuggestBuilder.SuggestionBuilder suggestion, Class clazz) { + return suggest(suggestion, retrieveIndexNameFromPersistentEntity(clazz)); + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/AbstractQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/AbstractQuery.java index 16781756b..73f7cf9b1 100755 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/AbstractQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/AbstractQuery.java @@ -40,6 +40,7 @@ abstract class AbstractQuery implements Query { protected List types = new ArrayList(); protected List fields = new ArrayList(); protected float minScore; + protected boolean explain; protected Collection ids; protected String route; protected SearchType searchType = SearchType.DFS_QUERY_THEN_FETCH; @@ -81,6 +82,15 @@ public void addIndices(String... indices) { addAll(this.indices, indices); } + @Override + public boolean isExplain() { + return explain; + } + + public void setExplain(boolean explain) { + this.explain = explain; + } + @Override public void addTypes(String... types) { addAll(this.types, types); 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..682f209d2 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 @@ -54,11 +54,18 @@ public class NativeSearchQueryBuilder { private String route; private SearchType searchType; - public NativeSearchQueryBuilder withQuery(QueryBuilder queryBuilder) { + private boolean withExplanation; + + public NativeSearchQueryBuilder withQuery(QueryBuilder queryBuilder) { this.queryBuilder = queryBuilder; return this; } + public NativeSearchQueryBuilder withExplanation(boolean withExplanation) { + this.withExplanation = withExplanation; + return this; + } + public NativeSearchQueryBuilder withFilter(FilterBuilder filterBuilder) { this.filterBuilder = filterBuilder; return this; @@ -166,6 +173,9 @@ public NativeSearchQuery build() { nativeSearchQuery.setSearchType(searchType); } + if (withExplanation) + nativeSearchQuery.setExplain(withExplanation); + return nativeSearchQuery; } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java b/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java index 8ee137bee..09559eb80 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/Query.java @@ -139,4 +139,7 @@ public interface Query { * @return */ SearchType getSearchType(); + + + boolean isExplain(); } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ExplanationResultMapper.java b/src/test/java/org/springframework/data/elasticsearch/core/ExplanationResultMapper.java new file mode 100644 index 000000000..1134b0248 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/ExplanationResultMapper.java @@ -0,0 +1,20 @@ +package org.springframework.data.elasticsearch.core; + +import org.elasticsearch.search.SearchHit; +import org.springframework.data.elasticsearch.entities.SampleExplanableEntity; + +/** + * Created with IntelliJ IDEA. + * User: ghiron + * Date: 1/28/15 + * Time: 4:35 PM + * To change this template use File | Settings | File Templates. + */ +public class ExplanationResultMapper extends DefaultResultMapper { + @Override + public void mapExplanation(T result, SearchHit hit) { + if (result instanceof SampleExplanableEntity){ + ((SampleExplanableEntity)result).setExplanation(hit.getExplanation().getDescription()); + } + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/query/ExplanationQueryTests.java b/src/test/java/org/springframework/data/elasticsearch/core/query/ExplanationQueryTests.java new file mode 100644 index 000000000..6707cb7fb --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/query/ExplanationQueryTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2013-2014 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.query; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.elasticsearch.core.ElasticsearchTemplate; +import org.springframework.data.elasticsearch.entities.SampleExplanableEntity; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.apache.commons.lang.RandomStringUtils.randomNumeric; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; + +/** + * @author Rizwan Idrees + * @author Mohsin Husen + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:elasticsearch-explanation-test.xml") +public class ExplanationQueryTests { + + + @Autowired + private ElasticsearchTemplate elasticsearchTemplate ; + + @Before + public void before() { + elasticsearchTemplate.deleteIndex(SampleExplanableEntity.class); + elasticsearchTemplate.createIndex(SampleExplanableEntity.class); + elasticsearchTemplate.refresh(SampleExplanableEntity.class, true); + } + + @Test + public void shouldPerformAndOperation() { + + // given + String documentId = randomNumeric(5); + SampleExplanableEntity sampleEntity = new SampleExplanableEntity(); + sampleEntity.setId(documentId); + sampleEntity.setMessage("some test message"); + sampleEntity.setVersion(System.currentTimeMillis()); + + IndexQuery indexQuery = new IndexQuery(); + indexQuery.setId(documentId); + indexQuery.setObject(sampleEntity); + elasticsearchTemplate.index(indexQuery); + elasticsearchTemplate.refresh(SampleExplanableEntity.class, true); + + CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria("message").contains("test").and("message") + .contains("some")); + criteriaQuery.setExplain(true); + + // when + SampleExplanableEntity sampleEntity1 = elasticsearchTemplate.queryForObject(criteriaQuery, SampleExplanableEntity.class); + + // then + assertThat(sampleEntity1.getExplanation(), is(notNullValue())); + } + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/entities/SampleExplanableEntity.java b/src/test/java/org/springframework/data/elasticsearch/entities/SampleExplanableEntity.java new file mode 100644 index 000000000..10c9f85b5 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/entities/SampleExplanableEntity.java @@ -0,0 +1,154 @@ +/* + * 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.entities; + +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.Transient; +import org.springframework.data.annotation.Version; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.core.geo.GeoPoint; + +/** + * @author Rizwan Idrees + * @author Mohsin Husen + */ +@Document(indexName = "test-index", type = "test-type", indexStoreType = "memory", shards = 1, replicas = 0, refreshInterval = "-1") +public class SampleExplanableEntity { + + @Id + private String id; + private String type; + private String message; + private int rate; + private boolean available; + private String highlightedMessage; + + private GeoPoint location; + + @Transient + private String explanation; + + public String getExplanation() { + return explanation; + } + + public void setExplanation(String explanation) { + this.explanation = explanation; + } + + + + @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 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 GeoPoint getLocation() { + return location; + } + + public void setLocation(GeoPoint location) { + this.location = location; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SampleExplanableEntity)) { + return false; + } + if (this == obj) { + return true; + } + SampleExplanableEntity rhs = (SampleExplanableEntity) 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/resources/elasticsearch-explanation-test.xml b/src/test/resources/elasticsearch-explanation-test.xml new file mode 100644 index 000000000..8d06fa482 --- /dev/null +++ b/src/test/resources/elasticsearch-explanation-test.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + \ No newline at end of file