diff --git a/.gitignore b/.gitignore index 6f6082b8..20068080 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,11 @@ atlassian-ide-plugin.xml ## Ignore svn files .svn +## ignore any target dir target -data + +##ignore only top level data dir - local node data files for unit tests +/data ## Ignore project files created by Eclipse .settings 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 c4ed4aaa..79887edc 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java @@ -20,20 +20,26 @@ import java.lang.annotation.*; /** * @author Rizwan Idrees * @author Mohsin Husen + * @author Artur Konczak + * @author Jonathan Yan */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @Documented public @interface Field { - String type() default ""; + String type() default ""; - String index() default ""; + String index() default ""; - boolean store() default false; + boolean store() default false; - String searchAnalyzer() default ""; + String searchAnalyzer() default ""; - String indexAnalyzer() default ""; + String indexAnalyzer() default ""; + + boolean facetable() default false; + + boolean sortable() default false; } 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 8611f199..e8b0a0fe 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java @@ -84,7 +84,7 @@ public interface ElasticsearchOperations { * @param clazz * @return */ - Page queryForPage(SearchQuery query, Class clazz); + FacetedPage queryForPage(SearchQuery query, Class clazz); /** * Execute the query against elasticsearch and return result as {@link Page} @@ -93,7 +93,7 @@ public interface ElasticsearchOperations { * @param resultsMapper * @return */ - Page queryForPage(SearchQuery query, ResultsMapper resultsMapper); + FacetedPage queryForPage(SearchQuery query, ResultsMapper resultsMapper); /** * Execute the query against elasticsearch and return result as {@link Page} @@ -111,7 +111,7 @@ public interface ElasticsearchOperations { * @param clazz * @return */ - Page queryForPage(StringQuery query, Class clazz); + FacetedPage queryForPage(StringQuery query, Class clazz); /** * Execute the criteria query against elasticsearch and return result as {@link List} 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 15db96e5..b2f89018 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java @@ -15,6 +15,7 @@ */ package org.springframework.data.elasticsearch.core; +import org.apache.commons.collections.CollectionUtils; import org.codehaus.jackson.map.DeserializationConfig; import org.codehaus.jackson.map.ObjectMapper; import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; @@ -33,19 +34,20 @@ import org.elasticsearch.client.Requests; import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.index.query.FilterBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.search.SearchHit; -import org.elasticsearch.search.sort.SortBuilder; +import org.elasticsearch.search.facet.Facet; +import org.elasticsearch.search.facet.FacetBuilder; import org.elasticsearch.search.sort.SortOrder; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.elasticsearch.ElasticsearchException; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter; +import org.springframework.data.elasticsearch.core.facet.FacetMapper; +import org.springframework.data.elasticsearch.core.facet.FacetResult; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; import org.springframework.data.elasticsearch.core.query.*; @@ -69,443 +71,460 @@ import static org.springframework.data.elasticsearch.core.MappingBuilder.buildMa /** * ElasticsearchTemplate - * + * * @author Rizwan Idrees * @author Mohsin Husen */ public class ElasticsearchTemplate implements ElasticsearchOperations { - private Client client; - private ElasticsearchConverter elasticsearchConverter; - private ObjectMapper objectMapper = new ObjectMapper(); + private Client client; + private ElasticsearchConverter elasticsearchConverter; + private ObjectMapper objectMapper = new ObjectMapper(); - { - objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false); - } + { + objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } - public ElasticsearchTemplate(Client client) { - this(client, null); - } + public ElasticsearchTemplate(Client client) { + this(client, null); + } - public ElasticsearchTemplate(Client client, ElasticsearchConverter elasticsearchConverter) { - this.client = client; - this.elasticsearchConverter = (elasticsearchConverter == null) ? new MappingElasticsearchConverter( - new SimpleElasticsearchMappingContext()) : elasticsearchConverter; - } + public ElasticsearchTemplate(Client client, ElasticsearchConverter elasticsearchConverter) { + this.client = client; + this.elasticsearchConverter = (elasticsearchConverter == null) ? new MappingElasticsearchConverter( + new SimpleElasticsearchMappingContext()) : elasticsearchConverter; + } - @Override - public boolean createIndex(Class clazz) { - ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); - return createIndexIfNotCreated(clazz); - } + @Override + public boolean createIndex(Class clazz) { + ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); + return createIndexIfNotCreated(clazz); + } - @Override - public boolean putMapping(Class clazz) { - ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); - PutMappingRequestBuilder requestBuilder = client.admin().indices() - .preparePutMapping(persistentEntity.getIndexName()).setType(persistentEntity.getIndexType()); + @Override + public boolean putMapping(Class clazz) { + ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); + PutMappingRequestBuilder requestBuilder = client.admin().indices() + .preparePutMapping(persistentEntity.getIndexName()).setType(persistentEntity.getIndexType()); - try { - XContentBuilder xContentBuilder = buildMapping(clazz, persistentEntity.getIndexType(), persistentEntity - .getIdProperty().getFieldName()); - return requestBuilder.setSource(xContentBuilder).execute().actionGet().isAcknowledged(); - } catch (Exception e) { - throw new ElasticsearchException("Failed to build mapping for " + clazz.getSimpleName(), e); - } - } + try { + XContentBuilder xContentBuilder = buildMapping(clazz, persistentEntity.getIndexType(), persistentEntity + .getIdProperty().getFieldName()); + return requestBuilder.setSource(xContentBuilder).execute().actionGet().isAcknowledged(); + } catch (Exception e) { + throw new ElasticsearchException("Failed to build mapping for " + clazz.getSimpleName(), e); + } + } - @Override - public ElasticsearchConverter getElasticsearchConverter() { - return elasticsearchConverter; - } + @Override + public ElasticsearchConverter getElasticsearchConverter() { + return elasticsearchConverter; + } - @Override - public T queryForObject(GetQuery query, Class clazz) { - ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); - GetResponse response = client - .prepareGet(persistentEntity.getIndexName(), persistentEntity.getIndexType(), query.getId()).execute() - .actionGet(); - return mapResult(response.getSourceAsString(), clazz); - } + @Override + public T queryForObject(GetQuery query, Class clazz) { + ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); + GetResponse response = client + .prepareGet(persistentEntity.getIndexName(), persistentEntity.getIndexType(), query.getId()).execute() + .actionGet(); + return mapResult(response.getSourceAsString(), clazz); + } - @Override - public T queryForObject(CriteriaQuery query, Class clazz) { - Page page = queryForPage(query, clazz); - Assert.isTrue(page.getTotalElements() < 2, "Expected 1 but found " + page.getTotalElements() + " results"); - return page.getTotalElements() > 0 ? page.getContent().get(0) : null; - } + @Override + public T queryForObject(CriteriaQuery query, Class clazz) { + Page page = queryForPage(query, clazz); + Assert.isTrue(page.getTotalElements() < 2, "Expected 1 but found " + page.getTotalElements() + " results"); + return page.getTotalElements() > 0 ? page.getContent().get(0) : null; + } - @Override - public T queryForObject(StringQuery query, Class clazz) { - Page page = queryForPage(query, clazz); - Assert.isTrue(page.getTotalElements() < 2, "Expected 1 but found " + page.getTotalElements() + " results"); - return page.getTotalElements() > 0 ? page.getContent().get(0) : null; - } + @Override + public T queryForObject(StringQuery query, Class clazz) { + Page page = queryForPage(query, clazz); + Assert.isTrue(page.getTotalElements() < 2, "Expected 1 but found " + page.getTotalElements() + " results"); + return page.getTotalElements() > 0 ? page.getContent().get(0) : null; + } - @Override - public Page queryForPage(SearchQuery query, Class clazz) { - SearchResponse response = doSearch(prepareSearch(query, clazz), query.getQuery(), query.getFilter(), - query.getElasticsearchSort()); - return mapResults(response, clazz, query.getPageable()); - } + @Override + public FacetedPage queryForPage(SearchQuery query, Class clazz) { + SearchResponse response = doSearch(prepareSearch(query, clazz), query); + return mapResults(response, clazz, query.getPageable()); + } - @Override - public Page queryForPage(SearchQuery query, ResultsMapper resultsMapper) { - SearchResponse response = doSearch(prepareSearch(query), query.getQuery(), query.getFilter(), - query.getElasticsearchSort()); - return resultsMapper.mapResults(response); - } + @Override + public FacetedPage queryForPage(SearchQuery query, ResultsMapper resultsMapper) { + SearchResponse response = doSearch(prepareSearch(query), query); + return resultsMapper.mapResults(response); + } - @Override - public List queryForList(CriteriaQuery query, Class clazz) { - return queryForPage(query, clazz).getContent(); - } + @Override + public List queryForList(CriteriaQuery query, Class clazz) { + return queryForPage(query, clazz).getContent(); + } - @Override - public List queryForList(StringQuery query, Class clazz) { - return queryForPage(query, clazz).getContent(); - } + @Override + public List queryForList(StringQuery query, Class clazz) { + return queryForPage(query, clazz).getContent(); + } - @Override - public List queryForIds(SearchQuery query) { - SearchRequestBuilder request = prepareSearch(query).setQuery(query.getQuery()).setNoFields(); - if (query.getFilter() != null) { - request.setFilter(query.getFilter()); - } - SearchResponse response = request.execute().actionGet(); - return extractIds(response); - } + @Override + public List queryForIds(SearchQuery query) { + SearchRequestBuilder request = prepareSearch(query).setQuery(query.getQuery()).setNoFields(); + if (query.getFilter() != null) { + request.setFilter(query.getFilter()); + } + SearchResponse response = request.execute().actionGet(); + return extractIds(response); + } - @Override - public Page queryForPage(CriteriaQuery criteriaQuery, Class clazz) { - QueryBuilder query = new CriteriaQueryProcessor().createQueryFromCriteria(criteriaQuery.getCriteria()); - SearchResponse response = prepareSearch(criteriaQuery, clazz).setQuery(query).execute().actionGet(); - return mapResults(response, clazz, criteriaQuery.getPageable()); - } + @Override + public Page queryForPage(CriteriaQuery criteriaQuery, Class clazz) { + QueryBuilder query = new CriteriaQueryProcessor().createQueryFromCriteria(criteriaQuery.getCriteria()); + SearchResponse response = prepareSearch(criteriaQuery, clazz).setQuery(query).execute().actionGet(); + return mapResults(response, clazz, criteriaQuery.getPageable()); + } - @Override - public Page queryForPage(StringQuery query, Class clazz) { - SearchResponse response = prepareSearch(query, clazz).setQuery(query.getSource()).execute().actionGet(); - return mapResults(response, clazz, query.getPageable()); - } + @Override + public FacetedPage queryForPage(StringQuery query, Class clazz) { + SearchResponse response = prepareSearch(query, clazz).setQuery(query.getSource()).execute().actionGet(); + return mapResults(response, clazz, query.getPageable()); + } - @Override - public long count(SearchQuery query, Class clazz) { - ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); - CountRequestBuilder countRequestBuilder = client.prepareCount(persistentEntity.getIndexName()).setTypes( - persistentEntity.getIndexType()); - if (query.getQuery() != null) { - countRequestBuilder.setQuery(query.getQuery()); - } - return countRequestBuilder.execute().actionGet().getCount(); - } + @Override + public long count(SearchQuery query, Class clazz) { + ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); + CountRequestBuilder countRequestBuilder = client.prepareCount(persistentEntity.getIndexName()).setTypes( + persistentEntity.getIndexType()); + if (query.getQuery() != null) { + countRequestBuilder.setQuery(query.getQuery()); + } + return countRequestBuilder.execute().actionGet().getCount(); + } - @Override - public String index(IndexQuery query) { - return prepareIndex(query).execute().actionGet().getId(); - } + @Override + public String index(IndexQuery query) { + return prepareIndex(query).execute().actionGet().getId(); + } - @Override - public void bulkIndex(List queries) { - BulkRequestBuilder bulkRequest = client.prepareBulk(); - for (IndexQuery query : queries) { - bulkRequest.add(prepareIndex(query)); - } - BulkResponse bulkResponse = bulkRequest.execute().actionGet(); - if (bulkResponse.hasFailures()) { - Map failedDocuments = new HashMap(); - for (BulkItemResponse item : bulkResponse.getItems()) { - if (item.isFailed()) - failedDocuments.put(item.getId(), item.getFailureMessage()); - } - throw new ElasticsearchException( - "Bulk indexing has failures. Use ElasticsearchException.getFailedDocuments() for detailed messages [" - + failedDocuments + "]", failedDocuments); - } - } + @Override + public void bulkIndex(List queries) { + BulkRequestBuilder bulkRequest = client.prepareBulk(); + for (IndexQuery query : queries) { + bulkRequest.add(prepareIndex(query)); + } + BulkResponse bulkResponse = bulkRequest.execute().actionGet(); + if (bulkResponse.hasFailures()) { + Map failedDocuments = new HashMap(); + for (BulkItemResponse item : bulkResponse.getItems()) { + if (item.isFailed()) + failedDocuments.put(item.getId(), item.getFailureMessage()); + } + throw new ElasticsearchException( + "Bulk indexing has failures. Use ElasticsearchException.getFailedDocuments() for detailed messages [" + + failedDocuments + "]", failedDocuments); + } + } - @Override - public boolean indexExists(Class clazz) { - return indexExists(getPersistentEntityFor(clazz).getIndexName()); - } + @Override + public boolean indexExists(Class clazz) { + return indexExists(getPersistentEntityFor(clazz).getIndexName()); + } - @Override - public boolean deleteIndex(Class clazz) { - String indexName = getPersistentEntityFor(clazz).getIndexName(); - if (indexExists(indexName)) { - return client.admin().indices().delete(new DeleteIndexRequest(indexName)).actionGet().isAcknowledged(); - } - return false; - } + @Override + public boolean deleteIndex(Class clazz) { + String indexName = getPersistentEntityFor(clazz).getIndexName(); + if (indexExists(indexName)) { + return client.admin().indices().delete(new DeleteIndexRequest(indexName)).actionGet().isAcknowledged(); + } + return false; + } - @Override - public String delete(String indexName, String type, String id) { - return client.prepareDelete(indexName, type, id).execute().actionGet().getId(); - } + @Override + public String delete(String indexName, String type, String id) { + return client.prepareDelete(indexName, type, id).execute().actionGet().getId(); + } - @Override - public String delete(Class clazz, String id) { - ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); - return delete(persistentEntity.getIndexName(), persistentEntity.getIndexType(), id); - } + @Override + public String delete(Class clazz, String id) { + ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); + return delete(persistentEntity.getIndexName(), persistentEntity.getIndexType(), id); + } - @Override - public void delete(DeleteQuery deleteQuery, Class clazz) { - ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); - client.prepareDeleteByQuery(persistentEntity.getIndexName()).setTypes(persistentEntity.getIndexType()) - .setQuery(deleteQuery.getQuery()).execute().actionGet(); - } + @Override + public void delete(DeleteQuery deleteQuery, Class clazz) { + ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); + client.prepareDeleteByQuery(persistentEntity.getIndexName()).setTypes(persistentEntity.getIndexType()) + .setQuery(deleteQuery.getQuery()).execute().actionGet(); + } - @Override - public String scan(SearchQuery searchQuery, long scrollTimeInMillis, boolean noFields) { - Assert.notNull(searchQuery.getIndices(), "No index defined for Query"); - Assert.notNull(searchQuery.getTypes(), "No type define for Query"); - Assert.notNull(searchQuery.getPageable(), "Query.pageable is required for scan & scroll"); + @Override + public String scan(SearchQuery searchQuery, long scrollTimeInMillis, boolean noFields) { + Assert.notNull(searchQuery.getIndices(), "No index defined for Query"); + Assert.notNull(searchQuery.getTypes(), "No type define for Query"); + Assert.notNull(searchQuery.getPageable(), "Query.pageable is required for scan & scroll"); - SearchRequestBuilder requestBuilder = client.prepareSearch(toArray(searchQuery.getIndices())).setSearchType(SCAN) - .setQuery(searchQuery.getQuery()).setTypes(toArray(searchQuery.getTypes())) - .setScroll(TimeValue.timeValueMillis(scrollTimeInMillis)).setFrom(0) - .setSize(searchQuery.getPageable().getPageSize()); + SearchRequestBuilder requestBuilder = client.prepareSearch(toArray(searchQuery.getIndices())).setSearchType(SCAN) + .setQuery(searchQuery.getQuery()).setTypes(toArray(searchQuery.getTypes())) + .setScroll(TimeValue.timeValueMillis(scrollTimeInMillis)).setFrom(0) + .setSize(searchQuery.getPageable().getPageSize()); - if (searchQuery.getFilter() != null) { - requestBuilder.setFilter(searchQuery.getFilter()); - } + if (searchQuery.getFilter() != null) { + requestBuilder.setFilter(searchQuery.getFilter()); + } - if (noFields) { - requestBuilder.setNoFields(); - } - return requestBuilder.execute().actionGet().getScrollId(); - } + 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 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(); + @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"); + 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()); + MoreLikeThisRequestBuilder requestBuilder = client.prepareMoreLikeThis(indexName, type, query.getId()); - if (query.getPageable() != null) { - startRecord = query.getPageable().getPageNumber() * query.getPageable().getPageSize(); - requestBuilder.setSearchSize(query.getPageable().getPageSize()); - } - requestBuilder.setSearchFrom(startRecord); + if (query.getPageable() != null) { + startRecord = query.getPageable().getPageNumber() * query.getPageable().getPageSize(); + requestBuilder.setSearchSize(query.getPageable().getPageSize()); + } + requestBuilder.setSearchFrom(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()); - } + 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()); - } + 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); - } + private SearchResponse doSearch(SearchRequestBuilder searchRequest, SearchQuery searchQuery) { + if (searchQuery.getFilter() != null) { + searchRequest.setFilter(searchQuery.getFilter()); + } - if (sortBuilder != null) { - searchRequest.addSort(sortBuilder); - } + if (searchQuery.getElasticsearchSort() != null) { + searchRequest.addSort(searchQuery.getElasticsearchSort()); + } - return searchRequest.setQuery(query).execute().actionGet(); - } + if (CollectionUtils.isNotEmpty(searchQuery.getFacets())) { + for (FacetRequest facetRequest : searchQuery.getFacets()) { + FacetBuilder facet = facetRequest.getFacet(); + if (facetRequest.applyQueryFilter() && searchQuery.getFilter() != null) { + facet.facetFilter(searchQuery.getFilter()); + } + searchRequest.addFacet(facet); + } + } - private boolean createIndexIfNotCreated(Class clazz) { - return indexExists(getPersistentEntityFor(clazz).getIndexName()) || createIndexWithSettings(clazz); - } + return searchRequest.setQuery(searchQuery.getQuery()).execute().actionGet(); + } - private boolean indexExists(String indexName) { - return client.admin().indices().exists(indicesExistsRequest(indexName)).actionGet().isExists(); - } + private boolean createIndexIfNotCreated(Class clazz) { + return indexExists(getPersistentEntityFor(clazz).getIndexName()) || createIndexWithSettings(clazz); + } - private boolean createIndexWithSettings(Class clazz) { - ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); - return client.admin().indices() - .create(Requests.createIndexRequest(persistentEntity.getIndexName()).settings(getSettings(persistentEntity))) - .actionGet().isAcknowledged(); - } + private boolean indexExists(String indexName) { + return client.admin().indices().exists(indicesExistsRequest(indexName)).actionGet().isExists(); + } - private Map getSettings(ElasticsearchPersistentEntity persistentEntity) { - return new MapBuilder().put("index.number_of_shards", String.valueOf(persistentEntity.getShards())) - .put("index.number_of_replicas", String.valueOf(persistentEntity.getReplicas())) - .put("index.refresh_interval", persistentEntity.getRefreshInterval()) - .put("index.store.type", persistentEntity.getIndexStoreType()).map(); - } + private boolean createIndexWithSettings(Class clazz) { + ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); + return client.admin().indices() + .create(Requests.createIndexRequest(persistentEntity.getIndexName()).settings(getSettings(persistentEntity))) + .actionGet().isAcknowledged(); + } - private SearchRequestBuilder prepareSearch(Query query, Class clazz) { - if (query.getIndices().isEmpty()) { - query.addIndices(retrieveIndexNameFromPersistentEntity(clazz)); - } - if (query.getTypes().isEmpty()) { - query.addTypes(retrieveTypeFromPersistentEntity(clazz)); - } - return prepareSearch(query); - } + private Map getSettings(ElasticsearchPersistentEntity persistentEntity) { + return new MapBuilder().put("index.number_of_shards", String.valueOf(persistentEntity.getShards())) + .put("index.number_of_replicas", String.valueOf(persistentEntity.getReplicas())) + .put("index.refresh_interval", persistentEntity.getRefreshInterval()) + .put("index.store.type", persistentEntity.getIndexStoreType()).map(); + } - private SearchRequestBuilder prepareSearch(Query query) { - Assert.notNull(query.getIndices(), "No index defined for Query"); - Assert.notNull(query.getTypes(), "No type defined for Query"); + private SearchRequestBuilder prepareSearch(Query query, Class clazz) { + if (query.getIndices().isEmpty()) { + query.addIndices(retrieveIndexNameFromPersistentEntity(clazz)); + } + if (query.getTypes().isEmpty()) { + query.addTypes(retrieveTypeFromPersistentEntity(clazz)); + } + return prepareSearch(query); + } - int startRecord = 0; - SearchRequestBuilder searchRequestBuilder = client.prepareSearch(toArray(query.getIndices())) - .setSearchType(DFS_QUERY_THEN_FETCH).setTypes(toArray(query.getTypes())); + private SearchRequestBuilder prepareSearch(Query query) { + Assert.notNull(query.getIndices(), "No index defined for Query"); + Assert.notNull(query.getTypes(), "No type defined for Query"); - if (query.getPageable() != null) { - startRecord = query.getPageable().getPageNumber() * query.getPageable().getPageSize(); - searchRequestBuilder.setSize(query.getPageable().getPageSize()); - } - searchRequestBuilder.setFrom(startRecord); + int startRecord = 0; + SearchRequestBuilder searchRequestBuilder = client.prepareSearch(toArray(query.getIndices())) + .setSearchType(DFS_QUERY_THEN_FETCH).setTypes(toArray(query.getTypes())); - if (!query.getFields().isEmpty()) { - searchRequestBuilder.addFields(toArray(query.getFields())); - } + if (query.getPageable() != null) { + startRecord = query.getPageable().getPageNumber() * query.getPageable().getPageSize(); + searchRequestBuilder.setSize(query.getPageable().getPageSize()); + } + searchRequestBuilder.setFrom(startRecord); - if (query.getSort() != null) { - for (Sort.Order order : query.getSort()) { - searchRequestBuilder.addSort(order.getProperty(), order.getDirection() == Sort.Direction.DESC ? SortOrder.DESC - : SortOrder.ASC); - } - } - return searchRequestBuilder; - } + if (!query.getFields().isEmpty()) { + searchRequestBuilder.addFields(toArray(query.getFields())); + } - private IndexRequestBuilder prepareIndex(IndexQuery query) { - try { - String indexName = isBlank(query.getIndexName()) ? retrieveIndexNameFromPersistentEntity(query.getObject() - .getClass())[0] : query.getIndexName(); - String type = isBlank(query.getType()) ? retrieveTypeFromPersistentEntity(query.getObject().getClass())[0] - : query.getType(); + if (query.getSort() != null) { + for (Sort.Order order : query.getSort()) { + searchRequestBuilder.addSort(order.getProperty(), order.getDirection() == Sort.Direction.DESC ? SortOrder.DESC + : SortOrder.ASC); + } + } + return searchRequestBuilder; + } - IndexRequestBuilder indexRequestBuilder = client.prepareIndex(indexName, type, query.getId()).setSource( - objectMapper.writeValueAsString(query.getObject())); + private IndexRequestBuilder prepareIndex(IndexQuery query) { + try { + String indexName = isBlank(query.getIndexName()) ? retrieveIndexNameFromPersistentEntity(query.getObject() + .getClass())[0] : query.getIndexName(); + String type = isBlank(query.getType()) ? retrieveTypeFromPersistentEntity(query.getObject().getClass())[0] + : query.getType(); - if (query.getVersion() != null) { - indexRequestBuilder.setVersion(query.getVersion()); - indexRequestBuilder.setVersionType(EXTERNAL); - } - return indexRequestBuilder; - } catch (IOException e) { - throw new ElasticsearchException("failed to index the document [id: " + query.getId() + "]", e); - } - } + IndexRequestBuilder indexRequestBuilder = client.prepareIndex(indexName, type, query.getId()).setSource( + objectMapper.writeValueAsString(query.getObject())); - public void refresh(String indexName, boolean waitForOperation) { - client.admin().indices().refresh(refreshRequest(indexName).waitForOperations(waitForOperation)).actionGet(); - } + if (query.getVersion() != null) { + indexRequestBuilder.setVersion(query.getVersion()); + indexRequestBuilder.setVersionType(EXTERNAL); + } + return indexRequestBuilder; + } catch (IOException e) { + throw new ElasticsearchException("failed to index the document [id: " + query.getId() + "]", e); + } + } - public void refresh(Class clazz, boolean waitForOperation) { - ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); - client.admin().indices() - .refresh(refreshRequest(persistentEntity.getIndexName()).waitForOperations(waitForOperation)).actionGet(); - } + public void refresh(String indexName, boolean waitForOperation) { + client.admin().indices().refresh(refreshRequest(indexName).waitForOperations(waitForOperation)).actionGet(); + } - 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\")"); - return elasticsearchConverter.getMappingContext().getPersistentEntity(clazz); - } + public void refresh(Class clazz, boolean waitForOperation) { + ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz); + client.admin().indices() + .refresh(refreshRequest(persistentEntity.getIndexName()).waitForOperations(waitForOperation)).actionGet(); + } - private String[] retrieveIndexNameFromPersistentEntity(Class clazz) { - return new String[] { getPersistentEntityFor(clazz).getIndexName() }; - } + 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\")"); + return elasticsearchConverter.getMappingContext().getPersistentEntity(clazz); + } - private String[] retrieveTypeFromPersistentEntity(Class clazz) { - return new String[] { getPersistentEntityFor(clazz).getIndexType() }; - } + private String[] retrieveIndexNameFromPersistentEntity(Class clazz) { + return new String[]{getPersistentEntityFor(clazz).getIndexName()}; + } - private Page mapResults(SearchResponse response, final Class elementType, final Pageable pageable) { - ResultsMapper resultsMapper = new ResultsMapper() { - @Override - public Page mapResults(SearchResponse response) { - long totalHits = response.getHits().totalHits(); - List results = new ArrayList(); - for (SearchHit hit : response.getHits()) { - if (hit != null) { - results.add(mapResult(hit.sourceAsString(), elementType)); - } - } - return new PageImpl(results, pageable, totalHits); - } - }; - return resultsMapper.mapResults(response); - } + private String[] retrieveTypeFromPersistentEntity(Class clazz) { + return new String[]{getPersistentEntityFor(clazz).getIndexType()}; + } - private List extractIds(SearchResponse response) { - List ids = new ArrayList(); - for (SearchHit hit : response.getHits()) { - if (hit != null) { - ids.add(hit.getId()); - } - } - return ids; - } + private FacetedPage mapResults(SearchResponse response, final Class elementType, final Pageable pageable) { + ResultsMapper resultsMapper = new ResultsMapper() { + @Override + public FacetedPage mapResults(SearchResponse response) { + long totalHits = response.getHits().totalHits(); + List results = new ArrayList(); + for (SearchHit hit : response.getHits()) { + if (hit != null) { + results.add(mapResult(hit.sourceAsString(), elementType)); + } + } + List facets = new ArrayList(); + if (response.getFacets() != null) { + for (Facet facet : response.getFacets()) { + FacetResult facetResult = FacetMapper.parse(facet); + if (facetResult != null) { + facets.add(facetResult); + } + } + } - private T mapResult(String source, Class clazz) { - if (isBlank(source)) { - return null; - } - try { - return objectMapper.readValue(source, clazz); - } catch (IOException e) { - throw new ElasticsearchException("failed to map source [ " + source + "] to class " + clazz.getSimpleName(), e); - } - } + return new FacetedPageImpl(results, pageable, totalHits, facets); + } + }; + return resultsMapper.mapResults(response); + } - private static String[] toArray(List values) { - String[] valuesAsArray = new String[values.size()]; - return values.toArray(valuesAsArray); + private List extractIds(SearchResponse response) { + List ids = new ArrayList(); + for (SearchHit hit : response.getHits()) { + if (hit != null) { + ids.add(hit.getId()); + } + } + return ids; + } - } + private T mapResult(String source, Class clazz) { + if (isBlank(source)) { + return null; + } + try { + return objectMapper.readValue(source, clazz); + } catch (IOException e) { + throw new ElasticsearchException("failed to map source [ " + source + "] to class " + clazz.getSimpleName(), e); + } + } + + private static String[] toArray(List values) { + String[] valuesAsArray = new String[values.size()]; + return values.toArray(valuesAsArray); + + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/FacetedPage.java b/src/main/java/org/springframework/data/elasticsearch/core/FacetedPage.java new file mode 100644 index 00000000..f2287052 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/FacetedPage.java @@ -0,0 +1,23 @@ +package org.springframework.data.elasticsearch.core; + +import org.springframework.data.domain.Page; +import org.springframework.data.elasticsearch.core.facet.FacetResult; + +import java.util.List; + +/** + * + * @author Rizwan Idrees + * @author Mohsin Husen + * @author Artur Konczak + * @author Jonathan Yan + */ +public interface FacetedPage extends Page { + + boolean hasFacets(); + + List getFacets(); + + FacetResult getFacet(String name); + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/FacetedPageImpl.java b/src/main/java/org/springframework/data/elasticsearch/core/FacetedPageImpl.java new file mode 100644 index 00000000..7a378a32 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/FacetedPageImpl.java @@ -0,0 +1,55 @@ +package org.springframework.data.elasticsearch.core; + +import org.apache.commons.collections.CollectionUtils; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.elasticsearch.core.facet.FacetResult; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Container for query result and facet results + * + * @author Rizwan Idrees + * @author Mohsin Husen + * @author Artur Konczak + * @author Jonathan Yan + */ +public class FacetedPageImpl extends PageImpl implements FacetedPage { + + private List facets; + private Map mapOfFacets = new HashMap(); + + public FacetedPageImpl(List content) { + super(content); + } + + public FacetedPageImpl(List content, Pageable pageable, long total) { + super(content, pageable, total); + } + + public FacetedPageImpl(List content, Pageable pageable, long total, List facets) { + super(content, pageable, total); + this.facets = facets; + for (FacetResult facet : facets) { + mapOfFacets.put(facet.getName(), facet); + } + } + + @Override + public boolean hasFacets() { + return CollectionUtils.isNotEmpty(facets); + } + + @Override + public List getFacets() { + return facets; + } + + @Override + public FacetResult getFacet(String name) { + return mapOfFacets.get(name); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/FactedPageImpl.java b/src/main/java/org/springframework/data/elasticsearch/core/FactedPageImpl.java new file mode 100644 index 00000000..cc59ded3 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/FactedPageImpl.java @@ -0,0 +1,52 @@ +package org.springframework.data.elasticsearch.core; + +import org.apache.commons.collections.CollectionUtils; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.elasticsearch.core.facet.FacetResult; + +import java.util.List; +import java.util.Map; + +/** + * @author Rizwan Idrees + * @author Mohsin Husen + * @author Artur Konczak + * @author Jonathan Yan + */ +public class FactedPageImpl extends PageImpl implements FacetedPage { + + private List facets; + private Map mapOfFacets; + + public FactedPageImpl(List content) { + super(content); + } + + public FactedPageImpl(List content, Pageable pageable, long total) { + super(content, pageable, total); + } + + public FactedPageImpl(List content, Pageable pageable, long total, List facets) { + super(content, pageable, total); + this.facets = facets; + for (FacetResult facet : facets) { + mapOfFacets.put(facet.getName(), facet); + } + } + + @Override + public boolean hasFacets() { + return CollectionUtils.isNotEmpty(facets); + } + + @Override + public List getFacets() { + return facets; + } + + @Override + public FacetResult getFacet(String name) { + return mapOfFacets.get(name); + } +} 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 cea71d65..eaa0c316 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/MappingBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/MappingBuilder.java @@ -17,6 +17,7 @@ package org.springframework.data.elasticsearch.core; import org.elasticsearch.common.xcontent.XContentBuilder; import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.core.query.FacetRequest; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; @@ -31,90 +32,177 @@ import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; /** * @author Rizwan Idrees * @author Mohsin Husen + * @author Artur Konczak */ class MappingBuilder { - private static SimpleTypeHolder SIMPLE_TYPE_HOLDER = new SimpleTypeHolder(); + public static final String FIELD_STORE = "store"; + public static final String FIELD_TYPE = "type"; + public static final String FIELD_INDEX = "index"; + public static final String FIELD_SEARCH_ANALYZER = "search_analyzer"; + public static final String FIELD_INDEX_ANALYZER = "index_analyzer"; + public static final String FIELD_PROPERTIES = "properties"; - static XContentBuilder buildMapping(Class clazz, String indexType, String idFieldName) throws IOException { - XContentBuilder xContentBuilder = jsonBuilder().startObject().startObject(indexType).startObject("properties"); + public static final String INDEX_VALUE_NOT_ANALYZED = "not_analyzed"; + public static final String TYPE_VALUE_STRING = "string"; + public static final String TYPE_VALUE_OBJECT = "object"; - mapEntity(xContentBuilder, clazz, true, idFieldName, EMPTY); + private static SimpleTypeHolder SIMPLE_TYPE_HOLDER = new SimpleTypeHolder(); - return xContentBuilder.endObject().endObject().endObject(); - } + static XContentBuilder buildMapping(Class clazz, String indexType, String idFieldName) throws IOException { + XContentBuilder xContentBuilder = jsonBuilder().startObject().startObject(indexType).startObject(FIELD_PROPERTIES); - private static void mapEntity(XContentBuilder xContentBuilder, Class clazz, boolean isRootObject, String idFieldName, - String nestedObjectFieldName) throws IOException { + mapEntity(xContentBuilder, clazz, true, idFieldName, EMPTY); - java.lang.reflect.Field[] fields = clazz.getDeclaredFields(); + return xContentBuilder.endObject().endObject().endObject(); + } - if (!isRootObject && isAnyPropertyAnnotatedAsField(fields)) { - xContentBuilder.startObject(nestedObjectFieldName).field("type", "object").startObject("properties"); - } + private static void mapEntity(XContentBuilder xContentBuilder, Class clazz, boolean isRootObject, String idFieldName, + String nestedObjectFieldName) throws IOException { - for (java.lang.reflect.Field field : fields) { - if (isEntity(field)) { - mapEntity(xContentBuilder, field.getType(), false, EMPTY, field.getName()); - } - Field fieldAnnotation = field.getAnnotation(Field.class); - if (isRootObject && fieldAnnotation != null && isIdField(field, idFieldName)) { - applyDefaultIdFieldMapping(xContentBuilder, field); - } else if (fieldAnnotation != null) { - applyFieldAnnotationMapping(xContentBuilder, field, fieldAnnotation); - } - } + java.lang.reflect.Field[] fields = clazz.getDeclaredFields(); - if (!isRootObject && isAnyPropertyAnnotatedAsField(fields)) { - xContentBuilder.endObject().endObject(); - } + if (!isRootObject && isAnyPropertyAnnotatedAsField(fields)) { + xContentBuilder.startObject(nestedObjectFieldName).field(FIELD_TYPE, TYPE_VALUE_OBJECT).startObject(FIELD_PROPERTIES); + } - } + for (java.lang.reflect.Field field : fields) { + if (isEntity(field)) { + mapEntity(xContentBuilder, field.getType(), false, EMPTY, field.getName()); + } + Field fieldAnnotation = field.getAnnotation(Field.class); + if (isRootObject && fieldAnnotation != null && isIdField(field, idFieldName)) { + applyDefaultIdFieldMapping(xContentBuilder, field); + } else if (fieldAnnotation != null) { + if ((fieldAnnotation.sortable() || fieldAnnotation.facetable()) && TYPE_VALUE_STRING.equals(fieldAnnotation.type())) { + addMultiFieldMapping(xContentBuilder, field, fieldAnnotation); + } else { + addSimpleFieldMapping(xContentBuilder, field, fieldAnnotation); + } + } + } - private static void applyDefaultIdFieldMapping(XContentBuilder xContentBuilder, java.lang.reflect.Field field) - throws IOException { - xContentBuilder.startObject(field.getName()).field("type", "string").field("index", "not_analyzed").endObject(); - } + if (!isRootObject && isAnyPropertyAnnotatedAsField(fields)) { + xContentBuilder.endObject().endObject(); + } - private static void applyFieldAnnotationMapping(XContentBuilder xContentBuilder, java.lang.reflect.Field field, - Field fieldAnnotation) throws IOException { - xContentBuilder.startObject(field.getName()); - xContentBuilder.field("store", fieldAnnotation.store()); - if (isNotBlank(fieldAnnotation.type())) { - xContentBuilder.field("type", fieldAnnotation.type()); - } - if (isNotBlank(fieldAnnotation.index())) { - xContentBuilder.field("index", fieldAnnotation.index()); - } - if (isNotBlank(fieldAnnotation.searchAnalyzer())) { - xContentBuilder.field("search_analyzer", fieldAnnotation.searchAnalyzer()); - } - if (isNotBlank(fieldAnnotation.indexAnalyzer())) { - xContentBuilder.field("index_analyzer", fieldAnnotation.indexAnalyzer()); - } - xContentBuilder.endObject(); - } + } - private static boolean isEntity(java.lang.reflect.Field field) { - TypeInformation typeInformation = ClassTypeInformation.from(field.getType()); - TypeInformation actualType = typeInformation.getActualType(); - boolean isComplexType = actualType == null ? false : !SIMPLE_TYPE_HOLDER.isSimpleType(actualType.getType()); - return isComplexType && !actualType.isCollectionLike() && !Map.class.isAssignableFrom(typeInformation.getType()); - } + private static void applyDefaultIdFieldMapping(XContentBuilder xContentBuilder, java.lang.reflect.Field field) + throws IOException { + xContentBuilder.startObject(field.getName()) + .field(FIELD_TYPE, TYPE_VALUE_STRING) + .field(FIELD_INDEX, INDEX_VALUE_NOT_ANALYZED); + xContentBuilder.endObject(); + } - private static boolean isAnyPropertyAnnotatedAsField(java.lang.reflect.Field[] fields) { - if (fields != null) { - for (java.lang.reflect.Field field : fields) { - if (field.isAnnotationPresent(Field.class)) { - return true; - } - } - } - return false; - } + /** + * Apply mapping for a single @Field annotation + * + * @param xContentBuilder + * @param field + * @param fieldAnnotation + * @throws IOException + */ + private static void addSimpleFieldMapping(XContentBuilder xContentBuilder, java.lang.reflect.Field field, + Field fieldAnnotation) throws IOException { + xContentBuilder.startObject(field.getName()); + xContentBuilder.field(FIELD_STORE, fieldAnnotation.store()); + if (isNotBlank(fieldAnnotation.type())) { + xContentBuilder.field(FIELD_TYPE, fieldAnnotation.type()); + } + if (isNotBlank(fieldAnnotation.index())) { + xContentBuilder.field(FIELD_INDEX, fieldAnnotation.index()); + } + if (isNotBlank(fieldAnnotation.searchAnalyzer())) { + xContentBuilder.field(FIELD_SEARCH_ANALYZER, fieldAnnotation.searchAnalyzer()); + } + if (isNotBlank(fieldAnnotation.indexAnalyzer())) { + xContentBuilder.field(FIELD_INDEX_ANALYZER, fieldAnnotation.indexAnalyzer()); + } + xContentBuilder.endObject(); + } - private static boolean isIdField(java.lang.reflect.Field field, String idFieldName) { - return idFieldName.equals(field.getName()); - } + /** + * Multi field mappings for string type fields, support for sorts and facets + * + * @param builder + * @param field + * @param annotation + * @throws IOException + */ + private static void addMultiFieldMapping(XContentBuilder builder, java.lang.reflect.Field field, + Field annotation) throws IOException { + builder.startObject(field.getName()); + builder.field(FIELD_TYPE, "multi_field"); + builder.startObject("fields"); + //add standard field + addSimpleFieldMapping(builder, field, annotation); + //facet field - untouched, not analise, stored + if (annotation.facetable()) { + addFacetMapping(builder, field, annotation); + } + //sort field - lowercase, not analise, stored + if (annotation.sortable()) { + addSortMapping(builder, field, annotation); + } + builder.endObject(); + builder.endObject(); + } + + /** + * Facet field for string type, for other types we don't need it(long, int, double, float) + * + * @param builder + * @param field + * @param annotation + * @throws IOException + */ + private static void addFacetMapping(XContentBuilder builder, java.lang.reflect.Field field, Field annotation) throws IOException { + builder.startObject(FacetRequest.FIELD_UNTOUCHED) + .field(FIELD_TYPE, TYPE_VALUE_STRING) + .field(FIELD_INDEX, INDEX_VALUE_NOT_ANALYZED) + .field(FIELD_STORE, true); + builder.endObject(); + } + + /** + * Sort field for string type, for other types we don't need it(long, int, double, float) + * value of the field should be converted to lowercase and not analise + * + * @param builder + * @param field + * @param annotation + * @throws IOException + */ + private static void addSortMapping(XContentBuilder builder, java.lang.reflect.Field field, Field annotation) throws IOException { + builder.startObject(FacetRequest.FIELD_SORT) + .field(FIELD_TYPE, TYPE_VALUE_STRING) + .field(FIELD_INDEX, "keyword") + .field(FIELD_STORE, true); + builder.endObject(); + } + + private static boolean isEntity(java.lang.reflect.Field field) { + TypeInformation typeInformation = ClassTypeInformation.from(field.getType()); + TypeInformation actualType = typeInformation.getActualType(); + boolean isComplexType = actualType == null ? false : !SIMPLE_TYPE_HOLDER.isSimpleType(actualType.getType()); + return isComplexType && !actualType.isCollectionLike() && !Map.class.isAssignableFrom(typeInformation.getType()); + } + + private static boolean isAnyPropertyAnnotatedAsField(java.lang.reflect.Field[] fields) { + if (fields != null) { + for (java.lang.reflect.Field field : fields) { + if (field.isAnnotationPresent(Field.class)) { + return true; + } + } + } + return false; + } + + private static boolean isIdField(java.lang.reflect.Field field, String idFieldName) { + return idFieldName.equals(field.getName()); + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ResultsMapper.java b/src/main/java/org/springframework/data/elasticsearch/core/ResultsMapper.java index b3e0a569..c1249228 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ResultsMapper.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ResultsMapper.java @@ -25,11 +25,12 @@ import org.springframework.data.domain.Page; * * @author Rizwan Idrees * @author Mohsin Husen + * @author Artur Konczak * */ public interface ResultsMapper { - Page mapResults(SearchResponse response); + FacetedPage mapResults(SearchResponse response); } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/facet/AbstactFacetResult.java b/src/main/java/org/springframework/data/elasticsearch/core/facet/AbstactFacetResult.java new file mode 100644 index 00000000..23d29ce6 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/facet/AbstactFacetResult.java @@ -0,0 +1,31 @@ +package org.springframework.data.elasticsearch.core.facet; + +import org.springframework.util.Assert; + +/** + * @author Rizwan Idrees + * @author Mohsin Husen + * @author Artur Konczak + * @author Jonathan Yan + */ +public class AbstactFacetResult implements FacetResult { + + private final String name; + private final FacetType type; + + protected AbstactFacetResult(String name, FacetType type) { + Assert.hasText(name, "Facet name can't be null and should have a value"); + this.name = name; + this.type = type; + } + + @Override + public String getName() { + return name; + } + + @Override + public FacetType getType() { + return type; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/facet/FacetMapper.java b/src/main/java/org/springframework/data/elasticsearch/core/facet/FacetMapper.java new file mode 100644 index 00000000..de44dab4 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/facet/FacetMapper.java @@ -0,0 +1,29 @@ +package org.springframework.data.elasticsearch.core.facet; + +import org.elasticsearch.search.facet.Facet; +import org.elasticsearch.search.facet.terms.TermsFacet; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Artur Konczak + */ +public class FacetMapper { + + public static FacetResult parse(Facet facet){ + if(facet instanceof TermsFacet){ + return parseTerm((TermsFacet) facet); + } + return null; + } + + private static FacetResult parseTerm(TermsFacet facet) { + List terms = new ArrayList(); + for(TermsFacet.Entry entry:facet.getEntries()){ + terms.add(new Term(entry.getTerm().toString(),entry.getCount())); + } + return new TermResult(facet.getName(),terms); + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/facet/FacetResult.java b/src/main/java/org/springframework/data/elasticsearch/core/facet/FacetResult.java new file mode 100644 index 00000000..e1d17825 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/facet/FacetResult.java @@ -0,0 +1,18 @@ +package org.springframework.data.elasticsearch.core.facet; + +/** + * Generic interface for all facets + * + * @author Rizwan Idrees + * @author Mohsin Husen + * @author Artur Konczak + * @author Jonathan Yan + * + */ +public interface FacetResult { + + String getName(); + + FacetType getType(); + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/facet/FacetType.java b/src/main/java/org/springframework/data/elasticsearch/core/facet/FacetType.java new file mode 100644 index 00000000..e3c8b664 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/facet/FacetType.java @@ -0,0 +1,7 @@ +package org.springframework.data.elasticsearch.core.facet; + +public enum FacetType { + + term, renage, histogram + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/facet/Term.java b/src/main/java/org/springframework/data/elasticsearch/core/facet/Term.java new file mode 100644 index 00000000..964abfb8 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/facet/Term.java @@ -0,0 +1,28 @@ +package org.springframework.data.elasticsearch.core.facet; + +/** + * Single term + * + * @author Rizwan Idrees + * @author Mohsin Husen + * @author Artur Konczak + * @author Jonathan Yan + */ +public class Term { + + private String term; + private int count; + + public Term(String term, int count) { + this.term = term; + this.count = count; + } + + public String getTerm() { + return term; + } + + public int getCount() { + return count; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/facet/TermResult.java b/src/main/java/org/springframework/data/elasticsearch/core/facet/TermResult.java new file mode 100644 index 00000000..8efe719a --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/facet/TermResult.java @@ -0,0 +1,28 @@ +package org.springframework.data.elasticsearch.core.facet; + +import org.springframework.util.Assert; + +import java.util.List; + +/** + * Basic term facet result + * + * @author Rizwan Idrees + * @author Mohsin Husen + * @author Artur Konczak + * @author Jonathan Yan + */ +public class TermResult extends AbstactFacetResult { + + private List terms; + + public TermResult(String name, List terms) { + super(name, FacetType.term); + this.terms = terms; + } + + public List getTerms() { + return terms; + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/AbstractFacetRequest.java b/src/main/java/org/springframework/data/elasticsearch/core/query/AbstractFacetRequest.java new file mode 100644 index 00000000..d3ab6f0f --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/AbstractFacetRequest.java @@ -0,0 +1,30 @@ +package org.springframework.data.elasticsearch.core.query; + +import org.springframework.util.Assert; + +/** + * @author Artur Konczak + */ +public abstract class AbstractFacetRequest implements FacetRequest { + + private String name; + private boolean applyQueryFilter; + + public AbstractFacetRequest(String name) { + Assert.hasText(name, "Facet can't be null or empty !!!"); + this.name = name; + } + + protected String getName(){ + return name; + } + + public void setApplyQueryFilter(boolean applyQueryFilter) { + this.applyQueryFilter = applyQueryFilter; + } + + @Override + public boolean applyQueryFilter() { + return applyQueryFilter; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/FacetRequest.java b/src/main/java/org/springframework/data/elasticsearch/core/query/FacetRequest.java new file mode 100644 index 00000000..45976a9c --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/FacetRequest.java @@ -0,0 +1,17 @@ +package org.springframework.data.elasticsearch.core.query; + +import org.elasticsearch.search.facet.FacetBuilder; + +/** + * @author Artur Koczak + */ +public interface FacetRequest { + + public static final String FIELD_UNTOUCHED = "untouched"; + public static final String FIELD_SORT = "sort"; + + FacetBuilder getFacet(); + + boolean applyQueryFilter(); + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/NativeFacetRequest.java b/src/main/java/org/springframework/data/elasticsearch/core/query/NativeFacetRequest.java new file mode 100644 index 00000000..fd8e9a01 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/NativeFacetRequest.java @@ -0,0 +1,31 @@ +package org.springframework.data.elasticsearch.core.query; + +import org.elasticsearch.search.facet.FacetBuilder; + +/** + * @author Artur Konczak + */ +public class NativeFacetRequest implements FacetRequest { + + private FacetBuilder facet; + private boolean applyQueryFilter; + + public NativeFacetRequest(FacetBuilder facet) { + this(facet, false); + } + + public NativeFacetRequest(FacetBuilder facet, boolean applyQueryFilter) { + this.facet = facet; + this.applyQueryFilter = applyQueryFilter; + } + + @Override + public FacetBuilder getFacet() { + return facet; + } + + @Override + public boolean applyQueryFilter() { + return applyQueryFilter; + } +} 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 86596e2b..2f179c4f 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 @@ -17,8 +17,12 @@ package org.springframework.data.elasticsearch.core.query; import org.elasticsearch.index.query.FilterBuilder; import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.search.facet.FacetBuilder; import org.elasticsearch.search.sort.SortBuilder; +import java.util.ArrayList; +import java.util.List; + /** * NativeSearchQuery * @@ -31,6 +35,17 @@ public class NativeSearchQuery extends AbstractQuery implements SearchQuery { private QueryBuilder query; private FilterBuilder filter; private SortBuilder sort; + private List facets; + + + public NativeSearchQuery(QueryBuilder query) { + this.query = query; + } + + public NativeSearchQuery(QueryBuilder query, FilterBuilder filter) { + this.query = query; + this.filter = filter; + } public NativeSearchQuery(QueryBuilder query, FilterBuilder filter, SortBuilder sort) { this.query = query; @@ -49,4 +64,20 @@ public class NativeSearchQuery extends AbstractQuery implements SearchQuery { public SortBuilder getElasticsearchSort() { return sort; } + + public void addFacet(FacetRequest facetRequest){ + if(facets==null){ + facets = new ArrayList(); + } + facets.add(facetRequest); + } + + public void setFacets(List facets){ + this.facets = facets; + } + + @Override + public List getFacets() { + return facets; + } } 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 b31573d7..6047023b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQueryBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/NativeSearchQueryBuilder.java @@ -15,14 +15,18 @@ */ package org.springframework.data.elasticsearch.core.query; +import org.apache.commons.collections.CollectionUtils; import org.elasticsearch.index.query.FilterBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.search.sort.SortBuilder; import org.springframework.data.domain.Pageable; +import java.util.ArrayList; +import java.util.List; + /** * NativeSearchQuery - * + * * @author Rizwan Idrees * @author Mohsin Husen * @author Artur Konczak @@ -30,63 +34,72 @@ import org.springframework.data.domain.Pageable; public class NativeSearchQueryBuilder { - private QueryBuilder queryBuilder; - private FilterBuilder filterBuilder; - private SortBuilder sortBuilder; - private Pageable pageable; - private String[] indices; - private String[] types; - private String[] fields; + private QueryBuilder queryBuilder; + private FilterBuilder filterBuilder; + private SortBuilder sortBuilder; + private List facetRequests = new ArrayList(); + private Pageable pageable; + private String[] indices; + private String[] types; + private String[] fields; - public NativeSearchQueryBuilder withQuery(QueryBuilder queryBuilder) { - this.queryBuilder = queryBuilder; - return this; - } + public NativeSearchQueryBuilder withQuery(QueryBuilder queryBuilder) { + this.queryBuilder = queryBuilder; + return this; + } - public NativeSearchQueryBuilder withFilter(FilterBuilder filterBuilder) { - this.filterBuilder = filterBuilder; - return this; - } + public NativeSearchQueryBuilder withFilter(FilterBuilder filterBuilder) { + this.filterBuilder = filterBuilder; + return this; + } - public NativeSearchQueryBuilder withSort(SortBuilder sortBuilder) { - this.sortBuilder = sortBuilder; - return this; - } + public NativeSearchQueryBuilder withSort(SortBuilder sortBuilder) { + this.sortBuilder = sortBuilder; + return this; + } - public NativeSearchQueryBuilder withPageable(Pageable pageable) { - this.pageable = pageable; - return this; - } + public NativeSearchQueryBuilder withFacet(FacetRequest facetRequest) { + facetRequests.add(facetRequest); + return this; + } - public NativeSearchQueryBuilder withIndices(String... indices) { - this.indices = indices; - return this; - } + public NativeSearchQueryBuilder withPageable(Pageable pageable) { + this.pageable = pageable; + return this; + } - public NativeSearchQueryBuilder withTypes(String... types) { - this.types = types; - return this; - } + public NativeSearchQueryBuilder withIndices(String... indices) { + this.indices = indices; + return this; + } - public NativeSearchQueryBuilder withFields(String... fields) { - this.fields = fields; - return this; - } + public NativeSearchQueryBuilder withTypes(String... types) { + this.types = types; + return this; + } - public NativeSearchQuery build() { - NativeSearchQuery nativeSearchQuery = new NativeSearchQuery(queryBuilder, filterBuilder, sortBuilder); - if (pageable != null) { - nativeSearchQuery.setPageable(pageable); - } - if (indices != null) { - nativeSearchQuery.addIndices(indices); - } - if (types != null) { - nativeSearchQuery.addTypes(types); - } - if (fields != null) { - nativeSearchQuery.addFields(fields); - } - return nativeSearchQuery; - } + public NativeSearchQueryBuilder withFields(String... fields) { + this.fields = fields; + return this; + } + + public NativeSearchQuery build() { + NativeSearchQuery nativeSearchQuery = new NativeSearchQuery(queryBuilder, filterBuilder, sortBuilder); + if (pageable != null) { + nativeSearchQuery.setPageable(pageable); + } + if (indices != null) { + nativeSearchQuery.addIndices(indices); + } + if (types != null) { + nativeSearchQuery.addTypes(types); + } + if (fields != null) { + nativeSearchQuery.addFields(fields); + } + if (CollectionUtils.isNotEmpty(facetRequests)) { + nativeSearchQuery.setFacets(facetRequests); + } + return nativeSearchQuery; + } } 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 cbd53581..5c8463fa 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 @@ -17,8 +17,11 @@ package org.springframework.data.elasticsearch.core.query; import org.elasticsearch.index.query.FilterBuilder; import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.search.facet.FacetBuilder; import org.elasticsearch.search.sort.SortBuilder; +import java.util.List; + /** * NativeSearchQuery * @@ -32,4 +35,6 @@ public interface SearchQuery extends Query { FilterBuilder getFilter(); SortBuilder getElasticsearchSort(); + + List getFacets(); } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/TermFacetRequest.java b/src/main/java/org/springframework/data/elasticsearch/core/query/TermFacetRequest.java new file mode 100644 index 00000000..c19d7b67 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/TermFacetRequest.java @@ -0,0 +1,83 @@ +package org.springframework.data.elasticsearch.core.query; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang.ArrayUtils; +import org.elasticsearch.search.facet.FacetBuilder; +import org.elasticsearch.search.facet.FacetBuilders; +import org.elasticsearch.search.facet.terms.TermsFacet; +import org.elasticsearch.search.facet.terms.TermsFacetBuilder; +import org.springframework.util.Assert; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Basic term facet + * + * @author Artur Konczak + */ +public class TermFacetRequest extends AbstractFacetRequest { + + private String[] stringFields; + private String[] numberFields; + private int size = 10; + private TermsFacet.ComparatorType order; + + public TermFacetRequest(String name) { + super(name); + } + + public void setStringFields(String... fields) { + this.stringFields = fields; + } + + public void setNumberFields(String... fields) { + this.numberFields = fields; + } + + public void setSize(int size) { + Assert.isTrue(size <= 0, "Size should be bigger then zero !!!"); + this.size = size; + } + + public void ascTerm() { + order = TermsFacet.ComparatorType.TERM; + } + + public void descTerm() { + order = TermsFacet.ComparatorType.REVERSE_TERM; + } + + public void ascCount() { + order = TermsFacet.ComparatorType.REVERSE_COUNT; + } + + public void descCount() { + order = TermsFacet.ComparatorType.COUNT; + } + + private List convertStringFieldsToFullNames() { + List result = new ArrayList(); + if (ArrayUtils.isNotEmpty(stringFields)) { + for (String stringField : stringFields) { + result.add(stringField + "." + FIELD_UNTOUCHED); + } + } + if (ArrayUtils.isNotEmpty(numberFields)) { + Collections.addAll(result, numberFields); + } + return result; + } + + @Override + public FacetBuilder getFacet() { + List fields = convertStringFieldsToFullNames(); + Assert.notEmpty(fields, "Please select at last one field !!!"); + TermsFacetBuilder builder = FacetBuilders.termsFacet(getName()).fields(fields.toArray(new String[fields.size()])).size(size); + if (order != null) { + builder.order(order); + } + return builder; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/TermFacetRequestBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/query/TermFacetRequestBuilder.java new file mode 100644 index 00000000..19b0cbbb --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/TermFacetRequestBuilder.java @@ -0,0 +1,59 @@ +package org.springframework.data.elasticsearch.core.query; + +/** + * Basic term facet + * + * @author Artur Konczak + */ +public class TermFacetRequestBuilder { + + private TermFacetRequest result; + + public TermFacetRequestBuilder(String name) { + result = new TermFacetRequest(name); + } + + public TermFacetRequestBuilder withStringFields(String... fields) { + result.setStringFields(fields); + return this; + } + + public TermFacetRequestBuilder withNumberFields(String... fields) { + result.setNumberFields(fields); + return this; + } + + public TermFacetRequestBuilder withSize(int size) { + result.setSize(size); + return this; + } + + public TermFacetRequestBuilder ascTerm() { + result.ascTerm(); + return this; + } + + public TermFacetRequestBuilder descTerm() { + result.descTerm(); + return this; + } + + public TermFacetRequestBuilder ascCount() { + result.ascCount(); + return this; + } + + public TermFacetRequestBuilder descCount() { + result.descCount(); + return this; + } + + public TermFacetRequestBuilder applyQueryFilter() { + result.setApplyQueryFilter(true); + return this; + } + + public TermFacetRequest build() { + return result; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/ElasticsearchRepository.java b/src/main/java/org/springframework/data/elasticsearch/repository/ElasticsearchRepository.java index 352a6891..3ea08286 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/ElasticsearchRepository.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/ElasticsearchRepository.java @@ -18,6 +18,7 @@ package org.springframework.data.elasticsearch.repository; import org.elasticsearch.index.query.QueryBuilder; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.elasticsearch.core.FacetedPage; import org.springframework.data.elasticsearch.core.query.SearchQuery; import org.springframework.data.repository.NoRepositoryBean; @@ -37,9 +38,9 @@ public interface ElasticsearchRepository extends Ela Iterable search(QueryBuilder query); - Page search(QueryBuilder query, Pageable pageable); + FacetedPage search(QueryBuilder query, Pageable pageable); - Page search(SearchQuery searchQuery); + FacetedPage search(SearchQuery searchQuery); - Page searchSimilar(T entity, SearchQuery searchQuery); + Page searchSimilar(T entity, SearchQuery searchQuery); } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/AbstractElasticsearchRepository.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/AbstractElasticsearchRepository.java index 11f135a0..6a3868f8 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/AbstractElasticsearchRepository.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/AbstractElasticsearchRepository.java @@ -19,6 +19,7 @@ import org.elasticsearch.index.query.QueryBuilder; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.*; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.FacetedPage; import org.springframework.data.elasticsearch.core.query.*; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.util.Assert; @@ -180,13 +181,13 @@ public abstract class AbstractElasticsearchRepository search(QueryBuilder query, Pageable pageable) { + public FacetedPage search(QueryBuilder query, Pageable pageable) { SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(query).withPageable(pageable).build(); return elasticsearchOperations.queryForPage(searchQuery, getEntityClass()); } @Override - public Page search(SearchQuery query) { + public FacetedPage search(SearchQuery query) { return elasticsearchOperations.queryForPage(query, getEntityClass()); } diff --git a/src/test/java/org/springframework/data/elasticsearch/Article.java b/src/test/java/org/springframework/data/elasticsearch/Article.java new file mode 100644 index 00000000..f7b933d9 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/Article.java @@ -0,0 +1,66 @@ +package org.springframework.data.elasticsearch; + +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; + +import java.util.ArrayList; +import java.util.List; + +/** + * Simple type to test facets + */ +@Document(indexName = "articles", type = "article", shards = 1, replicas = 0, refreshInterval = "-1") +public class Article { + + @Id + private String id; + + private String title; + + @Field(type = "string", facetable = true) + private List authors = new ArrayList(); + + @Field(type = "integer", facetable = true) + private List publishedYears = new ArrayList(); + + public Article() { + + } + + public Article(String id) { + this.id = id; + } + + public void setId(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getAuthors() { + return authors; + } + + public void setAuthors(List authors) { + this.authors = authors; + } + + public List getPublishedYears() { + return publishedYears; + } + + public void setPublishedYears(List publishedYears) { + this.publishedYears = publishedYears; + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/ArticleBuilder.java b/src/test/java/org/springframework/data/elasticsearch/ArticleBuilder.java new file mode 100644 index 00000000..16a522a4 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/ArticleBuilder.java @@ -0,0 +1,46 @@ +package org.springframework.data.elasticsearch; + +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.core.query.IndexQuery; + +import java.util.ArrayList; + +/** + * Simple type to test facets + */ +public class ArticleBuilder { + + private Article resutl; + + public ArticleBuilder(String id) { + resutl = new Article(id); + } + + public ArticleBuilder title(String title) { + resutl.setTitle(title); + return this; + } + + public ArticleBuilder addAuthor(String author) { + resutl.getAuthors().add(author); + return this; + } + + public ArticleBuilder addPublishedYear(Integer year) { + resutl.getPublishedYears().add(year); + return this; + } + + public Article build() { + return resutl; + } + + public IndexQuery buildIndex(){ + IndexQuery indexQuery = new IndexQuery(); + indexQuery.setId(resutl.getId()); + indexQuery.setObject(resutl); + return indexQuery; + } + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateFacetTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateFacetTests.java new file mode 100644 index 00000000..9da718a3 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateFacetTests.java @@ -0,0 +1,350 @@ +/* + * 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; + +import org.elasticsearch.index.query.FilterBuilders; +import org.elasticsearch.search.facet.FacetBuilders; +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.Article; +import org.springframework.data.elasticsearch.ArticleBuilder; +import org.springframework.data.elasticsearch.core.facet.Term; +import org.springframework.data.elasticsearch.core.facet.TermResult; +import org.springframework.data.elasticsearch.core.query.*; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.elasticsearch.index.query.FilterBuilders.termFilter; +import static org.elasticsearch.index.query.QueryBuilders.fieldQuery; +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +/** + * @author Rizwan Idrees + * @author Mohsin Husen + * @author Jonathan Yan + * @author Artur Konczak + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:elasticsearch-template-test.xml") +public class ElasticsearchTemplateFacetTests { + + public static final String RIZWAN_IDREES = "Rizwan Idrees"; + public static final String MOHSIN_HUSEN = "Mohsin Husen"; + public static final String JONATHAN_YAN = "Jonathan Yan"; + public static final String ARTUR_KONCZAK = "Artur Konczak"; + public static final int YEAR_2002 = 2002; + public static final int YEAR_2001 = 2001; + public static final int YEAR_2000 = 2000; + @Autowired + private ElasticsearchTemplate elasticsearchTemplate; + + @Before + public void before() { + elasticsearchTemplate.deleteIndex(Article.class); + elasticsearchTemplate.createIndex(Article.class); + elasticsearchTemplate.putMapping(Article.class); + elasticsearchTemplate.refresh(Article.class, true); + + IndexQuery article1 = new ArticleBuilder("1").title("article four").addAuthor(RIZWAN_IDREES).addAuthor(ARTUR_KONCZAK).addAuthor(MOHSIN_HUSEN).addAuthor(JONATHAN_YAN).buildIndex(); + IndexQuery article2 = new ArticleBuilder("2").title("article three").addAuthor(RIZWAN_IDREES).addAuthor(ARTUR_KONCZAK).addAuthor(MOHSIN_HUSEN).addPublishedYear(YEAR_2000).buildIndex(); + IndexQuery article3 = new ArticleBuilder("3").title("article two").addAuthor(RIZWAN_IDREES).addAuthor(ARTUR_KONCZAK).addPublishedYear(YEAR_2001).addPublishedYear(YEAR_2000).buildIndex(); + IndexQuery article4 = new ArticleBuilder("4").title("article one").addAuthor(RIZWAN_IDREES).addPublishedYear(YEAR_2002).addPublishedYear(YEAR_2001).addPublishedYear(YEAR_2000).buildIndex(); + + elasticsearchTemplate.index(article1); + elasticsearchTemplate.index(article2); + elasticsearchTemplate.index(article3); + elasticsearchTemplate.index(article4); + elasticsearchTemplate.refresh(Article.class, true); + } + + @Test + public void shouldReturnFacetedAuthorsForGivenQueryWithDefaultOrder() { + + // given + String facetName = "fauthors"; + SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()).withFacet(new TermFacetRequestBuilder(facetName).withStringFields("authors").build()).build(); + // when + FacetedPage
result = elasticsearchTemplate.queryForPage(searchQuery, Article.class); + // then + assertThat(result.getNumberOfElements(), is(equalTo(4))); + + TermResult facet = (TermResult) result.getFacet(facetName); + Term term = facet.getTerms().get(0); + assertThat(term.getTerm(), is(RIZWAN_IDREES)); + assertThat(term.getCount(), is(4)); + + term = facet.getTerms().get(1); + assertThat(term.getTerm(), is(ARTUR_KONCZAK)); + assertThat(term.getCount(), is(3)); + + term = facet.getTerms().get(2); + assertThat(term.getTerm(), is(MOHSIN_HUSEN)); + assertThat(term.getCount(), is(2)); + + term = facet.getTerms().get(3); + assertThat(term.getTerm(), is(JONATHAN_YAN)); + assertThat(term.getCount(), is(1)); + } + + @Test + public void shouldReturnFacetedAuthorsForGivenFilteredQuery() { + + // given + String facetName = "fauthors"; + SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()) + .withFilter(FilterBuilders.notFilter(FilterBuilders.termFilter("title","four"))) + .withFacet(new TermFacetRequestBuilder(facetName).applyQueryFilter().withStringFields("authors").build()).build(); + // when + FacetedPage
result = elasticsearchTemplate.queryForPage(searchQuery, Article.class); + // then + assertThat(result.getNumberOfElements(), is(equalTo(3))); + + TermResult facet = (TermResult) result.getFacet(facetName); + Term term = facet.getTerms().get(0); + assertThat(term.getTerm(), is(RIZWAN_IDREES)); + assertThat(term.getCount(), is(3)); + + term = facet.getTerms().get(1); + assertThat(term.getTerm(), is(ARTUR_KONCZAK)); + assertThat(term.getCount(), is(2)); + + term = facet.getTerms().get(2); + assertThat(term.getTerm(), is(MOHSIN_HUSEN)); + assertThat(term.getCount(), is(1)); + } + + @Test + public void shouldReturnFacetedAuthorsForGivenQueryOrderedByTerm() { + + // given + String facetName = "fauthors"; + SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()) + .withFacet(new TermFacetRequestBuilder(facetName).withStringFields("authors").ascTerm().build()).build(); + // when + FacetedPage
result = elasticsearchTemplate.queryForPage(searchQuery, Article.class); + // then + assertThat(result.getNumberOfElements(), is(equalTo(4))); + + TermResult facet = (TermResult) result.getFacet(facetName); + Term term = facet.getTerms().get(0); + assertThat(term.getTerm(), is(ARTUR_KONCZAK)); + assertThat(term.getCount(), is(3)); + + term = facet.getTerms().get(1); + assertThat(term.getTerm(), is(JONATHAN_YAN)); + assertThat(term.getCount(), is(1)); + + term = facet.getTerms().get(2); + assertThat(term.getTerm(), is(MOHSIN_HUSEN)); + assertThat(term.getCount(), is(2)); + + term = facet.getTerms().get(3); + assertThat(term.getTerm(), is(RIZWAN_IDREES)); + assertThat(term.getCount(), is(4)); + + } + + @Test + public void shouldReturnFacetedAuthorsForGivenQueryOrderedByCountAsc() { + + // given + String facetName = "fauthors"; + SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()) + .withFacet(new TermFacetRequestBuilder(facetName).withStringFields("authors").ascCount().build()).build(); + // when + FacetedPage
result = elasticsearchTemplate.queryForPage(searchQuery, Article.class); + // then + assertThat(result.getNumberOfElements(), is(equalTo(4))); + + TermResult facet = (TermResult) result.getFacet(facetName); + Term term = facet.getTerms().get(0); + assertThat(term.getTerm(), is(JONATHAN_YAN)); + assertThat(term.getCount(), is(1)); + + term = facet.getTerms().get(1); + assertThat(term.getTerm(), is(MOHSIN_HUSEN)); + assertThat(term.getCount(), is(2)); + + term = facet.getTerms().get(2); + assertThat(term.getTerm(), is(ARTUR_KONCZAK)); + assertThat(term.getCount(), is(3)); + + term = facet.getTerms().get(3); + assertThat(term.getTerm(), is(RIZWAN_IDREES)); + assertThat(term.getCount(), is(4)); + } + + @Test + public void shouldReturnFacetedYearsForGivenQuery() { + + // given + String facetName = "fyears"; + SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()) + .withFacet(new TermFacetRequestBuilder(facetName).withNumberFields("publishedYears").descCount().build()).build(); + // when + FacetedPage
result = elasticsearchTemplate.queryForPage(searchQuery, Article.class); + // then + assertThat(result.getNumberOfElements(), is(equalTo(4))); + + TermResult facet = (TermResult) result.getFacet(facetName); + assertThat(facet.getTerms().size(), is(equalTo(3))); + + + Term term = facet.getTerms().get(0); + assertThat(term.getTerm(), is(Integer.toString(YEAR_2000))); + assertThat(term.getCount(), is(3)); + + term = facet.getTerms().get(1); + assertThat(term.getTerm(), is(Integer.toString(YEAR_2001))); + assertThat(term.getCount(), is(2)); + + term = facet.getTerms().get(2); + assertThat(term.getTerm(), is(Integer.toString(YEAR_2002))); + assertThat(term.getCount(), is(1)); + } + + + @Test + public void shouldReturnSingleFacetOverYearsAndAuthorsForGivenQuery() { + + // given + String facetName = "fyears"; + SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()) + .withFacet(new TermFacetRequestBuilder(facetName).withNumberFields("publishedYears").withStringFields("authors").ascTerm().build()).build(); + // when + FacetedPage
result = elasticsearchTemplate.queryForPage(searchQuery, Article.class); + // then + assertThat(result.getNumberOfElements(), is(equalTo(4))); + + TermResult facet = (TermResult) result.getFacet(facetName); + assertThat(facet.getTerms().size(), is(equalTo(7))); + + + Term term = facet.getTerms().get(0); + assertThat(term.getTerm(), is(Integer.toString(YEAR_2000))); + assertThat(term.getCount(), is(3)); + + term = facet.getTerms().get(1); + assertThat(term.getTerm(), is(Integer.toString(YEAR_2001))); + assertThat(term.getCount(), is(2)); + + term = facet.getTerms().get(2); + assertThat(term.getTerm(), is(Integer.toString(YEAR_2002))); + assertThat(term.getCount(), is(1)); + + term = facet.getTerms().get(3); + assertThat(term.getTerm(), is(ARTUR_KONCZAK)); + assertThat(term.getCount(), is(3)); + + term = facet.getTerms().get(4); + assertThat(term.getTerm(), is(JONATHAN_YAN)); + assertThat(term.getCount(), is(1)); + + term = facet.getTerms().get(5); + assertThat(term.getTerm(), is(MOHSIN_HUSEN)); + assertThat(term.getCount(), is(2)); + + term = facet.getTerms().get(6); + assertThat(term.getTerm(), is(RIZWAN_IDREES)); + assertThat(term.getCount(), is(4)); + + } + + @Test + public void shouldReturnFacetedYearsAndFacetedAuthorsForGivenQuery() { + + // given + String numberFacetName = "fAuthors"; + String stringFacetName = "fyears"; + SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()) + .withFacet(new TermFacetRequestBuilder(numberFacetName).withNumberFields("publishedYears").ascTerm().build()) + .withFacet(new TermFacetRequestBuilder(stringFacetName).withStringFields("authors").ascTerm().build()) + .build(); + // when + FacetedPage
result = elasticsearchTemplate.queryForPage(searchQuery, Article.class); + // then + assertThat(result.getNumberOfElements(), is(equalTo(4))); + + TermResult numberFacet = (TermResult) result.getFacet(numberFacetName); + assertThat(numberFacet.getTerms().size(), is(equalTo(3))); + + Term numberTerm = numberFacet.getTerms().get(0); + assertThat(numberTerm.getTerm(), is(Integer.toString(YEAR_2000))); + assertThat(numberTerm.getCount(), is(3)); + + numberTerm = numberFacet.getTerms().get(1); + assertThat(numberTerm.getTerm(), is(Integer.toString(YEAR_2001))); + assertThat(numberTerm.getCount(), is(2)); + + numberTerm = numberFacet.getTerms().get(2); + assertThat(numberTerm.getTerm(), is(Integer.toString(YEAR_2002))); + assertThat(numberTerm.getCount(), is(1)); + + + TermResult stringFacet = (TermResult) result.getFacet(stringFacetName); + Term stringTerm = stringFacet.getTerms().get(0); + assertThat(stringTerm.getTerm(), is(ARTUR_KONCZAK)); + assertThat(stringTerm.getCount(), is(3)); + + stringTerm = stringFacet.getTerms().get(1); + assertThat(stringTerm.getTerm(), is(JONATHAN_YAN)); + assertThat(stringTerm.getCount(), is(1)); + + stringTerm = stringFacet.getTerms().get(2); + assertThat(stringTerm.getTerm(), is(MOHSIN_HUSEN)); + assertThat(stringTerm.getCount(), is(2)); + + stringTerm = stringFacet.getTerms().get(3); + assertThat(stringTerm.getTerm(), is(RIZWAN_IDREES)); + assertThat(stringTerm.getCount(), is(4)); + } + + + @Test + public void shouldReturnFacetedYearsForNativeFacet() { + + // given + String facetName = "fyears"; + SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()) + .withFacet(new NativeFacetRequest(FacetBuilders.termsFacet(facetName).field("publishedYears"))).build(); + // when + FacetedPage
result = elasticsearchTemplate.queryForPage(searchQuery, Article.class); + // then + assertThat(result.getNumberOfElements(), is(equalTo(4))); + + TermResult facet = (TermResult) result.getFacet(facetName); + assertThat(facet.getTerms().size(), is(equalTo(3))); + + + Term term = facet.getTerms().get(0); + assertThat(term.getTerm(), is(Integer.toString(YEAR_2000))); + assertThat(term.getCount(), is(3)); + + term = facet.getTerms().get(1); + assertThat(term.getTerm(), is(Integer.toString(YEAR_2001))); + assertThat(term.getCount(), is(2)); + + term = facet.getTerms().get(2); + assertThat(term.getTerm(), is(Integer.toString(YEAR_2002))); + assertThat(term.getCount(), is(1)); + } + + +} 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 2ce11430..fde149a6 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java @@ -465,12 +465,12 @@ public class ElasticsearchTemplateTests { // when Page page = elasticsearchTemplate.queryForPage(searchQuery, new ResultsMapper() { @Override - public Page mapResults(SearchResponse response) { + public FacetedPage mapResults(SearchResponse response) { List values = new ArrayList(); for (SearchHit searchHit : response.getHits()) { values.add((String) searchHit.field("message").value()); } - return new PageImpl(values); + return new FacetedPageImpl(values); } }); // then @@ -543,7 +543,7 @@ public class ElasticsearchTemplateTests { while (hasRecords) { Page page = elasticsearchTemplate.scroll(scrollId, 5000L, new ResultsMapper() { @Override - public Page mapResults(SearchResponse response) { + public FacetedPage mapResults(SearchResponse response) { List chunk = new ArrayList(); for (SearchHit searchHit : response.getHits()) { if (response.getHits().getHits().length <= 0) { @@ -556,7 +556,7 @@ public class ElasticsearchTemplateTests { } if(chunk.size() > 0){ - return new PageImpl(chunk); + return new FacetedPageImpl(chunk); } return null; }