blob: bb86c3d0a56a1fbb790a49b33605f88c51bfeca0 [file] [log] [blame]
/*
* 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 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.*;
import org.apache.solr.schema.AbstractEnumField.EnumMapping;
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;
import java.io.IOException;
import java.util.*;
import static org.apache.solr.search.facet.FacetContext.SKIP_FACET;
class FacetRangeProcessor extends FacetProcessor<FacetRange> {
// 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<FacetParams.FacetRangeInclude> 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<Range> rangeList;
/** Build by {@link #createRangeList} if and only if needed for basic faceting */
List<Range> otherList;
/**
* Serves two purposes depending on the type of request.
* <ul>
* <li>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: <code>hardend:false</code> &amp; <code>other</code>
* that requires an end value low/high value calculation). And it wil be included in the response</li>
* <li>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
* <code>other</code> buckets that need them</li>
* </ul>
*/
@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 <em>term</em> faceting over a numeric field.
* This method is unused for <code>range</code> 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 <code>Calc</code> 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 <code>Calc</code> 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<Range> parseRanges(Object input) {
if (!(input instanceof List)) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
"Expected List for ranges but got " + input.getClass().getSimpleName() + " = " + input
);
}
@SuppressWarnings({"rawtypes"})
List intervals = (List) input;
List<Range> 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<String, Object> interval = (Map<String, Object>) obj;
if (interval.containsKey("range")) {
range = getRangeByOldFormat(interval);
} else {
range = getRangeByNewFormat(interval);
}
ranges.add(range);
}
return ranges;
}
private boolean getBoolean(Map<String,Object> 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<String,Object> 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<String, Object> 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<String, Object> 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<SimpleOrderedMap> 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;
intersections[slot] = intersection; // save for later // TODO: only save if number of slots is small enough?
}
int 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.
*/
static abstract 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 <code>range</code> faceting
*/
@SuppressWarnings({"rawtypes"})
public Comparable bitsToValue(long bits) {
return bits;
}
/**
* Used by {@link FacetFieldProcessorByHashDV} for field faceting on numeric types -- not used for <code>range</code> 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<Object> 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;
List<List> skip = FacetFieldProcessor.asList(fcontext.facetInfo.get("_s")); // 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.
// 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<Object> res = new SimpleOrderedMap<>();
List<SimpleOrderedMap> 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<String,Object> facetInfo = (Map<String, Object>) 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<String,Object> specialFacetInfo;
specialFacetInfo = (Map<String, Object>) fcontext.facetInfo.get(FacetParams.FacetRangeOther.BEFORE.toString());
if (null != specialFacetInfo) {
res.add(FacetParams.FacetRangeOther.BEFORE.toString(),
refineRange(buildBeforeRange(), skipThisFacet, specialFacetInfo));
}
specialFacetInfo = (Map<String, Object>) fcontext.facetInfo.get(FacetParams.FacetRangeOther.AFTER.toString());
if (null != specialFacetInfo) {
res.add(FacetParams.FacetRangeOther.AFTER.toString(),
refineRange(buildAfterRange(), skipThisFacet, specialFacetInfo));
}
specialFacetInfo = (Map<String, Object>) 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<Object> refineBucket(Object bucketVal, boolean skip, Map<String,Object> facetInfo) throws IOException {
String val = bucketVal.toString();
if (ranges != null) {
try {
Range range = parseRangeFromString(val, val);
final SimpleOrderedMap<Object> 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<Object> bucket = refineRange(range, skip, facetInfo);
bucket.add("val", range.label);
return bucket;
}
/** Helper method for refining a Range
* @see #fillBucket
*/
private SimpleOrderedMap<Object> refineRange(Range range, boolean skip, Map<String,Object> facetInfo) throws IOException {
final SimpleOrderedMap<Object> 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);
}
}