/** * 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.solr.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.Field; 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.RangeFacet; import com.liferay.portal.kernel.search.facet.collector.FacetCollector; import com.liferay.portal.kernel.search.facet.config.FacetConfiguration; import com.liferay.portal.kernel.search.filter.FilterTranslator; 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.GetterUtil; import com.liferay.portal.kernel.util.ListUtil; import com.liferay.portal.kernel.util.MapUtil; import com.liferay.portal.kernel.util.SetUtil; import com.liferay.portal.kernel.util.StringPool; import com.liferay.portal.kernel.util.StringUtil; import com.liferay.portal.kernel.util.Validator; import com.liferay.portal.search.solr.configuration.SolrConfiguration; import com.liferay.portal.search.solr.connection.SolrClientManager; import com.liferay.portal.search.solr.facet.FacetProcessor; import com.liferay.portal.search.solr.groupby.GroupByTranslator; import com.liferay.portal.search.solr.internal.facet.CompositeFacetProcessor; import com.liferay.portal.search.solr.internal.facet.SolrFacetFieldCollector; import com.liferay.portal.search.solr.internal.facet.SolrFacetQueryCollector; import com.liferay.portal.search.solr.internal.pagination.Pagination; import com.liferay.portal.search.solr.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.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrQuery.ORDER; import org.apache.solr.client.solrj.SolrQuery.SortClause; import org.apache.solr.client.solrj.SolrRequest.METHOD; import org.apache.solr.client.solrj.response.FacetField; import org.apache.solr.client.solrj.response.FieldStatsInfo; import org.apache.solr.client.solrj.response.Group; import org.apache.solr.client.solrj.response.GroupCommand; import org.apache.solr.client.solrj.response.GroupResponse; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrDocumentList; import org.apache.solr.common.params.FacetParams; 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 Bruno Farache * @author Zsolt Berentey * @author Raymond Augé */ @Component( configurationPid = "com.liferay.portal.search.solr.configuration.SolrConfiguration", immediate = true, property = {"search.engine.impl=Solr"}, service = IndexSearcher.class ) public class SolrIndexSearcher extends BaseIndexSearcher { @Override public String getQueryString(SearchContext searchContext, Query query) { return translateQuery(query, searchContext); } @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.getMessage(), 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=Solr)", unbind = "-") public void setQuerySuggester(QuerySuggester querySuggester) { super.setQuerySuggester(querySuggester); } @Activate @Modified protected void activate(Map<String, Object> properties) { _solrConfiguration = ConfigurableUtil.createConfigurable( SolrConfiguration.class, properties); _logExceptionsOnly = _solrConfiguration.logExceptionsOnly(); } protected void addFacets(SolrQuery solrQuery, SearchContext searchContext) { Map<String, Facet> facets = searchContext.getFacets(); for (Facet facet : facets.values()) { if (facet.isStatic()) { continue; } FacetConfiguration facetConfiguration = facet.getFacetConfiguration(); _facetProcessor.processFacet(solrQuery, facet); String facetSort = FacetParams.FACET_SORT_COUNT; String order = facetConfiguration.getOrder(); if (order.equals("OrderValueAsc")) { facetSort = FacetParams.FACET_SORT_INDEX; } solrQuery.add( "f." + facetConfiguration.getFieldName() + ".facet.sort", facetSort); } solrQuery.setFacetLimit(-1); } protected void addGroupBy( SolrQuery solrQuery, SearchContext searchContext, Pagination pagination) { GroupBy groupBy = searchContext.getGroupBy(); if (groupBy == null) { return; } _groupByTranslator.translate( solrQuery, searchContext, pagination.getStart(), pagination.getEnd()); } protected void addHighlightedField( SolrQuery solrQuery, QueryConfig queryConfig, String fieldName) { solrQuery.addHighlightField(fieldName); String localizedFieldName = DocumentImpl.getLocalizedName( queryConfig.getLocale(), fieldName); solrQuery.addHighlightField(localizedFieldName); } protected void addHighlights(SolrQuery solrQuery, QueryConfig queryConfig) { if (!queryConfig.isHighlightEnabled()) { return; } solrQuery.setHighlight(true); solrQuery.setHighlightFragsize(queryConfig.getHighlightFragmentSize()); solrQuery.setHighlightSimplePost(HighlightUtil.HIGHLIGHT_TAG_CLOSE); solrQuery.setHighlightSimplePre(HighlightUtil.HIGHLIGHT_TAG_OPEN); solrQuery.setHighlightSnippets(queryConfig.getHighlightSnippetSize()); for (String highlightFieldName : queryConfig.getHighlightFieldNames()) { addHighlightedField(solrQuery, queryConfig, highlightFieldName); } solrQuery.setHighlightRequireFieldMatch( queryConfig.isHighlightRequireFieldMatch()); } protected void addPagination( SolrQuery solrQuery, SearchContext searchContext, Pagination pagination) { GroupBy groupBy = searchContext.getGroupBy(); if (groupBy != null) { return; } Optional<Integer> fromOptional = pagination.getFrom(); fromOptional.ifPresent(solrQuery::setStart); Optional<Integer> sizeOptional = pagination.getSize(); sizeOptional.ifPresent(solrQuery::setRows); } protected void addSelectedFields( SolrQuery solrQuery, QueryConfig queryConfig) { if (queryConfig.isAllFieldsSelected()) { return; } Set<String> selectedFieldNames = SetUtil.fromArray( queryConfig.getSelectedFieldNames()); if (!selectedFieldNames.contains(Field.UID)) { selectedFieldNames.add(Field.UID); } solrQuery.setFields( selectedFieldNames.toArray(new String[selectedFieldNames.size()])); } protected void addSnippets( SolrDocument solrDocument, Document document, QueryConfig queryConfig, Set<String> queryTerms, QueryResponse queryResponse) { Map<String, Map<String, List<String>>> highlights = queryResponse.getHighlighting(); if (!queryConfig.isHighlightEnabled()) { return; } for (String highlightFieldName : queryConfig.getHighlightFieldNames()) { addSnippets( solrDocument, document, queryTerms, highlights, highlightFieldName, queryConfig.getLocale()); } } protected void addSnippets( SolrDocument solrDocument, Document document, Set<String> queryTerms, Map<String, Map<String, List<String>>> highlights, String fieldName, Locale locale) { if (MapUtil.isEmpty(highlights)) { return; } String key = (String)solrDocument.getFieldValue(Field.UID); Map<String, List<String>> uidHighlights = highlights.get(key); String localizedFieldName = DocumentImpl.getLocalizedName( locale, fieldName); String snippetFieldName = localizedFieldName; List<String> snippets = uidHighlights.get(localizedFieldName); if (snippets == null) { snippets = uidHighlights.get(fieldName); snippetFieldName = fieldName; } String snippet = StringPool.BLANK; if (ListUtil.isNotEmpty(snippets)) { snippet = StringUtil.merge(snippets, StringPool.TRIPLE_PERIOD); if (Validator.isNotNull(snippet)) { snippet = snippet.concat(StringPool.TRIPLE_PERIOD); } } HighlightUtil.addSnippet( document, queryTerms, snippet, snippetFieldName); } protected void addSort(SolrQuery solrQuery, 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); ORDER order = ORDER.asc; if (sort.isReverse() || sortFieldName.equals("score")) { order = ORDER.desc; } solrQuery.addSort(new SortClause(sortFieldName, order)); } } protected void addStats(SolrQuery solrQuery, SearchContext searchContext) { Map<String, Stats> statsMap = searchContext.getStats(); for (Stats stats : statsMap.values()) { _statsTranslator.translate(solrQuery, stats); } } protected QueryResponse doSearch( SearchContext searchContext, Query query, Pagination pagination, boolean count) throws Exception { QueryConfig queryConfig = query.getQueryConfig(); SolrQuery solrQuery = new SolrQuery(); addStats(solrQuery, searchContext); if (!count) { addFacets(solrQuery, searchContext); addGroupBy(solrQuery, searchContext, pagination); addHighlights(solrQuery, queryConfig); addPagination(solrQuery, searchContext, pagination); addSelectedFields(solrQuery, queryConfig); addSort(solrQuery, searchContext.getSorts()); solrQuery.setIncludeScore(queryConfig.isScoreEnabled()); } else { solrQuery.setRows(0); } String queryString = translateQuery(query, searchContext); solrQuery.setQuery(queryString); List<String> filterQueries = new ArrayList<>(); if (query.getPreBooleanFilter() != null) { String filterQuery = _filterTranslator.translate( query.getPreBooleanFilter(), searchContext); filterQueries.add(filterQuery); } if (query.getPostFilter() != null) { String filterQuery = _filterTranslator.translate( query.getPreBooleanFilter(), searchContext); filterQueries.add(filterQuery); } if (!filterQueries.isEmpty()) { solrQuery.setFilterQueries( filterQueries.toArray(new String[filterQueries.size()])); } QueryResponse queryResponse = executeSearchRequest(solrQuery); if (_log.isInfoEnabled()) { _log.info( "The search engine processed " + solrQuery.getQuery() + " in " + queryResponse.getElapsedTime() + " ms"); } return queryResponse; } protected long doSearchCount(SearchContext searchContext, Query query) throws Exception { QueryResponse queryResponse = doSearch( searchContext, query, null, true); SolrDocumentList solrDocumentList = queryResponse.getResults(); return solrDocumentList.getNumFound(); } protected Hits doSearchHits( SearchContext searchContext, Query query, Pagination pagination) throws Exception { QueryResponse queryResponse = doSearch( searchContext, query, pagination, false); return processResponse(queryResponse, searchContext, query); } protected QueryResponse executeSearchRequest(SolrQuery solrQuery) throws Exception { SolrClient solrClient = _solrClientManager.getSolrClient(); return solrClient.query(solrQuery, METHOD.POST); } protected Hits processResponse( QueryResponse queryResponse, SearchContext searchContext, Query query) { Hits hits = new HitsImpl(); updateFacetCollectors(queryResponse, searchContext); updateGroupedHits(queryResponse, searchContext, query, hits); updateStatsResults(searchContext, queryResponse, hits); hits.setQuery(query); hits.setSearchTime(queryResponse.getQTime()); processSearchHits( queryResponse, queryResponse.getResults(), query, hits); return hits; } protected void processSearchHits( QueryResponse queryResponse, SolrDocumentList solrDocumentList, Query query, Hits hits) { List<Document> documents = new ArrayList<>(); Set<String> queryTerms = new HashSet<>(); List<Float> scores = new ArrayList<>(); processSolrDocumentList( queryResponse, solrDocumentList, query, hits, documents, queryTerms, scores); hits.setDocs(documents.toArray(new Document[documents.size()])); hits.setQueryTerms(queryTerms.toArray(new String[queryTerms.size()])); hits.setScores(ArrayUtil.toFloatArray(scores)); } protected Document processSolrDocument( SolrDocument solrDocument, QueryConfig queryConfig) { Document document = new DocumentImpl(); Collection<String> fieldNames = solrDocument.getFieldNames(); for (String fieldName : fieldNames) { if (fieldName.equals(_VERSION_FIELD)) { continue; } Collection<Object> fieldValues = solrDocument.getFieldValues( fieldName); Field field = new Field( fieldName, ArrayUtil.toStringArray( fieldValues.toArray(new Object[fieldValues.size()]))); document.add(field); } populateUID(document, queryConfig); return document; } protected void processSolrDocumentList( QueryResponse queryResponse, SolrDocumentList solrDocumentList, Query query, Hits hits, List<Document> documents, Set<String> queryTerms, List<Float> scores) { if (solrDocumentList == null) { return; } hits.setLength((int)solrDocumentList.getNumFound()); for (SolrDocument solrDocument : solrDocumentList) { QueryConfig queryConfig = query.getQueryConfig(); Document document = processSolrDocument(solrDocument, queryConfig); documents.add(document); addSnippets( solrDocument, document, queryConfig, queryTerms, queryResponse); float score = GetterUtil.getFloat( String.valueOf(solrDocument.getFieldValue("score"))); scores.add(score); } } @Reference(service = CompositeFacetProcessor.class, unbind = "-") protected void setFacetProcessor(FacetProcessor<SolrQuery> facetProcessor) { _facetProcessor = facetProcessor; } @Reference(target = "(search.engine.impl=Solr)", unbind = "-") protected void setFilterTranslator( FilterTranslator<String> filterTranslator) { _filterTranslator = filterTranslator; } @Reference(unbind = "-") protected void setGroupByTranslator(GroupByTranslator groupByTranslator) { _groupByTranslator = groupByTranslator; } @Reference(target = "(search.engine.impl=Solr)", unbind = "-") protected void setQueryTranslator(QueryTranslator<String> queryTranslator) { _queryTranslator = queryTranslator; } @Reference(unbind = "-") protected void setSolrClientManager(SolrClientManager solrClientManager) { _solrClientManager = solrClientManager; } @Reference(unbind = "-") protected void setStatsTranslator(StatsTranslator statsTranslator) { _statsTranslator = statsTranslator; } protected String translateQuery(Query query, SearchContext searchContext) { return _queryTranslator.translate(query, searchContext); } protected void updateFacetCollectors( QueryResponse queryResponse, SearchContext searchContext) { Map<String, Facet> facetsMap = searchContext.getFacets(); List<FacetField> facetFields = queryResponse.getFacetFields(); if (ListUtil.isEmpty(facetFields)) { return; } for (FacetField facetField : facetFields) { Facet facet = facetsMap.get(facetField.getName()); FacetCollector facetCollector = null; if (facet instanceof RangeFacet) { facetCollector = new SolrFacetQueryCollector( facetField.getName(), queryResponse.getFacetQuery()); } else { facetCollector = new SolrFacetFieldCollector( facetField.getName(), facetField); } facet.setFacetCollector(facetCollector); } } protected void updateGroupedHits( QueryResponse queryResponse, SearchContext searchContext, Query query, Hits hits) { GroupBy groupBy = searchContext.getGroupBy(); if (groupBy == null) { return; } GroupResponse groupResponse = queryResponse.getGroupResponse(); List<GroupCommand> groupCommands = groupResponse.getValues(); for (GroupCommand groupCommand : groupCommands) { List<Group> groups = groupCommand.getValues(); for (Group group : groups) { Hits groupedHits = new HitsImpl(); processSearchHits( queryResponse, group.getResult(), query, groupedHits); hits.addGroupedHits(group.getGroupValue(), groupedHits); } } } protected void updateStatsResults( SearchContext searchContext, QueryResponse queryResponse, Hits hits) { Map<String, Stats> statsMap = searchContext.getStats(); if (statsMap.isEmpty()) { return; } Map<String, FieldStatsInfo> fieldsStatsInfo = queryResponse.getFieldStatsInfo(); if (MapUtil.isEmpty(fieldsStatsInfo)) { return; } for (Stats stats : statsMap.values()) { if (!stats.isEnabled()) { continue; } StatsResults statsResults = _statsTranslator.translate( fieldsStatsInfo.get(stats.getField()), stats); hits.addStatsResults(statsResults); } } private static final String _VERSION_FIELD = "_version_"; private static final Log _log = LogFactoryUtil.getLog( SolrIndexSearcher.class); private FacetProcessor<SolrQuery> _facetProcessor; private FilterTranslator<String> _filterTranslator; private GroupByTranslator _groupByTranslator; private boolean _logExceptionsOnly; private QueryTranslator<String> _queryTranslator; private SolrClientManager _solrClientManager; private volatile SolrConfiguration _solrConfiguration; private StatsTranslator _statsTranslator; }