SOLR-12516: Fix some bugs in 'type:range' Facet refinement when sub-facets are combined with non default values for the 'other' and 'include' options.

1) the optional other buckets (before/after/between) are not considered during refinement

2) when using the include option: if edge is specified, then the refinement of all range buckets mistakenly includes the lower bound of the range, regardless of whether lower was specified.
This commit is contained in:
Chris Hostetter 2018-07-06 09:34:05 -07:00
parent a09f3facfc
commit 7d8ef9e39d
7 changed files with 1226 additions and 189 deletions

View File

@ -140,6 +140,8 @@ Bug Fixes
* SOLR-2834: Fix SolrJ Field and Document analyzes for types that include CharacterFilter (Alexandre Rafalovitch)
* SOLR-12516: Fix some bugs in 'type:range' Facet refinement when sub-facets are combined with non
default values for the 'other' and 'include' options. (hossman)
Optimizations
----------------------

View File

@ -27,7 +27,8 @@ import java.util.Map;
import org.apache.lucene.search.Query;
import org.apache.lucene.util.NumericUtils;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.FacetParams;
import org.apache.solr.common.params.FacetParams.FacetRangeInclude;
import org.apache.solr.common.params.FacetParams.FacetRangeOther;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.schema.CurrencyFieldType;
import org.apache.solr.schema.CurrencyValue;
@ -44,13 +45,15 @@ import org.apache.solr.util.DateMathParser;
import static org.apache.solr.search.facet.FacetContext.SKIP_FACET;
public class FacetRange extends FacetRequestSorted {
static final String ACTUAL_END_JSON_KEY = "_actual_end";
String field;
Object start;
Object end;
Object gap;
boolean hardend = false;
EnumSet<FacetParams.FacetRangeInclude> include;
EnumSet<FacetParams.FacetRangeOther> others;
EnumSet<FacetRangeInclude> include;
EnumSet<FacetRangeOther> others;
{
// defaults
@ -82,30 +85,68 @@ public class FacetRange extends FacetRequestSorted {
class FacetRangeProcessor extends FacetProcessor<FacetRange> {
SchemaField sf;
Calc calc;
// 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<FacetRangeInclude> include;
final long effectiveMincount;
final Comparable start;
final Comparable end;
final String gap;
/** 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;
long effectiveMincount;
/**
* 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 requres 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>
*/
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);
start = calc.getValue(freq.start.toString());
end = calc.getValue(freq.end.toString());
gap = freq.gap.toString();
// 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
public void process() throws IOException {
super.process();
// 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
//
// Tather 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;
sf = fcontext.searcher.getSchema().getField(freq.field);
response = getRangeCounts();
if (fcontext.facetInfo != null) { // refinement?
response = refineFacets();
} else {
// phase#1: build list of all buckets and return full facets...
createRangeList();
response = getRangeCountsIndexed();
}
}
private static class Range {
@ -126,7 +167,7 @@ class FacetRangeProcessor extends FacetProcessor<FacetRange> {
/**
* Returns a {@link Calc} instance to use for <em>term</em> faceting over a numeric field.
* This metod is unused for <code>range</code> faceting, and exists solely as a helper method for other classes
* 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.
@ -190,68 +231,53 @@ class FacetRangeProcessor extends FacetProcessor<FacetRange> {
return calc;
}
private SimpleOrderedMap<Object> getRangeCounts() throws IOException {
/**
* 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()) {
switch (ft.getNumberType()) {
case FLOAT:
calc = new FloatCalc(sf);
break;
return new FloatCalc(sf);
case DOUBLE:
calc = new DoubleCalc(sf);
break;
return new DoubleCalc(sf);
case INTEGER:
calc = new IntCalc(sf);
break;
return new IntCalc(sf);
case LONG:
calc = new LongCalc(sf);
break;
return new LongCalc(sf);
case DATE:
calc = new DateCalc(sf, null);
break;
return new DateCalc(sf, null);
default:
throw new SolrException
(SolrException.ErrorCode.BAD_REQUEST,
"Unable to range facet on tried field of unexpected type:" + freq.field);
"Unable to range facet on numeric field of unexpected type:" + sf.getName());
}
} else if (ft instanceof CurrencyFieldType) {
calc = new CurrencyCalc(sf);
} else {
throw new SolrException
(SolrException.ErrorCode.BAD_REQUEST,
"Unable to range facet on field:" + sf);
return new CurrencyCalc(sf);
}
if (fcontext.facetInfo != null) {
return refineFacets();
}
createRangeList();
return getRangeCountsIndexed();
// 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());
}
private void createRangeList() throws IOException {
rangeList = new ArrayList<>();
otherList = new ArrayList<>(3);
Comparable start = calc.getValue(freq.start.toString());
Comparable end = calc.getValue(freq.end.toString());
EnumSet<FacetParams.FacetRangeInclude> include = freq.include;
String gap = freq.gap.toString();
Comparable low = start;
Comparable loop_end = this.end;
while (low.compareTo(end) < 0) {
Comparable high = calc.addGap(low, gap);
if (end.compareTo(high) < 0) {
if (freq.hardend) {
high = end;
high = loop_end;
} else {
end = high;
loop_end = high;
}
}
if (high.compareTo(low) < 0) {
@ -265,15 +291,11 @@ class FacetRangeProcessor extends FacetProcessor<FacetRange> {
"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)));
boolean incLower =(include.contains(FacetRangeInclude.LOWER) ||
(include.contains(FacetRangeInclude.EDGE) && 0 == low.compareTo(start)));
boolean incUpper = (include.contains(FacetRangeInclude.UPPER) ||
(include.contains(FacetRangeInclude.EDGE) && 0 == high.compareTo(end)));
Range range = new Range(calc.buildRangeLabel(low), low, high, incLower, incUpper);
rangeList.add( range );
@ -282,37 +304,28 @@ class FacetRangeProcessor extends FacetProcessor<FacetRange> {
// no matter what other values are listed, we don't do
// anything if "none" is specified.
if (! freq.others.contains(FacetParams.FacetRangeOther.NONE) ) {
if (! freq.others.contains(FacetRangeOther.NONE) ) {
final boolean all = freq.others.contains(FacetRangeOther.ALL);
boolean all = freq.others.contains(FacetParams.FacetRangeOther.ALL);
if (all || freq.others.contains(FacetParams.FacetRangeOther.BEFORE)) {
// include upper bound if "outer" or if first gap doesn't already include it
boolean incUpper = (include.contains(FacetParams.FacetRangeInclude.OUTER) ||
(!(include.contains(FacetParams.FacetRangeInclude.LOWER) ||
include.contains(FacetParams.FacetRangeInclude.EDGE))));
otherList.add( new Range(FacetParams.FacetRangeOther.BEFORE.toString(), null, start, false, incUpper) );
if (all || freq.others.contains(FacetRangeOther.BEFORE)) {
otherList.add( buildBeforeRange() );
}
if (all || freq.others.contains(FacetParams.FacetRangeOther.AFTER)) {
// include lower bound if "outer" or if last gap doesn't already include it
boolean incLower = (include.contains(FacetParams.FacetRangeInclude.OUTER) ||
(!(include.contains(FacetParams.FacetRangeInclude.UPPER) ||
include.contains(FacetParams.FacetRangeInclude.EDGE))));
otherList.add( new Range(FacetParams.FacetRangeOther.AFTER.toString(), end, null, incLower, false));
if (all || freq.others.contains(FacetRangeOther.AFTER)) {
actual_end = loop_end;
otherList.add( buildAfterRange() );
}
if (all || freq.others.contains(FacetParams.FacetRangeOther.BETWEEN)) {
boolean incLower = (include.contains(FacetParams.FacetRangeInclude.LOWER) ||
include.contains(FacetParams.FacetRangeInclude.EDGE));
boolean incUpper = (include.contains(FacetParams.FacetRangeInclude.UPPER) ||
include.contains(FacetParams.FacetRangeInclude.EDGE));
otherList.add( new Range(FacetParams.FacetRangeOther.BETWEEN.toString(), start, end, incLower, incUpper) );
if (all || freq.others.contains(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;
}
}
private SimpleOrderedMap getRangeCountsIndexed() throws IOException {
int slotCount = rangeList.size() + otherList.size();
@ -353,6 +366,10 @@ class FacetRangeProcessor extends FacetProcessor<FacetRange> {
addStats(bucket, rangeList.size() + idx);
doSubs(bucket, rangeList.size() + idx);
}
if (null != actual_end) {
res.add(FacetRange.ACTUAL_END_JSON_KEY, calc.formatValue(actual_end));
}
return res;
}
@ -382,23 +399,6 @@ class FacetRangeProcessor extends FacetProcessor<FacetRange> {
}
}
private SimpleOrderedMap<Object> rangeStats(Range range, boolean special ) throws IOException {
SimpleOrderedMap<Object> bucket = new SimpleOrderedMap<>();
// typically the start value of the range, but null for before/after/between
if (!special) {
bucket.add("val", range.label);
}
Query rangeQ = 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, rangeQ, null, false, null);
return bucket;
}
// Essentially copied from SimpleFacets...
// would be nice to unify this stuff w/ analytics component...
/**
@ -712,34 +712,31 @@ class FacetRangeProcessor extends FacetProcessor<FacetRange> {
}
// this refineFacets method is patterned after FacetFieldProcessor.refineFacets and should
// probably be merged when range facet becomes more like field facet in it's ability to sort and limit
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 prbably be refactored to share more code.
boolean skipThisFacet = (fcontext.flags & SKIP_FACET) != 0;
List leaves = FacetFieldProcessor.asList(fcontext.facetInfo.get("_l")); // We have not seen this bucket: do full faceting for this bucket, including all sub-facets
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.
List<List> partial = FacetFieldProcessor.asList(fcontext.facetInfo.get("_p")); // We have not seen this bucket, do full faceting for this bucket, and most sub-facets... but some sub-facets are partial and should only visit specified buckets.
// currently, only _s should be present for range facets. In the future, range facets will
// be more like field facets and will have the same refinement cases. When that happens, we should try to unify the refinement code more
assert leaves.size() == 0;
assert partial.size() == 0;
// For leaf refinements, we do full faceting for each leaf bucket. Any sub-facets of these buckets will be fully evaluated. Because of this, we should never
// encounter leaf refinements that have sub-facets that return partial results.
// 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<>( leaves.size() + skip.size() + partial.size() );
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 (Object bucketVal : leaves) {
bucketList.add( refineBucket(bucketVal, false, null) );
}
for (List bucketAndFacetInfo : skip) {
assert bucketAndFacetInfo.size() == 2;
Object bucketVal = bucketAndFacetInfo.get(0);
@ -748,51 +745,78 @@ class FacetRangeProcessor extends FacetProcessor<FacetRange> {
bucketList.add( refineBucket(bucketVal, true, facetInfo ) );
}
// The only difference between skip and missing is the value of "skip" passed to refineBucket
for (List bucketAndFacetInfo : partial) {
assert bucketAndFacetInfo.size() == 2;
Object bucketVal = bucketAndFacetInfo.get(0);
Map<String,Object> facetInfo = (Map<String, Object>) bucketAndFacetInfo.get(1);
{ // refine the special "other" buckets
// NOTE: we're re-useing this variable for each special we look for...
Map<String,Object> specialFacetInfo;
bucketList.add( refineBucket(bucketVal, false, facetInfo ) );
}
/*** special buckets
if (freq.missing) {
Map<String,Object> bucketFacetInfo = (Map<String,Object>)fcontext.facetInfo.get("missing");
if (bucketFacetInfo != null || !skipThisFacet) {
SimpleOrderedMap<Object> missingBucket = new SimpleOrderedMap<>();
fillBucket(missingBucket, getFieldMissingQuery(fcontext.searcher, freq.field), null, skipThisFacet, bucketFacetInfo);
res.add("missing", missingBucket);
specialFacetInfo = (Map<String, Object>) fcontext.facetInfo.get(FacetRangeOther.BEFORE.toString());
if (null != specialFacetInfo) {
res.add(FacetRangeOther.BEFORE.toString(),
refineRange(buildBeforeRange(), skipThisFacet, specialFacetInfo));
}
specialFacetInfo = (Map<String, Object>) fcontext.facetInfo.get(FacetRangeOther.AFTER.toString());
if (null != specialFacetInfo) {
res.add(FacetRangeOther.AFTER.toString(),
refineRange(buildAfterRange(), skipThisFacet, specialFacetInfo));
}
specialFacetInfo = (Map<String, Object>) fcontext.facetInfo.get(FacetRangeOther.BETWEEN.toString());
if (null != specialFacetInfo) {
res.add(FacetRangeOther.BETWEEN.toString(),
refineRange(buildBetweenRange(), skipThisFacet, specialFacetInfo));
}
}
**********/
// If there are just a couple of leaves, and if the domain is large, then
// going by term is likely the most efficient?
// If the domain is small, or if the number of leaves is large, then doing
// the normal collection method may be best.
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
*/
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;
}
private SimpleOrderedMap<Object> refineBucket(Object bucketVal, boolean skip, Map<String,Object> facetInfo) throws IOException {
// TODO: refactor this repeated code from above
Comparable start = calc.getValue(bucketVal.toString());
Comparable end = calc.getValue(freq.end.toString());
EnumSet<FacetParams.FacetRangeInclude> include = freq.include;
String gap = freq.gap.toString();
Comparable low = calc.getValue(bucketVal.toString());
Comparable high = calc.addGap(low, gap);
Comparable max_end = end;
if (end.compareTo(high) < 0) {
if (freq.hardend) {
high = end;
high = max_end;
} else {
end = high;
max_end = high;
}
}
if (high.compareTo(low) < 0) {
@ -806,26 +830,58 @@ class FacetRangeProcessor extends FacetProcessor<FacetRange> {
"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)));
boolean incLower = (include.contains(FacetRangeInclude.LOWER) ||
(include.contains(FacetRangeInclude.EDGE) && 0 == low.compareTo(start)));
boolean incUpper = (include.contains(FacetRangeInclude.UPPER) ||
(include.contains(FacetRangeInclude.EDGE) && 0 == high.compareTo(max_end)));
Range range = new Range(calc.buildRangeLabel(low), low, high, incLower, incUpper);
// now refine this range
SimpleOrderedMap<Object> bucket = new SimpleOrderedMap<>();
final SimpleOrderedMap<Object> bucket = refineRange(range, skip, facetInfo);
bucket.add("val", range.label);
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 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(FacetRangeInclude.OUTER) ||
(!(include.contains(FacetRangeInclude.LOWER) ||
include.contains(FacetRangeInclude.EDGE))));
return new Range(FacetRangeOther.BEFORE.toString(), null, start, false, incUpper);
}
/** Helper method for building a "after" Range */
private Range buildAfterRange() {
final Comparable the_end = getOrComputeActualEndForRefinement();
assert null != the_end;
final boolean incLower = (include.contains(FacetRangeInclude.OUTER) ||
(!(include.contains(FacetRangeInclude.UPPER) ||
include.contains(FacetRangeInclude.EDGE))));
return new Range(FacetRangeOther.AFTER.toString(), the_end, null, incLower, false);
}
/** Helper method for building a "between" Range */
private Range buildBetweenRange() {
final Comparable the_end = getOrComputeActualEndForRefinement();
assert null != the_end;
final boolean incLower = (include.contains(FacetRangeInclude.LOWER) ||
include.contains(FacetRangeInclude.EDGE));
final boolean incUpper = (include.contains(FacetRangeInclude.UPPER) ||
include.contains(FacetRangeInclude.EDGE));
return new Range(FacetRangeOther.BETWEEN.toString(), start, the_end, incLower, incUpper);
}
}

View File

@ -19,7 +19,10 @@
package org.apache.solr.search.facet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.solr.common.params.FacetParams;
import org.apache.solr.common.util.SimpleOrderedMap;
@ -28,16 +31,12 @@ public class FacetRangeMerger extends FacetRequestSortedMerger<FacetRange> {
FacetBucket beforeBucket;
FacetBucket afterBucket;
FacetBucket betweenBucket;
Object actual_end = null;
public FacetRangeMerger(FacetRange freq) {
super(freq);
}
@Override
FacetMerger createFacetMerger(String key, Object val) {
return super.createFacetMerger(key, val);
}
@Override
public void merge(Object facetResult, Context mcontext) {
super.merge(facetResult, mcontext);
@ -54,12 +53,47 @@ public class FacetRangeMerger extends FacetRequestSortedMerger<FacetRange> {
public void finish(Context mcontext) {
// nothing to do
}
@Override
Map<String, Object> getRefinementSpecial(Context mcontext, Map<String, Object> refinement, Collection<String> tagsWithPartial) {
if (!tagsWithPartial.isEmpty()) {
// Since 'other' buckets will always be included, we only need to worry about subfacets being partial.
refinement = getRefinementSpecial(mcontext, refinement, tagsWithPartial, beforeBucket, FacetParams.FacetRangeOther.BEFORE.toString());
refinement = getRefinementSpecial(mcontext, refinement, tagsWithPartial, afterBucket, FacetParams.FacetRangeOther.AFTER.toString());
refinement = getRefinementSpecial(mcontext, refinement, tagsWithPartial, betweenBucket, FacetParams.FacetRangeOther.BETWEEN.toString());
// if we need an actual end to compute either of these buckets,
// and it's been returned to us by at least one shard
// send it back as part of the refinement request
if ( (!freq.hardend) &&
actual_end != null &&
refinement != null &&
(refinement.containsKey(FacetParams.FacetRangeOther.AFTER.toString()) ||
refinement.containsKey(FacetParams.FacetRangeOther.BETWEEN.toString())) ) {
refinement.put("_actual_end", actual_end);
}
}
return refinement;
}
private Map<String, Object> getRefinementSpecial(Context mcontext, Map<String, Object> refinement, Collection<String> tagsWithPartial, FacetBucket bucket, String label) {
if (null == bucket) {
return refinement;
}
Map<String, Object> bucketRefinement = bucket.getRefinement(mcontext, tagsWithPartial);
if (bucketRefinement != null) {
refinement = refinement == null ? new HashMap<>(2) : refinement;
refinement.put(label, bucketRefinement);
}
return refinement;
}
public void merge(SimpleOrderedMap facetResult, Context mcontext) {
boolean all = freq.others.contains(FacetParams.FacetRangeOther.ALL);
if (all || freq.others.contains(FacetParams.FacetRangeOther.BEFORE)) {
Object o = facetResult.get("before");
Object o = facetResult.get(FacetParams.FacetRangeOther.BEFORE.toString());
if (o != null) {
if (beforeBucket == null) {
beforeBucket = newBucket(null, mcontext);
@ -69,7 +103,7 @@ public class FacetRangeMerger extends FacetRequestSortedMerger<FacetRange> {
}
if (all || freq.others.contains(FacetParams.FacetRangeOther.AFTER)) {
Object o = facetResult.get("after");
Object o = facetResult.get(FacetParams.FacetRangeOther.AFTER.toString());
if (o != null) {
if (afterBucket == null) {
afterBucket = newBucket(null, mcontext);
@ -79,7 +113,7 @@ public class FacetRangeMerger extends FacetRequestSortedMerger<FacetRange> {
}
if (all || freq.others.contains(FacetParams.FacetRangeOther.BETWEEN)) {
Object o = facetResult.get("between");
Object o = facetResult.get(FacetParams.FacetRangeOther.BETWEEN.toString());
if (o != null) {
if (betweenBucket == null) {
betweenBucket = newBucket(null, mcontext);
@ -88,6 +122,15 @@ public class FacetRangeMerger extends FacetRequestSortedMerger<FacetRange> {
}
}
Object shard_actual_end = facetResult.get(FacetRange.ACTUAL_END_JSON_KEY);
if (null != shard_actual_end) {
if (null == actual_end) {
actual_end = shard_actual_end;
} else {
assert actual_end.equals(shard_actual_end) : actual_end + " != " + shard_actual_end;
}
}
List<SimpleOrderedMap> bucketList = (List<SimpleOrderedMap>) facetResult.get("buckets");
mergeBucketList(bucketList , mcontext);
}
@ -110,13 +153,13 @@ public class FacetRangeMerger extends FacetRequestSortedMerger<FacetRange> {
result.add("buckets", resultBuckets);
if (beforeBucket != null) {
result.add("before", beforeBucket.getMergedBucket());
result.add(FacetParams.FacetRangeOther.BEFORE.toString(), beforeBucket.getMergedBucket());
}
if (afterBucket != null) {
result.add("after", afterBucket.getMergedBucket());
result.add(FacetParams.FacetRangeOther.AFTER.toString(), afterBucket.getMergedBucket());
}
if (betweenBucket != null) {
result.add("between", betweenBucket.getMergedBucket());
result.add(FacetParams.FacetRangeOther.BETWEEN.toString(), betweenBucket.getMergedBucket());
}
return result;

View File

@ -95,7 +95,6 @@ public class CurrencyRangeFacetCloudTest extends SolrCloudTestCase {
}
public void testSimpleRangeFacetsOfSymetricRates() throws Exception {
for (boolean use_mincount : Arrays.asList(true, false)) {
// exchange rates relative to USD...
@ -201,7 +200,6 @@ public class CurrencyRangeFacetCloudTest extends SolrCloudTestCase {
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
@ -342,10 +340,15 @@ public class CurrencyRangeFacetCloudTest extends SolrCloudTestCase {
// the *facet* results should be the same regardless of wether we filter via fq, or using a domain filter on the top facet
for (boolean use_domain : Arrays.asList(true, false)) {
final String domain = use_domain ? "domain: { filter:'" + filter + "'}," : "";
// both of these options should produce same results since hardened:false is default
final String end = random().nextBoolean() ? "end:'20,EUR'" : "end:'15,EUR'";
final SolrQuery solrQuery = new SolrQuery("q", (use_domain ? "*:*" : filter),
"rows", "0", "json.facet",
"{ bar:{ type:range, field:"+FIELD+", " + domain +
" start:'0,EUR', gap:'10,EUR', end:'20,EUR', other:all " +
" start:'0,EUR', gap:'10,EUR', "+end+", other:all " +
" facet: { foo:{ type:terms, field:x_s, " +
" refine:true, limit:2, overrequest:0" +
" } } } }");

View File

@ -0,0 +1,786 @@
/*
* 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 java.lang.invoke.MethodHandles;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import org.apache.lucene.util.TestUtil;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.UpdateRequest;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.cloud.SolrCloudTestCase;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.CoreAdminParams;
import org.apache.solr.common.params.FacetParams.FacetRangeOther;
import org.apache.solr.common.util.NamedList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.junit.BeforeClass;
/**
* Builds a random index of a few simple fields, maintaining an in-memory model of the expected
* doc counts so that we can verify the results of range facets w/ nested field facets that need refinement.
*
* The focus here is on stressing the casees where the document values fall directonly on the
* range boundaries, and how the various "include" options affects refinement.
*/
public class RangeFacetCloudTest extends SolrCloudTestCase {
private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final String COLLECTION = MethodHandles.lookup().lookupClass().getName();
private static final String CONF = COLLECTION + "_configSet";
private static final String INT_FIELD = "range_i";
private static final String STR_FIELD = "facet_s";
private static final int NUM_RANGE_VALUES = 6;
private static final int TERM_VALUES_RANDOMIZER = 100;
// TODO: add 'count asc' once SOLR-12343 is fixed
private static final List<String> SORTS = Arrays.asList("count desc", "index asc", "index desc");
private static final List<EnumSet<FacetRangeOther>> OTHERS = buildListOfFacetRangeOtherOptions();
private static final List<FacetRangeOther> BEFORE_AFTER_BETWEEN
= Arrays.asList(FacetRangeOther.BEFORE, FacetRangeOther.AFTER, FacetRangeOther.BETWEEN);
/**
* the array indexes represent values in our numeric field, while the array values
* track the number of docs that will have that value.
*/
private static final int[] RANGE_MODEL = new int[NUM_RANGE_VALUES];
/**
* the array indexes represent values in our numeric field, while the array values
* track the mapping from string field terms to facet counts for docs that have that numeric value
*/
private static final Map<String,Integer>[] TERM_MODEL = new Map[NUM_RANGE_VALUES];
@BeforeClass
public static void setupCluster() throws Exception {
final int numShards = TestUtil.nextInt(random(),1,5);
final int numReplicas = 1;
final int maxShardsPerNode = 1;
final int nodeCount = numShards * numReplicas;
configureCluster(nodeCount)
.addConfig(CONF, Paths.get(TEST_HOME(), "collection1", "conf"))
.configure();
assertEquals(0, (CollectionAdminRequest.createCollection(COLLECTION, CONF, numShards, numReplicas)
.setMaxShardsPerNode(maxShardsPerNode)
.setProperties(Collections.singletonMap(CoreAdminParams.CONFIG, "solrconfig-minimal.xml"))
.process(cluster.getSolrClient())).getStatus());
cluster.getSolrClient().setDefaultCollection(COLLECTION);
final int numDocs = atLeast(1000);
final int maxTermId = atLeast(TERM_VALUES_RANDOMIZER);
// seed the TERM_MODEL Maps so we don't have null check later
for (int i = 0; i < NUM_RANGE_VALUES; i++) {
TERM_MODEL[i] = new LinkedHashMap<>();
}
// build our index & our models
for (int id = 0; id < numDocs; id++) {
final int rangeVal = random().nextInt(NUM_RANGE_VALUES);
final String termVal = "x" + random().nextInt(maxTermId);
final SolrInputDocument doc = sdoc("id", ""+id,
INT_FIELD, ""+rangeVal,
STR_FIELD, termVal);
RANGE_MODEL[rangeVal]++;
TERM_MODEL[rangeVal].merge(termVal, 1, Integer::sum);
assertEquals(0, (new UpdateRequest().add(doc)).process(cluster.getSolrClient()).getStatus());
}
assertEquals(0, cluster.getSolrClient().commit().getStatus());
}
public void testInclude_Lower() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
for (String include : Arrays.asList(", include:lower", "")) { // same behavior
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single low value from our ranges
"{ foo:{ type:range, field:"+INT_FIELD+" start:1, end:5, gap:1"+otherStr+include+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 4, buckets.size());
for (int i = 0; i < 4; i++) {
int expectedVal = i+1;
assertBucket("bucket#" + i, expectedVal, modelVals(expectedVal), subFacetLimit, buckets.get(i));
}
assertBeforeAfterBetween(other, modelVals(0), modelVals(5), modelVals(1,4), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
}
public void testInclude_Lower_Gap2() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
for (String include : Arrays.asList(", include:lower", "")) { // same behavior
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
"{ foo:{ type:range, field:"+INT_FIELD+" start:0, end:5, gap:2"+otherStr+include+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 3, buckets.size());
assertBucket("bucket#0", 0, modelVals(0,1), subFacetLimit, buckets.get(0));
assertBucket("bucket#1", 2, modelVals(2,3), subFacetLimit, buckets.get(1));
assertBucket("bucket#2", 4, modelVals(4,5), subFacetLimit, buckets.get(2));
assertBeforeAfterBetween(other, emptyVals(), emptyVals(), modelVals(0,5), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
}
public void testInclude_Lower_Gap2_hardend() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
for (String include : Arrays.asList(", include:lower", "")) { // same behavior
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
"{ foo:{ type:range, field:"+INT_FIELD+" start:0, end:5, gap:2, hardend:true"
+ otherStr+include+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 3, buckets.size());
assertBucket("bucket#0", 0, modelVals(0,1), subFacetLimit, buckets.get(0));
assertBucket("bucket#1", 2, modelVals(2,3), subFacetLimit, buckets.get(1));
assertBucket("bucket#2", 4, modelVals(4), subFacetLimit, buckets.get(2));
assertBeforeAfterBetween(other, emptyVals(), modelVals(5), modelVals(0,4), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
}
public void testInclude_Upper() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single high value from our ranges
"{ foo:{ type:range, field:"+INT_FIELD+" start:0, end:4, gap:1, include:upper"+otherStr+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 4, buckets.size());
for (int i = 0; i < 4; i++) {
assertBucket("bucket#" + i, i, modelVals(i+1), subFacetLimit, buckets.get(i));
}
assertBeforeAfterBetween(other, modelVals(0), modelVals(5), modelVals(1,4), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
public void testInclude_Upper_Gap2() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single high value from our ranges
"{ foo:{ type:range, field:"+INT_FIELD+" start:0, end:4, gap:2, include:upper"+otherStr+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 2, buckets.size());
assertBucket("bucket#0", 0, modelVals(1,2), subFacetLimit, buckets.get(0));
assertBucket("bucket#1", 2, modelVals(3,4), subFacetLimit, buckets.get(1));
assertBeforeAfterBetween(other, modelVals(0), modelVals(5), modelVals(1,4), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
public void testInclude_Edge() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single low/high value from our ranges
"{ foo:{ type:range, field:"+INT_FIELD+" start:1, end:4, gap:1, include:edge"+otherStr+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 3, buckets.size());
assertBucket("bucket#0", 1, modelVals(1), subFacetLimit, buckets.get(0));
// middle bucket doesn't include lower or upper so it's empty
assertBucket("bucket#1", 2, emptyVals(), subFacetLimit, buckets.get(1));
assertBucket("bucket#2", 3, modelVals(4), subFacetLimit, buckets.get(2));
assertBeforeAfterBetween(other, modelVals(0), modelVals(5), modelVals(1,4), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
public void testInclude_EdgeLower() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
for (String include : Arrays.asList(", include:'edge,lower'", ", include:[edge,lower]")) { // same
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single low/high value from our ranges
"{ foo:{ type:range, field:"+INT_FIELD+" start:1, end:4, gap:1"+otherStr+include+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 3, buckets.size());
assertBucket("bucket#0", 1, modelVals(1), subFacetLimit, buckets.get(0));
assertBucket("bucket#1", 2, modelVals(2), subFacetLimit, buckets.get(1));
assertBucket("bucket#2", 3, modelVals(3,4), subFacetLimit, buckets.get(2));
assertBeforeAfterBetween(other, modelVals(0), modelVals(5), modelVals(1,4), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
}
public void testInclude_EdgeUpper() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
for (String include : Arrays.asList(", include:'edge,upper'", ", include:[edge,upper]")) { // same
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single low/high value from our ranges
"{ foo:{ type:range, field:"+INT_FIELD+" start:1, end:4, gap:1"+otherStr+include+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 3, buckets.size());
assertBucket("bucket#0", 1, modelVals(1,2), subFacetLimit, buckets.get(0));
assertBucket("bucket#1", 2, modelVals(3), subFacetLimit, buckets.get(1));
assertBucket("bucket#2", 3, modelVals(4), subFacetLimit, buckets.get(2));
assertBeforeAfterBetween(other, modelVals(0), modelVals(5), modelVals(1,4), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
}
public void testInclude_EdgeLowerUpper() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
for (String include : Arrays.asList(", include:'edge,lower,upper'", ", include:[edge,lower,upper]")) { // same
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single low/high value from our ranges
"{ foo:{ type:range, field:"+INT_FIELD+" start:1, end:4, gap:1"+otherStr+include+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 3, buckets.size());
assertBucket("bucket#0", 1, modelVals(1,2), subFacetLimit, buckets.get(0));
assertBucket("bucket#1", 2, modelVals(2,3), subFacetLimit, buckets.get(1));
assertBucket("bucket#2", 3, modelVals(3,4), subFacetLimit, buckets.get(2));
assertBeforeAfterBetween(other, modelVals(0), modelVals(5), modelVals(1,4), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
}
public void testInclude_All() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
for (String include : Arrays.asList(", include:'edge,lower,upper,outer'",
", include:[edge,lower,upper,outer]",
", include:all")) { // same
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single low/high value from our ranges
"{ foo:{ type:range, field:"+INT_FIELD+" start:1, end:4, gap:1"+otherStr+include+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 3, buckets.size());
assertBucket("bucket#0", 1, modelVals(1,2), subFacetLimit, buckets.get(0));
assertBucket("bucket#1", 2, modelVals(2,3), subFacetLimit, buckets.get(1));
assertBucket("bucket#2", 3, modelVals(3,4), subFacetLimit, buckets.get(2));
assertBeforeAfterBetween(other, modelVals(0,1), modelVals(4,5), modelVals(1,4), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
}
/**
* This test will also sanity check that mincount is working properly
*/
public void testInclude_All_Gap2() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
for (String include : Arrays.asList(", include:'edge,lower,upper,outer'",
", include:[edge,lower,upper,outer]",
", include:all")) { // same
// we also want to sanity check that mincount doesn't bork anything,
// so we're going to do the query twice:
// 1) no mincount, keep track of which bucket has the highest count & what it was
// 2) use that value as the mincount, assert that the other bucket isn't returned
long mincount_to_use = -1;
Object expected_mincount_bucket_val = null; // HACK: use null to mean neither in case of tie
// initial query, no mincount...
SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single low/high value from our ranges
"{ foo:{ type:range, field:"+INT_FIELD+", start:1, end:4, gap:2"+otherStr+include+subFacet+" } }");
QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 2, buckets.size());
assertBucket("bucket#0", 1, modelVals(1,3), subFacetLimit, buckets.get(0));
assertBucket("bucket#1", 3, modelVals(3,5), subFacetLimit, buckets.get(1));
assertBeforeAfterBetween(other, modelVals(0,1), modelVals(5), modelVals(1,5), subFacetLimit, foo);
// if we've made it this far, then our buckets match the model
// now use our buckets to pick a mincount to use based on the MIN(+1) count seen
long count0 = ((Number)buckets.get(0).get("count")).longValue();
long count1 = ((Number)buckets.get(1).get("count")).longValue();
mincount_to_use = 1 + Math.min(count0, count1);
if (count0 > count1) {
expected_mincount_bucket_val = buckets.get(0).get("val");
} else if (count1 > count0) {
expected_mincount_bucket_val = buckets.get(1).get("val");
}
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
// second query, using mincount...
solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single low/high value from our ranges,
"{ foo:{ type:range, field:"+INT_FIELD+", mincount:" + mincount_to_use +
", start:1, end:4, gap:2"+otherStr+include+subFacet+" } }");
rsp = cluster.getSolrClient().query(solrQuery);
try {
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
if (null == expected_mincount_bucket_val) {
assertEquals("num buckets", 0, buckets.size());
} else {
assertEquals("num buckets", 1, buckets.size());
final Object actualBucket = buckets.get(0);
if (expected_mincount_bucket_val.equals(1)) {
assertBucket("bucket#0(0)", 1, modelVals(1,3), subFacetLimit, actualBucket);
} else {
assertBucket("bucket#0(1)", 3, modelVals(3,5), subFacetLimit, actualBucket);
}
}
// regardless of mincount, the before/after/between special buckets should always be returned
assertBeforeAfterBetween(other, modelVals(0,1), modelVals(5), modelVals(1,5), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
}
public void testInclude_All_Gap2_hardend() throws Exception {
for (boolean doSubFacet : Arrays.asList(false, true)) {
final Integer subFacetLimit = pickSubFacetLimit(doSubFacet);
final CharSequence subFacet = makeSubFacet(subFacetLimit);
for (EnumSet<FacetRangeOther> other : OTHERS) {
final String otherStr = formatFacetRangeOther(other);
for (String include : Arrays.asList(", include:'edge,lower,upper,outer'",
", include:[edge,lower,upper,outer]",
", include:all")) { // same
final SolrQuery solrQuery = new SolrQuery
("q", "*:*", "rows", "0", "json.facet",
// exclude a single low/high value from our ranges
"{ foo:{ type:range, field:"+INT_FIELD+" start:1, end:4, gap:2, hardend:true"
+ otherStr+include+subFacet+" } }");
final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
try {
final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
final List<NamedList<Object>> buckets = (List<NamedList<Object>>) foo.get("buckets");
assertEquals("num buckets", 2, buckets.size());
assertBucket("bucket#0", 1, modelVals(1,3), subFacetLimit, buckets.get(0));
assertBucket("bucket#1", 3, modelVals(3,4), subFacetLimit, buckets.get(1));
assertBeforeAfterBetween(other, modelVals(0,1), modelVals(4,5), modelVals(1,4), subFacetLimit, foo);
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
}
}
}
}
}
/**
* Helper method for validating a single 'bucket' from a Range facet.
*
* @param label to use in assertions
* @param expectedVal <code>"val"</code> to assert for this bucket, use <code>null</code> for special "buckets" like before, after, between.
* @param expectedRangeValues a range of the expected values in the numeric field whose cumulative counts should match this buckets <code>"count"</code>
* @param subFacetLimitUsed if null, then assert this bucket has no <code>"bar"</code> subfacet, otherwise assert expected term counts for each actual term, and sanity check the number terms returnd against the model and/or this limit.
* @param actualBucket the actual bucket returned from a query for all assertions to be conducted against.
*/
private static void assertBucket(final String label,
final Integer expectedVal,
final ModelRange expectedRangeValues,
final Integer subFacetLimitUsed,
final Object actualBucket) {
try {
assertNotNull("null bucket", actualBucket);
assertNotNull("expectedRangeValues", expectedRangeValues);
assertTrue("bucket is not a NamedList", actualBucket instanceof NamedList);
final NamedList<Object> bucket = (NamedList<Object>) actualBucket;
if (null != expectedVal) {
assertEquals("val", expectedVal, bucket.get("val"));
}
// figure out the model from our range of values...
long expectedCount = 0;
List<Map<String,Integer>> toMerge = new ArrayList<>(NUM_RANGE_VALUES);
for (int i = expectedRangeValues.lower; i <= expectedRangeValues.upper; i++) {
expectedCount += RANGE_MODEL[i];
toMerge.add(TERM_MODEL[i]);
}
assertEqualsHACK("count", expectedCount, bucket.get("count"));
// merge the maps of our range values by summing the (int) values on key collisions
final Map<String,Long> expectedTermCounts = toMerge.stream()
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(Entry::getKey, (e -> e.getValue().longValue()), Long::sum));
if (null == subFacetLimitUsed || 0 == expectedCount) {
assertNull("unexpected subfacets", bucket.get("bar"));
} else {
NamedList<Object> bar = ((NamedList<Object>)bucket.get("bar"));
assertNotNull("can't find subfacet 'bar'", bar);
final int numBucketsExpected = subFacetLimitUsed < 0
? expectedTermCounts.size() : Math.min(subFacetLimitUsed, expectedTermCounts.size());
final List<NamedList<Object>> subBuckets = (List<NamedList<Object>>) bar.get("buckets");
// we should either have filled out the expected limit, or
assertEquals("num subfacet buckets", numBucketsExpected, subBuckets.size());
// assert sub-facet term counts for the subBuckets that do exist
for (NamedList<Object> subBucket : subBuckets) {
final Object term = subBucket.get("val");
assertNotNull("subfacet bucket with null term: " + subBucket, term);
final Long expectedTermCount = expectedTermCounts.get(term.toString());
assertNotNull("unexpected subfacet bucket: " + subBucket, expectedTermCount);
assertEqualsHACK("subfacet count for term: " + term, expectedTermCount, subBucket.get("count"));
}
}
} catch (AssertionError|RuntimeException ae) {
throw new AssertionError(label + ": " + ae.getMessage(), ae);
}
}
/**
* A convinience method for calling {@link #assertBucket} on the before/after/between buckets
* of a facet result, based on the {@link FacetRangeOther} specified for this facet.
*
* @see #assertBucket
* @see #buildListOfFacetRangeOtherOptions
*/
private static void assertBeforeAfterBetween(final EnumSet<FacetRangeOther> other,
final ModelRange before,
final ModelRange after,
final ModelRange between,
final Integer subFacetLimitUsed,
final NamedList<Object> facet) {
//final String[] names = new String[] { "before", "after", "between" };
assertEquals(3, BEFORE_AFTER_BETWEEN.size());
final ModelRange[] expected = new ModelRange[] { before, after, between };
for (int i = 0; i < 3; i++) {
FacetRangeOther key = BEFORE_AFTER_BETWEEN.get(i);
String name = key.toString();
if (other.contains(key) || other.contains(FacetRangeOther.ALL)) {
assertBucket(name, null, expected[i], subFacetLimitUsed, facet.get(name));
} else {
assertNull("unexpected other=" + name, facet.get(name));
}
}
}
/**
* A little helper struct to make the method sig of {@link #assertBucket} more readable.
* If lower (or upper) is negative, then both must be negative and upper must be less then
* lower -- this indicate that the bucket should be empty.
* @see #modelVals
* @see #emptyVals
*/
private static final class ModelRange {
public final int lower;
public final int upper;
/** Don't use, use the convinience methods */
public ModelRange(int lower, int upper) {
if (lower < 0 || upper < 0) {
assert(lower < 0 && upper < lower);
} else {
assert(lower <= upper);
}
this.lower = lower;
this.upper = upper;
}
}
private static final ModelRange emptyVals() {
return new ModelRange(-1, -100);
}
private static final ModelRange modelVals(int value) {
return modelVals(value, value);
}
private static final ModelRange modelVals(int lower, int upper) {
assertTrue(upper + " < " + lower, lower <= upper);
assertTrue("negative lower", 0 <= lower);
assertTrue("negative upper", 0 <= upper);
return new ModelRange(lower, upper);
}
/** randomized helper */
private static final Integer pickSubFacetLimit(final boolean doSubFacet) {
if (! doSubFacet) { return null; }
int result = TestUtil.nextInt(random(), -10, atLeast(TERM_VALUES_RANDOMIZER));
return (result <= 0) ? -1 : result;
}
/** randomized helper */
private static final CharSequence makeSubFacet(final Integer subFacetLimit) {
if (null == subFacetLimit) {
return "";
}
final StringBuilder result = new StringBuilder(", facet:{ bar:{ type:terms, refine:true, field:"+STR_FIELD);
// constrain overrequesting to stress refiement, but still test those codepaths
final String overrequest = random().nextBoolean() ? "0" : "1";
result.append(", overrequest:").append(overrequest).append(", limit:").append(subFacetLimit);
// order should have no affect on our testing
if (random().nextBoolean()) {
result.append(", sort:'").append(SORTS.get(random().nextInt(SORTS.size()))).append("'");
}
result.append("} }");
return result;
}
/**
* Helper for seeding the re-used static struct, and asserting no one changes the Enum w/o updating this test
*
* @see #assertBeforeAfterBetween
* @see #formatFacetRangeOther
* @see #OTHERS
*/
private static final List<EnumSet<FacetRangeOther>> buildListOfFacetRangeOtherOptions() {
assertEquals("If someone adds to FacetRangeOther this method (and bulk of test) needs updated",
5, EnumSet.allOf(FacetRangeOther.class).size());
// we're not overly concerned about testing *EVERY* permutation,
// we just want to make sure we test multiple code paths (some, all, "ALL", none)
//
// NOTE: Don't mix "ALL" or "NONE" with other options so we don't have to make assertBeforeAfterBetween
// overly complicated
ArrayList<EnumSet<FacetRangeOther>> results = new ArrayList(5);
results.add(EnumSet.of(FacetRangeOther.ALL));
results.add(EnumSet.of(FacetRangeOther.BEFORE, FacetRangeOther.AFTER, FacetRangeOther.BETWEEN));
results.add(EnumSet.of(FacetRangeOther.BEFORE, FacetRangeOther.AFTER));
results.add(EnumSet.of(FacetRangeOther.BETWEEN));
results.add(EnumSet.of(FacetRangeOther.NONE));
return results;
}
/**
* @see #assertBeforeAfterBetween
* @see #buildListOfFacetRangeOtherOptions
*/
private static final String formatFacetRangeOther(EnumSet<FacetRangeOther> other) {
if (other.contains(FacetRangeOther.NONE) && random().nextBoolean()) {
return ""; // sometimes don't output a param at all when we're dealing with the default NONE
}
String val = other.toString();
if (random().nextBoolean()) {
// two valid syntaxes to randomize between:
// - a JSON list of items (conviniently the default toString of EnumSet),
// - a single quoted string containing the comma seperated list
val = val.replaceAll("\\[|\\]","'");
// HACK: work around SOLR-12539...
//
// when sending a single string containing a comma seperated list of values, JSON Facets 'other'
// parsing can't handle any leading (or trailing?) whitespace
val = val.replaceAll("\\s","");
}
return ", other:" + val;
}
/**
* HACK to work around SOLR-11775.
* Asserts that the 'actual' argument is a (non-null) Number, then compares it's 'longValue' to the 'expected' argument
*/
private static void assertEqualsHACK(String msg, long expected, Object actual) {
assertNotNull(msg, actual);
assertTrue(msg + " ... NOT A NUMBER: " + actual.getClass(), Number.class.isInstance(actual));
assertEquals(msg, expected, ((Number)actual).longValue());
}
}

View File

@ -18,6 +18,7 @@
package org.apache.solr.search.facet;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import org.apache.solr.JSONTestUtil;
@ -227,6 +228,64 @@ public class TestJsonFacetRefinement extends SolrTestCaseHS {
"}"
);
// same test, but now the range facet includes "other" buckets
// (so we also verify that the "_actual_end" is echoed back)
doTestRefine("{top:{type:range, other:all, field:R, start:0, end:1, gap:1, " +
" facet:{x : {type:terms, field:X, limit:2, refine:true} } } }",
// phase #1
"{top: {buckets:[{val:0, count:2, x:{more:true,buckets:[{val:x1, count:5},{val:x2, count:3}]} } ]," +
" before:{count:0},after:{count:0}," +
" between:{count:2,x:{more:true,buckets:[{val:x1, count:5},{val:x2, count:3}]} }," +
" '_actual_end':'does_not_matter_must_be_echoed_back' } }",
"{top: {buckets:[{val:0, count:1, x:{more:true,buckets:[{val:x2, count:4},{val:x3, count:2}]} } ]," +
" before:{count:0},after:{count:0}," +
" between:{count:1,x:{more:true,buckets:[{val:x2, count:4},{val:x3, count:2}]} }," +
" '_actual_end':'does_not_matter_must_be_echoed_back' } }",
// refinement...
null,
"=={top: {" +
" _s:[ [0 , {x:{_l:[x1]}} ] ]," +
" between:{ x:{_l : [x1]} }," +
" '_actual_end':'does_not_matter_must_be_echoed_back'" +
"} } ");
// imagine that all the nodes we query in phase#1 are running "old" versions of solr that
// don't know they are suppose to compute _actual_end ... our merger should not fail or freak out
// trust that in the phase#2 refinement request either:
// - the processor will re-compute it (if refine request goes to "new" version of solr)
// - the processor wouldn't know what to do with an _actual_end sent by the merger anyway
doTestRefine("{top:{type:range, other:all, field:R, start:0, end:1, gap:1, " +
" facet:{x : {type:terms, field:X, limit:2, refine:true} } } }",
// phase #1
"{top: {buckets:[{val:0, count:2, x:{more:true,buckets:[{val:x1, count:5},{val:x2, count:3}]} } ]," +
" before:{count:0},after:{count:0}," +
" between:{count:2,x:{more:true,buckets:[{val:x1, count:5},{val:x2, count:3}]} }," +
" } }", // no actual_end
"{top: {buckets:[{val:0, count:1, x:{more:true,buckets:[{val:x2, count:4},{val:x3, count:2}]} } ]," +
" before:{count:0},after:{count:0}," +
" between:{count:1,x:{more:true,buckets:[{val:x2, count:4},{val:x3, count:2}]} }," +
" } }", // no actual_end
// refinement...
null,
"=={top: {" +
" _s:[ [0 , {x:{_l:[x1]}} ] ]," +
" between:{ x:{_l : [x1]} }" +
"} } ");
// a range face w/o any sub facets shouldn't require any refinement
doTestRefine("{top:{type:range, other:all, field:R, start:0, end:3, gap:2 } }" +
// phase #1
"{top: {buckets:[{val:0, count:2}, {val:2, count:2}]," +
" before:{count:3},after:{count:47}," +
" between:{count:5}," +
" } }",
"{top: {buckets:[{val:0, count:2}, {val:2, count:19}]," +
" before:{count:22},after:{count:0}," +
" between:{count:21}," +
" } }",
// refinement...
null,
null);
// for testing partial _p, we need a partial facet within a partial facet
doTestRefine("{top:{type:terms, field:Afield, refine:true, limit:1, facet:{x : {type:terms, field:X, limit:1, refine:true} } } }",
"{top: {buckets:[{val:'A', count:2, x:{buckets:[{val:x1, count:5},{val:x2, count:3}],more:true} } ],more:true } }",
@ -380,17 +439,34 @@ public class TestJsonFacetRefinement extends SolrTestCaseHS {
);
// basic refining test through/under a range facet
client.testJQ(params(p, "q", "*:*",
"json.facet", "{" +
"r1 : { type:range, field:${num_d} start:-20, end:20, gap:40 , facet:{" +
"cat0:{${terms} type:terms, field:${cat_s}, sort:'count desc', limit:1, overrequest:0, refine:true}" +
"}}" +
"}"
)
, "facets=={ count:8" +
", r1:{ buckets:[{val:-20.0,count:8, cat0:{buckets:[{val:A,count:4}]} }] }" +
"}"
);
for (String end : Arrays.asList(// all of these end+hardened options should produce the same buckets
"end:20, hardend:true", // evenly divisible so shouldn't matter
"end:20, hardend:false", "end:20", // defaults to hardened:false
"end:5, hardend:false", "end:5")) {
// since the gap divides the start/end divide eveningly,
// all of these hardend params should we should produce identical results
String sub = "cat0:{${terms} type:terms, field:${cat_s}, sort:'count desc', limit:1, overrequest:0, refine:true}";
// single bucket, all 'other' buckets
client.testJQ(params(p, "q", "*:*", "json.facet"
, "{ r1 : { type:range, field:${num_d} other:all, start:-20, gap:40, " + end
+ " , facet:{" + sub + "}}}")
, "facets=={ count:8"
+ ", r1:{ buckets:[{val:-20.0,count:8, cat0:{buckets:[{val:A,count:4}]} }],"
+ " before:{count:0}, after:{count:0}"
+ " between:{count:8, cat0:{buckets:[{val:A,count:4}]}}"
+ "}}");
// multiple buckets, only one 'other' buckets
client.testJQ(params(p, "q", "*:*", "json.facet"
, "{ r1 : { type:range, field:${num_d} other:between, start:-20, gap:20, " + end
+ " , facet:{" + sub + "}}}")
, "facets=={ count:8"
// NOTE: in both buckets A & B are tied, but index order should break tie
+ ", r1:{ buckets:[{val:-20.0, count:4, cat0:{buckets:[{val:A,count:2}]} },"
+ " {val: 0.0, count:4, cat0:{buckets:[{val:A,count:2}]} } ],"
+ " between:{count:8, cat0:{buckets:[{val:A,count:4}]}}"
+ "}}");
}
// test that basic stats work for refinement
client.testJQ(params(p, "q", "*:*",

View File

@ -212,6 +212,76 @@ public class TestJsonFacets extends SolrTestCaseHS {
client.commit();
}
/**
* whitebox sanity checks that a shard request range facet that returns "between" or "after"
* will cause the correct "actual_end" to be returned
*/
public void testRangeOtherWhitebox() throws Exception {
Client client = Client.localClient();
indexSimple(client);
// false is default, but randomly check explicit false as well
final String nohardend = random().nextBoolean() ? "" : " hardend:false, ";
{ // first check some "phase #1" requests
final SolrParams p = params("q", "*:*", "rows", "0", "isShard", "true", "distrib", "false",
"_facet_", "{}", "shards.purpose", ""+FacetModule.PURPOSE_GET_JSON_FACETS);
final String basic_opts = "type:range, field:num_d, start:-5, end:10, gap:7, ";
final String buckets = "buckets:[ {val:-5.0,count:1}, {val:2.0,count:2}, {val:9.0,count:1} ], ";
client.testJQ(params(p, "json.facet", "{f:{ " + basic_opts + nohardend + " other:before}}")
, "facets=={count:6, f:{" + buckets
// before doesn't need actual_end
+ " before:{count:1}"
+ "} }"
);
client.testJQ(params(p, "json.facet", "{f:{" + basic_opts + nohardend + "other:after}}")
, "facets=={count:6, f:{" + buckets
+ " after:{count:0}, _actual_end:'16.0'"
+ "} }"
);
client.testJQ(params(p, "json.facet", "{f:{ " + basic_opts + nohardend + "other:between}}")
, "facets=={count:6, f:{" + buckets
+ " between:{count:4}, _actual_end:'16.0'"
+ "} }"
);
client.testJQ(params(p, "json.facet", "{f:{ " + basic_opts + nohardend + "other:all}}")
, "facets=={count:6, f:{" + buckets
+ " before:{count:1},"
+ " after:{count:0},"
+ " between:{count:4},"
+ " _actual_end:'16.0'"
+ "} }"
);
// with hardend:true, not only do the buckets change, but actual_end should not need to be returned
client.testJQ(params(p, "json.facet", "{f:{ " + basic_opts + " hardend:true, other:after}}")
, "facets=={count:6, f:{"
+ " buckets:[ {val:-5.0,count:1}, {val:2.0,count:2}, {val:9.0,count:0} ], "
+ " after:{count:1}"
+ "} }"
);
}
{ // now check some "phase #2" requests with refinement buckets already specified
final String facet
= "{ top:{ type:range, field:num_i, start:-5, end:5, gap:7," + nohardend
+ " other:all, facet:{ x:{ type:terms, field:cat_s, limit:1, refine:true } } } }";
// the behavior should be the same, regardless of wether we pass actual_end to the shards
// because in a "mixed mode" rolling update, the shards should be smart enough to re-compute if
// the merging node is running an older version that doesn't send it
for (String actual_end : Arrays.asList(", _actual_end:'9'", "")) {
client.testJQ(params("q", "*:*", "rows", "0", "isShard", "true", "distrib", "false",
"shards.purpose", ""+FacetModule.PURPOSE_REFINE_JSON_FACETS,
"json.facet", facet,
"_facet_", "{ refine: { top: { between:{ x:{ _l:[B] } }" + actual_end + "} } }")
, "facets=={top:{ buckets:[], between:{x:{buckets:[{val:B,count:3}] }} } }");
}
}
}
@Test
public void testExplicitQueryDomain() throws Exception {
Client client = Client.localClient();
@ -401,7 +471,8 @@ public class TestJsonFacets extends SolrTestCaseHS {
// to verify the raw counts/sizes
assertJQ(req(nestedSKG,
// fake an initial shard request
"distrib", "false", "isShard", "true", "_facet_", "{}", "shards.purpose", "2097216")
"distrib", "false", "isShard", "true", "_facet_", "{}",
"shards.purpose", ""+FacetModule.PURPOSE_GET_JSON_FACETS)
, "facets=={count:5, x:{ buckets:["
+ " { val:'B', count:3, "
+ " skg : { "