| /** |
| * |
| */ |
| package org.eclipse.smila.solr.query; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.ListIterator; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| |
| import org.apache.commons.collections.Factory; |
| import org.apache.commons.collections.MapUtils; |
| import org.apache.commons.collections.map.LazyMap; |
| import org.apache.commons.lang.ArrayUtils; |
| import org.apache.commons.lang.BooleanUtils; |
| import org.apache.commons.lang.StringUtils; |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| import org.apache.solr.client.solrj.SolrQuery; |
| import org.apache.solr.client.solrj.SolrQuery.ORDER; |
| import org.apache.solr.common.params.CommonParams; |
| import org.apache.solr.common.params.FacetParams; |
| import org.apache.solr.common.params.GroupParams; |
| import org.eclipse.smila.datamodel.Any; |
| import org.eclipse.smila.datamodel.AnyMap; |
| import org.eclipse.smila.datamodel.AnySeq; |
| import org.eclipse.smila.datamodel.DataFactory; |
| import org.eclipse.smila.datamodel.InvalidValueTypeException; |
| import org.eclipse.smila.datamodel.Value; |
| import org.eclipse.smila.search.api.QueryConstants; |
| import org.eclipse.smila.search.api.QueryConstants.SortOrder; |
| import org.eclipse.smila.solr.SolrConfig; |
| import org.eclipse.smila.solr.SolrConstants; |
| import org.eclipse.smila.solr.SolrConstants.FacetSort; |
| import org.eclipse.smila.solr.SolrUtils; |
| import org.eclipse.smila.solr.SolrUtils.LocalParamsMode; |
| import org.eclipse.smila.solr.params.QueryParams; |
| |
| /** |
| * @author pwissel |
| * |
| */ |
| public class QueryTransformer { |
| |
| private final Log _log = LogFactory.getLog(getClass()); |
| |
| private final QueryParams _params; |
| |
| private final SolrConfig _config; |
| |
| private final String _index; |
| |
| private final Map<String, FilterGroup> _groupedFilters; |
| |
| private final StringBuilder _buffer = new StringBuilder(0xfff); |
| |
| @SuppressWarnings("unchecked") |
| public QueryTransformer(final QueryParams params, final SolrConfig config, final String index) { |
| _params = params; |
| _config = config; |
| _index = index; |
| _groupedFilters = LazyMap.decorate(new HashMap<String, FilterGroup>(), new Factory() { |
| @Override |
| public Object create() { |
| return new FilterGroup(); |
| } |
| }); |
| } |
| |
| public SolrQuery toSolrQuery() { |
| return toSolrQuery(new SolrQuery()); |
| } |
| |
| public SolrQuery toSolrQuery(final SolrQuery solrQuery) { |
| addCommonQueryParameters(solrQuery); |
| addQuery(solrQuery); |
| addHighlighting(solrQuery); |
| addSorting(solrQuery); |
| addFaceting(solrQuery); |
| addGrouping(solrQuery); |
| addFilters(solrQuery); |
| addNativeParams(solrQuery); |
| return solrQuery; |
| } |
| |
| SolrQuery addCommonQueryParameters(final SolrQuery solrQuery) { |
| // start |
| final int start = _params.getStart(); |
| solrQuery.setStart(start); |
| // rows |
| final int rows = _params.getRows(); |
| solrQuery.setRows(rows); |
| // fields |
| final String[] fields = _params.getFields(); |
| if (!ArrayUtils.isEmpty(fields)) { |
| solrQuery.setFields(fields); |
| } |
| // if highlighting is enabled the id field must be part of resultAttributes |
| if (_params.hasHighlightConfig()) { |
| final String idField = _config.getIdField(_index); |
| if (!ArrayUtils.contains(fields, idField)) { |
| solrQuery.addField(idField); |
| } |
| } |
| return solrQuery; |
| } |
| |
| SolrQuery addQuery(final SolrQuery solrQuery) { |
| final Any query = _params.getQueryObject(); |
| if (query != null) { |
| if (query.isValue()) { |
| final String queryString = query.asValue().asString(); |
| solrQuery.setQuery(queryString); |
| } else if (query.isSeq()) { |
| final String queryString = StringUtils.join(query.asSeq().asStrings(), QueryStringConstants.WHITESPACE); |
| solrQuery.setQuery(queryString); |
| } else if (query.isMap()) { |
| final AnyMap queryObj = query.asMap(); |
| final StringBuilder queryString = new StringBuilder(); |
| // append localParams |
| final Any localParams = queryObj.remove(SolrConstants.LOCAL_PARAMS); |
| if (localParams != null && localParams.isMap()) { |
| SolrUtils.writeLocalParams(queryString, localParams.asMap()); |
| } |
| // append query parts |
| for (Entry<String, Any> queryPart : queryObj.entrySet()) { |
| addFieldedQueryPart(queryString, queryPart.getKey(), queryPart.getValue()); |
| } |
| // set final query string |
| solrQuery.setQuery(queryString.toString()); |
| } |
| } |
| return solrQuery; |
| } |
| |
| private void addFieldedQueryPart(final StringBuilder queryString, final String field, final Any values) { |
| if (queryString.length() > 0) { |
| queryString.append(QueryStringConstants.WHITESPACE); |
| } |
| queryString.append(field).append(QueryStringConstants.COLON); |
| if (values.isValue()) { |
| queryString.append(values.asValue().asString()); |
| } else if (values.isSeq() && values.asSeq().size() > 1) { |
| final StringBuilder multiValue = new StringBuilder(); |
| multiValue.append(QueryStringConstants.BRACKET_OPEN); |
| for (final String value : values.asSeq().asStrings()) { |
| if (multiValue.length() > 1) { |
| multiValue.append(QueryStringConstants.WHITESPACE); |
| } |
| multiValue.append(value); |
| } |
| multiValue.append(QueryStringConstants.BRACKET_CLOSE); |
| queryString.append(multiValue); |
| } |
| } |
| |
| SolrQuery addNativeParams(final SolrQuery solrQuery) { |
| final AnyMap nativeParams = _params.getNativeParams(); |
| if (nativeParams != null && !nativeParams.isEmpty()) { |
| for (final String name : nativeParams.keySet()) { |
| final Any value = nativeParams.get(name); |
| if (value != null) { |
| // FIXME: use add or set (as previously)? |
| if (value.isValue()) { |
| solrQuery.add(name, value.asValue().asString()); |
| } else if (value.isSeq()) { |
| final List<String> strings = value.asSeq().asStrings(); |
| final String[] values = strings.toArray(new String[strings.size()]); |
| solrQuery.add(name, values); |
| } |
| } |
| } |
| } |
| return solrQuery; |
| } |
| |
| SolrQuery addHighlighting(final SolrQuery solrQuery) { |
| final AnySeq highlight = _params.getHighlightConfig(); |
| if (highlight != null && !highlight.isEmpty()) { |
| for (final Any any : highlight) { |
| if (any.isValue()) { |
| solrQuery.addHighlightField(any.asValue().asString()); |
| } else if (any.isMap()) { |
| solrQuery.addHighlightField(any.asMap().getStringValue(QueryConstants.ATTRIBUTE)); |
| } |
| } |
| } |
| return solrQuery; |
| } |
| |
| SolrQuery addSorting(final SolrQuery solrQuery) { |
| final List<AnyMap> sortByConfig = _params.getSortByConfig(); |
| if (sortByConfig != null && !sortByConfig.isEmpty()) { |
| for (final AnyMap sortBy : sortByConfig) { |
| final String field = sortBy.getStringValue(QueryConstants.ATTRIBUTE); |
| final String sortOrderValue = sortBy.getStringValue(QueryConstants.ORDER); |
| final SortOrder sortOrder = SortOrder.valueOf(sortOrderValue.toUpperCase()); |
| switch (sortOrder) { |
| case ASCENDING: |
| solrQuery.addSort(field, ORDER.asc); |
| break; |
| case DESCENDING: |
| solrQuery.addSort(field, ORDER.desc); |
| break; |
| default: |
| final String message = "Unsupported SortOrder: " + sortOrderValue; |
| handleError(message); |
| } |
| } |
| } |
| return solrQuery; |
| } |
| |
| SolrQuery addFaceting(final SolrQuery solrQuery) { |
| final List<AnyMap> facetByConfig = _params.getFacetByConfig(); |
| if (facetByConfig != null && !facetByConfig.isEmpty()) { |
| for (final AnyMap facet : facetByConfig) { |
| String facetField = null; |
| // facet.field |
| if (facet.containsKey(QueryConstants.ATTRIBUTE)) { |
| facetField = facet.getStringValue(QueryConstants.ATTRIBUTE); |
| addFacetSettings(solrQuery, facet, facetField); |
| addFacetFilters(facet, facetField); |
| final String field = addFacetLocalParams(facet, facetField); |
| solrQuery.addFacetField(field); |
| // facet.query |
| } else if (facet.containsKey(SolrConstants.QUERY)) { |
| facetField = facet.getStringValue(QueryConstants.QUERY); |
| addFacetFilters(facet, facetField); |
| addFacetQueries(solrQuery, facet); |
| // facet.range |
| } else if (facet.containsKey(SolrConstants.RANGE)) { |
| facetField = facet.getStringValue(SolrConstants.RANGE); |
| addFacetSettings(solrQuery, facet, facetField); |
| addFacetFilters(facet, facetField); |
| final String range = addFacetLocalParams(facet, facetField); |
| final Value start = facet.getValue(SolrConstants.START); |
| if (start == null) { |
| final String message = SolrConstants.START + " must not be null."; |
| handleError(message); |
| } |
| final Value end = facet.getValue(SolrConstants.END); |
| if (end == null) { |
| final String message = SolrConstants.END + " must not be null."; |
| handleError(message); |
| } |
| final Value gap = facet.getValue(SolrConstants.GAP); |
| if (gap == null) { |
| final String message = SolrConstants.GAP + " must not be null."; |
| handleError(message); |
| } |
| if (start.isNumber() && end.isNumber() && gap.isNumber()) { |
| solrQuery.addNumericRangeFacet(range, start.asDouble(), end.asDouble(), gap.asDouble()); |
| } else if (start.isDateTime() && end.isDateTime() && gap.isString()) { |
| solrQuery.addDateRangeFacet(range, start.asDate(), end.asDate(), gap.asString()); |
| } else { |
| final String message = "Invalid facet range arguments"; |
| handleError(message); |
| } |
| // facet.pivot |
| } else if (facet.containsKey(SolrConstants.PIVOT)) { |
| final Any any = facet.get(SolrConstants.PIVOT); |
| if (any.isSeq()) { |
| final AnySeq pivot = any.asSeq(); |
| if (!pivot.isEmpty()) { |
| final String[] fields = new String[pivot.size()]; |
| int pivotCount = 0; |
| for (String val : pivot.asSeq().asStrings()) { |
| fields[pivotCount] = addFacetLocalParams(facet, val); |
| pivotCount++; |
| } |
| solrQuery.addFacetField(fields); |
| } |
| } |
| } |
| // facet.inverval |
| else if (facet.containsKey(SolrConstants.INTERVAL)) { |
| facetField = facet.getStringValue(SolrConstants.INTERVAL); |
| addFacetFilters(facet, facetField); |
| final String field = addFacetLocalParams(facet, facetField); |
| final AnySeq set = facet.getSeq(SolrConstants.SET); |
| String[] intervals = ArrayUtils.EMPTY_STRING_ARRAY; |
| if (set != null) { |
| intervals = set.asStrings().toArray(new String[set.size()]); |
| } |
| solrQuery.addIntervalFacets(field, intervals); |
| } else { |
| final String message = "Unsupported facet type"; |
| handleError(message); |
| } |
| } |
| } |
| return solrQuery; |
| } |
| |
| private void addFacetSettings(final SolrQuery solrQuery, final AnyMap facet, final String field) { |
| // set maxcount as per-field value |
| final Long maxcount = facet.getLongValue(QueryConstants.MAXCOUNT); |
| if (maxcount != null) { |
| final String limitParam = SolrUtils.getPerFieldParameter(field, FacetParams.FACET_LIMIT); |
| solrQuery.set(limitParam, String.valueOf(maxcount)); |
| } |
| // set sortby as per-field value |
| if (facet.containsKey(QueryConstants.SORTBY)) { |
| final String sortString = |
| facet.getMap(QueryConstants.SORTBY).getStringValue(QueryConstants.FACETBY_SORTCRITERION); |
| if (sortString != null) { |
| final FacetSort sort = FacetSort.get(sortString); |
| final String sortParam = SolrUtils.getPerFieldParameter(field, FacetParams.FACET_SORT); |
| solrQuery.set(sortParam, sort.toString()); |
| } |
| } |
| } |
| |
| private void addFacetFilters(final AnyMap facet, final String defaultLabel) { |
| final AnySeq filters = facet.getSeq(QueryConstants.FILTER); |
| if (filters != null) { |
| for (final Any any : filters) { |
| if (any.isMap()) { |
| final AnyMap filter = any.asMap(); |
| final String filterString = parseFilterStrings(filter, defaultLabel); |
| final String label = StringUtils.defaultIfEmpty(filter.getStringValue(SolrConstants.GROUP), defaultLabel); |
| // tag and exclude filter if multiselect is enabled |
| final boolean multiselect = BooleanUtils.toBoolean(facet.getBooleanValue(SolrConstants.MULTISELECT)); |
| AnyMap localParams = null; |
| if (multiselect) { |
| // tag |
| final String tag = |
| new StringBuffer(CommonParams.FQ).append(QueryStringConstants.UNDERSCORE).append(label).toString(); |
| localParams = SolrUtils.putLocalParam(filter, QueryStringConstants.TAG, tag, multiselect, |
| LocalParamsMode.OVERWRITE); |
| // ex |
| SolrUtils.putLocalParam(facet, QueryStringConstants.EX, tag, true, LocalParamsMode.ADD); |
| } |
| _groupedFilters.get(label).add(filterString, localParams); |
| } |
| } |
| } |
| } |
| |
| private String addFacetLocalParams(final AnyMap facet, final String param) { |
| // add name (only if key is not available) |
| final String name = facet.getStringValue(SolrConstants.NAME); |
| if (!StringUtils.isBlank(name)) { |
| SolrUtils.putLocalParam(facet, QueryStringConstants.KEY, name, true); |
| } |
| // add localParams to param |
| final AnyMap localParams = facet.getMap(SolrConstants.LOCAL_PARAMS); |
| if (!MapUtils.isEmpty(localParams)) { |
| return SolrUtils.addLocalParams(param, localParams).toString(); |
| } |
| return param; |
| } |
| |
| private void addFacetQueries(final SolrQuery solrQuery, final AnyMap facet) { |
| final String attribute = facet.getStringValue(QueryConstants.QUERY); |
| final AnySeq expression = facet.getSeq(SolrConstants.QUERIES); |
| final AnyMap localParams = facet.getMap(SolrConstants.LOCAL_PARAMS, true); |
| // combine attribute and expression as query |
| final List<String> queries = new ArrayList<String>(); |
| for (final Any any : expression) { |
| if (any.isValue()) { |
| final StringBuilder sb = new StringBuilder(attribute); |
| sb.append(QueryStringConstants.COLON); |
| sb.append(any.asValue().asString()); |
| queries.add(sb.toString()); |
| } |
| } |
| // store original key from localParams |
| final String originalKey = |
| StringUtils.defaultIfEmpty(localParams.getStringValue(QueryStringConstants.KEY), attribute); |
| final ListIterator<String> query = queries.listIterator(); |
| while (query.hasNext()) { |
| // add index to localParam key |
| final int index = query.nextIndex(); |
| final String key = originalKey + QueryStringConstants.UNDERSCORE + index; |
| SolrUtils.putLocalParam(facet, QueryStringConstants.KEY, key, false, LocalParamsMode.OVERWRITE); |
| // append localParams to current facetQuery |
| final String facetQuery = SolrUtils.addLocalParams(query.next(), localParams).toString(); |
| solrQuery.addFacetQuery(facetQuery); |
| } |
| // set localParams key to originalKey |
| SolrUtils.putLocalParam(facet, QueryStringConstants.KEY, originalKey, false, LocalParamsMode.OVERWRITE); |
| } |
| |
| SolrQuery addGrouping(final SolrQuery solrQuery) { |
| final List<AnyMap> groupByConfig = _params.getGroupByConfig(); |
| if (groupByConfig != null && !groupByConfig.isEmpty()) { |
| solrQuery.add(GroupParams.GROUP, Boolean.toString(true)); |
| for (final AnyMap groupBy : groupByConfig) { |
| if (groupBy.containsKey(QueryConstants.ATTRIBUTE)) { |
| final String field = groupBy.getStringValue(QueryConstants.ATTRIBUTE); |
| solrQuery.add(GroupParams.GROUP_FIELD, field); |
| } else if (groupBy.containsKey(SolrConstants.FUNC)) { |
| final String func = groupBy.getStringValue(SolrConstants.FUNC); |
| solrQuery.add(GroupParams.GROUP_FUNC, func); |
| } else if (groupBy.containsKey(SolrConstants.QUERY)) { |
| final String query = groupBy.getStringValue(SolrConstants.QUERY); |
| solrQuery.add(GroupParams.GROUP_QUERY, query); |
| } else { |
| final String message = "Invalid group type"; |
| handleError(message); |
| } |
| } |
| } |
| return solrQuery; |
| } |
| |
| SolrQuery addFilters(final SolrQuery solrQuery) { |
| final List<AnyMap> filterConfig = _params.getFilterConfig(); |
| if (filterConfig != null && !filterConfig.isEmpty()) { |
| // prepare filter groups |
| for (final AnyMap filter : filterConfig) { |
| // read attribute |
| final String attribute = filter.getStringValue(QueryConstants.ATTRIBUTE); |
| if (StringUtils.isBlank(attribute)) { |
| throw new IllegalArgumentException("Filter syntax error: Attribute must not be blank"); |
| } |
| final String group = StringUtils.defaultString(filter.getStringValue(SolrConstants.GROUP), attribute); |
| final String filterString = parseFilterStrings(filter, attribute); |
| final AnyMap localParams = filter.getMap(SolrConstants.LOCAL_PARAMS); |
| _groupedFilters.get(group).add(filterString, localParams); |
| } |
| } |
| // add filter groups |
| for (final FilterGroup filterGroup : _groupedFilters.values()) { |
| final StringBuilder fq = resetBuffer(); |
| SolrUtils.writeLocalParams(fq, filterGroup._localParams); |
| String mergedFq = filterGroup.merge(fq); |
| solrQuery.addFilterQuery(mergedFq); |
| } |
| return solrQuery; |
| } |
| |
| private String parseFilterStrings(final AnyMap filterConfig, final String attribute) { |
| final StringBuilder fq = resetBuffer(); |
| for (final Entry<String, Any> condition : filterConfig.entrySet()) { |
| final String key = condition.getKey(); |
| final Any value = condition.getValue(); |
| switch (key) { |
| case QueryConstants.ATTRIBUTE: |
| case SolrConstants.GROUP: |
| case SolrConstants.LOCAL_PARAMS: |
| continue; |
| case QueryConstants.FILTER_ALLOF: |
| appendListFilter(fq, attribute, value.asSeq(), QueryStringConstants.AND); |
| break; |
| case QueryConstants.FILTER_ATLEAST: |
| appendBoundFilter(fq, attribute, value.asValue().asString(), QueryStringConstants.WILDCARD, false); |
| break; |
| case QueryConstants.FILTER_ATMOST: |
| appendBoundFilter(fq, attribute, QueryStringConstants.WILDCARD, value.asValue().asString(), false); |
| break; |
| case QueryConstants.FILTER_GREATERTHAN: |
| appendBoundFilter(fq, attribute, value.asValue().asString(), QueryStringConstants.WILDCARD, true); |
| break; |
| case QueryConstants.FILTER_LESSTHAN: |
| appendBoundFilter(fq, attribute, QueryStringConstants.WILDCARD, value.asValue().asString(), true); |
| break; |
| case QueryConstants.FILTER_NONEOF: |
| appendListFilter(fq, attribute, value.asSeq(), QueryStringConstants.NOT); |
| break; |
| case QueryConstants.FILTER_ONEOF: |
| appendListFilter(fq, attribute, value.asSeq(), QueryStringConstants.OR); |
| break; |
| default: |
| // TODO: error |
| break; |
| } |
| } |
| return fq.toString(); |
| } |
| |
| private void appendListFilter(final StringBuilder fq, final String attribute, final AnySeq values, |
| final String operator) { |
| final List<String> filterValues; |
| try { |
| filterValues = values.asStrings(); |
| } catch (InvalidValueTypeException exception) { |
| throw new InvalidValueTypeException( |
| "Filter syntax error: Must be of typ AnySeq and contain only Value elements."); |
| } |
| // append list filter -> operator(attribute:value) |
| for (final String val : filterValues) { |
| // FIXME: escape filters? |
| final String escapedVal = SolrUtils.escapeWS(val); |
| fq.append(operator); |
| fq.append(QueryStringConstants.BRACKET_OPEN); |
| fq.append(attribute); |
| fq.append(QueryStringConstants.COLON); |
| fq.append(val); |
| fq.append(QueryStringConstants.BRACKET_CLOSE); |
| } |
| } |
| |
| private void appendBoundFilter(final StringBuilder fq, final String attribute, String lower, String upper, |
| final boolean exclusive) { |
| if (StringUtils.isBlank(lower)) { |
| throw new IllegalArgumentException("Filter syntax error: Lower must not be blank."); |
| } |
| if (StringUtils.isBlank(upper)) { |
| throw new IllegalArgumentException("Filter syntax error: Upper must not be blank."); |
| } |
| lower = excludeNonWildcardBound(fq, attribute, lower, exclusive); |
| upper = excludeNonWildcardBound(fq, attribute, upper, exclusive); |
| // append bound filter +(attribute:[lower TO upper]) |
| fq.append(QueryStringConstants.AND); |
| fq.append(QueryStringConstants.BRACKET_OPEN); |
| fq.append(attribute); |
| fq.append(QueryStringConstants.COLON); |
| fq.append(QueryStringConstants.BRACKET_SQUARE_OPEN); |
| fq.append(lower); |
| fq.append(QueryStringConstants.WHITESPACE); |
| fq.append(QueryStringConstants.TO); |
| fq.append(QueryStringConstants.WHITESPACE); |
| fq.append(upper); |
| fq.append(QueryStringConstants.BRACKET_SQUARE_CLOSE); |
| fq.append(QueryStringConstants.BRACKET_CLOSE); |
| } |
| |
| private String excludeNonWildcardBound(final StringBuilder fq, final String attribute, String bound, |
| final boolean exclusive) { |
| if (!bound.equals(QueryStringConstants.WILDCARD)) { |
| bound = SolrUtils.escapeWS(bound); |
| // appent not filter -> -(attribute:bound) |
| if (exclusive) { |
| fq.append(QueryStringConstants.NOT); |
| fq.append(QueryStringConstants.BRACKET_OPEN); |
| fq.append(attribute); |
| fq.append(QueryStringConstants.COLON); |
| fq.append(bound); |
| fq.append(QueryStringConstants.BRACKET_CLOSE); |
| } |
| } |
| return bound; |
| } |
| |
| class FilterGroup { |
| final List<String> _filterStrings = new ArrayList<String>(); |
| |
| final AnyMap _localParams = DataFactory.DEFAULT.createAnyMap(); |
| |
| void add(final String filterString, final AnyMap localParams) { |
| _filterStrings.add(filterString); |
| if (localParams != null) { |
| _localParams.putAll(localParams); |
| } |
| } |
| |
| String merge(final StringBuilder fq) { |
| if (_filterStrings.isEmpty()) { |
| return StringUtils.EMPTY; |
| } else if (_filterStrings.size() == 1) { |
| fq.append(_filterStrings.get(0)); |
| return fq.toString(); |
| } else { |
| for (final String filterString : _filterStrings) { |
| fq.append(QueryStringConstants.AND); |
| fq.append(QueryStringConstants.BRACKET_OPEN); |
| fq.append(filterString); |
| fq.append(QueryStringConstants.BRACKET_CLOSE); |
| } |
| return fq.toString(); |
| } |
| } |
| } |
| |
| private void handleError(final String message) { |
| // TODO: error handling! |
| // case THROW: |
| // throw new IllegalArgumentException(message); |
| // case LOG: |
| // _log.warn(message); |
| // break; |
| // default: |
| // break; |
| // } |
| } |
| |
| private StringBuilder resetBuffer() { |
| _buffer.setLength(0); |
| return _buffer; |
| } |
| |
| } |