/**
* Copyright (c) 2000-present Liferay, Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*/
package com.liferay.portal.search.elasticsearch.internal;
import com.liferay.portal.configuration.metatype.bnd.util.ConfigurableUtil;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.search.BaseIndexSearcher;
import com.liferay.portal.kernel.search.Document;
import com.liferay.portal.kernel.search.DocumentImpl;
import com.liferay.portal.kernel.search.GeoDistanceSort;
import com.liferay.portal.kernel.search.GroupBy;
import com.liferay.portal.kernel.search.Hits;
import com.liferay.portal.kernel.search.HitsImpl;
import com.liferay.portal.kernel.search.IndexSearcher;
import com.liferay.portal.kernel.search.Query;
import com.liferay.portal.kernel.search.QueryConfig;
import com.liferay.portal.kernel.search.SearchContext;
import com.liferay.portal.kernel.search.SearchException;
import com.liferay.portal.kernel.search.Sort;
import com.liferay.portal.kernel.search.Stats;
import com.liferay.portal.kernel.search.StatsResults;
import com.liferay.portal.kernel.search.facet.Facet;
import com.liferay.portal.kernel.search.facet.collector.FacetCollector;
import com.liferay.portal.kernel.search.filter.FilterTranslator;
import com.liferay.portal.kernel.search.geolocation.GeoLocationPoint;
import com.liferay.portal.kernel.search.highlight.HighlightUtil;
import com.liferay.portal.kernel.search.query.QueryTranslator;
import com.liferay.portal.kernel.search.suggest.QuerySuggester;
import com.liferay.portal.kernel.util.ArrayUtil;
import com.liferay.portal.kernel.util.MapUtil;
import com.liferay.portal.kernel.util.StringBundler;
import com.liferay.portal.kernel.util.StringPool;
import com.liferay.portal.search.elasticsearch.configuration.ElasticsearchConfiguration;
import com.liferay.portal.search.elasticsearch.connection.ElasticsearchConnectionManager;
import com.liferay.portal.search.elasticsearch.facet.FacetProcessor;
import com.liferay.portal.search.elasticsearch.groupby.GroupByTranslator;
import com.liferay.portal.search.elasticsearch.index.IndexNameBuilder;
import com.liferay.portal.search.elasticsearch.internal.facet.CompositeFacetProcessor;
import com.liferay.portal.search.elasticsearch.internal.facet.FacetCollectorFactory;
import com.liferay.portal.search.elasticsearch.internal.pagination.Pagination;
import com.liferay.portal.search.elasticsearch.internal.util.DocumentTypes;
import com.liferay.portal.search.elasticsearch.stats.StatsTranslator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.apache.commons.lang.time.StopWatch;
import org.elasticsearch.action.search.SearchRequestBuilder;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.Client;
import org.elasticsearch.common.geo.GeoDistance;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.metrics.tophits.TopHits;
import org.elasticsearch.search.highlight.HighlightField;
import org.elasticsearch.search.sort.FieldSortBuilder;
import org.elasticsearch.search.sort.GeoDistanceSortBuilder;
import org.elasticsearch.search.sort.SortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
/**
* @author Michael C. Han
* @author Milen Dyankov
*/
@Component(
configurationPid = "com.liferay.portal.search.elasticsearch.configuration.ElasticsearchConfiguration",
immediate = true, property = {"search.engine.impl=Elasticsearch"},
service = IndexSearcher.class
)
public class ElasticsearchIndexSearcher extends BaseIndexSearcher {
@Override
public String getQueryString(SearchContext searchContext, Query query) {
QueryBuilder queryBuilder = queryTranslator.translate(
query, searchContext);
return queryBuilder.toString();
}
@Override
public Hits search(SearchContext searchContext, Query query)
throws SearchException {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
try {
Pagination pagination = new Pagination(
searchContext.getStart(), searchContext.getEnd());
Hits hits = null;
while (true) {
hits = doSearchHits(searchContext, query, pagination);
Document[] documents = hits.getDocs();
if (documents.length != 0) {
break;
}
Optional<Pagination> paginationOptional =
pagination.repageToLast(hits.getLength());
if (!paginationOptional.isPresent()) {
break;
}
pagination = paginationOptional.get();
}
hits.setStart(stopWatch.getStartTime());
return hits;
}
catch (Exception e) {
if (_log.isWarnEnabled()) {
_log.warn(e, e);
}
if (!_logExceptionsOnly) {
throw new SearchException(e.toString(), e);
}
return new HitsImpl();
}
finally {
if (_log.isInfoEnabled()) {
stopWatch.stop();
_log.info(
"Searching " + query.toString() + " took " +
stopWatch.getTime() + " ms");
}
}
}
@Override
public long searchCount(SearchContext searchContext, Query query)
throws SearchException {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
try {
return doSearchCount(searchContext, query);
}
catch (Exception e) {
if (_log.isWarnEnabled()) {
_log.warn(e, e);
}
if (!_logExceptionsOnly) {
throw new SearchException(e.getMessage(), e);
}
return 0;
}
finally {
if (_log.isInfoEnabled()) {
stopWatch.stop();
_log.info(
"Searching " + query.toString() + " took " +
stopWatch.getTime() + " ms");
}
}
}
@Override
@Reference(target = "(search.engine.impl=Elasticsearch)", unbind = "-")
public void setQuerySuggester(QuerySuggester querySuggester) {
super.setQuerySuggester(querySuggester);
}
@Activate
@Modified
protected void activate(Map<String, Object> properties) {
_elasticsearchConfiguration = ConfigurableUtil.createConfigurable(
ElasticsearchConfiguration.class, properties);
_logExceptionsOnly = _elasticsearchConfiguration.logExceptionsOnly();
}
protected void addFacets(
SearchRequestBuilder searchRequestBuilder,
SearchContext searchContext) {
Map<String, Facet> facetsMap = searchContext.getFacets();
for (Facet facet : facetsMap.values()) {
if (facet.isStatic()) {
continue;
}
facetProcessor.processFacet(searchRequestBuilder, facet);
}
}
protected void addGroupBy(
SearchRequestBuilder searchRequestBuilder, SearchContext searchContext,
Pagination pagination) {
GroupBy groupBy = searchContext.getGroupBy();
if (groupBy == null) {
return;
}
groupByTranslator.translate(
searchRequestBuilder, searchContext, pagination.getStart(),
pagination.getEnd());
}
protected void addHighlightedField(
SearchRequestBuilder searchRequestBuilder, QueryConfig queryConfig,
String fieldName) {
searchRequestBuilder.addHighlightedField(
fieldName, queryConfig.getHighlightFragmentSize(),
queryConfig.getHighlightSnippetSize());
String localizedFieldName = DocumentImpl.getLocalizedName(
queryConfig.getLocale(), fieldName);
searchRequestBuilder.addHighlightedField(
localizedFieldName, queryConfig.getHighlightFragmentSize(),
queryConfig.getHighlightSnippetSize());
}
protected void addHighlights(
SearchRequestBuilder searchRequestBuilder, QueryConfig queryConfig) {
if (!queryConfig.isHighlightEnabled()) {
return;
}
for (String highlightFieldName : queryConfig.getHighlightFieldNames()) {
addHighlightedField(
searchRequestBuilder, queryConfig, highlightFieldName);
}
searchRequestBuilder.setHighlighterPostTags(
HighlightUtil.HIGHLIGHT_TAG_CLOSE);
searchRequestBuilder.setHighlighterPreTags(
HighlightUtil.HIGHLIGHT_TAG_OPEN);
searchRequestBuilder.setHighlighterRequireFieldMatch(
queryConfig.isHighlightRequireFieldMatch());
}
protected void addPagination(
SearchRequestBuilder searchRequestBuilder, Pagination pagination) {
Optional<Integer> fromOptional = pagination.getFrom();
fromOptional.ifPresent(searchRequestBuilder::setFrom);
Optional<Integer> sizeOptional = pagination.getSize();
sizeOptional.ifPresent(searchRequestBuilder::setSize);
}
protected void addSelectedFields(
SearchRequestBuilder searchRequestBuilder, QueryConfig queryConfig) {
String[] selectedFieldNames = queryConfig.getSelectedFieldNames();
if (ArrayUtil.isEmpty(selectedFieldNames)) {
searchRequestBuilder.addField(StringPool.STAR);
}
else {
searchRequestBuilder.addFields(selectedFieldNames);
}
}
protected void addSnippets(
Document document, Set<String> queryTerms,
Map<String, HighlightField> highlightFields, String fieldName,
Locale locale) {
String snippet = StringPool.BLANK;
String localizedContentName = DocumentImpl.getLocalizedName(
locale, fieldName);
String snippetFieldName = localizedContentName;
HighlightField highlightField = highlightFields.get(
localizedContentName);
if (highlightField == null) {
highlightField = highlightFields.get(fieldName);
snippetFieldName = fieldName;
}
if (highlightField != null) {
Text[] texts = highlightField.fragments();
StringBundler sb = new StringBundler(texts.length * 2);
for (Text text : texts) {
sb.append(text);
sb.append(StringPool.TRIPLE_PERIOD);
}
sb.setIndex(sb.index() - 1);
snippet = sb.toString();
}
HighlightUtil.addSnippet(
document, queryTerms, snippet, snippetFieldName);
}
protected void addSnippets(
SearchHit hit, Document document, QueryConfig queryConfig,
Set<String> queryTerms) {
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if (MapUtil.isEmpty(highlightFields)) {
return;
}
for (String highlightFieldName : queryConfig.getHighlightFieldNames()) {
addSnippets(
document, queryTerms, highlightFields, highlightFieldName,
queryConfig.getLocale());
}
}
protected void addSort(
SearchRequestBuilder searchRequestBuilder, Sort[] sorts) {
if (ArrayUtil.isEmpty(sorts)) {
return;
}
Set<String> sortFieldNames = new HashSet<>(sorts.length);
for (Sort sort : sorts) {
if (sort == null) {
continue;
}
String sortFieldName = DocumentImpl.getSortFieldName(
sort, "_score");
if (sortFieldNames.contains(sortFieldName)) {
continue;
}
sortFieldNames.add(sortFieldName);
SortOrder sortOrder = SortOrder.ASC;
if (sort.isReverse() || sortFieldName.equals("_score")) {
sortOrder = SortOrder.DESC;
}
SortBuilder sortBuilder = null;
if (sortFieldName.equals("_score")) {
sortBuilder = SortBuilders.scoreSort();
}
else if (sort.getType() == Sort.GEO_DISTANCE_TYPE) {
GeoDistanceSort geoDistanceSort = (GeoDistanceSort)sort;
GeoDistanceSortBuilder geoDistanceSortBuilder =
SortBuilders.geoDistanceSort(sortFieldName);
geoDistanceSortBuilder.geoDistance(GeoDistance.DEFAULT);
for (GeoLocationPoint geoLocationPoint :
geoDistanceSort.getGeoLocationPoints()) {
geoDistanceSortBuilder.point(
geoLocationPoint.getLatitude(),
geoLocationPoint.getLongitude());
}
Collection<String> geoHashes = geoDistanceSort.getGeoHashes();
if (!geoHashes.isEmpty()) {
geoDistanceSort.addGeoHash(
geoHashes.toArray(new String[geoHashes.size()]));
}
sortBuilder = geoDistanceSortBuilder;
}
else {
FieldSortBuilder fieldSortBuilder = SortBuilders.fieldSort(
sortFieldName);
fieldSortBuilder.unmappedType("string");
sortBuilder = fieldSortBuilder;
}
sortBuilder.order(sortOrder);
searchRequestBuilder.addSort(sortBuilder);
}
}
protected void addStats(
SearchRequestBuilder searchRequestBuilder,
SearchContext searchContext) {
Map<String, Stats> statsMap = searchContext.getStats();
for (Stats stats : statsMap.values()) {
statsTranslator.translate(searchRequestBuilder, stats);
}
}
protected SearchResponse doSearch(
SearchContext searchContext, Query query, Pagination pagination,
boolean count)
throws Exception {
Client client = elasticsearchConnectionManager.getClient();
QueryConfig queryConfig = query.getQueryConfig();
SearchRequestBuilder searchRequestBuilder = client.prepareSearch(
getSelectedIndexNames(queryConfig, searchContext));
searchRequestBuilder.setTypes(getSelectedTypes(queryConfig));
addStats(searchRequestBuilder, searchContext);
if (!count) {
addFacets(searchRequestBuilder, searchContext);
addGroupBy(searchRequestBuilder, searchContext, pagination);
addHighlights(searchRequestBuilder, queryConfig);
addPagination(searchRequestBuilder, pagination);
addSelectedFields(searchRequestBuilder, queryConfig);
addSort(searchRequestBuilder, searchContext.getSorts());
searchRequestBuilder.setTrackScores(queryConfig.isScoreEnabled());
}
else {
searchRequestBuilder.setSize(0);
}
if (query.getPostFilter() != null) {
QueryBuilder postFilterQueryBuilder = filterTranslator.translate(
query.getPostFilter(), searchContext);
searchRequestBuilder.setPostFilter(postFilterQueryBuilder);
}
QueryBuilder queryBuilder = queryTranslator.translate(
query, searchContext);
if (query.getPreBooleanFilter() != null) {
QueryBuilder preFilterQueryBuilder = filterTranslator.translate(
query.getPreBooleanFilter(), searchContext);
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
boolQueryBuilder.filter(preFilterQueryBuilder);
boolQueryBuilder.must(queryBuilder);
queryBuilder = boolQueryBuilder;
}
searchRequestBuilder.setQuery(queryBuilder);
SearchResponse searchResponse = searchRequestBuilder.get();
if (_log.isInfoEnabled()) {
_log.info(
"The search engine processed " + queryBuilder.toString() +
" in " + searchResponse.getTook());
}
return searchResponse;
}
protected long doSearchCount(SearchContext searchContext, Query query)
throws Exception {
SearchResponse searchResponse = doSearch(
searchContext, query, null, true);
SearchHits searchHits = searchResponse.getHits();
return searchHits.getTotalHits();
}
protected Hits doSearchHits(
SearchContext searchContext, Query query, Pagination pagination)
throws Exception {
SearchResponse searchResponse = doSearch(
searchContext, query, pagination, false);
return processResponse(searchResponse, searchContext, query);
}
protected String[] getSelectedIndexNames(
QueryConfig queryConfig, SearchContext searchContext) {
String[] selectedIndexNames = queryConfig.getSelectedIndexNames();
if (ArrayUtil.isNotEmpty(selectedIndexNames)) {
return selectedIndexNames;
}
String indexName = indexNameBuilder.getIndexName(
searchContext.getCompanyId());
return new String[] {indexName};
}
protected String[] getSelectedTypes(QueryConfig queryConfig) {
String[] selectedTypes = queryConfig.getSelectedTypes();
if (ArrayUtil.isNotEmpty(selectedTypes)) {
return selectedTypes;
}
return new String[] {DocumentTypes.LIFERAY};
}
protected Hits processResponse(
SearchResponse searchResponse, SearchContext searchContext,
Query query) {
SearchHits searchHits = searchResponse.getHits();
Hits hits = new HitsImpl();
updateFacetCollectors(searchContext, searchResponse);
updateGroupedHits(searchResponse, searchContext, query, hits);
updateStatsResults(searchContext, searchResponse, hits);
TimeValue timeValue = searchResponse.getTook();
hits.setSearchTime((float)timeValue.getSecondsFrac());
return processSearchHits(searchHits, query, hits);
}
protected Document processSearchHit(
SearchHit searchHit, QueryConfig queryConfig) {
Document document = searchHitDocumentTranslator.translate(searchHit);
populateUID(document, queryConfig);
return document;
}
protected Hits processSearchHits(
SearchHits searchHits, Query query, Hits hits) {
List<Document> documents = new ArrayList<>();
Set<String> queryTerms = new HashSet<>();
List<Float> scores = new ArrayList<>();
if (searchHits.totalHits() > 0) {
SearchHit[] searchHitsArray = searchHits.getHits();
for (SearchHit searchHit : searchHitsArray) {
Document document = processSearchHit(
searchHit, query.getQueryConfig());
documents.add(document);
scores.add(searchHit.getScore());
addSnippets(
searchHit, document, query.getQueryConfig(), queryTerms);
}
}
hits.setDocs(documents.toArray(new Document[documents.size()]));
hits.setLength((int)searchHits.getTotalHits());
hits.setQuery(query);
hits.setQueryTerms(queryTerms.toArray(new String[queryTerms.size()]));
hits.setScores(ArrayUtil.toFloatArray(scores));
return hits;
}
protected void updateFacetCollectors(
SearchContext searchContext, SearchResponse searchResponse) {
Aggregations aggregations = searchResponse.getAggregations();
if (aggregations == null) {
return;
}
Map<String, Aggregation> aggregationsMap = aggregations.getAsMap();
Map<String, Facet> facetsMap = searchContext.getFacets();
for (Facet facet : facetsMap.values()) {
if (facet.isStatic()) {
continue;
}
FacetCollectorFactory facetCollectorFactory =
new FacetCollectorFactory();
FacetCollector facetCollector =
facetCollectorFactory.getFacetCollector(
aggregationsMap.get(facet.getFieldName()));
facet.setFacetCollector(facetCollector);
}
}
protected void updateGroupedHits(
SearchResponse searchResponse, SearchContext searchContext, Query query,
Hits hits) {
GroupBy groupBy = searchContext.getGroupBy();
if (groupBy == null) {
return;
}
Aggregations aggregations = searchResponse.getAggregations();
Map<String, Aggregation> aggregationsMap = aggregations.getAsMap();
Terms terms = (Terms)aggregationsMap.get(
GroupByTranslator.GROUP_BY_AGGREGATION_PREFIX + groupBy.getField());
List<Terms.Bucket> buckets = terms.getBuckets();
for (Terms.Bucket bucket : buckets) {
Aggregations bucketAggregations = bucket.getAggregations();
TopHits topHits = bucketAggregations.get(
GroupByTranslator.TOP_HITS_AGGREGATION_NAME);
SearchHits groupedSearchHits = topHits.getHits();
Hits groupedHits = new HitsImpl();
processSearchHits(groupedSearchHits, query, groupedHits);
groupedHits.setLength((int)groupedSearchHits.getTotalHits());
hits.addGroupedHits(bucket.getKeyAsString(), groupedHits);
}
}
protected void updateStatsResults(
SearchContext searchContext, SearchResponse searchResponse, Hits hits) {
Map<String, Stats> statsMap = searchContext.getStats();
if (statsMap.isEmpty()) {
return;
}
Aggregations aggregations = searchResponse.getAggregations();
if (aggregations == null) {
return;
}
Map<String, Aggregation> aggregationsMap = aggregations.getAsMap();
for (Stats stats : statsMap.values()) {
if (!stats.isEnabled()) {
continue;
}
StatsResults statsResults = statsTranslator.translate(
aggregationsMap, stats);
hits.addStatsResults(statsResults);
}
}
@Reference
protected ElasticsearchConnectionManager elasticsearchConnectionManager;
@Reference(service = CompositeFacetProcessor.class)
protected FacetProcessor<SearchRequestBuilder> facetProcessor;
@Reference(target = "(search.engine.impl=Elasticsearch)")
protected FilterTranslator<QueryBuilder> filterTranslator;
@Reference
protected GroupByTranslator groupByTranslator;
@Reference
protected IndexNameBuilder indexNameBuilder;
@Reference(target = "(search.engine.impl=Elasticsearch)")
protected QueryTranslator<QueryBuilder> queryTranslator;
@Reference
protected SearchHitDocumentTranslator searchHitDocumentTranslator;
@Reference
protected StatsTranslator statsTranslator;
private static final Log _log = LogFactoryUtil.getLog(
ElasticsearchIndexSearcher.class);
private volatile ElasticsearchConfiguration _elasticsearchConfiguration;
private boolean _logExceptionsOnly;
}