All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.solr.search.facet.FacetRangeProcessor Maven / Gradle / Ivy

There is a newer version: 9.7.0
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.solr.search.facet;

import static org.apache.solr.search.facet.FacetContext.SKIP_FACET;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import org.apache.lucene.search.Query;
import org.apache.lucene.util.NumericUtils;
import org.apache.solr.common.EnumFieldValue;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.FacetParams;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.schema.AbstractEnumField;
import org.apache.solr.schema.AbstractEnumField.EnumMapping;
import org.apache.solr.schema.CurrencyFieldType;
import org.apache.solr.schema.CurrencyValue;
import org.apache.solr.schema.DateRangeField;
import org.apache.solr.schema.ExchangeRateProvider;
import org.apache.solr.schema.FieldType;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.schema.TrieDateField;
import org.apache.solr.schema.TrieField;
import org.apache.solr.search.DocSet;
import org.apache.solr.search.ExtendedQuery;
import org.apache.solr.search.SyntaxError;
import org.apache.solr.search.WrappedQuery;
import org.apache.solr.util.DateMathParser;

class FacetRangeProcessor extends FacetProcessor {
  // TODO: the code paths for initial faceting, vs refinement, are very different...
  // TODO: ...it might make sense to have seperate classes w/a common base?
  // TODO: let FacetRange.createFacetProcessor decide which one to instantiate?

  final SchemaField sf;
  final Calc calc;
  final EnumSet include;
  final long effectiveMincount;

  @SuppressWarnings({"rawtypes"})
  final Comparable start;

  @SuppressWarnings({"rawtypes"})
  final Comparable end;

  final String gap;
  final Object ranges;

  /** Build by {@link #createRangeList} if and only if needed for basic faceting */
  List rangeList;

  /** Build by {@link #createRangeList} if and only if needed for basic faceting */
  List otherList;

  /**
   * Serves two purposes depending on the type of request.
   *
   * 
    *
  • If this is a phase#1 shard request, then {@link #createRangeList} will set this value * (non null) if and only if it is needed for refinement (ie: hardend:false * & other that requires an end value low/high value calculation). And it * wil be included in the response *
  • If this is a phase#2 refinement request, this variable will be used {@link * #getOrComputeActualEndForRefinement} to track the value sent with the refinement request * -- or to cache a recomputed value if the request omitted it -- for use in refining the * other buckets that need them *
*/ @SuppressWarnings({"rawtypes"}) Comparable actual_end = null; // null until/unless we need it FacetRangeProcessor(FacetContext fcontext, FacetRange freq) { super(fcontext, freq); include = freq.include; sf = fcontext.searcher.getSchema().getField(freq.field); calc = getCalcForField(sf); if (freq.ranges != null && (freq.start != null || freq.end != null || freq.gap != null)) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "Cannot set gap/start/end and ranges params together"); } if (freq.ranges != null) { ranges = freq.ranges; start = null; end = null; gap = null; } else { start = calc.getValue(freq.start.toString()); end = calc.getValue(freq.end.toString()); gap = freq.gap.toString(); ranges = null; } // Under the normal mincount=0, each shard will need to return 0 counts since we don't calculate // buckets at the top level. // If mincount>0 then we could *potentially* set our sub mincount to 1... // ...but that would require sorting the buckets (by their val) at the top level // // Rather then do that, which could be complicated by non trivial field types, we'll force the // sub-shard effectiveMincount to be 0, ensuring that we can trivially merge all the buckets // from every shard (we have to filter the merged buckets by the original mincount either way) effectiveMincount = fcontext.isShard() ? 0 : freq.mincount; } @Override @SuppressWarnings({"unchecked"}) public void process() throws IOException { super.process(); if (fcontext.facetInfo != null) { // refinement? response = refineFacets(); } else { // phase#1: build list of all buckets and return full facets... createRangeList(); response = getRangeCountsIndexed(); } } @SuppressWarnings({"rawtypes"}) private static class Range { Object label; Comparable low; Comparable high; boolean includeLower; boolean includeUpper; public Range( Object label, Comparable low, Comparable high, boolean includeLower, boolean includeUpper) { this.label = label; this.low = low; this.high = high; this.includeLower = includeLower; this.includeUpper = includeUpper; } } /** * Returns a {@link Calc} instance to use for term faceting over a numeric field. This * method is unused for range faceting, and exists solely as a helper method for * other classes * * @param sf A field to facet on, must be of a type such that {@link FieldType#getNumberType} is * non null * @return a Calc instance with {@link Calc#bitsToValue} and {@link * Calc#bitsToSortableBits} methods suitable for the specified field. * @see FacetFieldProcessorByHashDV */ public static Calc getNumericCalc(SchemaField sf) { final FieldType ft = sf.getType(); if (ft.getNumberType() != null) { if (ft instanceof AbstractEnumField) { return new EnumCalc(sf); } switch (ft.getNumberType()) { case FLOAT: return new FloatCalc(sf); case DOUBLE: return new DoubleCalc(sf); case INTEGER: return new IntCalc(sf); case LONG: return new LongCalc(sf); case DATE: return new DateCalc(sf, null); } } throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "Expected numeric field type :" + sf); } /** * Helper method used in processor constructor * * @return a Calc instance with {@link Calc#bitsToValue} and {@link * Calc#bitsToSortableBits} methods suitable for the specified field. */ private static Calc getCalcForField(SchemaField sf) { final FieldType ft = sf.getType(); if (ft instanceof TrieField || ft.isPointField()) { return getNumericCalc(sf); } else if (ft instanceof CurrencyFieldType) { return new CurrencyCalc(sf); } else if (ft instanceof DateRangeField) { return new DateCalc(sf, null); } // if we made it this far, we have no idea what it is... throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "Unable to range facet on field:" + sf.getName()); } @SuppressWarnings({"unchecked", "rawtypes"}) private void createRangeList() { rangeList = new ArrayList<>(); otherList = new ArrayList<>(3); Comparable low = start; Comparable loop_end = this.end; if (ranges != null) { rangeList.addAll(parseRanges(ranges)); return; } while (low.compareTo(end) < 0) { Comparable high = calc.addGap(low, gap); if (end.compareTo(high) < 0) { if (freq.hardend) { high = loop_end; } else { loop_end = high; } } if (high.compareTo(low) < 0) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "range facet infinite loop (is gap negative? did the math overflow?)"); } if (high.compareTo(low) == 0) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "range facet infinite loop: gap is either zero, or too small relative start/end and caused underflow: " + low + " + " + gap + " = " + high); } boolean incLower = (include.contains(FacetParams.FacetRangeInclude.LOWER) || (include.contains(FacetParams.FacetRangeInclude.EDGE) && 0 == low.compareTo(start))); boolean incUpper = (include.contains(FacetParams.FacetRangeInclude.UPPER) || (include.contains(FacetParams.FacetRangeInclude.EDGE) && 0 == high.compareTo(end))); Range range = new Range(calc.buildRangeLabel(low), low, high, incLower, incUpper); rangeList.add(range); low = high; } // no matter what other values are listed, we don't do // anything if "none" is specified. if (!freq.others.contains(FacetParams.FacetRangeOther.NONE)) { final boolean all = freq.others.contains(FacetParams.FacetRangeOther.ALL); if (all || freq.others.contains(FacetParams.FacetRangeOther.BEFORE)) { otherList.add(buildBeforeRange()); } if (all || freq.others.contains(FacetParams.FacetRangeOther.AFTER)) { actual_end = loop_end; otherList.add(buildAfterRange()); } if (all || freq.others.contains(FacetParams.FacetRangeOther.BETWEEN)) { actual_end = loop_end; otherList.add(buildBetweenRange()); } } // if we're not a shard request, or this is a hardend:true situation, then actual_end isn't // needed if (freq.hardend || (!fcontext.isShard())) { actual_end = null; } } /** * Parses the given list of maps and returns list of Ranges * * @param input - list of map containing the ranges * @return list of {@link Range} */ private List parseRanges(Object input) { if (!(input instanceof List)) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "Expected List for ranges but got " + input.getClass().getSimpleName() + " = " + input); } List intervals = (List) input; List ranges = new ArrayList<>(); for (Object obj : intervals) { if (!(obj instanceof Map)) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "Expected Map for range but got " + obj.getClass().getSimpleName() + " = " + obj); } Range range; @SuppressWarnings({"unchecked"}) Map interval = (Map) obj; if (interval.containsKey("range")) { range = getRangeByOldFormat(interval); } else { range = getRangeByNewFormat(interval); } ranges.add(range); } return ranges; } private boolean getBoolean(Map args, String paramName, boolean defVal) { Object o = args.get(paramName); if (o == null) { return defVal; } // TODO: should we be more flexible and accept things like "true" (strings)? // Perhaps wait until the use case comes up. if (!(o instanceof Boolean)) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "Expected boolean type for param '" + paramName + "' but got " + o.getClass().getSimpleName() + " = " + o); } return (Boolean) o; } private String getString(Map args, String paramName, boolean required) { Object o = args.get(paramName); if (o == null) { if (required) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "Missing required parameter '" + paramName + "' for " + args); } return null; } if (!(o instanceof String)) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "Expected string type for param '" + paramName + "' but got " + o.getClass().getSimpleName() + " = " + o); } return (String) o; } /** * Parses the range given in format {from:val1, to:val2, inclusive_to:true} and returns the {@link * Range} * * @param rangeMap Map containing the range info * @return {@link Range} */ @SuppressWarnings({"unchecked", "rawtypes"}) private Range getRangeByNewFormat(Map rangeMap) { Object fromObj = rangeMap.get("from"); Object toObj = rangeMap.get("to"); String fromStr = fromObj == null ? "*" : fromObj.toString(); String toStr = toObj == null ? "*" : toObj.toString(); boolean includeUpper = getBoolean(rangeMap, "inclusive_to", false); boolean includeLower = getBoolean(rangeMap, "inclusive_from", true); Object key = rangeMap.get("key"); // if (key == null) { // key = (includeLower? "[": "(") + fromStr + "," + toStr + (includeUpper? "]": ")"); // } // using the default key as custom key won't work with refine // refine would need both low and high values key = (includeLower ? "[" : "(") + fromStr + "," + toStr + (includeUpper ? "]" : ")"); Comparable from = getComparableFromString(fromStr); Comparable to = getComparableFromString(toStr); if (from != null && to != null && from.compareTo(to) > 0) { // allowing from and to be same throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "'from' is higher than 'to' in range for key: " + key); } return new Range(key, from, to, includeLower, includeUpper); } /** * Parses the range string from the map and Returns {@link Range} * * @param range map containing the interval * @return {@link Range} */ private Range getRangeByOldFormat(Map range) { String key = getString(range, "key", false); String rangeStr = getString(range, "range", true); try { return parseRangeFromString(key, rangeStr); } catch (SyntaxError e) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e); } } /** * Parses the given string and returns Range. This is adopted from {@link * org.apache.solr.request.IntervalFacets} * * @param key The name of range which would be used as {@link Range}'s label * @param rangeStr The string containing the Range * @return {@link Range} */ @SuppressWarnings({"rawtypes", "unchecked"}) private Range parseRangeFromString(String key, String rangeStr) throws SyntaxError { rangeStr = rangeStr.trim(); if (rangeStr.isEmpty()) { throw new SyntaxError("empty facet range"); } boolean includeLower = true, includeUpper = true; Comparable start = null, end = null; if (rangeStr.charAt(0) == '(') { includeLower = false; } else if (rangeStr.charAt(0) != '[') { throw new SyntaxError( "Invalid start character " + rangeStr.charAt(0) + " in facet range " + rangeStr); } final int lastNdx = rangeStr.length() - 1; if (rangeStr.charAt(lastNdx) == ')') { includeUpper = false; } else if (rangeStr.charAt(lastNdx) != ']') { throw new SyntaxError( "Invalid end character " + rangeStr.charAt(lastNdx) + " in facet range " + rangeStr); } StringBuilder startStr = new StringBuilder(lastNdx); int i = unescape(rangeStr, 1, lastNdx, startStr); if (i == lastNdx) { if (rangeStr.charAt(lastNdx - 1) == ',') { throw new SyntaxError("Empty range limit"); } throw new SyntaxError("Missing unescaped comma separating range ends in " + rangeStr); } start = getComparableFromString(startStr.toString()); StringBuilder endStr = new StringBuilder(lastNdx); i = unescape(rangeStr, i, lastNdx, endStr); if (i != lastNdx) { throw new SyntaxError("Extra unescaped comma at index " + i + " in range " + rangeStr); } end = getComparableFromString(endStr.toString()); if (start != null && end != null && start.compareTo(end) > 0) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "'start' is higher than 'end' in range for key: " + rangeStr); } // not using custom key as it won't work with refine // refine would need both low and high values return new Range(rangeStr, start, end, includeLower, includeUpper); } /* Fill in sb with a string from i to the first unescaped comma, or n. Return the index past the unescaped comma, or n if no unescaped comma exists */ private int unescape(String s, int i, int n, StringBuilder sb) throws SyntaxError { for (; i < n; ++i) { char c = s.charAt(i); if (c == '\\') { ++i; if (i < n) { c = s.charAt(i); } else { throw new SyntaxError("Unfinished escape at index " + i + " in facet range " + s); } } else if (c == ',') { return i + 1; } sb.append(c); } return n; } @SuppressWarnings({"rawtypes"}) private Comparable getComparableFromString(String value) { value = value.trim(); if ("*".equals(value)) { return null; } return calc.getValue(value); } @SuppressWarnings({"unchecked", "rawtypes"}) private SimpleOrderedMap getRangeCountsIndexed() throws IOException { final boolean hasSubFacets = !freq.getSubFacets().isEmpty(); int slotCount = rangeList.size() + otherList.size(); if (hasSubFacets) { intersections = new DocSet[slotCount]; filters = new Query[slotCount]; } else { intersections = null; filters = null; } createAccs(fcontext.base.size(), slotCount); for (int idx = 0; idx < rangeList.size(); idx++) { rangeStats(rangeList.get(idx), idx, hasSubFacets); } for (int idx = 0; idx < otherList.size(); idx++) { rangeStats(otherList.get(idx), rangeList.size() + idx, hasSubFacets); } final SimpleOrderedMap res = new SimpleOrderedMap<>(); List buckets = new ArrayList<>(); res.add("buckets", buckets); for (int idx = 0; idx < rangeList.size(); idx++) { if (effectiveMincount > 0 && countAcc.getCount(idx) < effectiveMincount) continue; Range range = rangeList.get(idx); SimpleOrderedMap bucket = new SimpleOrderedMap(); buckets.add(bucket); bucket.add("val", range.label); addStats(bucket, idx); if (hasSubFacets) doSubs(bucket, idx); } for (int idx = 0; idx < otherList.size(); idx++) { // we don't skip these buckets based on mincount Range range = otherList.get(idx); SimpleOrderedMap bucket = new SimpleOrderedMap(); res.add(range.label.toString(), bucket); addStats(bucket, rangeList.size() + idx); if (hasSubFacets) doSubs(bucket, rangeList.size() + idx); } if (null != actual_end) { res.add(FacetRange.ACTUAL_END_JSON_KEY, calc.formatValue(actual_end)); } return res; } private Query[] filters; private DocSet[] intersections; private void rangeStats(Range range, int slot, boolean hasSubFacets) throws IOException { final Query rangeQ; { final Query rangeQuery = sf.getType() .getRangeQuery( null, sf, range.low == null ? null : calc.formatValue(range.low), range.high == null ? null : calc.formatValue(range.high), range.includeLower, range.includeUpper); if (fcontext.cache) { rangeQ = rangeQuery; } else if (rangeQuery instanceof ExtendedQuery) { ((ExtendedQuery) rangeQuery).setCache(false); rangeQ = rangeQuery; } else { final WrappedQuery wrappedQuery = new WrappedQuery(rangeQuery); wrappedQuery.setCache(false); rangeQ = wrappedQuery; } } // TODO: specialize count only DocSet intersection = fcontext.searcher.getDocSet(rangeQ, fcontext.base); if (hasSubFacets) { filters[slot] = rangeQ; // save for later // TODO: only save if number of slots is small enough? intersections[slot] = intersection; } long num = collect( intersection, slot, slotNum -> { return new SlotAcc.SlotContext(rangeQ); }); countAcc.incrementCount(slot, num); // TODO: roll this into collect() } @SuppressWarnings({"unchecked", "rawtypes"}) private void doSubs(SimpleOrderedMap bucket, int slot) throws IOException { // handle sub-facets for this bucket DocSet subBase = intersections[slot]; try { processSubs(bucket, filters[slot], subBase, false, null); } finally { // subContext.base.decref(); // OFF-HEAP // subContext.base = null; // do not modify context after creation... there may be deferred // execution (i.e. streaming) } } // Essentially copied from SimpleFacets... // would be nice to unify this stuff w/ analytics component... /** * Perhaps someday instead of having a giant "instanceof" case statement to pick an impl, we can * add a "RangeFacetable" marker interface to FieldTypes and they can return instances of these * directly from some method -- but until then, keep this locked down and private. */ abstract static class Calc { protected final SchemaField field; public Calc(final SchemaField field) { this.field = field; } /** * Used by {@link FacetFieldProcessorByHashDV} for field faceting on numeric types -- not used * for range faceting */ @SuppressWarnings({"rawtypes"}) public Comparable bitsToValue(long bits) { return bits; } /** * Used by {@link FacetFieldProcessorByHashDV} for field faceting on numeric types -- not used * for range faceting */ public long bitsToSortableBits(long bits) { return bits; } /** * Given the low value for a bucket, generates the appropriate "label" object to use. By default * return the low object unmodified. */ public Object buildRangeLabel(@SuppressWarnings("rawtypes") Comparable low) { return low; } /** Formats a value into a label used in a response Default Impl just uses toString() */ public String formatValue(@SuppressWarnings("rawtypes") final Comparable val) { return val.toString(); } /** Parses a String param into a value throwing an exception if not possible */ @SuppressWarnings({"rawtypes"}) public final Comparable getValue(final String rawval) { try { return parseStr(rawval); } catch (Exception e) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "Can't parse value " + rawval + " for field: " + field.getName(), e); } } /** Parses a String param into a value. Can throw a low level format exception as needed. */ @SuppressWarnings({"rawtypes"}) protected abstract Comparable parseStr(final String rawval) throws java.text.ParseException; /** * Parses a String param into a value that represents the gap and can be included in the * response, throwing a useful exception if not possible. * *

Note: uses Object as the return type instead of T for things like Date where gap is just a * DateMathParser string */ public final Object getGap(final String gap) { try { return parseGap(gap); } catch (Exception e) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "Can't parse gap " + gap + " for field: " + field.getName(), e); } } /** * Parses a String param into a value that represents the gap and can be included in the * response. Can throw a low level format exception as needed. * *

Default Impl calls parseVal */ protected Object parseGap(final String rawval) throws java.text.ParseException { return parseStr(rawval); } /** * Adds the String gap param to a low Range endpoint value to determine the corresponding high * Range endpoint value, throwing a useful exception if not possible. */ @SuppressWarnings({"rawtypes"}) public final Comparable addGap(Comparable value, String gap) { try { return parseAndAddGap(value, gap); } catch (Exception e) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "Can't add gap " + gap + " to value " + value + " for field: " + field.getName(), e); } } /** * Adds the String gap param to a low Range endpoint value to determine the corresponding high * Range endpoint value. Can throw a low level format exception as needed. */ @SuppressWarnings({"rawtypes"}) protected abstract Comparable parseAndAddGap(Comparable value, String gap) throws java.text.ParseException; } private static class FloatCalc extends Calc { @SuppressWarnings("rawtypes") @Override public Comparable bitsToValue(long bits) { if (field.getType().isPointField() && field.multiValued()) { return NumericUtils.sortableIntToFloat((int) bits); } else { return Float.intBitsToFloat((int) bits); } } @Override public long bitsToSortableBits(long bits) { return NumericUtils.sortableDoubleBits(bits); } public FloatCalc(final SchemaField f) { super(f); } @Override protected Float parseStr(String rawval) { return Float.valueOf(rawval); } @Override public Float parseAndAddGap(@SuppressWarnings("rawtypes") Comparable value, String gap) { return ((Number) value).floatValue() + Float.parseFloat(gap); } } private static class DoubleCalc extends Calc { @Override @SuppressWarnings({"rawtypes"}) public Comparable bitsToValue(long bits) { if (field.getType().isPointField() && field.multiValued()) { return NumericUtils.sortableLongToDouble(bits); } else { return Double.longBitsToDouble(bits); } } @Override public long bitsToSortableBits(long bits) { return NumericUtils.sortableDoubleBits(bits); } public DoubleCalc(final SchemaField f) { super(f); } @Override protected Double parseStr(String rawval) { return Double.valueOf(rawval); } @Override public Double parseAndAddGap(@SuppressWarnings("rawtypes") Comparable value, String gap) { return ((Number) value).doubleValue() + Double.parseDouble(gap); } } private static class IntCalc extends Calc { public IntCalc(final SchemaField f) { super(f); } @Override @SuppressWarnings({"rawtypes"}) public Comparable bitsToValue(long bits) { return (int) bits; } @Override protected Integer parseStr(String rawval) { return Integer.valueOf(rawval); } @Override public Integer parseAndAddGap(@SuppressWarnings("rawtypes") Comparable value, String gap) { return ((Number) value).intValue() + Integer.parseInt(gap); } } private static class LongCalc extends Calc { public LongCalc(final SchemaField f) { super(f); } @Override protected Long parseStr(String rawval) { return Long.valueOf(rawval); } @Override public Long parseAndAddGap(@SuppressWarnings("rawtypes") Comparable value, String gap) { return ((Number) value).longValue() + Long.parseLong(gap); } } private static class EnumCalc extends Calc { private final EnumMapping mapping; public EnumCalc(final SchemaField f) { super(f); mapping = ((AbstractEnumField) field.getType()).getEnumMapping(); } @Override public EnumFieldValue bitsToValue(long bits) { Integer val = (int) bits; return new EnumFieldValue(val, mapping.intValueToStringValue(val)); } @Override protected EnumFieldValue parseStr(String rawval) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "Cannot perform range faceting over Enum fields!"); } @Override protected EnumFieldValue parseAndAddGap( @SuppressWarnings("rawtypes") Comparable value, String gap) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "Cannot perform range faceting over Enum fields!"); } } private static class DateCalc extends Calc { private final Date now; public DateCalc(final SchemaField f, final Date now) { super(f); this.now = now; if (!(field.getType() instanceof TrieDateField || field.getType().isPointField() || field.getType() instanceof DateRangeField)) { throw new IllegalArgumentException( "SchemaField must use field type extending TrieDateField, DateRangeField or PointField"); } } @Override @SuppressWarnings({"rawtypes"}) public Comparable bitsToValue(long bits) { return new Date(bits); } @Override public String formatValue(@SuppressWarnings("rawtypes") Comparable val) { return ((Date) val).toInstant().toString(); } @Override protected Date parseStr(String rawval) { return DateMathParser.parseMath(now, rawval); } @Override protected Object parseGap(final String rawval) { return rawval; } @Override public Date parseAndAddGap(@SuppressWarnings("rawtypes") Comparable value, String gap) throws java.text.ParseException { final DateMathParser dmp = new DateMathParser(); dmp.setNow((Date) value); return dmp.parseMath(gap); } } private static class CurrencyCalc extends Calc { private String defaultCurrencyCode; private ExchangeRateProvider exchangeRateProvider; public CurrencyCalc(final SchemaField field) { super(field); if (!(this.field.getType() instanceof CurrencyFieldType)) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "Cannot perform range faceting over non CurrencyField fields"); } defaultCurrencyCode = ((CurrencyFieldType) this.field.getType()).getDefaultCurrency(); exchangeRateProvider = ((CurrencyFieldType) this.field.getType()).getProvider(); } /** * Throws a Server Error that this type of operation is not supported for this field * {@inheritDoc} */ @Override @SuppressWarnings({"rawtypes"}) public Comparable bitsToValue(long bits) { throw new SolrException( SolrException.ErrorCode.SERVER_ERROR, "Currency Field " + field.getName() + " can not be used in this way"); } /** * Throws a Server Error that this type of operation is not supported for this field * {@inheritDoc} */ @Override public long bitsToSortableBits(long bits) { throw new SolrException( SolrException.ErrorCode.SERVER_ERROR, "Currency Field " + field.getName() + " can not be used in this way"); } /** * Returns the short string representation of the CurrencyValue * * @see CurrencyValue#strValue */ @Override public Object buildRangeLabel(@SuppressWarnings("rawtypes") Comparable low) { return ((CurrencyValue) low).strValue(); } @Override public String formatValue(@SuppressWarnings("rawtypes") Comparable val) { return ((CurrencyValue) val).strValue(); } @Override @SuppressWarnings({"rawtypes"}) protected Comparable parseStr(final String rawval) throws java.text.ParseException { return CurrencyValue.parse(rawval, defaultCurrencyCode); } @Override protected Object parseGap(final String rawval) throws java.text.ParseException { return parseStr(rawval); } @Override @SuppressWarnings({"rawtypes"}) protected Comparable parseAndAddGap(Comparable value, String gap) throws java.text.ParseException { if (value == null) { throw new NullPointerException("Cannot perform range faceting on null CurrencyValue"); } CurrencyValue val = (CurrencyValue) value; CurrencyValue gapCurrencyValue = CurrencyValue.parse(gap, defaultCurrencyCode); long gapAmount = CurrencyValue.convertAmount( this.exchangeRateProvider, gapCurrencyValue.getCurrencyCode(), gapCurrencyValue.getAmount(), val.getCurrencyCode()); return new CurrencyValue(val.getAmount() + gapAmount, val.getCurrencyCode()); } } @SuppressWarnings({"unchecked", "rawtypes"}) protected SimpleOrderedMap refineFacets() throws IOException { // this refineFacets method is patterned after FacetFieldProcessor.refineFacets such that // the same "_s" skip bucket syntax is used and FacetRangeMerger can subclass // FacetRequestSortedMerger for dealing with them & the refinement requests. // // But range faceting does *NOT* use the "leaves" and "partial" syntax // // If/When range facet becomes more like field facet in it's ability to sort and limit the // "range buckets" FacetRangeProcessor and FacetFieldProcessor should probably be refactored to // share more code. boolean skipThisFacet = (fcontext.flags & SKIP_FACET) != 0; // We have seen this bucket, so skip stats on it, and skip sub-facets except for the specified // sub-facets that should calculate specified buckets. List skip = FacetFieldProcessor.asList(fcontext.facetInfo.get("_s")); // sanity check our merger's super class didn't send us something we can't handle ... assert 0 == FacetFieldProcessor.asList(fcontext.facetInfo.get("_l")).size(); assert 0 == FacetFieldProcessor.asList(fcontext.facetInfo.get("_p")).size(); SimpleOrderedMap res = new SimpleOrderedMap<>(); List bucketList = new ArrayList<>(skip.size()); res.add("buckets", bucketList); // TODO: an alternate implementations can fill all accs at once createAccs(-1, 1); for (List bucketAndFacetInfo : skip) { assert bucketAndFacetInfo.size() == 2; Object bucketVal = bucketAndFacetInfo.get(0); Map facetInfo = (Map) bucketAndFacetInfo.get(1); bucketList.add(refineBucket(bucketVal, true, facetInfo)); } { // refine the special "other" buckets // NOTE: we're re-using this variable for each special we look for... Map specialFacetInfo; specialFacetInfo = (Map) fcontext.facetInfo.get(FacetParams.FacetRangeOther.BEFORE.toString()); if (null != specialFacetInfo) { res.add( FacetParams.FacetRangeOther.BEFORE.toString(), refineRange(buildBeforeRange(), skipThisFacet, specialFacetInfo)); } specialFacetInfo = (Map) fcontext.facetInfo.get(FacetParams.FacetRangeOther.AFTER.toString()); if (null != specialFacetInfo) { res.add( FacetParams.FacetRangeOther.AFTER.toString(), refineRange(buildAfterRange(), skipThisFacet, specialFacetInfo)); } specialFacetInfo = (Map) fcontext.facetInfo.get(FacetParams.FacetRangeOther.BETWEEN.toString()); if (null != specialFacetInfo) { res.add( FacetParams.FacetRangeOther.BETWEEN.toString(), refineRange(buildBetweenRange(), skipThisFacet, specialFacetInfo)); } } return res; } /** * Returns the "Actual End" value sent from the merge as part of the refinement request (if any) * or re-computes it as needed using the Calc and caches the result for re-use */ @SuppressWarnings({"rawtypes", "unchecked"}) private Comparable getOrComputeActualEndForRefinement() { if (null != actual_end) { return actual_end; } if (freq.hardend) { actual_end = this.end; } else if (fcontext.facetInfo.containsKey(FacetRange.ACTUAL_END_JSON_KEY)) { actual_end = calc.getValue(fcontext.facetInfo.get(FacetRange.ACTUAL_END_JSON_KEY).toString()); } else { // a quick and dirty loop over the ranges (we don't need) to compute the actual_end... Comparable low = start; while (low.compareTo(end) < 0) { Comparable high = calc.addGap(low, gap); if (end.compareTo(high) < 0) { actual_end = high; break; } if (high.compareTo(low) <= 0) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "Garbage input for facet refinement w/o " + FacetRange.ACTUAL_END_JSON_KEY); } low = high; } } assert null != actual_end; return actual_end; } @SuppressWarnings({"unchecked", "rawtypes"}) private SimpleOrderedMap refineBucket( Object bucketVal, boolean skip, Map facetInfo) throws IOException { String val = bucketVal.toString(); if (ranges != null) { try { Range range = parseRangeFromString(val, val); final SimpleOrderedMap bucket = refineRange(range, skip, facetInfo); bucket.add("val", range.label); return bucket; } catch (SyntaxError e) { // execution won't reach here as ranges are already validated throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e); } } Comparable low = calc.getValue(val); Comparable high = calc.addGap(low, gap); Comparable max_end = end; if (end.compareTo(high) < 0) { if (freq.hardend) { high = max_end; } else { max_end = high; } } if (high.compareTo(low) < 0) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "range facet infinite loop (is gap negative? did the math overflow?)"); } if (high.compareTo(low) == 0) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "range facet infinite loop: gap is either zero, or too small relative start/end and caused underflow: " + low + " + " + gap + " = " + high); } boolean incLower = (include.contains(FacetParams.FacetRangeInclude.LOWER) || (include.contains(FacetParams.FacetRangeInclude.EDGE) && 0 == low.compareTo(start))); boolean incUpper = (include.contains(FacetParams.FacetRangeInclude.UPPER) || (include.contains(FacetParams.FacetRangeInclude.EDGE) && 0 == high.compareTo(max_end))); Range range = new Range(calc.buildRangeLabel(low), low, high, incLower, incUpper); // now refine this range final SimpleOrderedMap bucket = refineRange(range, skip, facetInfo); bucket.add("val", range.label); return bucket; } /** * Helper method for refining a Range * * @see #fillBucket */ private SimpleOrderedMap refineRange( Range range, boolean skip, Map facetInfo) throws IOException { final SimpleOrderedMap bucket = new SimpleOrderedMap<>(); final Query domainQ = sf.getType() .getRangeQuery( null, sf, range.low == null ? null : calc.formatValue(range.low), range.high == null ? null : calc.formatValue(range.high), range.includeLower, range.includeUpper); fillBucket(bucket, domainQ, null, skip, facetInfo); return bucket; } /** Helper method for building a "before" Range */ private Range buildBeforeRange() { // include upper bound if "outer" or if first gap doesn't already include it final boolean incUpper = (include.contains(FacetParams.FacetRangeInclude.OUTER) || (!(include.contains(FacetParams.FacetRangeInclude.LOWER) || include.contains(FacetParams.FacetRangeInclude.EDGE)))); return new Range(FacetParams.FacetRangeOther.BEFORE.toString(), null, start, false, incUpper); } /** Helper method for building a "after" Range */ private Range buildAfterRange() { @SuppressWarnings({"rawtypes"}) final Comparable the_end = getOrComputeActualEndForRefinement(); assert null != the_end; final boolean incLower = (include.contains(FacetParams.FacetRangeInclude.OUTER) || (!(include.contains(FacetParams.FacetRangeInclude.UPPER) || include.contains(FacetParams.FacetRangeInclude.EDGE)))); return new Range(FacetParams.FacetRangeOther.AFTER.toString(), the_end, null, incLower, false); } /** Helper method for building a "between" Range */ private Range buildBetweenRange() { @SuppressWarnings({"rawtypes"}) final Comparable the_end = getOrComputeActualEndForRefinement(); assert null != the_end; final boolean incLower = (include.contains(FacetParams.FacetRangeInclude.LOWER) || include.contains(FacetParams.FacetRangeInclude.EDGE)); final boolean incUpper = (include.contains(FacetParams.FacetRangeInclude.UPPER) || include.contains(FacetParams.FacetRangeInclude.EDGE)); return new Range( FacetParams.FacetRangeOther.BETWEEN.toString(), start, the_end, incLower, incUpper); } }