diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index daa10104119..40e59705c96 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -214,6 +214,11 @@ New Features * SOLR-1925: Add CSVResponseWriter (use wt=csv) that returns the list of documents in CSV format. (Chris Mattmann, yonik) +* SOLR-1240: "Rnage Faceting has been added. This is a generalization + of the existing "Date Faceting" logic so that it now supports any + all stock numeric field types that support range queries in addition + to dates. + (Gijs Kunze, hossman) Optimizations ---------------------- diff --git a/solr/src/common/org/apache/solr/common/params/FacetParams.java b/solr/src/common/org/apache/solr/common/params/FacetParams.java index 17f2557a454..d119c0ea256 100644 --- a/solr/src/common/org/apache/solr/common/params/FacetParams.java +++ b/solr/src/common/org/apache/solr/common/params/FacetParams.java @@ -153,36 +153,10 @@ public interface FacetParams { * String indicating what "other" ranges should be computed for a * date facet range (multi-value). * Can be overriden on a per field basis. - * @see FacetDateOther + * @see FacetRangeOther */ public static final String FACET_DATE_OTHER = FACET_DATE + ".other"; - /** - * An enumeration of the legal values for FACET_DATE_OTHER... - * - * @see #FACET_DATE_OTHER - * @see #FACET_DATE_INCLUDE - */ - public enum FacetDateOther { - BEFORE, AFTER, BETWEEN, ALL, NONE; - public String toString() { return super.toString().toLowerCase(Locale.ENGLISH); } - public static FacetDateOther get(String label) { - try { - return valueOf(label.toUpperCase(Locale.ENGLISH)); - } catch (IllegalArgumentException e) { - throw new SolrException - (SolrException.ErrorCode.BAD_REQUEST, - label+" is not a valid type of 'other' date facet information",e); - } - } - } - /** *

* Multivalued string indicating what rules should be applied to determine @@ -195,12 +169,124 @@ public interface FacetParams { *

* Can be overriden on a per field basis. *

- * @see FacetDateInclude + * @see FacetRangeInclude */ public static final String FACET_DATE_INCLUDE = FACET_DATE + ".include"; /** - * An enumeration of the legal values for FACET_DATE_INCLUDE... + * Any numerical field whose terms the user wants to enumerate over + * Facet Contraint Counts for selected ranges. + */ + public static final String FACET_RANGE = FACET + ".range"; + /** + * Number indicating the starting point for a numerical range facet. + * Can be overriden on a per field basis. + */ + public static final String FACET_RANGE_START = FACET_RANGE + ".start"; + /** + * Number indicating the ending point for a numerical range facet. + * Can be overriden on a per field basis. + */ + public static final String FACET_RANGE_END = FACET_RANGE + ".end"; + /** + * Number indicating the interval of sub-ranges for a numerical + * facet range. + * Can be overriden on a per field basis. + */ + public static final String FACET_RANGE_GAP = FACET_RANGE + ".gap"; + /** + * Boolean indicating how counts should be computed if the range + * between 'start' and 'end' is not evenly divisible by 'gap'. If + * this value is true, then all counts of ranges involving the 'end' + * point will use the exact endpoint specified -- this includes the + * 'between' and 'after' counts as well as the last range computed + * using the 'gap'. If the value is false, then 'gap' is used to + * compute the effective endpoint closest to the 'end' param which + * results in the range between 'start' and 'end' being evenly + * divisible by 'gap'. + * The default is false. + * Can be overriden on a per field basis. + */ + public static final String FACET_RANGE_HARD_END = FACET_RANGE + ".hardend"; + /** + * String indicating what "other" ranges should be computed for a + * numerical range facet (multi-value). + * Can be overriden on a per field basis. + * @see FacetNumberOther + */ + public static final String FACET_RANGE_OTHER = FACET_RANGE + ".other"; + /** + * String indicating whether ranges for numerical range faceting + * should be exclusive or inclusive. By default both the start and + * end point are inclusive. + * Can be overriden on a per field basis. + * @see FacetNumberExclusive + */ + + /** + *

+ * Multivalued string indicating what rules should be applied to determine + * when the the ranges generated for numeric faceting should be inclusive or + * exclusive of their end points. + *

+ *

+ * The default value if none are specified is: [lower,upper,edge] + *

+ *

+ * Can be overriden on a per field basis. + *

+ * @see FacetRangeInclude + */ + public static final String FACET_RANGE_INCLUDE = FACET_RANGE + ".include"; + + + /** + * An enumeration of the legal values for {@link #FACET_RANGE_OTHER} and {@link #FACET_DATE_OTHER} ... + * + * @see #FACET_RANGE_OTHER + * @see #FACET_DATE_OTHER + */ + public enum FacetRangeOther { + BEFORE, AFTER, BETWEEN, ALL, NONE; + public String toString() { return super.toString().toLowerCase(); } + public static FacetRangeOther get(String label) { + try { + return valueOf(label.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new SolrException + (SolrException.ErrorCode.BAD_REQUEST, + label+" is not a valid type of 'other' range facet information",e); + } + } + } + + /** + * @deprecated Use {@link FacetRangeOther} + */ + @Deprecated + public enum FacetDateOther { + BEFORE, AFTER, BETWEEN, ALL, NONE; + public String toString() { return super.toString().toLowerCase(); } + public static FacetDateOther get(String label) { + try { + return valueOf(label.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new SolrException + (SolrException.ErrorCode.BAD_REQUEST, + label+" is not a valid type of 'other' range facet information",e); + } + } + } + + /** + * An enumeration of the legal values for {@link #FACET_DATE_INCLUDE} and {@link #FACET_RANGE_INCLUDE} + * * * @see #FACET_DATE_INCLUDE + * @see #FACET_RANGE_INCLUDE */ - public enum FacetDateInclude { + public enum FacetRangeInclude { ALL, LOWER, UPPER, EDGE, OUTER; public String toString() { return super.toString().toLowerCase(Locale.ENGLISH); } - public static FacetDateInclude get(String label) { + public static FacetRangeInclude get(String label) { try { return valueOf(label.toUpperCase(Locale.ENGLISH)); } catch (IllegalArgumentException e) { throw new SolrException (SolrException.ErrorCode.BAD_REQUEST, - label+" is not a valid type of for "+FACET_DATE_INCLUDE+" information",e); + label+" is not a valid type of for range 'include' information",e); } } /** - * Convinience method for parsing the param value according to the correct semantics. + * Convinience method for parsing the param value according to the + * correct semantics. */ - public static EnumSet parseParam(final String[] param) { + public static EnumSet parseParam(final String[] param) { // short circut for default behavior if (null == param || 0 == param.length ) return EnumSet.of(LOWER, UPPER, EDGE); // build up set containing whatever is specified - final EnumSet include = EnumSet.noneOf(FacetDateInclude.class); + final EnumSet include = EnumSet.noneOf(FacetRangeInclude.class); for (final String o : param) { - include.add(FacetDateInclude.get(o)); + include.add(FacetRangeInclude.get(o)); } // if set contains all, then we're back to short circuting - if (include.contains(FacetDateInclude.ALL)) - return EnumSet.allOf(FacetDateInclude.class); + if (include.contains(FacetRangeInclude.ALL)) + return EnumSet.allOf(FacetRangeInclude.class); // use whatever we've got. return include; diff --git a/solr/src/java/org/apache/solr/handler/component/FacetComponent.java b/solr/src/java/org/apache/solr/handler/component/FacetComponent.java index 78fc84b7480..f5f3a89b344 100644 --- a/solr/src/java/org/apache/solr/handler/component/FacetComponent.java +++ b/solr/src/java/org/apache/solr/handler/component/FacetComponent.java @@ -404,8 +404,9 @@ public class FacetComponent extends SearchComponent } } - // TODO: facet dates + // TODO: facet dates & numbers facet_counts.add("facet_dates", new SimpleOrderedMap()); + facet_counts.add("facet_ranges", new SimpleOrderedMap()); rb.rsp.add("facet_counts", facet_counts); @@ -688,4 +689,4 @@ public class FacetComponent extends SearchComponent return "{term="+name+",termNum="+termNum+",count="+count+"}"; } } -} \ No newline at end of file +} diff --git a/solr/src/java/org/apache/solr/request/SimpleFacets.java b/solr/src/java/org/apache/solr/request/SimpleFacets.java index 14bd8eaa14a..4e462a0d43c 100644 --- a/solr/src/java/org/apache/solr/request/SimpleFacets.java +++ b/solr/src/java/org/apache/solr/request/SimpleFacets.java @@ -31,13 +31,14 @@ import org.apache.solr.common.params.FacetParams; import org.apache.solr.common.params.RequiredSolrParams; import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.params.CommonParams; -import org.apache.solr.common.params.FacetParams.FacetDateOther; -import org.apache.solr.common.params.FacetParams.FacetDateInclude; +import org.apache.solr.common.params.FacetParams.FacetRangeOther; +import org.apache.solr.common.params.FacetParams.FacetRangeInclude; import org.apache.solr.common.util.NamedList; import org.apache.solr.common.util.SimpleOrderedMap; import org.apache.solr.common.util.StrUtils; import org.apache.solr.core.SolrCore; import org.apache.solr.schema.*; +import org.apache.solr.schema.TrieField.TrieTypes; import org.apache.solr.search.*; import org.apache.solr.util.BoundedTreeSet; import org.apache.solr.util.ByteUtils; @@ -63,11 +64,14 @@ public class SimpleFacets { protected DocSet docs; /** Configuration params behavior should be driven by */ protected SolrParams params; + protected SolrParams required; /** Searcher to use for all calculations */ protected SolrIndexSearcher searcher; protected SolrQueryRequest req; protected ResponseBuilder rb; + public final Date NOW = new Date(); + // per-facet values SolrParams localParams; // localParams on this particular facet command String facetValue; // the field to or query to facet on (minus local params) @@ -89,6 +93,7 @@ public class SimpleFacets { this.searcher = req.getSearcher(); this.base = this.docs = docs; this.params = params; + this.required = new RequiredSolrParams(params); this.rb = rb; } @@ -166,6 +171,7 @@ public class SimpleFacets { * @see #getFacetQueryCounts * @see #getFacetFieldCounts * @see #getFacetDateCounts + * @see #getFacetRangeCounts * @see FacetParams#FACET * @return a NamedList of Facet Count info or null */ @@ -181,7 +187,8 @@ public class SimpleFacets { res.add("facet_queries", getFacetQueryCounts()); res.add("facet_fields", getFacetFieldCounts()); res.add("facet_dates", getFacetDateCounts()); - + res.add("facet_ranges", getFacetRangeCounts()); + } catch (Exception e) { SolrException.logOnce(SolrCore.log, "Exception during facet counts", e); res.add("exception", SolrException.toStr(e)); @@ -723,12 +730,10 @@ public class SimpleFacets { * @see FacetParams#FACET_DATE */ public NamedList getFacetDateCounts() - throws IOException, ParseException { + throws IOException, ParseException { - final SolrParams required = new RequiredSolrParams(params); final NamedList resOuter = new SimpleOrderedMap(); final String[] fields = params.getParams(FacetParams.FACET_DATE); - final Date NOW = new Date(); if (null == fields || 0 == fields.length) return resOuter; @@ -778,9 +783,9 @@ public class SimpleFacets { final DateMathParser dmp = new DateMathParser(ft.UTC, Locale.US); dmp.setNow(NOW); - int minCount = params.getFieldInt(f,FacetParams.FACET_MINCOUNT, 0); + final int minCount = params.getFieldInt(f,FacetParams.FACET_MINCOUNT, 0); - final EnumSet include = FacetDateInclude.parseParam + final EnumSet include = FacetRangeInclude.parseParam (params.getFieldParams(f,FacetParams.FACET_DATE_INCLUDE)); try { @@ -802,14 +807,14 @@ public class SimpleFacets { (SolrException.ErrorCode.BAD_REQUEST, "date facet infinite loop (is gap negative?)"); } - boolean includeLower = - (include.contains(FacetDateInclude.LOWER) || - (include.contains(FacetDateInclude.EDGE) && low.equals(start))); - boolean includeUpper = - (include.contains(FacetDateInclude.UPPER) || - (include.contains(FacetDateInclude.EDGE) && high.equals(end))); + final boolean includeLower = + (include.contains(FacetRangeInclude.LOWER) || + (include.contains(FacetRangeInclude.EDGE) && low.equals(start))); + final boolean includeUpper = + (include.contains(FacetRangeInclude.UPPER) || + (include.contains(FacetRangeInclude.EDGE) && high.equals(end))); - int count = rangeCount(sf,low,high,includeLower,includeUpper); + final int count = rangeCount(sf,low,high,includeLower,includeUpper); if (count >= minCount) { resInner.add(label, count); } @@ -821,49 +826,52 @@ public class SimpleFacets { "date facet 'gap' is not a valid Date Math string: " + gap, e); } - // explicitly return the gap and end so all the counts are meaningful + // explicitly return the gap and end so all the counts + // (including before/after/between) are meaningful - even if mincount + // has removed the neighboring ranges resInner.add("gap", gap); + resInner.add("start", start); resInner.add("end", end); final String[] othersP = params.getFieldParams(f,FacetParams.FACET_DATE_OTHER); if (null != othersP && 0 < othersP.length ) { - Set others = EnumSet.noneOf(FacetDateOther.class); + final Set others = EnumSet.noneOf(FacetRangeOther.class); for (final String o : othersP) { - others.add(FacetDateOther.get(o)); + others.add(FacetRangeOther.get(o)); } // no matter what other values are listed, we don't do // anything if "none" is specified. - if (! others.contains(FacetDateOther.NONE) ) { - boolean all = others.contains(FacetDateOther.ALL); + if (! others.contains(FacetRangeOther.NONE) ) { + boolean all = others.contains(FacetRangeOther.ALL); - if (all || others.contains(FacetDateOther.BEFORE)) { + if (all || others.contains(FacetRangeOther.BEFORE)) { // include upper bound if "outer" or if first gap doesn't already include it - resInner.add(FacetDateOther.BEFORE.toString(), + resInner.add(FacetRangeOther.BEFORE.toString(), rangeCount(sf,null,start, false, - (include.contains(FacetDateInclude.OUTER) || - (! (include.contains(FacetDateInclude.LOWER) || - include.contains(FacetDateInclude.EDGE)))))); + (include.contains(FacetRangeInclude.OUTER) || + (! (include.contains(FacetRangeInclude.LOWER) || + include.contains(FacetRangeInclude.EDGE)))))); } - if (all || others.contains(FacetDateOther.AFTER)) { + if (all || others.contains(FacetRangeOther.AFTER)) { // include lower bound if "outer" or if last gap doesn't already include it - resInner.add(FacetDateOther.AFTER.toString(), + resInner.add(FacetRangeOther.AFTER.toString(), rangeCount(sf,end,null, - (include.contains(FacetDateInclude.OUTER) || - (! (include.contains(FacetDateInclude.UPPER) || - include.contains(FacetDateInclude.EDGE)))), + (include.contains(FacetRangeInclude.OUTER) || + (! (include.contains(FacetRangeInclude.UPPER) || + include.contains(FacetRangeInclude.EDGE)))), false)); } - if (all || others.contains(FacetDateOther.BETWEEN)) { - resInner.add(FacetDateOther.BETWEEN.toString(), + if (all || others.contains(FacetRangeOther.BETWEEN)) { + resInner.add(FacetRangeOther.BETWEEN.toString(), rangeCount(sf,start,end, - (include.contains(FacetDateInclude.LOWER) || - include.contains(FacetDateInclude.EDGE)), - (include.contains(FacetDateInclude.UPPER) || - include.contains(FacetDateInclude.EDGE)))); + (include.contains(FacetRangeInclude.LOWER) || + include.contains(FacetRangeInclude.EDGE)), + (include.contains(FacetRangeInclude.UPPER) || + include.contains(FacetRangeInclude.EDGE)))); } } } @@ -872,6 +880,197 @@ public class SimpleFacets { return resOuter; } + + /** + * Returns a list of value constraints and the associated facet + * counts for each facet numerical field, range, and interval + * specified in the SolrParams + * + * @see FacetParams#FACET_RANGE + */ + public NamedList getFacetRangeCounts() + throws IOException, ParseException { + + final NamedList resOuter = new SimpleOrderedMap(); + final String[] fields = params.getParams(FacetParams.FACET_RANGE); + + if (null == fields || 0 == fields.length) return resOuter; + + final IndexSchema schema = searcher.getSchema(); + for (String f : fields) { + parseParams(FacetParams.FACET_RANGE, f); + f = facetValue; + + final SchemaField sf = schema.getField(f); + final FieldType ft = sf.getType(); + + RangeEndpointCalculator calc = null; + + if (ft instanceof TrieField) { + final TrieField trie = (TrieField)ft; + + switch (trie.getType()) { + case FLOAT: + calc = new FloatRangeEndpointCalculator(sf); + break; + case DOUBLE: + calc = new DoubleRangeEndpointCalculator(sf); + break; + case INTEGER: + calc = new IntegerRangeEndpointCalculator(sf); + break; + case LONG: + calc = new LongRangeEndpointCalculator(sf); + break; + default: + throw new SolrException + (SolrException.ErrorCode.BAD_REQUEST, + "Unable to range facet on tried field of unexpected type:" + f); + } + } else if (ft instanceof DateField) { + calc = new DateRangeEndpointCalculator(sf, NOW); + } else if (ft instanceof SortableIntField) { + calc = new IntegerRangeEndpointCalculator(sf); + } else if (ft instanceof SortableLongField) { + calc = new LongRangeEndpointCalculator(sf); + } else if (ft instanceof SortableFloatField) { + calc = new FloatRangeEndpointCalculator(sf); + } else if (ft instanceof SortableDoubleField) { + calc = new DoubleRangeEndpointCalculator(sf); + } else { + throw new SolrException + (SolrException.ErrorCode.BAD_REQUEST, + "Unable to range facet on field:" + sf); + } + + resOuter.add(key, getFacetRangeCounts(sf, calc)); + } + + return resOuter; + } + + private > NamedList getFacetRangeCounts + (final SchemaField sf, + final RangeEndpointCalculator calc) throws IOException { + + final String f = sf.getName(); + final NamedList res = new SimpleOrderedMap(); + final NamedList counts = new SimpleOrderedMap(); + res.add("counts", counts); + + final T start = calc.getValue(required.getFieldParam(f,FacetParams.FACET_RANGE_START)); + // not final, hardend may change this + T end = calc.getValue(required.getFieldParam(f,FacetParams.FACET_RANGE_END)); + if (end.compareTo(start) < 0) { + throw new SolrException + (SolrException.ErrorCode.BAD_REQUEST, + "range facet 'end' comes before 'start': "+end+" < "+start); + } + + final String gap = required.getFieldParam(f, FacetParams.FACET_RANGE_GAP); + // explicitly return the gap. compute this early so we are more + // likely to catch parse errors before attempting math + res.add("gap", calc.getGap(gap)); + + final int minCount = params.getFieldInt(f,FacetParams.FACET_MINCOUNT, 0); + + final EnumSet include = FacetRangeInclude.parseParam + (params.getFieldParams(f,FacetParams.FACET_RANGE_INCLUDE)); + + T low = start; + + while (low.compareTo(end) < 0) { + T high = calc.addGap(low, gap); + if (end.compareTo(high) < 0) { + if (params.getFieldBool(f,FacetParams.FACET_RANGE_HARD_END,false)) { + high = end; + } else { + 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?)"); + } + + final boolean includeLower = + (include.contains(FacetRangeInclude.LOWER) || + (include.contains(FacetRangeInclude.EDGE) && + 0 == low.compareTo(start))); + final boolean includeUpper = + (include.contains(FacetRangeInclude.UPPER) || + (include.contains(FacetRangeInclude.EDGE) && + 0 == high.compareTo(end))); + + final String lowS = calc.formatValue(low); + final String highS = calc.formatValue(high); + + final int count = rangeCount(sf, lowS, highS, + includeLower,includeUpper); + if (count >= minCount) { + counts.add(lowS, count); + } + + low = high; + } + + // explicitly return the start and end so all the counts + // (including before/after/between) are meaningful - even if mincount + // has removed the neighboring ranges + res.add("start", start); + res.add("end", end); + + final String[] othersP = + params.getFieldParams(f,FacetParams.FACET_RANGE_OTHER); + if (null != othersP && 0 < othersP.length ) { + Set others = EnumSet.noneOf(FacetRangeOther.class); + + for (final String o : othersP) { + others.add(FacetRangeOther.get(o)); + } + + // no matter what other values are listed, we don't do + // anything if "none" is specified. + if (! others.contains(FacetRangeOther.NONE) ) { + + boolean all = others.contains(FacetRangeOther.ALL); + final String startS = calc.formatValue(start); + final String endS = calc.formatValue(end); + + if (all || others.contains(FacetRangeOther.BEFORE)) { + // include upper bound if "outer" or if first gap doesn't already include it + res.add(FacetRangeOther.BEFORE.toString(), + rangeCount(sf,null,startS, + false, + (include.contains(FacetRangeInclude.OUTER) || + (! (include.contains(FacetRangeInclude.LOWER) || + include.contains(FacetRangeInclude.EDGE)))))); + + } + if (all || others.contains(FacetRangeOther.AFTER)) { + // include lower bound if "outer" or if last gap doesn't already include it + res.add(FacetRangeOther.AFTER.toString(), + rangeCount(sf,endS,null, + (include.contains(FacetRangeInclude.OUTER) || + (! (include.contains(FacetRangeInclude.UPPER) || + include.contains(FacetRangeInclude.EDGE)))), + false)); + } + if (all || others.contains(FacetRangeOther.BETWEEN)) { + res.add(FacetRangeOther.BETWEEN.toString(), + rangeCount(sf,startS,endS, + (include.contains(FacetRangeInclude.LOWER) || + include.contains(FacetRangeInclude.EDGE)), + (include.contains(FacetRangeInclude.UPPER) || + include.contains(FacetRangeInclude.EDGE)))); + + } + } + } + return res; + } + /** * Macro for getting the numDocs of range over docs * @see SolrIndexSearcher#numDocs @@ -914,5 +1113,173 @@ public class SimpleFacets { return (0 != vc ? vc : key.compareTo(o.key)); } } + + + /** + * 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. + */ + private static abstract class RangeEndpointCalculator> { + protected final SchemaField field; + public RangeEndpointCalculator(final SchemaField field) { + this.field = field; + } + + /** + * Formats a Range endpoint for use as a range label name in the response. + * Default Impl just uses toString() + */ + public String formatValue(final T val) { + return val.toString(); + } + /** + * Parses a String param into an Range endpoint value throwing + * a useful exception if not possible + */ + public final T getValue(final String rawval) { + try { + return parseVal(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 an Range endpoint. + * Can throw a low level format exception as needed. + */ + protected abstract T parseVal(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 parseVal(rawval); + } + + /** + * Adds the String gap param to a low Range endpoint value to determine + * the corrisponding high Range endpoint value, throwing + * a useful exception if not possible. + */ + public final T addGap(T 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 corrisponding high Range endpoint value. + * Can throw a low level format exception as needed. + */ + protected abstract T parseAndAddGap(T value, String gap) + throws java.text.ParseException; + + } + + private static class FloatRangeEndpointCalculator + extends RangeEndpointCalculator { + + public FloatRangeEndpointCalculator(final SchemaField f) { super(f); } + protected Float parseVal(String rawval) { + return Float.valueOf(rawval); + } + public Float parseAndAddGap(Float value, String gap) { + return new Float(value.floatValue() + Float.valueOf(gap).floatValue()); + } + } + private static class DoubleRangeEndpointCalculator + extends RangeEndpointCalculator { + + public DoubleRangeEndpointCalculator(final SchemaField f) { super(f); } + protected Double parseVal(String rawval) { + return Double.valueOf(rawval); + } + public Double parseAndAddGap(Double value, String gap) { + return new Double(value.floatValue() + Double.valueOf(gap).floatValue()); + } + } + private static class IntegerRangeEndpointCalculator + extends RangeEndpointCalculator { + + public IntegerRangeEndpointCalculator(final SchemaField f) { super(f); } + protected Integer parseVal(String rawval) { + return Integer.valueOf(rawval); + } + public Integer parseAndAddGap(Integer value, String gap) { + return new Integer(value.intValue() + Integer.valueOf(gap).intValue()); + } + } + private static class LongRangeEndpointCalculator + extends RangeEndpointCalculator { + + public LongRangeEndpointCalculator(final SchemaField f) { super(f); } + protected Long parseVal(String rawval) { + return Long.valueOf(rawval); + } + public Long parseAndAddGap(Long value, String gap) { + return new Long(value.intValue() + Long.valueOf(gap).intValue()); + } + } + private static class DateRangeEndpointCalculator + extends RangeEndpointCalculator { + private final Date now; + public DateRangeEndpointCalculator(final SchemaField f, + final Date now) { + super(f); + this.now = now; + if (! (field.getType() instanceof DateField) ) { + throw new IllegalArgumentException + ("SchemaField must use filed type extending DateField"); + } + } + public String formatValue(Date val) { + return ((DateField)field.getType()).toExternal(val); + } + protected Date parseVal(String rawval) { + return ((DateField)field.getType()).parseMath(now, rawval); + } + protected Object parseGap(final String rawval) { + return rawval; + } + public Date parseAndAddGap(Date value, String gap) throws java.text.ParseException { + final DateMathParser dmp = new DateMathParser(DateField.UTC, Locale.US); + dmp.setNow(value); + return dmp.parseMath(gap); + } + } + } diff --git a/solr/src/test/org/apache/solr/ConvertedLegacyTest.java b/solr/src/test/org/apache/solr/ConvertedLegacyTest.java index d5a8e667e0e..7592f37c459 100644 --- a/solr/src/test/org/apache/solr/ConvertedLegacyTest.java +++ b/solr/src/test/org/apache/solr/ConvertedLegacyTest.java @@ -1150,7 +1150,7 @@ public class ConvertedLegacyTest extends SolrTestCaseJ4 { ,"//str[.='Yonik'] " ,"//float[.='1.4142135'] " ,"//float[@name='score'] " - ,"*[count(//doc/*)=13]" + ,"*[count(//doc/*)>=13]" ); args = new HashMap(); args.put("version","2.0"); @@ -1161,7 +1161,7 @@ public class ConvertedLegacyTest extends SolrTestCaseJ4 { ,"//str[.='Yonik'] " ,"//float[.='1.4142135'] " ,"//float[@name='score'] " - ,"*[count(//doc/*)=13]" + ,"*[count(//doc/*)>=13]" ); args = new HashMap(); args.put("version","2.0"); diff --git a/solr/src/test/org/apache/solr/request/SimpleFacetsTest.java b/solr/src/test/org/apache/solr/request/SimpleFacetsTest.java index 44101f19876..64dfc448ffb 100644 --- a/solr/src/test/org/apache/solr/request/SimpleFacetsTest.java +++ b/solr/src/test/org/apache/solr/request/SimpleFacetsTest.java @@ -72,18 +72,29 @@ public class SimpleFacetsTest extends SolrTestCaseJ4 { } static void indexSimpleFacetCounts() { - add_doc("id", "42", "trait_s", "Tool", "trait_s", "Obnoxious", - "name", "Zapp Brannigan"); + add_doc("id", "42", + "range_facet_f", "35.3", + "trait_s", "Tool", "trait_s", "Obnoxious", + "name", "Zapp Brannigan"); add_doc("id", "43" , - "title", "Democratic Order of Planets"); - add_doc("id", "44", "trait_s", "Tool", - "name", "The Zapper"); - add_doc("id", "45", "trait_s", "Chauvinist", - "title", "25 star General"); - add_doc("id", "46", "trait_s", "Obnoxious", - "subject", "Defeated the pacifists of the Gandhi nebula"); - add_doc("id", "47", "trait_s", "Pig", - "text", "line up and fly directly at the enemy death cannons, clogging them with wreckage!"); + "range_facet_f", "28.789", + "title", "Democratic Order of Planets"); + add_doc("id", "44", + "range_facet_f", "15.97", + "trait_s", "Tool", + "name", "The Zapper"); + add_doc("id", "45", + "range_facet_f", "30.0", + "trait_s", "Chauvinist", + "title", "25 star General"); + add_doc("id", "46", + "range_facet_f", "20.0", + "trait_s", "Obnoxious", + "subject", "Defeated the pacifists of the Gandhi nebula"); + add_doc("id", "47", + "range_facet_f", "28.62", + "trait_s", "Pig", + "text", "line up and fly directly at the enemy death cannons, clogging them with wreckage!"); } @Test @@ -281,23 +292,44 @@ public class SimpleFacetsTest extends SolrTestCaseJ4 { add_doc(i, "2014", f, "1976-07-05T22:22:22.222Z"); } + @Test + public void testTrieDateFacets() { + helpTestDateFacets("bday", false); + } @Test public void testDateFacets() { - final String f = "bday"; - final String pre = "//lst[@name='facet_dates']/lst[@name='"+f+"']"; + helpTestDateFacets("bday_pdt", false); + } + + @Test + public void testTrieDateRangeFacets() { + helpTestDateFacets("bday", true); + } + @Test + public void testDateRangeFacets() { + helpTestDateFacets("bday_pdt", true); + } + + private void helpTestDateFacets(final String fieldName, + final boolean rangeMode) { + final String p = rangeMode ? "facet.range" : "facet.date"; + final String b = rangeMode ? "facet_ranges" : "facet_dates"; + final String f = fieldName; + final String c = (rangeMode ? "/lst[@name='counts']" : ""); + final String pre = "//lst[@name='"+b+"']/lst[@name='"+f+"']" + c; + final String meta = pre + (rangeMode ? "/../" : ""); assertQ("check counts for month of facet by day", req( "q", "*:*" ,"rows", "0" ,"facet", "true" - ,"facet.date", f - ,"facet.date.start", "1976-07-01T00:00:00.000Z" - ,"facet.date.end", "1976-07-01T00:00:00.000Z+1MONTH" - ,"facet.date.gap", "+1DAY" - ,"facet.date.other", "all" + ,p, f + ,p+".start", "1976-07-01T00:00:00.000Z" + ,p+".end", "1976-07-01T00:00:00.000Z+1MONTH" + ,p+".gap", "+1DAY" + ,p+".other", "all" ) - // 31 days + pre+post+inner = 34 - ,"*[count("+pre+"/int)=34]" + ,"*[count("+pre+"/int)="+(rangeMode ? 31 : 34)+"]" ,pre+"/int[@name='1976-07-01T00:00:00Z'][.='0' ]" ,pre+"/int[@name='1976-07-02T00:00:00Z'][.='0' ]" ,pre+"/int[@name='1976-07-03T00:00:00Z'][.='2' ]" @@ -331,9 +363,9 @@ public class SimpleFacetsTest extends SolrTestCaseJ4 { ,pre+"/int[@name='1976-07-30T00:00:00Z'][.='1' ]" ,pre+"/int[@name='1976-07-31T00:00:00Z'][.='0']" - ,pre+"/int[@name='before' ][.='2']" - ,pre+"/int[@name='after' ][.='1']" - ,pre+"/int[@name='between'][.='11']" + ,meta+"/int[@name='before' ][.='2']" + ,meta+"/int[@name='after' ][.='1']" + ,meta+"/int[@name='between'][.='11']" ); @@ -341,15 +373,14 @@ public class SimpleFacetsTest extends SolrTestCaseJ4 { req( "q", "*:*" ,"rows", "0" ,"facet", "true" - ,"facet.date", f - ,"facet.date.start", "1976-07-01T00:00:00.000Z" - ,"facet.date.end", "1976-07-01T00:00:00.000Z+1MONTH" - ,"facet.date.gap", "+1DAY" - ,"facet.date.other", "all" + ,p, f + ,p+".start", "1976-07-01T00:00:00.000Z" + ,p+".end", "1976-07-01T00:00:00.000Z+1MONTH" + ,p+".gap", "+1DAY" + ,p+".other", "all" ,"facet.mincount", "1" ) - // 31 days + pre+post+inner = 34 - ,"*[count("+pre+"/int)=11]" + ,"*[count("+pre+"/int)="+(rangeMode ? 8 : 11)+"]" ,pre+"/int[@name='1976-07-03T00:00:00Z'][.='2' ]" // july4th = 2 because exists doc @ 00:00:00.000 on July5 // (date faceting is inclusive) @@ -360,68 +391,65 @@ public class SimpleFacetsTest extends SolrTestCaseJ4 { ,pre+"/int[@name='1976-07-15T00:00:00Z'][.='2' ]" ,pre+"/int[@name='1976-07-21T00:00:00Z'][.='1' ]" ,pre+"/int[@name='1976-07-30T00:00:00Z'][.='1' ]" - ,pre+"/int[@name='before' ][.='2']" - ,pre+"/int[@name='after' ][.='1']" - ,pre+"/int[@name='between'][.='11']" + ,meta+"/int[@name='before' ][.='2']" + ,meta+"/int[@name='after' ][.='1']" + ,meta+"/int[@name='between'][.='11']" ); assertQ("check counts for month of facet by day with field mincount = 1", req( "q", "*:*" ,"rows", "0" ,"facet", "true" - ,"facet.date", f - ,"facet.date.start", "1976-07-01T00:00:00.000Z" - ,"facet.date.end", "1976-07-01T00:00:00.000Z+1MONTH" - ,"facet.date.gap", "+1DAY" - ,"facet.date.other", "all" + ,p, f + ,p+".start", "1976-07-01T00:00:00.000Z" + ,p+".end", "1976-07-01T00:00:00.000Z+1MONTH" + ,p+".gap", "+1DAY" + ,p+".other", "all" ,"f." + f + ".facet.mincount", "2" ) - // 31 days + pre+post+inner = 34 - ,"*[count("+pre+"/int)=7]" + ,"*[count("+pre+"/int)="+(rangeMode ? 4 : 7)+"]" ,pre+"/int[@name='1976-07-03T00:00:00Z'][.='2' ]" // july4th = 2 because exists doc @ 00:00:00.000 on July5 // (date faceting is inclusive) ,pre+"/int[@name='1976-07-04T00:00:00Z'][.='2' ]" ,pre+"/int[@name='1976-07-05T00:00:00Z'][.='2' ]" ,pre+"/int[@name='1976-07-15T00:00:00Z'][.='2' ]" - ,pre+"/int[@name='before' ][.='2']" - ,pre+"/int[@name='after' ][.='1']" - ,pre+"/int[@name='between'][.='11']" + ,meta+"/int[@name='before' ][.='2']" + ,meta+"/int[@name='after' ][.='1']" + ,meta+"/int[@name='between'][.='11']" ); assertQ("check before is not inclusive of upper bound by default", req("q", "*:*" ,"rows", "0" ,"facet", "true" - ,"facet.date", f - ,"facet.date.start", "1976-07-05T00:00:00.000Z" - ,"facet.date.end", "1976-07-07T00:00:00.000Z" - ,"facet.date.gap", "+1DAY" - ,"facet.date.other", "all" + ,p, f + ,p+".start", "1976-07-05T00:00:00.000Z" + ,p+".end", "1976-07-07T00:00:00.000Z" + ,p+".gap", "+1DAY" + ,p+".other", "all" ) - // 2 gaps + pre+post+inner = 5 - ,"*[count("+pre+"/int)=5]" + ,"*[count("+pre+"/int)="+(rangeMode ? 2 : 5)+"]" ,pre+"/int[@name='1976-07-05T00:00:00Z'][.='2' ]" ,pre+"/int[@name='1976-07-06T00:00:00Z'][.='0' ]" - ,pre+"/int[@name='before' ][.='5']" + ,meta+"/int[@name='before' ][.='5']" ); assertQ("check after is not inclusive of lower bound by default", req("q", "*:*" ,"rows", "0" ,"facet", "true" - ,"facet.date", f - ,"facet.date.start", "1976-07-03T00:00:00.000Z" - ,"facet.date.end", "1976-07-05T00:00:00.000Z" - ,"facet.date.gap", "+1DAY" - ,"facet.date.other", "all" + ,p, f + ,p+".start", "1976-07-03T00:00:00.000Z" + ,p+".end", "1976-07-05T00:00:00.000Z" + ,p+".gap", "+1DAY" + ,p+".other", "all" ) - // 2 gaps + pre+post+inner = 5 - ,"*[count("+pre+"/int)=5]" + ,"*[count("+pre+"/int)="+(rangeMode ? 2 : 5)+"]" ,pre+"/int[@name='1976-07-03T00:00:00Z'][.='2' ]" ,pre+"/int[@name='1976-07-04T00:00:00Z'][.='2' ]" - ,pre+"/int[@name='after' ][.='8']" + ,meta+"/int[@name='after' ][.='8']" ); @@ -429,68 +457,88 @@ public class SimpleFacetsTest extends SolrTestCaseJ4 { req( "q", "*:*" ,"rows", "0" ,"facet", "true" - ,"facet.date", f - ,"facet.date.start", "1976-07-01T00:00:00.000Z" - ,"facet.date.end", "1976-07-13T00:00:00.000Z" - ,"facet.date.gap", "+5DAYS" - ,"facet.date.other", "all" - ,"facet.date.hardend","false" + ,p, f + ,p+".start", "1976-07-01T00:00:00.000Z" + ,p+".end", "1976-07-13T00:00:00.000Z" + ,p+".gap", "+5DAYS" + ,p+".other", "all" + ,p+".hardend","false" ) - // 3 gaps + pre+post+inner = 6 - ,"*[count("+pre+"/int)=6]" + ,"*[count("+pre+"/int)="+(rangeMode ? 3 : 6)+"]" ,pre+"/int[@name='1976-07-01T00:00:00Z'][.='5' ]" ,pre+"/int[@name='1976-07-06T00:00:00Z'][.='0' ]" ,pre+"/int[@name='1976-07-11T00:00:00Z'][.='4' ]" - ,pre+"/int[@name='before' ][.='2']" - ,pre+"/int[@name='after' ][.='3']" - ,pre+"/int[@name='between'][.='9']" + ,meta+"/int[@name='before' ][.='2']" + ,meta+"/int[@name='after' ][.='3']" + ,meta+"/int[@name='between'][.='9']" ); assertQ("check hardend=true", req( "q", "*:*" ,"rows", "0" ,"facet", "true" - ,"facet.date", f - ,"facet.date.start", "1976-07-01T00:00:00.000Z" - ,"facet.date.end", "1976-07-13T00:00:00.000Z" - ,"facet.date.gap", "+5DAYS" - ,"facet.date.other", "all" - ,"facet.date.hardend","true" + ,p, f + ,p+".start", "1976-07-01T00:00:00.000Z" + ,p+".end", "1976-07-13T00:00:00.000Z" + ,p+".gap", "+5DAYS" + ,p+".other", "all" + ,p+".hardend","true" ) - // 3 gaps + pre+post+inner = 6 - ,"*[count("+pre+"/int)=6]" + ,"*[count("+pre+"/int)="+(rangeMode ? 3 : 6)+"]" ,pre+"/int[@name='1976-07-01T00:00:00Z'][.='5' ]" ,pre+"/int[@name='1976-07-06T00:00:00Z'][.='0' ]" ,pre+"/int[@name='1976-07-11T00:00:00Z'][.='1' ]" - ,pre+"/int[@name='before' ][.='2']" - ,pre+"/int[@name='after' ][.='6']" - ,pre+"/int[@name='between'][.='6']" + ,meta+"/int[@name='before' ][.='2']" + ,meta+"/int[@name='after' ][.='6']" + ,meta+"/int[@name='between'][.='6']" ); } - /** similar to testDateFacets, but a differnet field with test data - exactly on on boundary marks */ + @Test + public void testTrieDateFacetsWithIncludeOption() { + helpTestDateFacetsWithIncludeOption("a_tdt", false); + } @Test public void testDateFacetsWithIncludeOption() { - final String f = "a_tdt"; - final String pre = "//lst[@name='facet_dates']/lst[@name='"+f+"']"; + helpTestDateFacetsWithIncludeOption("a_pdt", false); + } + + @Test + public void testTrieDateRangeFacetsWithIncludeOption() { + helpTestDateFacetsWithIncludeOption("a_tdt", true); + } + @Test + public void testDateRangeFacetsWithIncludeOption() { + helpTestDateFacetsWithIncludeOption("a_pdt", true); + } + + /** similar to helpTestDateFacets, but for differnet fields with test data + exactly on on boundary marks */ + private void helpTestDateFacetsWithIncludeOption(final String fieldName, + final boolean rangeMode) { + final String p = rangeMode ? "facet.range" : "facet.date"; + final String b = rangeMode ? "facet_ranges" : "facet_dates"; + final String f = fieldName; + final String c = (rangeMode ? "/lst[@name='counts']" : ""); + final String pre = "//lst[@name='"+b+"']/lst[@name='"+f+"']" + c; + final String meta = pre + (rangeMode ? "/../" : ""); assertQ("checking counts for lower", req( "q", "*:*" ,"rows", "0" ,"facet", "true" - ,"facet.date", f - ,"facet.date.start", "1976-07-01T00:00:00.000Z" - ,"facet.date.end", "1976-07-16T00:00:00.000Z" - ,"facet.date.gap", "+1DAY" - ,"facet.date.other", "all" - ,"facet.date.include", "lower" + ,p, f + ,p+".start", "1976-07-01T00:00:00.000Z" + ,p+".end", "1976-07-16T00:00:00.000Z" + ,p+".gap", "+1DAY" + ,p+".other", "all" + ,p+".include", "lower" ) // 15 days + pre+post+inner = 18 - ,"*[count("+pre+"/int)=18]" + ,"*[count("+pre+"/int)="+(rangeMode ? 15 : 18)+"]" ,pre+"/int[@name='1976-07-01T00:00:00Z'][.='1' ]" ,pre+"/int[@name='1976-07-02T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-03T00:00:00Z'][.='0']" @@ -506,24 +554,25 @@ public class SimpleFacetsTest extends SolrTestCaseJ4 { ,pre+"/int[@name='1976-07-13T00:00:00Z'][.='2' ]" ,pre+"/int[@name='1976-07-14T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-15T00:00:00Z'][.='1' ]" - ,pre+"/int[@name='before' ][.='1']" - ,pre+"/int[@name='after' ][.='1']" - ,pre+"/int[@name='between'][.='8']" + // + ,meta+"/int[@name='before' ][.='1']" + ,meta+"/int[@name='after' ][.='1']" + ,meta+"/int[@name='between'][.='8']" ); assertQ("checking counts for upper", req( "q", "*:*" ,"rows", "0" ,"facet", "true" - ,"facet.date", f - ,"facet.date.start", "1976-07-01T00:00:00.000Z" - ,"facet.date.end", "1976-07-16T00:00:00.000Z" - ,"facet.date.gap", "+1DAY" - ,"facet.date.other", "all" - ,"facet.date.include", "upper" + ,p, f + ,p+".start", "1976-07-01T00:00:00.000Z" + ,p+".end", "1976-07-16T00:00:00.000Z" + ,p+".gap", "+1DAY" + ,p+".other", "all" + ,p+".include", "upper" ) // 15 days + pre+post+inner = 18 - ,"*[count("+pre+"/int)=18]" + ,"*[count("+pre+"/int)="+(rangeMode ? 15 : 18)+"]" ,pre+"/int[@name='1976-07-01T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-02T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-03T00:00:00Z'][.='1' ]" @@ -539,25 +588,26 @@ public class SimpleFacetsTest extends SolrTestCaseJ4 { ,pre+"/int[@name='1976-07-13T00:00:00Z'][.='1' ]" ,pre+"/int[@name='1976-07-14T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-15T00:00:00Z'][.='1' ]" - ,pre+"/int[@name='before' ][.='2']" - ,pre+"/int[@name='after' ][.='1']" - ,pre+"/int[@name='between'][.='7']" + // + ,meta+"/int[@name='before' ][.='2']" + ,meta+"/int[@name='after' ][.='1']" + ,meta+"/int[@name='between'][.='7']" ); assertQ("checking counts for lower & upper", req( "q", "*:*" ,"rows", "0" ,"facet", "true" - ,"facet.date", f - ,"facet.date.start", "1976-07-01T00:00:00.000Z" - ,"facet.date.end", "1976-07-16T00:00:00.000Z" - ,"facet.date.gap", "+1DAY" - ,"facet.date.other", "all" - ,"facet.date.include", "lower" - ,"facet.date.include", "upper" + ,p, f + ,p+".start", "1976-07-01T00:00:00.000Z" + ,p+".end", "1976-07-16T00:00:00.000Z" + ,p+".gap", "+1DAY" + ,p+".other", "all" + ,p+".include", "lower" + ,p+".include", "upper" ) // 15 days + pre+post+inner = 18 - ,"*[count("+pre+"/int)=18]" + ,"*[count("+pre+"/int)="+(rangeMode ? 15 : 18)+"]" ,pre+"/int[@name='1976-07-01T00:00:00Z'][.='1' ]" ,pre+"/int[@name='1976-07-02T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-03T00:00:00Z'][.='1' ]" @@ -573,25 +623,26 @@ public class SimpleFacetsTest extends SolrTestCaseJ4 { ,pre+"/int[@name='1976-07-13T00:00:00Z'][.='2' ]" ,pre+"/int[@name='1976-07-14T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-15T00:00:00Z'][.='1' ]" - ,pre+"/int[@name='before' ][.='1']" - ,pre+"/int[@name='after' ][.='1']" - ,pre+"/int[@name='between'][.='8']" + // + ,meta+"/int[@name='before' ][.='1']" + ,meta+"/int[@name='after' ][.='1']" + ,meta+"/int[@name='between'][.='8']" ); assertQ("checking counts for upper & edge", req( "q", "*:*" ,"rows", "0" ,"facet", "true" - ,"facet.date", f - ,"facet.date.start", "1976-07-01T00:00:00.000Z" - ,"facet.date.end", "1976-07-16T00:00:00.000Z" - ,"facet.date.gap", "+1DAY" - ,"facet.date.other", "all" - ,"facet.date.include", "upper" - ,"facet.date.include", "edge" + ,p, f + ,p+".start", "1976-07-01T00:00:00.000Z" + ,p+".end", "1976-07-16T00:00:00.000Z" + ,p+".gap", "+1DAY" + ,p+".other", "all" + ,p+".include", "upper" + ,p+".include", "edge" ) // 15 days + pre+post+inner = 18 - ,"*[count("+pre+"/int)=18]" + ,"*[count("+pre+"/int)="+(rangeMode ? 15 : 18)+"]" ,pre+"/int[@name='1976-07-01T00:00:00Z'][.='1' ]" ,pre+"/int[@name='1976-07-02T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-03T00:00:00Z'][.='1' ]" @@ -607,25 +658,26 @@ public class SimpleFacetsTest extends SolrTestCaseJ4 { ,pre+"/int[@name='1976-07-13T00:00:00Z'][.='1' ]" ,pre+"/int[@name='1976-07-14T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-15T00:00:00Z'][.='1' ]" - ,pre+"/int[@name='before' ][.='1']" - ,pre+"/int[@name='after' ][.='1']" - ,pre+"/int[@name='between'][.='8']" + // + ,meta+"/int[@name='before' ][.='1']" + ,meta+"/int[@name='after' ][.='1']" + ,meta+"/int[@name='between'][.='8']" ); assertQ("checking counts for upper & outer", req( "q", "*:*" ,"rows", "0" ,"facet", "true" - ,"facet.date", f - ,"facet.date.start", "1976-07-01T00:00:00.000Z" - ,"facet.date.end", "1976-07-13T00:00:00.000Z" // smaller now - ,"facet.date.gap", "+1DAY" - ,"facet.date.other", "all" - ,"facet.date.include", "upper" - ,"facet.date.include", "outer" + ,p, f + ,p+".start", "1976-07-01T00:00:00.000Z" + ,p+".end", "1976-07-13T00:00:00.000Z" // smaller now + ,p+".gap", "+1DAY" + ,p+".other", "all" + ,p+".include", "upper" + ,p+".include", "outer" ) // 12 days + pre+post+inner = 15 - ,"*[count("+pre+"/int)=15]" + ,"*[count("+pre+"/int)="+(rangeMode ? 12 : 15)+"]" ,pre+"/int[@name='1976-07-01T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-02T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-03T00:00:00Z'][.='1' ]" @@ -638,25 +690,26 @@ public class SimpleFacetsTest extends SolrTestCaseJ4 { ,pre+"/int[@name='1976-07-10T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-11T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-12T00:00:00Z'][.='1' ]" - ,pre+"/int[@name='before' ][.='2']" - ,pre+"/int[@name='after' ][.='4']" - ,pre+"/int[@name='between'][.='5']" + // + ,meta+"/int[@name='before' ][.='2']" + ,meta+"/int[@name='after' ][.='4']" + ,meta+"/int[@name='between'][.='5']" ); assertQ("checking counts for lower & edge", req( "q", "*:*" ,"rows", "0" ,"facet", "true" - ,"facet.date", f - ,"facet.date.start", "1976-07-01T00:00:00.000Z" - ,"facet.date.end", "1976-07-13T00:00:00.000Z" // smaller now - ,"facet.date.gap", "+1DAY" - ,"facet.date.other", "all" - ,"facet.date.include", "lower" - ,"facet.date.include", "edge" + ,p, f + ,p+".start", "1976-07-01T00:00:00.000Z" + ,p+".end", "1976-07-13T00:00:00.000Z" // smaller now + ,p+".gap", "+1DAY" + ,p+".other", "all" + ,p+".include", "lower" + ,p+".include", "edge" ) // 12 days + pre+post+inner = 15 - ,"*[count("+pre+"/int)=15]" + ,"*[count("+pre+"/int)="+(rangeMode ? 12 : 15)+"]" ,pre+"/int[@name='1976-07-01T00:00:00Z'][.='1' ]" ,pre+"/int[@name='1976-07-02T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-03T00:00:00Z'][.='0']" @@ -669,25 +722,26 @@ public class SimpleFacetsTest extends SolrTestCaseJ4 { ,pre+"/int[@name='1976-07-10T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-11T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-12T00:00:00Z'][.='1' ]" - ,pre+"/int[@name='before' ][.='1']" - ,pre+"/int[@name='after' ][.='3']" - ,pre+"/int[@name='between'][.='6']" + // + ,meta+"/int[@name='before' ][.='1']" + ,meta+"/int[@name='after' ][.='3']" + ,meta+"/int[@name='between'][.='6']" ); assertQ("checking counts for lower & outer", req( "q", "*:*" ,"rows", "0" ,"facet", "true" - ,"facet.date", f - ,"facet.date.start", "1976-07-01T00:00:00.000Z" - ,"facet.date.end", "1976-07-13T00:00:00.000Z" // smaller now - ,"facet.date.gap", "+1DAY" - ,"facet.date.other", "all" - ,"facet.date.include", "lower" - ,"facet.date.include", "outer" + ,p, f + ,p+".start", "1976-07-01T00:00:00.000Z" + ,p+".end", "1976-07-13T00:00:00.000Z" // smaller now + ,p+".gap", "+1DAY" + ,p+".other", "all" + ,p+".include", "lower" + ,p+".include", "outer" ) // 12 days + pre+post+inner = 15 - ,"*[count("+pre+"/int)=15]" + ,"*[count("+pre+"/int)="+(rangeMode ? 12 : 15)+"]" ,pre+"/int[@name='1976-07-01T00:00:00Z'][.='1' ]" ,pre+"/int[@name='1976-07-02T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-03T00:00:00Z'][.='0']" @@ -700,26 +754,27 @@ public class SimpleFacetsTest extends SolrTestCaseJ4 { ,pre+"/int[@name='1976-07-10T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-11T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-12T00:00:00Z'][.='0']" - ,pre+"/int[@name='before' ][.='2']" - ,pre+"/int[@name='after' ][.='4']" - ,pre+"/int[@name='between'][.='5']" + // + ,meta+"/int[@name='before' ][.='2']" + ,meta+"/int[@name='after' ][.='4']" + ,meta+"/int[@name='between'][.='5']" ); assertQ("checking counts for lower & edge & outer", req( "q", "*:*" ,"rows", "0" ,"facet", "true" - ,"facet.date", f - ,"facet.date.start", "1976-07-01T00:00:00.000Z" - ,"facet.date.end", "1976-07-13T00:00:00.000Z" // smaller now - ,"facet.date.gap", "+1DAY" - ,"facet.date.other", "all" - ,"facet.date.include", "lower" - ,"facet.date.include", "edge" - ,"facet.date.include", "outer" + ,p, f + ,p+".start", "1976-07-01T00:00:00.000Z" + ,p+".end", "1976-07-13T00:00:00.000Z" // smaller now + ,p+".gap", "+1DAY" + ,p+".other", "all" + ,p+".include", "lower" + ,p+".include", "edge" + ,p+".include", "outer" ) // 12 days + pre+post+inner = 15 - ,"*[count("+pre+"/int)=15]" + ,"*[count("+pre+"/int)="+(rangeMode ? 12 : 15)+"]" ,pre+"/int[@name='1976-07-01T00:00:00Z'][.='1' ]" ,pre+"/int[@name='1976-07-02T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-03T00:00:00Z'][.='0']" @@ -732,24 +787,25 @@ public class SimpleFacetsTest extends SolrTestCaseJ4 { ,pre+"/int[@name='1976-07-10T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-11T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-12T00:00:00Z'][.='1' ]" - ,pre+"/int[@name='before' ][.='2']" - ,pre+"/int[@name='after' ][.='4']" - ,pre+"/int[@name='between'][.='6']" + // + ,meta+"/int[@name='before' ][.='2']" + ,meta+"/int[@name='after' ][.='4']" + ,meta+"/int[@name='between'][.='6']" ); assertQ("checking counts for all", req( "q", "*:*" ,"rows", "0" ,"facet", "true" - ,"facet.date", f - ,"facet.date.start", "1976-07-01T00:00:00.000Z" - ,"facet.date.end", "1976-07-13T00:00:00.000Z" // smaller now - ,"facet.date.gap", "+1DAY" - ,"facet.date.other", "all" - ,"facet.date.include", "all" + ,p, f + ,p+".start", "1976-07-01T00:00:00.000Z" + ,p+".end", "1976-07-13T00:00:00.000Z" // smaller now + ,p+".gap", "+1DAY" + ,p+".other", "all" + ,p+".include", "all" ) // 12 days + pre+post+inner = 15 - ,"*[count("+pre+"/int)=15]" + ,"*[count("+pre+"/int)="+(rangeMode ? 12 : 15)+"]" ,pre+"/int[@name='1976-07-01T00:00:00Z'][.='1' ]" ,pre+"/int[@name='1976-07-02T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-03T00:00:00Z'][.='1' ]" @@ -762,12 +818,302 @@ public class SimpleFacetsTest extends SolrTestCaseJ4 { ,pre+"/int[@name='1976-07-10T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-11T00:00:00Z'][.='0']" ,pre+"/int[@name='1976-07-12T00:00:00Z'][.='1' ]" - ,pre+"/int[@name='before' ][.='2']" - ,pre+"/int[@name='after' ][.='4']" - ,pre+"/int[@name='between'][.='6']" + // + ,meta+"/int[@name='before' ][.='2']" + ,meta+"/int[@name='after' ][.='4']" + ,meta+"/int[@name='between'][.='6']" ); } + @Test + public void testNumericRangeFacetsTrieFloat() { + helpTestFractionalNumberRangeFacets("range_facet_f"); + } + @Test + public void testNumericRangeFacetsTrieDouble() { + helpTestFractionalNumberRangeFacets("range_facet_d"); + } + @Test + public void testNumericRangeFacetsSortableFloat() { + helpTestFractionalNumberRangeFacets("range_facet_sf"); + } + @Test + public void testNumericRangeFacetsSortableDouble() { + helpTestFractionalNumberRangeFacets("range_facet_sd"); + } + private void helpTestFractionalNumberRangeFacets(final String fieldName) { + + final String f = fieldName; + final String pre = "//lst[@name='facet_ranges']/lst[@name='"+f+"']/lst[@name='counts']"; + final String meta = pre + "/../"; + + assertQ(f+": checking counts for lower", + req( "q", "*:*" + ,"rows", "0" + ,"facet", "true" + ,"facet.range", f + ,"facet.range.start", "10" + ,"facet.range.end", "50" + ,"facet.range.gap", "10" + ,"facet.range.other", "all" + ,"facet.range.include", "lower" + ) + ,"*[count("+pre+"/int)=4]" + ,pre+"/int[@name='10.0'][.='1' ]" + ,pre+"/int[@name='20.0'][.='3' ]" + ,pre+"/int[@name='30.0'][.='2' ]" + ,pre+"/int[@name='40.0'][.='0' ]" + // + ,meta+"/int[@name='before' ][.='0']" + ,meta+"/int[@name='after' ][.='0']" + ,meta+"/int[@name='between'][.='6']" + ); + + assertQ(f + ":checking counts for upper", + req( "q", "*:*" + ,"rows", "0" + ,"facet", "true" + ,"facet.range", f + ,"facet.range.start", "10" + ,"facet.range.end", "50" + ,"facet.range.gap", "10" + ,"facet.range.other", "all" + ,"facet.range.include", "upper" + ) + ,"*[count("+pre+"/int)=4]" + ,pre+"/int[@name='10.0'][.='2' ]" + ,pre+"/int[@name='20.0'][.='3' ]" + ,pre+"/int[@name='30.0'][.='1' ]" + ,pre+"/int[@name='40.0'][.='0' ]" + // + ,meta+"/int[@name='before' ][.='0']" + ,meta+"/int[@name='after' ][.='0']" + ,meta+"/int[@name='between'][.='6']" + ); + + assertQ(f + ":checking counts for lower & upper", + req( "q", "*:*" + ,"rows", "0" + ,"facet", "true" + ,"facet.range", f + ,"facet.range.start", "10" + ,"facet.range.end", "50" + ,"facet.range.gap", "10" + ,"facet.range.other", "all" + ,"facet.range.include", "upper" + ,"facet.range.include", "lower" + ) + ,"*[count("+pre+"/int)=4]" + ,pre+"/int[@name='10.0'][.='2' ]" + ,pre+"/int[@name='20.0'][.='4' ]" + ,pre+"/int[@name='30.0'][.='2' ]" + ,pre+"/int[@name='40.0'][.='0' ]" + // + ,meta+"/int[@name='before' ][.='0']" + ,meta+"/int[@name='after' ][.='0']" + ,meta+"/int[@name='between'][.='6']" + ); + + assertQ(f + ": checking counts for upper & edge", + req( "q", "*:*" + ,"rows", "0" + ,"facet", "true" + ,"facet.range", f + ,"facet.range.start", "20" + ,"facet.range.end", "50" + ,"facet.range.gap", "10" + ,"facet.range.other", "all" + ,"facet.range.include", "upper" + ,"facet.range.include", "edge" + ) + ,"*[count("+pre+"/int)=3]" + ,pre+"/int[@name='20.0'][.='4' ]" + ,pre+"/int[@name='30.0'][.='1' ]" + ,pre+"/int[@name='40.0'][.='0' ]" + // + ,meta+"/int[@name='before' ][.='1']" + ,meta+"/int[@name='after' ][.='0']" + ,meta+"/int[@name='between'][.='5']" + ); + + assertQ(f + ": checking counts for upper & outer", + req( "q", "*:*" + ,"rows", "0" + ,"facet", "true" + ,"facet.range", f + ,"facet.range.start", "10" + ,"facet.range.end", "30" + ,"facet.range.gap", "10" + ,"facet.range.other", "all" + ,"facet.range.include", "upper" + ,"facet.range.include", "outer" + ) + ,"*[count("+pre+"/int)=2]" + ,pre+"/int[@name='10.0'][.='2' ]" + ,pre+"/int[@name='20.0'][.='3' ]" + // + ,meta+"/int[@name='before' ][.='0']" + ,meta+"/int[@name='after' ][.='2']" + ,meta+"/int[@name='between'][.='5']" + ); + + assertQ(f + ": checking counts for lower & edge", + req( "q", "*:*" + ,"rows", "0" + ,"facet", "true" + ,"facet.range", f + ,"facet.range.start", "10" + ,"facet.range.end", "30" + ,"facet.range.gap", "10" + ,"facet.range.other", "all" + ,"facet.range.include", "lower" + ,"facet.range.include", "edge" + ) + ,"*[count("+pre+"/int)=2]" + ,pre+"/int[@name='10.0'][.='1' ]" + ,pre+"/int[@name='20.0'][.='4' ]" + // + ,meta+"/int[@name='before' ][.='0']" + ,meta+"/int[@name='after' ][.='1']" + ,meta+"/int[@name='between'][.='5']" + ); + + assertQ(f + ": checking counts for lower & outer", + req( "q", "*:*" + ,"rows", "0" + ,"facet", "true" + ,"facet.range", f + ,"facet.range.start", "20" + ,"facet.range.end", "40" + ,"facet.range.gap", "10" + ,"facet.range.other", "all" + ,"facet.range.include", "lower" + ,"facet.range.include", "outer" + ) + ,"*[count("+pre+"/int)=2]" + ,pre+"/int[@name='20.0'][.='3' ]" + ,pre+"/int[@name='30.0'][.='2' ]" + // + ,meta+"/int[@name='before' ][.='2']" + ,meta+"/int[@name='after' ][.='0']" + ,meta+"/int[@name='between'][.='5']" + ); + + assertQ(f + ": checking counts for lower & edge & outer", + req( "q", "*:*" + ,"rows", "0" + ,"facet", "true" + ,"facet.range", f + ,"facet.range.start", "20" + ,"facet.range.end", "35.3" + ,"facet.range.gap", "10" + ,"facet.range.other", "all" + ,"facet.range.hardend", "true" + ,"facet.range.include", "lower" + ,"facet.range.include", "edge" + ,"facet.range.include", "outer" + ) + ,"*[count("+pre+"/int)=2]" + ,pre+"/int[@name='20.0'][.='3' ]" + ,pre+"/int[@name='30.0'][.='2' ]" + // + ,meta+"/int[@name='before' ][.='2']" + ,meta+"/int[@name='after' ][.='1']" + ,meta+"/int[@name='between'][.='5']" + ); + + assertQ(f + ": checking counts for include all", + req( "q", "*:*" + ,"rows", "0" + ,"facet", "true" + ,"facet.range", f + ,"facet.range.start", "20" + ,"facet.range.end", "35.3" + ,"facet.range.gap", "10" + ,"facet.range.other", "all" + ,"facet.range.hardend", "true" + ,"facet.range.include", "all" + ) + ,"*[count("+pre+"/int)=2]" + ,pre+"/int[@name='20.0'][.='4' ]" + ,pre+"/int[@name='30.0'][.='2' ]" + // + ,meta+"/int[@name='before' ][.='2']" + ,meta+"/int[@name='after' ][.='1']" + ,meta+"/int[@name='between'][.='5']" + ); + } + + @Test + public void testNumericRangeFacetsTrieInt() { + helpTestWholeNumberRangeFacets("id"); + } + @Test + public void testNumericRangeFacetsTrieLong() { + helpTestWholeNumberRangeFacets("range_facet_l"); + } + @Test + public void testNumericRangeFacetsSortableInt() { + helpTestWholeNumberRangeFacets("range_facet_si"); + } + @Test + public void testNumericRangeFacetsSortableLong() { + helpTestWholeNumberRangeFacets("range_facet_sl"); + } + + private void helpTestWholeNumberRangeFacets(final String fieldName) { + + // the float test covers a lot of the weird edge cases + // here we just need some basic sanity checking of the parsing + + final String f = fieldName; + final String pre = "//lst[@name='facet_ranges']/lst[@name='"+f+"']/lst[@name='counts']"; + final String meta = pre + "/../"; + + assertQ(f+": checking counts for lower", + req( "q", "id:[30 TO 60]" + ,"rows", "0" + ,"facet", "true" + ,"facet.range", f + ,"facet.range.start", "35" + ,"facet.range.end", "50" + ,"facet.range.gap", "5" + ,"facet.range.other", "all" + ,"facet.range.include", "lower" + ) + ,"*[count("+pre+"/int)=3]" + ,pre+"/int[@name='35'][.='0' ]" + ,pre+"/int[@name='40'][.='3' ]" + ,pre+"/int[@name='45'][.='3' ]" + // + ,meta+"/int[@name='before' ][.='0']" + ,meta+"/int[@name='after' ][.='0']" + ,meta+"/int[@name='between'][.='6']" + ); + + assertQ(f + ":checking counts for upper", + req( "q", "id:[30 TO 60]" + ,"rows", "0" + ,"facet", "true" + ,"facet.range", f + ,"facet.range.start", "35" + ,"facet.range.end", "50" + ,"facet.range.gap", "5" + ,"facet.range.other", "all" + ,"facet.range.include", "upper" + ) + ,"*[count("+pre+"/int)=3]" + ,pre+"/int[@name='35'][.='0' ]" + ,pre+"/int[@name='40'][.='4' ]" + ,pre+"/int[@name='45'][.='2' ]" + // + ,meta+"/int[@name='before' ][.='0']" + ,meta+"/int[@name='after' ][.='0']" + ,meta+"/int[@name='between'][.='6']" + ); + + } + static void indexFacetSingleValued() { indexFacets("40","t_s1"); } diff --git a/solr/src/test/test-files/solr/conf/schema.xml b/solr/src/test/test-files/solr/conf/schema.xml index ea10c11edab..f53e7b46ab9 100644 --- a/solr/src/test/test-files/solr/conf/schema.xml +++ b/solr/src/test/test-files/solr/conf/schema.xml @@ -564,7 +564,18 @@ + + + + + + + + + + +