diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java index bdfe37861..dd0896238 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java @@ -202,5 +202,13 @@ public interface ElasticsearchOperations { */ Page scroll(String scrollId, long scrollTimeInMillis, ResultsMapper resultsMapper); + /** + * more like this query to search for documents that are "like" a specific document. + * @param query + * @param clazz + * @param + * @return + */ + Page moreLikeThis(MoreLikeThisQuery query, Class 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 99924809c..29e8ba608 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java @@ -24,6 +24,7 @@ import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.count.CountRequestBuilder; import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.index.IndexRequestBuilder; +import org.elasticsearch.action.mlt.MoreLikeThisRequestBuilder; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.client.Client; @@ -54,7 +55,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import static org.apache.commons.collections.CollectionUtils.isNotEmpty; import static org.apache.commons.lang.StringUtils.isBlank; +import static org.apache.commons.lang.StringUtils.isNotBlank; import static org.elasticsearch.action.search.SearchType.DFS_QUERY_THEN_FETCH; import static org.elasticsearch.action.search.SearchType.SCAN; import static org.elasticsearch.client.Requests.indicesExistsRequest; @@ -127,7 +130,7 @@ public class ElasticsearchTemplate implements ElasticsearchOperations { return mapResults(response, clazz, query.getPageable()); } - + @Override public Page queryForPage(SearchQuery query, ResultsMapper resultsMapper) { SearchResponse response = doSearch(prepareSearch(query), query.getElasticsearchQuery(), query.getElasticsearchFilter(),query.getElasticsearchSort()); return resultsMapper.mapResults(response); @@ -144,18 +147,6 @@ public class ElasticsearchTemplate implements ElasticsearchOperations { return extractIds(response); } - private SearchResponse doSearch(SearchRequestBuilder searchRequest, QueryBuilder query, FilterBuilder filter, SortBuilder sortBuilder){ - if(filter != null){ - searchRequest.setFilter(filter); - } - - if(sortBuilder != null){ - searchRequest.addSort(sortBuilder); - } - - return searchRequest.setQuery(query).execute().actionGet(); - } - @Override public Page queryForPage(CriteriaQuery query, Class clazz) { QueryBuilder elasticsearchQuery = new CriteriaQueryProcessor().createQueryFromCriteria(query.getCriteria()); @@ -229,6 +220,114 @@ public class ElasticsearchTemplate implements ElasticsearchOperations { .execute().actionGet(); } + @Override + public String scan(SearchQuery query, long scrollTimeInMillis, boolean noFields) { + Assert.notNull(query.getIndices(), "No index defined for Query"); + Assert.notNull(query.getTypes(), "No type define for Query"); + Assert.notNull(query.getPageable(), "Query.pageable is required for scan & scroll"); + + SearchRequestBuilder requestBuilder = client.prepareSearch(toArray(query.getIndices())) + .setSearchType(SCAN) + .setQuery(query.getElasticsearchQuery()) + .setTypes(toArray(query.getTypes())) + .setScroll(TimeValue.timeValueMillis(scrollTimeInMillis)) + .setFrom(0) + .setSize(query.getPageable().getPageSize()); + + if(query.getElasticsearchFilter() != null){ + requestBuilder.setFilter(query.getElasticsearchFilter()); + } + + if(noFields){ + requestBuilder.setNoFields(); + } + return requestBuilder.execute().actionGet().getScrollId(); + } + + @Override + public Page scroll(String scrollId, long scrollTimeInMillis, ResultsMapper resultsMapper) { + SearchResponse response = client.prepareSearchScroll(scrollId) + .setScroll(TimeValue.timeValueMillis(scrollTimeInMillis)) + .execute().actionGet(); + return resultsMapper.mapResults(response); + } + + @Override + public Page moreLikeThis(MoreLikeThisQuery query, Class clazz) { + int startRecord = 0; + ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); + String indexName = isNotBlank(query.getIndexName())? query.getIndexName(): persistentEntity.getIndexName(); + String type = isNotBlank(query.getType())? query.getType() : persistentEntity.getIndexType(); + + Assert.notNull(indexName,"No 'indexName' defined for MoreLikeThisQuery"); + Assert.notNull(type, "No 'type' defined for MoreLikeThisQuery"); + Assert.notNull(query.getId(), "No document id defined for MoreLikeThisQuery"); + + MoreLikeThisRequestBuilder requestBuilder = + client.prepareMoreLikeThis(indexName,type, query.getId()); + + if(query.getPageable() != null){ + startRecord = ((query.getPageable().getPageNumber() - 1) * query.getPageable().getPageSize()); + requestBuilder.setSearchSize(query.getPageable().getPageSize()); + } + requestBuilder.setSearchFrom(startRecord < 0 ? 0 : startRecord); + + if(isNotEmpty(query.getSearchIndices())){ + requestBuilder.setSearchIndices(toArray(query.getSearchIndices())); + } + if(isNotEmpty(query.getSearchTypes())){ + requestBuilder.setSearchTypes(toArray(query.getSearchTypes())); + } + if(isNotEmpty(query.getFields())){ + requestBuilder.setField(toArray(query.getFields())); + } + if(isNotBlank(query.getRouting())){ + requestBuilder.setRouting(query.getRouting()); + } + if(query.getPercentTermsToMatch() != null){ + requestBuilder.setPercentTermsToMatch(query.getPercentTermsToMatch()); + } + if(query.getMinTermFreq() != null){ + requestBuilder.setMinTermFreq(query.getMinTermFreq()); + } + if(query.getMaxQueryTerms() != null){ + requestBuilder.maxQueryTerms(query.getMaxQueryTerms()); + } + if(isNotEmpty(query.getStopWords())){ + requestBuilder.setStopWords(toArray(query.getStopWords())); + } + if(query.getMinDocFreq() != null){ + requestBuilder.setMinDocFreq(query.getMinDocFreq()); + } + if(query.getMaxDocFreq() != null){ + requestBuilder.setMaxDocFreq(query.getMaxDocFreq()); + } + if(query.getMinWordLen() != null){ + requestBuilder.setMinWordLen(query.getMinWordLen()); + } + if(query.getMaxWordLen() != null){ + requestBuilder.setMaxWordLen(query.getMaxWordLen()); + } + if(query.getBoostTerms() != null){ + requestBuilder.setBoostTerms(query.getBoostTerms()); + } + + SearchResponse response = requestBuilder.execute().actionGet(); + return mapResults(response, clazz, query.getPageable()); + } + + private SearchResponse doSearch(SearchRequestBuilder searchRequest, QueryBuilder query, FilterBuilder filter, SortBuilder sortBuilder){ + if(filter != null){ + searchRequest.setFilter(filter); + } + + if(sortBuilder != null){ + searchRequest.addSort(sortBuilder); + } + + return searchRequest.setQuery(query).execute().actionGet(); + } + private boolean createIndexIfNotCreated(String indexName) { return indexExists(indexName) || createIndex(indexName); } @@ -256,7 +355,7 @@ public class ElasticsearchTemplate implements ElasticsearchOperations { private SearchRequestBuilder prepareSearch(Query query){ Assert.notNull(query.getIndices(), "No index defined for Query"); - Assert.notNull(query.getTypes(), "No type define for Query"); + Assert.notNull(query.getTypes(), "No type defined for Query"); int startRecord = 0; SearchRequestBuilder searchRequestBuilder = client.prepareSearch(toArray(query.getIndices())) @@ -320,38 +419,6 @@ public class ElasticsearchTemplate implements ElasticsearchOperations { .refresh(refreshRequest(persistentEntity.getIndexName()).waitForOperations(waitForOperation)).actionGet(); } - @Override - public String scan(SearchQuery query, long scrollTimeInMillis, boolean noFields) { - Assert.notNull(query.getIndices(), "No index defined for Query"); - Assert.notNull(query.getTypes(), "No type define for Query"); - Assert.notNull(query.getPageable(), "Query.pageable is required for scan & scroll"); - - SearchRequestBuilder requestBuilder = client.prepareSearch(toArray(query.getIndices())) - .setSearchType(SCAN) - .setQuery(query.getElasticsearchQuery()) - .setTypes(toArray(query.getTypes())) - .setScroll(TimeValue.timeValueMillis(scrollTimeInMillis)) - .setFrom(0) - .setSize(query.getPageable().getPageSize()); - - if(query.getElasticsearchFilter() != null){ - requestBuilder.setFilter(query.getElasticsearchFilter()); - } - - if(noFields){ - requestBuilder.setNoFields(); - } - return requestBuilder.execute().actionGet().getScrollId(); - } - - @Override - public Page scroll(String scrollId, long scrollTimeInMillis, ResultsMapper resultsMapper) { - SearchResponse response = client.prepareSearchScroll(scrollId) - .setScroll(TimeValue.timeValueMillis(scrollTimeInMillis)) - .execute().actionGet(); - return resultsMapper.mapResults(response); - } - private ElasticsearchPersistentEntity getPersistentEntityFor(Class clazz){ Assert.isTrue(clazz.isAnnotationPresent(Document.class), "Unable to identify index name. " + clazz.getSimpleName() + " is not a Document. Make sure the document class is annotated with @Document(indexName=\"foo\")"); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/MoreLikeThisQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/MoreLikeThisQuery.java new file mode 100644 index 000000000..c068624d1 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/MoreLikeThisQuery.java @@ -0,0 +1,190 @@ +/* +* 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.query; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.util.ArrayList; +import java.util.List; + +import static org.apache.commons.collections.CollectionUtils.addAll; +import static org.springframework.data.elasticsearch.core.query.Query.DEFAULT_PAGE_SIZE; + +/** + * MoreLikeThisQuery + * + * @author Rizwan Idrees + * @author Mohsin Husen + */ + +public class MoreLikeThisQuery { + + private String id; + private String indexName; + private String type; + private List searchIndices = new ArrayList(); + private List searchTypes = new ArrayList(); + private List fields = new ArrayList(); + private String routing; + private Float percentTermsToMatch; + private Integer minTermFreq; + private Integer maxQueryTerms; + private List stopWords = new ArrayList(); + private Integer minDocFreq; + private Integer maxDocFreq; + private Integer minWordLen; + private Integer maxWordLen; + private Float boostTerms; + private Pageable pageable = new PageRequest(0, DEFAULT_PAGE_SIZE); + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getIndexName() { + return indexName; + } + + public void setIndexName(String indexName) { + this.indexName = indexName; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public List getSearchIndices() { + return searchIndices; + } + + public void addSearchIndices(String...searchIndices) { + addAll(this.searchIndices, searchIndices); + } + + public List getSearchTypes() { + return searchTypes; + } + + public void addSearchTypes(String...searchTypes) { + addAll(this.searchTypes, searchTypes); + } + + public List getFields() { + return fields; + } + + public void addFields(String...fields) { + addAll(this.fields,fields); + } + + public String getRouting() { + return routing; + } + + public void setRouting(String routing) { + this.routing = routing; + } + + public Float getPercentTermsToMatch() { + return percentTermsToMatch; + } + + public void setPercentTermsToMatch(Float percentTermsToMatch) { + this.percentTermsToMatch = percentTermsToMatch; + } + + public Integer getMinTermFreq() { + return minTermFreq; + } + + public void setMinTermFreq(Integer minTermFreq) { + this.minTermFreq = minTermFreq; + } + + public Integer getMaxQueryTerms() { + return maxQueryTerms; + } + + public void setMaxQueryTerms(Integer maxQueryTerms) { + this.maxQueryTerms = maxQueryTerms; + } + + public List getStopWords() { + return stopWords; + } + + public void addStopWords(String...stopWords) { + addAll(this.stopWords,stopWords); + } + + public Integer getMinDocFreq() { + return minDocFreq; + } + + public void setMinDocFreq(Integer minDocFreq) { + this.minDocFreq = minDocFreq; + } + + public Integer getMaxDocFreq() { + return maxDocFreq; + } + + public void setMaxDocFreq(Integer maxDocFreq) { + this.maxDocFreq = maxDocFreq; + } + + public Integer getMinWordLen() { + return minWordLen; + } + + public void setMinWordLen(Integer minWordLen) { + this.minWordLen = minWordLen; + } + + public Integer getMaxWordLen() { + return maxWordLen; + } + + public void setMaxWordLen(Integer maxWordLen) { + this.maxWordLen = maxWordLen; + } + + public Float getBoostTerms() { + return boostTerms; + } + + public void setBoostTerms(Float boostTerms) { + this.boostTerms = boostTerms; + } + + public Pageable getPageable() { + return pageable; + } + + public void setPageable(Pageable pageable) { + this.pageable = pageable; + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTest.java b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTest.java index 03b2087c1..e81ce7fd9 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTest.java @@ -111,7 +111,6 @@ public class ElasticsearchTemplateTest { @Test public void shouldReturnPageForGivenSearchQuery(){ - //given //given String documentId = randomNumeric(5); SampleEntity sampleEntity = new SampleEntity(); @@ -247,7 +246,7 @@ public class ElasticsearchTemplateTest { } @Test - public void shouldTestFilterBuilder(){ + public void shouldFilterSearchResultsGivenFilter(){ //given String documentId = randomNumeric(5); SampleEntity sampleEntity = new SampleEntity(); @@ -271,7 +270,7 @@ public class ElasticsearchTemplateTest { } @Test - public void shouldTestSortBuilder(){ + public void shouldSortResultsGivenSortCriteria(){ //given List indexQueries = new ArrayList(); //first document @@ -494,4 +493,51 @@ public class ElasticsearchTemplateTest { assertThat(page.getTotalElements(), is(equalTo(1L))); assertThat(page.getContent().get(0), is(message)); } + + @Test + public void shouldReturnSimilarResultsGivenMoreLikeThisQuery(){ + //given + String sampleMessage = "So we build a web site or an application and want to add search to it, " + + "and then it hits us: getting search working is hard. We want our search solution to be fast," + + " we want a painless setup and a completely free search schema, we want to be able to index data simply using JSON over HTTP, " + + "we want our search server to be always available, we want to be able to start with one machine and scale to hundreds, " + + "we want real-time search, we want simple multi-tenancy, and we want a solution that is built for the cloud."; + + String documentId1 = randomNumeric(5); + SampleEntity sampleEntity1 = new SampleEntity(); + sampleEntity1.setId(documentId1); + sampleEntity1.setMessage(sampleMessage); + sampleEntity1.setVersion(System.currentTimeMillis()); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(documentId1); + indexQuery1.setObject(sampleEntity1); + + elasticsearchTemplate.index(indexQuery1); + + String documentId2 = randomNumeric(5); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setId(documentId2); + sampleEntity2.setMessage(sampleMessage); + sampleEntity2.setVersion(System.currentTimeMillis()); + + IndexQuery indexQuery2 = new IndexQuery(); + indexQuery2.setId(documentId2); + indexQuery2.setObject(sampleEntity2); + + elasticsearchTemplate.index(indexQuery2); + elasticsearchTemplate.refresh(SampleEntity.class, true); + + + MoreLikeThisQuery moreLikeThisQuery = new MoreLikeThisQuery(); + moreLikeThisQuery.setId(documentId2); + moreLikeThisQuery.addFields("message"); + moreLikeThisQuery.setMinDocFreq(1); + //when + Page sampleEntities = elasticsearchTemplate.moreLikeThis(moreLikeThisQuery, SampleEntity.class); + + //then + assertThat(sampleEntities.getTotalElements(), is(equalTo(1L))); + assertThat(sampleEntities.getContent(), hasItem(sampleEntity1)); + } }