package gov.nysenate.openleg.dao.base; import com.google.common.base.Splitter; import com.google.common.primitives.Ints; import gov.nysenate.openleg.model.search.SearchException; import gov.nysenate.openleg.model.search.SearchResult; import gov.nysenate.openleg.model.search.SearchResults; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsRequest; import org.elasticsearch.action.bulk.BulkRequestBuilder; import org.elasticsearch.action.delete.DeleteAction; import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.delete.DeleteRequestBuilder; import org.elasticsearch.action.get.GetRequest; import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchType; import org.elasticsearch.client.Client; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.highlight.HighlightBuilder; import org.elasticsearch.search.rescore.RescoreBuilder; import org.elasticsearch.search.sort.SortBuilder; import org.elasticsearch.search.sort.SortBuilders; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import javax.annotation.PostConstruct; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Function; /** * Base class for Elastic Search layer classes to inherit common functionality from. */ public abstract class ElasticBaseDao { private static final Logger logger = LoggerFactory.getLogger(ElasticBaseDao.class); @Autowired protected Client searchClient; @PostConstruct private void init() { createIndices(); } /** --- Public methods --- */ public void createIndices() { getIndices().stream() .filter(index -> !indicesExist(index)) .forEach(this::createIndex); } public void purgeIndices() { getIndices().forEach(this::deleteIndex); } /** --- Abstract methods --- */ /** * Returns a list containing the names of all indices used by the inheriting Dao * * @return */ protected abstract List<String> getIndices(); /** --- Common Elastic Search methods --- */ /** * Generates a typical search request that involves a query, filter, sort string, and a limit + offset * @see #getSearchRequest(String, QueryBuilder, QueryBuilder, List, LimitOffset) * * Highlighting, rescoring, and full source response are not supported via this method. */ protected SearchRequestBuilder getSearchRequest(String indexName, QueryBuilder query, QueryBuilder postFilter, List<SortBuilder> sort, LimitOffset limitOffset) { return getSearchRequest(indexName, query, postFilter, null, null, sort, limitOffset, false); } /** * Generates a SearchRequest with support for various functions. * * @param indexName - The name of the index to search. * @param query - The QueryBuilder instance to perform the search with. * @param postFilter - Optional FilterBuilder to filter out the results. * @param highlightedFields - Optional list of field names to return as highlighted fields. * @param rescorer - Optional rescorer that can be used to fine tune the query ranking. * @param sort - List of SortBuilders specifying the desired sorting * @param limitOffset - Restrict the number of results returned as well as paginate. * @param fetchSource - Will return the indexed source fields when set to true * @return SearchRequestBuilder */ protected SearchRequestBuilder getSearchRequest(String indexName, QueryBuilder query, QueryBuilder postFilter, List<HighlightBuilder.Field> highlightedFields, RescoreBuilder.Rescorer rescorer, List<SortBuilder> sort, LimitOffset limitOffset, boolean fetchSource) { SearchRequestBuilder searchBuilder = searchClient.prepareSearch(indexName) .setSearchType(SearchType.QUERY_THEN_FETCH) .setQuery(query) .setRescorer(rescorer) .setFrom(limitOffset.getOffsetStart() - 1) .setSize((limitOffset.hasLimit()) ? limitOffset.getLimit() : Integer.MAX_VALUE) .setMinScore(0.05f) .setFetchSource(fetchSource); if (highlightedFields != null) { highlightedFields.stream().forEach(searchBuilder::addHighlightedField); } // if (rescorer != null) { // searchBuilder.addRescorer(rescorer); // } // Post filters take effect after the search is completed if (postFilter != null) { searchBuilder.setPostFilter(postFilter); } // Add the sort by fields sort.forEach(searchBuilder::addSort); logger.debug("{}", searchBuilder); return searchBuilder; } /** * Extracts search results from a search response * * template <R> is the desired return type * * @param response a SearchResponse generated by a SearchRequest * @param limitOffset the LimitOffset used in the SearchRequest * @param hitMapper a function that maps a SearchHit to the desired return type R * @return SearchResults<R> */ protected <R> SearchResults<R> getSearchResults(SearchResponse response, LimitOffset limitOffset, Function<SearchHit, R> hitMapper) { List<SearchResult<R>> resultList = new ArrayList<>(); for (SearchHit hit : response.getHits().hits()) { SearchResult<R> result = new SearchResult<>( hitMapper.apply(hit), // Result (!Float.isNaN(hit.getScore())) ? BigDecimal.valueOf(hit.getScore()) : BigDecimal.ONE, // Rank hit.getHighlightFields()); // Highlights resultList.add(result); } return new SearchResults<>(Ints.checkedCast(response.getHits().getTotalHits()), resultList, limitOffset); } /** * Performs a get request on the given index for the document designated by the given type and id * returns an optional that is empty if a document does not exist for the given request parameters * @param index String - a search index * @param type String - a search type * @param id String - the id of the desired document * @param responseMapper Function<GetResponse, T> - a function that maps the response to the desired class * @param <T> The type to be returned * @return Optional<T></T> */ protected <T> Optional<T> getRequest(String index, String type, String id, Function<GetResponse, T> responseMapper) { GetResponse getResponse = searchClient.prepareGet(index, type, id).execute().actionGet(); if (getResponse.isExists()) { return Optional.of(responseMapper.apply(getResponse)); } return Optional.empty(); } /** * Performs a bulk request execution while making sure that the bulk request is actually valid to * prevent exceptions. * @param bulkRequest BulkRequestBuilder */ protected void safeBulkRequestExecute(BulkRequestBuilder bulkRequest) { if (bulkRequest != null && bulkRequest.numberOfActions() > 0) { bulkRequest.execute().actionGet(); } } protected void deleteEntry(String indexName, String type, String id) { DeleteRequestBuilder request = searchClient.prepareDelete(); request.setIndex(indexName); request.setType(type); request.setId(id); request.execute().actionGet(); } protected boolean indicesExist(String... indices) { return searchClient.admin().indices().exists(new IndicesExistsRequest(indices)).actionGet().isExists(); } protected void createIndex(String indexName) { searchClient.admin().indices().prepareCreate(indexName).execute().actionGet(); } protected void deleteIndex(String index) { try { logger.info("Deleting search index {}", index); searchClient.admin().indices().delete(new DeleteIndexRequest(index)).actionGet(); } catch (IndexNotFoundException ex) { logger.info("Cannot delete index {} because it doesn't exist.", index); } } }