package org.gbif.occurrence.search; import org.gbif.api.model.checklistbank.NameUsageMatch; import org.gbif.api.model.checklistbank.NameUsageMatch.MatchType; import org.gbif.api.model.common.paging.Pageable; import org.gbif.api.model.common.search.SearchResponse; import org.gbif.api.model.occurrence.Occurrence; import org.gbif.api.model.occurrence.search.OccurrenceSearchParameter; import org.gbif.api.model.occurrence.search.OccurrenceSearchRequest; import org.gbif.api.service.checklistbank.NameUsageMatchingService; import org.gbif.api.service.occurrence.OccurrenceSearchService; import org.gbif.api.service.occurrence.OccurrenceService; import org.gbif.common.search.SearchException; import org.gbif.common.search.solr.QueryUtils; import org.gbif.occurrence.search.solr.OccurrenceSolrField; import org.gbif.occurrence.search.solr.SolrQueryUtils; import org.gbif.occurrence.search.solr.SpellCheckResponseBuilder; import java.io.IOException; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import javax.annotation.Nullable; import javax.validation.constraints.Min; import com.google.common.base.Objects; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.inject.Inject; import com.google.inject.name.Named; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.client.solrj.response.TermsResponse; import org.apache.solr.client.solrj.response.TermsResponse.Term; import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrDocumentList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.gbif.api.model.common.search.SearchConstants.DEFAULT_SUGGEST_LIMIT; import static org.gbif.common.search.solr.QueryUtils.buildTermQuery; import static org.gbif.common.search.solr.SolrConstants.DEFAULT_FILTER_QUERY; import static org.gbif.common.search.solr.SolrConstants.SOLR_REQUEST_HANDLER; import static org.gbif.occurrence.search.OccurrenceSearchRequestBuilder.QUERY_FIELD_MAPPING; /** * Occurrence search service. * Executes {@link OccurrenceSearchRequest} by transforming the request into {@link SolrQuery}. */ public class OccurrenceSearchImpl implements OccurrenceSearchService { /** * Default limit value for auto-suggest services. */ private static final Logger LOG = LoggerFactory.getLogger(OccurrenceSearchImpl.class); private static final Map<String, OccurrenceSearchParameter> FIELD_PARAMETER_MAPPING = new HashMap<String, OccurrenceSearchParameter>(QUERY_FIELD_MAPPING.size()); static { for (Map.Entry<OccurrenceSearchParameter, OccurrenceSolrField> paramField : QUERY_FIELD_MAPPING.entrySet()) { FIELD_PARAMETER_MAPPING.put(paramField.getValue().getFieldName(), paramField.getKey()); } } // Default order of results private static final Map<String, SolrQuery.ORDER> SORT_ORDER = new LinkedHashMap<String, SolrQuery.ORDER>(2); private final OccurrenceService occurrenceService; static { SORT_ORDER.put(OccurrenceSolrField.YEAR.getFieldName(), SolrQuery.ORDER.desc); SORT_ORDER.put(OccurrenceSolrField.MONTH.getFieldName(), SolrQuery.ORDER.asc); } private final SolrClient solrClient; private final OccurrenceSearchRequestBuilder occurrenceSearchRequestBuilder; private final NameUsageMatchingService nameUsageMatchingService; @Inject public OccurrenceSearchImpl(SolrClient solrClient, @Named(SOLR_REQUEST_HANDLER) String requestHandler, OccurrenceService occurrenceService, NameUsageMatchingService nameUsageMatchingService, @Named("max.offset") int maxOffset, @Named("max.limit") int maxLimit, @Named("facets.enable") boolean facetsEnable) { this.solrClient = solrClient; occurrenceSearchRequestBuilder = new OccurrenceSearchRequestBuilder(requestHandler, SORT_ORDER, maxOffset, maxLimit, facetsEnable); this.occurrenceService = occurrenceService; this.nameUsageMatchingService = nameUsageMatchingService; } /** * Builds a SearchResponse instance using the current builder state. * * @return a new instance of a SearchResponse. */ public SearchResponse<Occurrence, OccurrenceSearchParameter> buildResponse(QueryResponse queryResponse, Pageable request) { // Create response SearchResponse<Occurrence, OccurrenceSearchParameter> response = new SearchResponse<Occurrence, OccurrenceSearchParameter>(request); SolrDocumentList results = queryResponse.getResults(); // set total count response.setCount(results.getNumFound()); // Populates the results List<Occurrence> occurrences = Lists.newArrayListWithCapacity(results.size()); for (SolrDocument doc : results) { // Only field key is returned in the result Integer occKey = (Integer) doc.getFieldValue(OccurrenceSolrField.KEY.getFieldName()); Occurrence occ = occurrenceService.get(occKey); if (occ == null || occ.getKey() == null) { LOG.warn("Occurrence {} not found in store, but present in solr", occKey); } else { occurrences.add(occ); } } if (request.getLimit() > OccurrenceSearchRequestBuilder.MAX_PAGE_SIZE) { response.setLimit(OccurrenceSearchRequestBuilder.MAX_PAGE_SIZE); } if (queryResponse.getSpellCheckResponse() != null) { response.setSpellCheckResponse(SpellCheckResponseBuilder.build(queryResponse.getSpellCheckResponse())); } response.setResults(occurrences); if (occurrenceSearchRequestBuilder.isFacetsEnable()) { response.setFacets(SolrQueryUtils.getFacetsFromResponse(queryResponse, FIELD_PARAMETER_MAPPING)); } return response; } @Override public SearchResponse<Occurrence, OccurrenceSearchParameter> search(@Nullable OccurrenceSearchRequest request) { try { if (hasReplaceableScientificNames(request)) { SolrQuery solrQuery = occurrenceSearchRequestBuilder.build(request); QueryResponse queryResponse = solrClient.query(solrQuery); return buildResponse(queryResponse, request); } else { return new SearchResponse<Occurrence, OccurrenceSearchParameter>(request); } } catch (SolrServerException | IOException e) { LOG.error("Error executing the search operation", e); throw new SearchException(e); } } @Override public List<String> suggestCatalogNumbers(String prefix, @Nullable Integer limit) { return suggestTermByField(prefix, OccurrenceSearchParameter.CATALOG_NUMBER, limit); } @Override public List<String> suggestCollectionCodes(String prefix, @Nullable Integer limit) { return suggestTermByField(prefix, OccurrenceSearchParameter.COLLECTION_CODE, limit); } @Override public List<String> suggestRecordedBy(String prefix, @Nullable Integer limit) { return suggestTermByField(prefix, OccurrenceSearchParameter.RECORDED_BY, limit); } @Override public List<String> suggestInstitutionCodes(String prefix, @Nullable Integer limit) { return suggestTermByField(prefix, OccurrenceSearchParameter.INSTITUTION_CODE, limit); } @Override public List<String> suggestRecordNumbers(String prefix, @Nullable Integer limit) { return suggestTermByField(prefix, OccurrenceSearchParameter.RECORD_NUMBER, limit); } @Override public List<String> suggestOccurrenceIds(String prefix, @Nullable Integer limit) { return suggestTermByField(prefix, OccurrenceSearchParameter.OCCURRENCE_ID, limit); } @Override public List<String> suggestOrganismIds(@Min(1L) String prefix, @Nullable Integer limit) { return suggestTermByField(prefix, OccurrenceSearchParameter.ORGANISM_ID, limit); } @Override public List<String> suggestLocalities(@Min(1L) String prefix, @Nullable Integer limit) { return suggestTermByField(prefix, OccurrenceSearchParameter.LOCALITY, limit); } @Override public List<String> suggestWaterBodies(@Min(1L) String prefix, @Nullable Integer limit) { return suggestTermByField(prefix, OccurrenceSearchParameter.WATER_BODY, limit); } @Override public List<String> suggestStateProvinces(@Min(1L) String prefix, @Nullable Integer limit) { return suggestTermByField(prefix, OccurrenceSearchParameter.STATE_PROVINCE, limit); } /** * Searches a indexed terms of a field that matched against the prefix parameter. * * @param prefix search term * @param parameter mapped field to be searched * @param limit of maximum matches * @return a list of elements that matched against the prefix */ public List<String> suggestTermByField(String prefix, OccurrenceSearchParameter parameter, Integer limit) { try { String solrField = QUERY_FIELD_MAPPING.get(parameter).getFieldName(); SolrQuery solrQuery = buildTermQuery(parseTermsQueryValue(prefix).toLowerCase(), solrField, Objects.firstNonNull(limit, DEFAULT_SUGGEST_LIMIT)); final QueryResponse queryResponse = solrClient.query(solrQuery); final TermsResponse termsResponse = queryResponse.getTermsResponse(); return termsResponse.getTerms(solrField).stream().map(Term::getTerm).collect(Collectors.toList()); } catch (SolrServerException | IOException e) { LOG.error("Error executing/building the request", e); throw new SearchException(e); } } /** * Escapes a query value and transform it into a phrase query if necessary. */ private static String parseTermsQueryValue(final String q) { // return default query for empty queries String qValue = Strings.nullToEmpty(q).trim(); if (Strings.isNullOrEmpty(qValue)) { return DEFAULT_FILTER_QUERY; } // If default query was sent, must not be escaped if (!qValue.equals(DEFAULT_FILTER_QUERY)) { qValue = QueryUtils.clearConsecutiveBlanks(qValue); qValue =QueryUtils. escapeQuery(qValue); } return qValue; } /** * Tries to get the corresponding name usage keys from the scientific_name parameter values. * * @return true: if the request doesn't contain any scientific_name parameter or if any scientific name was found * false: if none scientific name was found */ private boolean hasReplaceableScientificNames(OccurrenceSearchRequest request) { boolean hasValidReplaces = true; if (request.getParameters().containsKey(OccurrenceSearchParameter.SCIENTIFIC_NAME)) { hasValidReplaces = false; Collection<String> values = request.getParameters().get(OccurrenceSearchParameter.SCIENTIFIC_NAME); for (String value : values) { NameUsageMatch nameUsageMatch = nameUsageMatchingService.match(value, null, null, true, false); if (nameUsageMatch.getMatchType() == MatchType.EXACT) { hasValidReplaces = true; values.remove(value); request.addParameter(OccurrenceSearchParameter.TAXON_KEY, nameUsageMatch.getUsageKey()); } } } return hasValidReplaces; } }