package org.gbif.occurrence.search.solr; import org.gbif.api.model.common.paging.Pageable; import org.gbif.api.model.common.search.Facet; import org.gbif.api.model.common.search.FacetedSearchRequest; import org.gbif.api.model.common.search.SearchParameter; import org.gbif.api.util.VocabularyUtils; import org.gbif.api.vocabulary.Country; import org.gbif.api.vocabulary.Language; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.primitives.Ints; import g18.com.google.common.base.MoreObjects; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.common.params.FacetParams; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.gbif.common.search.solr.QueryUtils.PARAMS_OR_JOINER; import static org.gbif.common.search.solr.SolrConstants.APOSTROPHE; import static org.gbif.common.search.solr.SolrConstants.FACET_FILTER_EX; import static org.gbif.common.search.solr.SolrConstants.FACET_FILTER_TAG; import static org.gbif.common.search.solr.SolrConstants.PARAM_FACET_MISSING; import static org.gbif.common.search.solr.SolrConstants.PARAM_FACET_SORT; import static org.gbif.common.search.solr.SolrConstants.TAG_FIELD_PARAM; /** * Utility class to perform transformations from API to Solr queries and vice versa. * * moved from common-search, see: https://github.com/gbif/common-search/commit/c9529087d5b34228b045f30323901074218c5d90 */ public class SolrQueryUtils { private static final Logger LOG = LoggerFactory.getLogger(SolrQueryUtils.class); public static final FacetField.SortOrder DEFAULT_FACET_SORT = FacetField.SortOrder.COUNT; public static final int DEFAULT_FACET_COUNT = 1; public static final boolean DEFAULT_FACET_MISSING = true; private static final Pattern TAG_FIELD_PARAM_PATTERN = Pattern.compile(TAG_FIELD_PARAM, Pattern.LITERAL); // Pattern for setting the facet method on single field private static final String FACET_METHOD_FMT = "f.%s." + FacetParams.FACET_METHOD; private static final ImmutableMap<FacetField.Method, String> FACET_METHOD_MAP = new ImmutableMap.Builder<FacetField.Method, String>() .put(FacetField.Method.ENUM, FacetParams.FACET_METHOD_enum) .put(FacetField.Method.FIELD_CACHE, FacetParams.FACET_METHOD_fc) .put(FacetField.Method.FIELD_CACHE_SEGMENT, FacetParams.FACET_METHOD_fcs) .build(); /** * Helper method that sets the parameter for a faceted query. * * @param searchRequest the searchRequest used to extract the parameters * @param solrQuery this object is modified by adding the facets parameters */ public static <P extends SearchParameter> void applyFacetSettings(FacetedSearchRequest<P> searchRequest, SolrQuery solrQuery, Map<P,FacetFieldConfiguration> configurations) { if (!searchRequest.getFacets().isEmpty()) { // Only show facets that contains at least 1 record solrQuery.setFacet(true); // defaults if not overridden on per field basis solrQuery.setFacetMinCount(MoreObjects.firstNonNull(searchRequest.getFacetMinCount(), DEFAULT_FACET_COUNT)); solrQuery.setFacetMissing(DEFAULT_FACET_MISSING); solrQuery.setFacetSort(DEFAULT_FACET_SORT.toString().toLowerCase()); if (searchRequest.getFacetLimit() != null) { solrQuery.setFacetLimit(searchRequest.getFacetLimit()); } if(searchRequest.getFacetOffset() != null) { solrQuery.setParam(FacetParams.FACET_OFFSET, searchRequest.getFacetOffset().toString()); } for (final P facet : searchRequest.getFacets()) { if (!configurations.containsKey(facet)) { LOG.warn("{} is no valid facet. Ignore", facet); continue; } FacetFieldConfiguration facetFieldConfiguration = configurations.get(facet); final String field = facetFieldConfiguration.getField(); if (searchRequest.isMultiSelectFacets()) { // use exclusion filter with same name as used in filter query // http://wiki.apache.org/solr/SimpleFacetParameters#Tagging_and_excluding_Filters solrQuery.addFacetField(taggedField(field,FACET_FILTER_EX)); } else { solrQuery.addFacetField(field); } if (facetFieldConfiguration.isMissing() != DEFAULT_FACET_MISSING) { solrQuery.setParam(perFieldParamName(field, PARAM_FACET_MISSING), facetFieldConfiguration.isMissing()); } if (facetFieldConfiguration.getSortOrder() != DEFAULT_FACET_SORT) { solrQuery.setParam(perFieldParamName(field, PARAM_FACET_SORT), facetFieldConfiguration.getSortOrder().toString().toLowerCase()); } setFacetMethod(solrQuery, field, facetFieldConfiguration.getMethod()); Pageable facetPage = searchRequest.getFacetPage(facet); if (facetPage != null) { solrQuery.setParam(perFieldParamName(field, FacetParams.FACET_OFFSET), Long.toString(facetPage.getOffset())); solrQuery.setParam(perFieldParamName(field, FacetParams.FACET_LIMIT), Integer.toString(facetPage.getLimit())); } } } } /** * Utility method that creates the resulting Solr expression for facet and general query filters parameters. */ public static StringBuilder buildFilterQuery(final boolean isFacetedRequest, final String solrFieldName, List<String> filterQueriesComponents) { //Setting initial max capacity StringBuilder filterQuery = new StringBuilder(filterQueriesComponents.size() + 4); if (isFacetedRequest) { filterQuery.append(taggedField(solrFieldName)); } if (filterQueriesComponents.size() > 1) { filterQuery.append('('); filterQuery.append(PARAMS_OR_JOINER.join(filterQueriesComponents)); filterQuery.append(')'); } else { filterQuery.append(PARAMS_OR_JOINER.join(filterQueriesComponents)); } return filterQuery; } /** * Interprets the value of parameter "value" using types pType (Parameter type) and eType (Enumeration). */ public static String getInterpretedValue(final Class<?> pType, final String value) { // By default use a phrase query is surrounded by " String interpretedValue = APOSTROPHE + value + APOSTROPHE; if (Enum.class.isAssignableFrom(pType)) { // treat country codes special, they use iso codes Enum<?> e; if (Country.class.isAssignableFrom(pType)) { e = Country.fromIsoCode(value); } else { e = VocabularyUtils.lookupEnum(value, (Class<? extends Enum<?>>) pType); } if (value == null) { throw new IllegalArgumentException("Value Null is invalid for filter parameter " + pType.getName()); } interpretedValue = String.valueOf(e.ordinal()); } else if (UUID.class.isAssignableFrom(pType)) { interpretedValue = UUID.fromString(value).toString(); } else if (Double.class.isAssignableFrom(pType)) { interpretedValue = String.valueOf(Double.parseDouble(value)); } else if (Integer.class.isAssignableFrom(pType)) { interpretedValue = String.valueOf(Integer.parseInt(value)); } else if (Boolean.class.isAssignableFrom(pType)) { interpretedValue = String.valueOf(Boolean.parseBoolean(value)); } return interpretedValue; } /** * @param field the solr field * @param param the parameter to use on a per field basis * @return per field facet parameter, e.g. f.dataset_type.facet.sort */ public static String perFieldParamName(String field, String param) { return "f." + field + "." + param; } public static String taggedField(String fieldName){ return taggedField(fieldName,FACET_FILTER_TAG); } public static String taggedField(String solrFieldName, String matcher){ return TAG_FIELD_PARAM_PATTERN.matcher(matcher).replaceAll(Matcher.quoteReplacement(solrFieldName)); } /** * Helper method that takes Solr response and extracts the facets results. * The facets are converted to a list of Facets understood by the search API. * The result of this method can be a empty list. * * @param queryResponse that contains the facets information returned by Solr * @return the List of facets retrieved from the Solr response */ public static <P extends SearchParameter> List<Facet<P>> getFacetsFromResponse(final QueryResponse queryResponse, Map<String,P> fieldToParamMap) { List<Facet<P>> facets = Lists.newArrayList(); if (queryResponse.getFacetFields() != null) { for (final org.apache.solr.client.solrj.response.FacetField facetField : queryResponse.getFacetFields()) { P facetParam = fieldToParamMap.get(facetField.getName()); Facet<P> facet = new Facet<P>(facetParam); List<Facet.Count> counts = Lists.newArrayList(); if (facetField.getValues() != null) { for (final org.apache.solr.client.solrj.response.FacetField.Count count : facetField.getValues()) { String value = count.getName(); if (!Strings.isNullOrEmpty(value) && Enum.class.isAssignableFrom(facetParam.type())) { value = getFacetEnumValue(facetParam, value); } counts.add(new Facet.Count(value, count.getCount())); } } facet.setCounts(counts); facets.add(facet); } } return facets; } /** * Gets the facet value of Enum type parameter. * If the Enum is either a Country or a Language, its iso2Letter code it's used. */ public static <P extends SearchParameter> String getFacetEnumValue(P facetParam, String value) { // the expected enum type for the value if it is an enum - otherwise null final Enum<?>[] enumValues = ((Class<? extends Enum<?>>) facetParam.type()).getEnumConstants(); // if we find integers these are ordinals, translate back to enum names final Integer intValue = Ints.tryParse(value); if (null != intValue) { final Enum<?> enumValue = enumValues[intValue]; if (Country.class.equals(facetParam.type())) { return ((Country) enumValue).getIso2LetterCode(); } else if (Language.class.equals(facetParam.type())) { return ((Language) enumValue).getIso2LetterCode(); } else { return enumValue.name(); } } else { if (Country.class.equals(facetParam.type())) { return Country.fromIsoCode(value).getIso2LetterCode(); } else if (Language.class.equals(facetParam.type())) { return Language.fromIsoCode(value).getIso2LetterCode(); } else { return VocabularyUtils.lookupEnum(value, (Class<? extends Enum<?>>) facetParam.type()).name(); } } } /** * Sets the Solr facet.method for the field parameter according to the method parameter. */ public static void setFacetMethod(SolrQuery solrQuery, String field, FacetField.Method facetFieldMethod) { solrQuery.setParam(String.format(FACET_METHOD_FMT, field), FACET_METHOD_MAP.get(facetFieldMethod)); } }