You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by tf...@apache.org on 2015/04/23 21:27:34 UTC

svn commit: r1675706 - in /lucene/dev/trunk/solr: ./ core/src/java/org/apache/solr/request/ core/src/test-files/solr/collection1/conf/ core/src/test/org/apache/solr/ core/src/test/org/apache/solr/request/ solrj/src/java/org/apache/solr/common/params/

Author: tflobbe
Date: Thu Apr 23 19:27:34 2015
New Revision: 1675706

URL: http://svn.apache.org/r1675706
Log:
SOLR-7406: Add facet.range.method parameter with options 'filter' and 'dv' for range faceting

Modified:
    lucene/dev/trunk/solr/CHANGES.txt
    lucene/dev/trunk/solr/core/src/java/org/apache/solr/request/IntervalFacets.java
    lucene/dev/trunk/solr/core/src/java/org/apache/solr/request/SimpleFacets.java
    lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/schema.xml
    lucene/dev/trunk/solr/core/src/test/org/apache/solr/TestDistributedSearch.java
    lucene/dev/trunk/solr/core/src/test/org/apache/solr/request/SimpleFacetsTest.java
    lucene/dev/trunk/solr/solrj/src/java/org/apache/solr/common/params/FacetParams.java

Modified: lucene/dev/trunk/solr/CHANGES.txt
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/CHANGES.txt?rev=1675706&r1=1675705&r2=1675706&view=diff
==============================================================================
--- lucene/dev/trunk/solr/CHANGES.txt (original)
+++ lucene/dev/trunk/solr/CHANGES.txt Thu Apr 23 19:27:34 2015
@@ -114,6 +114,10 @@ New Features
 * SOLR-7417: JSON Facet API - unique() is now implemented for numeric and date fields.
   (yonik)
 
+* SOLR-7406: Add a new "facet.range.method" parameter to let users choose how to do range 
+  faceting between an implementation based on filters (previous algorithm, using 
+  "facet.range.method=filter") or DocValues ("facet.range.method=dv"). 
+  Input parameters and output of both methods are the same. (Tomás Fernández Löbbe)
 
 Bug Fixes
 ----------------------

Modified: lucene/dev/trunk/solr/core/src/java/org/apache/solr/request/IntervalFacets.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/java/org/apache/solr/request/IntervalFacets.java?rev=1675706&r1=1675705&r2=1675706&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/java/org/apache/solr/request/IntervalFacets.java (original)
+++ lucene/dev/trunk/solr/core/src/java/org/apache/solr/request/IntervalFacets.java Thu Apr 23 19:27:34 2015
@@ -104,6 +104,10 @@ public class IntervalFacets implements I
   private final DocSet docs;
   private final FacetInterval[] intervals;
 
+  /**
+   * Constructor that accepts un-parsed intervals using "interval faceting" syntax. See {@link IntervalFacets} for syntax.
+   * Intervals don't need to be in order.
+   */
   public IntervalFacets(SchemaField schemaField, SolrIndexSearcher searcher, DocSet docs, String[] intervals, SolrParams params) throws SyntaxError, IOException {
     this.schemaField = schemaField;
     this.searcher = searcher;
@@ -111,6 +115,18 @@ public class IntervalFacets implements I
     this.intervals = getSortedIntervals(intervals, params);
     doCount();
   }
+  
+  /**
+   * Constructor that accepts an already constructed array of {@link FacetInterval} objects. This array needs to be sorted
+   * by start value in weakly ascending order. null values are not allowed in the array.
+   */
+  IntervalFacets(SchemaField schemaField, SolrIndexSearcher searcher, DocSet docs, FacetInterval[] intervals) throws IOException {
+    this.schemaField = schemaField;
+    this.searcher = searcher;
+    this.docs = docs;
+    this.intervals = intervals;
+    doCount();
+  }
 
   private FacetInterval[] getSortedIntervals(String[] intervals, SolrParams params) throws SyntaxError {
     FacetInterval[] sortedIntervals = new FacetInterval[intervals.length];
@@ -406,6 +422,14 @@ public class IntervalFacets implements I
      */
     private int count;
 
+    /**
+     * 
+     * Constructor that accepts un-parsed interval faceting syntax. See {@link IntervalFacets} for details
+     * 
+     * @param schemaField schemaField for this range
+     * @param intervalStr String the interval. See {@link IntervalFacets} for syntax
+     * @param params SolrParams of this request, mostly used to get local params
+     */
     FacetInterval(SchemaField schemaField, String intervalStr, SolrParams params) throws SyntaxError {
       if (intervalStr == null) throw new SyntaxError("empty facet interval");
       intervalStr = intervalStr.trim();
@@ -484,6 +508,31 @@ public class IntervalFacets implements I
     }
 
     /**
+     * 
+     * Constructor that accepts already parsed values of start and end. This constructor
+     * can only be used with numeric field types.
+     * 
+     * @param schemaField schemaField for this range
+     * @param startStr String representation of the start value of this interval. Can be a "*".
+     * @param endStr String representation of the end value of this interval. Can be a "*".
+     * @param includeLower Indicates weather this interval should include values equal to start
+     * @param includeUpper Indicates weather this interval should include values equal to end
+     * @param key String key of this interval
+     */
+    FacetInterval(SchemaField schemaField, String startStr, String endStr,
+        boolean includeLower, boolean includeUpper, String key) {
+      assert schemaField.getType().getNumericType() != null: "Only numeric fields supported with this constructor";
+      this.key = key;
+      this.startOpen = !includeLower;
+      this.endOpen = !includeUpper;
+      this.start = getLimitFromString(schemaField, startStr);
+      this.end = getLimitFromString(schemaField, endStr);
+      assert start == null || end == null || start.compareTo(end) < 0: 
+        "Bad start/end limits: " + startStr + "/" + endStr;
+      setNumericLimits(schemaField);
+    }
+
+    /**
      * Set startLimit and endLimit for numeric values. The limits in this case
      * are going to be the <code>long</code> representation of the original
      * value. <code>startLimit</code> will be incremented by one in case of the
@@ -554,6 +603,10 @@ public class IntervalFacets implements I
       if (value.length() == 0) {
         throw new SyntaxError("Empty interval limit");
       }
+      return getLimitFromString(schemaField, value);
+    }
+    
+    private BytesRef getLimitFromString(SchemaField schemaField, String value) {
       if ("*".equals(value)) {
         return null;
       }

Modified: lucene/dev/trunk/solr/core/src/java/org/apache/solr/request/SimpleFacets.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/java/org/apache/solr/request/SimpleFacets.java?rev=1675706&r1=1675705&r2=1675706&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/java/org/apache/solr/request/SimpleFacets.java (original)
+++ lucene/dev/trunk/solr/core/src/java/org/apache/solr/request/SimpleFacets.java Thu Apr 23 19:27:34 2015
@@ -18,6 +18,7 @@
 package org.apache.solr.request;
 
 import org.apache.commons.lang.StringUtils;
+import org.apache.log4j.Logger;
 import org.apache.lucene.index.Fields;
 import org.apache.lucene.index.LeafReader;
 import org.apache.lucene.index.LeafReaderContext;
@@ -46,6 +47,7 @@ import org.apache.solr.common.SolrExcept
 import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.FacetParams;
 import org.apache.solr.common.params.FacetParams.FacetRangeInclude;
+import org.apache.solr.common.params.FacetParams.FacetRangeMethod;
 import org.apache.solr.common.params.FacetParams.FacetRangeOther;
 import org.apache.solr.common.params.GroupParams;
 import org.apache.solr.common.params.RequiredSolrParams;
@@ -106,6 +108,8 @@ import java.util.concurrent.TimeUnit;
  * to leverage any of its functionality.
  */
 public class SimpleFacets {
+  
+  private final static Logger log = Logger.getLogger(SimpleFacets.class);
 
   /** The main set of documents all facet counts should be relative to */
   protected DocSet docsOrig;
@@ -1083,6 +1087,15 @@ public class SimpleFacets {
 
     parseParams(FacetParams.FACET_RANGE, facetRange);
     String f = facetValue;
+    String methodStr = params.get(FacetParams.FACET_RANGE_METHOD);
+    FacetRangeMethod method = (methodStr==null?FacetRangeMethod.getDefault():FacetRangeMethod.get(methodStr));
+    boolean groupFacet = params.getBool(GroupParams.GROUP_FACET, false);
+    if (groupFacet && method.equals(FacetRangeMethod.DV)) {
+      // the user has explicitly selected the FacetRangeMethod.DV method
+      log.warn("Range facet method '" + FacetRangeMethod.DV + "' is not supported together with '" + 
+              GroupParams.GROUP_FACET + "'. Will use method '" + FacetRangeMethod.FILTER + "' instead");
+      method = FacetRangeMethod.FILTER;
+    }
 
     final SchemaField sf = schema.getField(f);
     final FieldType ft = sf.getType();
@@ -1115,16 +1128,26 @@ public class SimpleFacets {
       }
     } else if (ft instanceof DateRangeField) {
       calc = new DateRangeFieldEndpointCalculator(sf, null);
+      if (method.equals(FacetRangeMethod.DV)) {
+        // the user has explicitly selected the FacetRangeMethod.DV method
+        log.warn("Range facet method '" + FacetRangeMethod.DV + "' is not supported together with field type '" + 
+            DateRangeField.class + "'. Will use method '" + FacetRangeMethod.FILTER + "' instead");
+        method = FacetRangeMethod.FILTER;
+      }
     } else {
       throw new SolrException
           (SolrException.ErrorCode.BAD_REQUEST,
               "Unable to range facet on field:" + sf);
     }
-
-    resOuter.add(key, getFacetRangeCounts(sf, calc));
+    if (method.equals(FacetRangeMethod.DV)) {
+      assert ft instanceof TrieField;
+      resOuter.add(key, getFacetRangeCountsDocValues(sf, calc));
+    } else {
+      resOuter.add(key, getFacetRangeCounts(sf, calc));
+    }
   }
 
-  private <T extends Comparable<T>> NamedList getFacetRangeCounts
+  private <T extends Comparable<T>> NamedList<Object> getFacetRangeCounts
     (final SchemaField sf,
      final RangeEndpointCalculator<T> calc) throws IOException {
     
@@ -1251,6 +1274,183 @@ public class SimpleFacets {
     return res;
   }  
   
+  private <T extends Comparable<T>> NamedList<Object> getFacetRangeCountsDocValues(final SchemaField sf,
+   final RangeEndpointCalculator<T> calc) throws IOException {
+  
+  final String f = sf.getName();
+  final NamedList<Object> res = new SimpleOrderedMap<>();
+  final NamedList<Integer> counts = new NamedList<>();
+  res.add("counts", counts);
+  
+  String globalStartS = required.getFieldParam(f,FacetParams.FACET_RANGE_START);
+  String globalEndS = required.getFieldParam(f,FacetParams.FACET_RANGE_END);
+
+  final T start = calc.getValue(globalStartS);
+  // not final, hardend may change this
+  T end = calc.getValue(globalEndS);
+  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<FacetRangeInclude> include = FacetRangeInclude.parseParam
+    (params.getFieldParams(f,FacetParams.FACET_RANGE_INCLUDE));
+  ArrayList<IntervalFacets.FacetInterval> intervals = new ArrayList<>();
+  
+  final String[] othersP =
+      params.getFieldParams(f,FacetParams.FACET_RANGE_OTHER);
+  
+  boolean includeBefore = false;
+  boolean includeBetween = false;
+  boolean includeAfter = false;
+  
+  if (othersP != null && othersP.length > 0) {
+    Set<FacetRangeOther> others = EnumSet.noneOf(FacetRangeOther.class);
+    // Intervals must be in order (see IntervalFacets.getSortedIntervals), if "BEFORE" or
+    // "BETWEEN" are set, they must be added first
+    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)) {
+      
+      if (others.contains(FacetRangeOther.ALL) || others.contains(FacetRangeOther.BEFORE)) {
+        // We'll add an interval later in this position
+        intervals.add(null);
+        includeBefore = true;
+      }
+      
+      if (others.contains(FacetRangeOther.ALL) || others.contains(FacetRangeOther.BETWEEN)) {
+        // We'll add an interval later in this position
+        intervals.add(null);
+        includeBetween = true;
+      }
+      
+      if (others.contains(FacetRangeOther.ALL) || others.contains(FacetRangeOther.AFTER)) {
+        includeAfter = true;
+      }
+    }
+    
+  }
+  
+  
+  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?)");
+    }
+    if (high.compareTo(low) == 0) {
+      throw new SolrException
+        (SolrException.ErrorCode.BAD_REQUEST,
+         "range facet infinite loop: gap is either zero, or too small relative start/end and caused underflow: " + low + " + " + gap + " = " + high );
+    }
+    
+    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);
+    
+    intervals.add(new IntervalFacets.FacetInterval(sf, lowS, highS, includeLower, includeUpper, lowS));
+    
+    low = high;
+  }
+  
+  if (includeBefore) {
+    // include upper bound if "outer" or if first gap doesn't already include it
+    intervals.set(0, new IntervalFacets.FacetInterval(sf, "*", globalStartS, true, 
+        include.contains(FacetRangeInclude.OUTER) ||
+          (! (include.contains(FacetRangeInclude.LOWER) ||
+            include.contains(FacetRangeInclude.EDGE))), FacetRangeOther.BEFORE.toString()));
+  }
+  
+  if (includeBetween) {
+    int intervalIndex = (includeBefore?1:0);
+    intervals.set(intervalIndex, new IntervalFacets.FacetInterval(sf, globalStartS, calc.formatValue(end), 
+        include.contains(FacetRangeInclude.LOWER) ||
+        include.contains(FacetRangeInclude.EDGE), 
+        include.contains(FacetRangeInclude.UPPER) ||
+        include.contains(FacetRangeInclude.EDGE), 
+        FacetRangeOther.BETWEEN.toString()));
+   }
+  
+  if (includeAfter) {
+    // include lower bound if "outer" or if last gap doesn't already include it
+    intervals.add(new IntervalFacets.FacetInterval(sf, calc.formatValue(end), "*", 
+        (include.contains(FacetRangeInclude.OUTER) ||
+        (! (include.contains(FacetRangeInclude.UPPER) ||
+            include.contains(FacetRangeInclude.EDGE)))),  
+       false, FacetRangeOther.AFTER.toString()));
+  }
+  
+  IntervalFacets.FacetInterval[] intervalsArray = intervals.toArray(new IntervalFacets.FacetInterval[intervals.size()]);
+  // don't use the ArrayList anymore
+  intervals = null;
+  
+  new IntervalFacets(sf, searcher, docs, intervalsArray);
+  
+  int intervalIndex = 0;
+  int lastIntervalIndex = intervalsArray.length - 1;
+  // if the user requested "BEFORE", it will be the first of the intervals. Needs to be added to the 
+  // response named list instead of with the counts
+  if (includeBefore) {
+    res.add(intervalsArray[intervalIndex].getKey(), intervalsArray[intervalIndex].getCount());
+    intervalIndex++;
+  }
+  
+  // if the user requested "BETWEEN", it will be the first or second of the intervals (depending on if 
+  // "BEFORE" was also requested). Needs to be added to the response named list instead of with the counts
+  if (includeBetween) {
+    res.add(intervalsArray[intervalIndex].getKey(), intervalsArray[intervalIndex].getCount());
+    intervalIndex++;
+  }
+  
+  // if the user requested "AFTER", it will be the last of the intervals.
+  // Needs to be added to the response named list instead of with the counts
+  if (includeAfter) {
+    res.add(intervalsArray[lastIntervalIndex].getKey(), intervalsArray[lastIntervalIndex].getCount());
+    lastIntervalIndex--;
+  }
+  // now add all other intervals to the counts NL
+  while (intervalIndex <= lastIntervalIndex) {
+    FacetInterval interval = intervalsArray[intervalIndex];
+    if (interval.getCount() >= minCount) {
+      counts.add(interval.getKey(), interval.getCount());
+    }
+    intervalIndex++;
+  }
+  
+  res.add("start", start);
+  res.add("end", end);
+  return res;
+}  
+  
   /**
    * Macro for getting the numDocs of range over docs
    * @see SolrIndexSearcher#numDocs

Modified: lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/schema.xml
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/schema.xml?rev=1675706&r1=1675705&r2=1675706&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/schema.xml (original)
+++ lucene/dev/trunk/solr/core/src/test-files/solr/collection1/conf/schema.xml Thu Apr 23 19:27:34 2015
@@ -640,6 +640,15 @@
    <dynamicField name="*_ancestor"  type="ancestor_path" indexed="true" stored="true" omitNorms="true" multiValued="true" />
 
    <dynamicField name="*_sev_enum" type="severityType" indexed="true" stored="true" docValues="true" multiValued="true" />
+   
+   <!-- With DocValues=true -->
+   <dynamicField name="*_i_dv"  type="int"    indexed="true"  stored="true" docValues="true"/>
+   <dynamicField name="*_l_dv"  type="long"    indexed="true"  stored="true" docValues="true"/>
+   <dynamicField name="*_f_dv"  type="float"    indexed="true"  stored="true" docValues="true"/>
+   <dynamicField name="*_d_dv"  type="double"    indexed="true"  stored="true" docValues="true"/>
+   <dynamicField name="*_dt_dv"  type="date"    indexed="true"  stored="true" docValues="true"/>
+   <dynamicField name="*_f1_dv"  type="float"    indexed="true"  stored="true" docValues="true" multiValued="false"/>
+   
  </fields>
 
  <defaultSearchField>text</defaultSearchField>
@@ -663,6 +672,13 @@
 
 	 <copyField source="id"            dest="range_facet_l"/>
 	 <copyField source="range_facet_f" dest="range_facet_d"/>
+	 <copyField source="range_facet_f1" dest="range_facet_f1_dv"/>
+	 
+	 <copyField source="id"            dest="range_facet_l_dv"/>
+	 <copyField source="id"            dest="range_facet_i_dv"/>
+	 <copyField source="range_facet_f" dest="range_facet_f_dv"/>
+	 <copyField source="range_facet_f" dest="range_facet_d_dv"/>
+	 <copyField source="bday" dest="range_facet_dt_dv"/>
 
    <!-- dynamic destination -->
    <copyField source="*_dynamic" dest="dynamic_*"/>

Modified: lucene/dev/trunk/solr/core/src/test/org/apache/solr/TestDistributedSearch.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test/org/apache/solr/TestDistributedSearch.java?rev=1675706&r1=1675705&r2=1675706&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/test/org/apache/solr/TestDistributedSearch.java (original)
+++ lucene/dev/trunk/solr/core/src/test/org/apache/solr/TestDistributedSearch.java Thu Apr 23 19:27:34 2015
@@ -46,6 +46,7 @@ import org.apache.solr.common.params.Mod
 import org.apache.solr.common.params.ShardParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.params.StatsParams;
+import org.apache.solr.common.params.FacetParams.FacetRangeMethod;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.handler.component.ShardResponse;
 import org.apache.solr.handler.component.StatsComponentTest.StatSetCombinations;
@@ -246,7 +247,17 @@ public class TestDistributedSearch exten
           "facet.range",tlong,
           "facet.range.start",200, 
           "facet.range.gap",100, 
-          "facet.range.end",900);
+          "facet.range.end",900,
+          "facet.range.method", FacetRangeMethod.FILTER);
+    
+    // simple range facet on one field using dv method
+    query("q",facetQuery, "rows",100, "facet","true", 
+          "facet.range",tlong,
+          "facet.range",tlong,
+          "facet.range.start",200, 
+          "facet.range.gap",100, 
+          "facet.range.end",900,
+          "facet.range.method", FacetRangeMethod.DV);
 
     // range facet on multiple fields
     query("q",facetQuery, "rows",100, "facet","true", 
@@ -257,7 +268,9 @@ public class TestDistributedSearch exten
           "facet.range.end",900,
           "facet.range.start",200, 
           "facet.range.gap",100, 
-          "f."+tlong+".facet.range.end",900);
+          "f."+tlong+".facet.range.end",900,
+          "f."+i1+".facet.range.method", FacetRangeMethod.FILTER,
+          "f."+tlong+".facet.range.method", FacetRangeMethod.DV);
     
     // range facet with "other" param
     QueryResponse response = query("q",facetQuery, "rows",100, "facet","true", 

Modified: lucene/dev/trunk/solr/core/src/test/org/apache/solr/request/SimpleFacetsTest.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test/org/apache/solr/request/SimpleFacetsTest.java?rev=1675706&r1=1675705&r2=1675706&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/test/org/apache/solr/request/SimpleFacetsTest.java (original)
+++ lucene/dev/trunk/solr/core/src/test/org/apache/solr/request/SimpleFacetsTest.java Thu Apr 23 19:27:34 2015
@@ -17,21 +17,31 @@
 
 package org.apache.solr.request;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
 import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.common.params.FacetParams;
+import org.apache.solr.common.params.FacetParams.FacetRangeInclude;
+import org.apache.solr.common.params.FacetParams.FacetRangeMethod;
+import org.apache.solr.common.params.FacetParams.FacetRangeOther;
 import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.response.SolrQueryResponse;
 import org.apache.solr.schema.SchemaField;
+import org.apache.solr.schema.TrieDateField;
 import org.apache.solr.util.TimeZoneUtils;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.noggit.ObjectBuilder;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
 
 public class SimpleFacetsTest extends SolrTestCaseJ4 {
 
@@ -98,36 +108,60 @@ public class SimpleFacetsTest extends So
 
   static void indexSimpleFacetCounts() {
     add_doc("id", "42", 
-            "range_facet_f", "35.3", 
+            "range_facet_f", "35.3",
+            "range_facet_f1", "35.3",
             "trait_s", "Tool", "trait_s", "Obnoxious",
             "name", "Zapp Brannigan",
-             "foo_s","A", "foo_s","B"
+             "foo_s","A", "foo_s","B",
+             "range_facet_mv_f", "1.0",
+             "range_facet_mv_f", "2.5",
+             "range_facet_mv_f", "3.7",
+             "range_facet_mv_f", "3.3"
     );
     add_doc("id", "43" ,
-            "range_facet_f", "28.789", 
+            "range_facet_f", "28.789",
+            "range_facet_f1", "28.789",
             "title", "Democratic Order of Planets",
-            "foo_s","A", "foo_s","B"
+            "foo_s","A", "foo_s","B",
+            "range_facet_mv_f", "3.0",
+            "range_facet_mv_f", "7.5",
+            "range_facet_mv_f", "12.0"
     );
     add_doc("id", "44", 
-            "range_facet_f", "15.97", 
+            "range_facet_f", "15.97",
+            "range_facet_f1", "15.97",
             "trait_s", "Tool",
             "name", "The Zapper",
-            "foo_s","A", "foo_s","B", "foo_s","C"
+            "foo_s","A", "foo_s","B", "foo_s","C",
+            "range_facet_mv_f", "0.0",
+            "range_facet_mv_f", "5",
+            "range_facet_mv_f", "74"
     );
     add_doc("id", "45", 
-            "range_facet_f", "30.0", 
+            "range_facet_f", "30.0",
+            "range_facet_f1", "30.0",
             "trait_s", "Chauvinist",
             "title", "25 star General",
-            "foo_s","A", "foo_s","B"
+            "foo_s","A", "foo_s","B",
+            "range_facet_mv_f_f", "12.0",
+            "range_facet_mv_f", "212.452",
+            "range_facet_mv_f", "32.77",
+            "range_facet_mv_f", "0.123"
     );
     add_doc("id", "46", 
-            "range_facet_f", "20.0", 
+            "range_facet_f", "20.0",
+            "range_facet_f1", "20.0",
             "trait_s", "Obnoxious",
             "subject", "Defeated the pacifists of the Gandhi nebula",
-            "foo_s","A", "foo_s","B"
+            "foo_s","A", "foo_s","B",
+            "range_facet_mv_f", "123.0",
+            "range_facet_mv_f", "2.0",
+            "range_facet_mv_f", "7.3",
+            "range_facet_mv_f", "0.123"
     );
     add_doc("id", "47", 
             "range_facet_f", "28.62", 
+            "range_facet_f1", "28.62", 
             "trait_s", "Pig",
             "text", "line up and fly directly at the enemy death cannons, clogging them with wreckage!",
             "zerolen_s","",
@@ -301,6 +335,31 @@ public class SimpleFacetsTest extends So
         "//lst[@name='facet_ranges']/lst[@name='duration_i1']/lst[@name='counts']/int[@name='9'][.='0']",
         "//lst[@name='facet_ranges']/lst[@name='duration_i1']/lst[@name='counts']/int[@name='10'][.='2']"
     );
+    
+    // repeat the same query using DV method. This is not supported and the query should use filter method instead
+    assertQ(
+        req(
+            "q", "*:*",
+            "fq", "id:[2000 TO 2004]",
+            "fq", "{!tag=dus}airport_s1:dus",
+            "group", "true",
+            "group.facet", "true",
+            "group.field", "hotel_s1",
+            "facet", "true",
+            "facet.limit", facetLimit,
+            "facet.range", "{!ex=dus}duration_i1",
+            "facet.range.start", "5",
+            "facet.range.end", "11",
+            "facet.range.gap", "1",
+            "facet.range.method", FacetRangeMethod.DV.toString()
+        ),
+        "//lst[@name='facet_ranges']/lst[@name='duration_i1']/lst[@name='counts']/int[@name='5'][.='2']",
+        "//lst[@name='facet_ranges']/lst[@name='duration_i1']/lst[@name='counts']/int[@name='6'][.='0']",
+        "//lst[@name='facet_ranges']/lst[@name='duration_i1']/lst[@name='counts']/int[@name='7'][.='0']",
+        "//lst[@name='facet_ranges']/lst[@name='duration_i1']/lst[@name='counts']/int[@name='8'][.='0']",
+        "//lst[@name='facet_ranges']/lst[@name='duration_i1']/lst[@name='counts']/int[@name='9'][.='0']",
+        "//lst[@name='facet_ranges']/lst[@name='duration_i1']/lst[@name='counts']/int[@name='10'][.='2']"
+    );
   }
 
   @Test
@@ -726,21 +785,32 @@ public class SimpleFacetsTest extends So
 
   @Test
   public void testTrieDateFacets() {
-    helpTestDateFacets("bday", false);
+    helpTestDateFacets("bday", false, FacetRangeMethod.FILTER);
   }
 
   @Test
   public void testTrieDateRangeFacets() {
-    helpTestDateFacets("bday", true);
+    helpTestDateFacets("bday", true, FacetRangeMethod.FILTER);
+  }
+  
+  @Test
+  public void testTrieDateFacetsDocValues() {
+    helpTestDateFacets("bday", false, FacetRangeMethod.DV);
+  }
+
+  @Test
+  public void testTrieDateRangeFacetsDocValues() {
+    helpTestDateFacets("bday", true, FacetRangeMethod.DV);
   }
 
   @Test
   public void testDateRangeFieldFacets() {
-    helpTestDateFacets("bday_drf", true);
+    helpTestDateFacets("bday_drf", true, FacetRangeMethod.FILTER);
   }
 
   private void helpTestDateFacets(final String fieldName, 
-                                  final boolean rangeMode) {
+                                  final boolean rangeMode, 
+                                  final FacetRangeMethod rangeFacetMethod) {
     final String p = rangeMode ? "facet.range" : "facet.date";
     final String b = rangeMode ? "facet_ranges" : "facet_dates";
     final String f = fieldName;
@@ -763,6 +833,7 @@ public class SimpleFacetsTest extends So
                 ,p+".end",   "1976-07-01T00:00:00.000Z+1MONTH"
                 ,p+".gap",   "+1DAY"
                 ,p+".other", "all"
+                ,p+".method", rangeFacetMethod.toString()  //This only applies to range faceting, won't be use for date faceting
                 )
             ,"*[count("+pre+"/int)="+(rangeMode ? 31 : 34)+"]"
             ,pre+"/int[@name='1976-07-01T00:00:00Z'][.='0']"
@@ -1351,13 +1422,27 @@ public class SimpleFacetsTest extends So
   public void testNumericRangeFacetsTrieDouble() {
     helpTestFractionalNumberRangeFacets("range_facet_d");
   }
+  
+  @Test
+  public void testNumericRangeFacetsTrieFloatDocValues() {
+    helpTestFractionalNumberRangeFacets("range_facet_f", FacetRangeMethod.DV);
+  }
+  @Test
+  public void testNumericRangeFacetsTrieDoubleDocValues() {
+    helpTestFractionalNumberRangeFacets("range_facet_d", FacetRangeMethod.DV);
+  }
 
   @Test
   public void testNumericRangeFacetsOverflowTrieDouble() {
-    helpTestNumericRangeFacetsDoubleOverflow("range_facet_d");
+    helpTestNumericRangeFacetsDoubleOverflow("range_facet_d", FacetRangeMethod.FILTER);
+  }
+  
+  @Test
+  public void testNumericRangeFacetsOverflowTrieDoubleDocValue() {
+    helpTestNumericRangeFacetsDoubleOverflow("range_facet_d", FacetRangeMethod.DV);
   }
 
-  private void helpTestNumericRangeFacetsDoubleOverflow(final String fieldName) {
+  private void helpTestNumericRangeFacetsDoubleOverflow(final String fieldName, final FacetRangeMethod method) {
     final String f = fieldName;
     final String pre = "//lst[@name='facet_ranges']/lst[@name='"+f+"']/lst[@name='counts']";
     final String meta = pre + "/../";
@@ -1372,6 +1457,7 @@ public class SimpleFacetsTest extends So
                 ,"rows", "0"
                 ,"facet", "true"
                 ,"facet.range", f
+                ,"facet.range.method", method.toString()
                 ,"facet.range.start", start
                 ,"facet.range.end",   end
                 ,"facet.range.gap",   gap
@@ -1388,7 +1474,11 @@ public class SimpleFacetsTest extends So
             ,meta+"/int[@name='between'][.='6']"
             );
   }
-   private void helpTestFractionalNumberRangeFacets(final String fieldName) {
+  
+  private void helpTestFractionalNumberRangeFacets(final String fieldName) {
+    helpTestFractionalNumberRangeFacets(fieldName, FacetRangeMethod.FILTER);
+  }
+   private void helpTestFractionalNumberRangeFacets(final String fieldName, FacetRangeMethod method) {
 
     final String f = fieldName;
     final String pre = "//lst[@name='facet_ranges']/lst[@name='"+f+"']/lst[@name='counts']";
@@ -1399,6 +1489,7 @@ public class SimpleFacetsTest extends So
                 ,"rows", "0"
                 ,"facet", "true"
                 ,"facet.range", f
+                ,"facet.range.method", method.toString()
                 ,"facet.range.start", "10"
                 ,"facet.range.end",   "50"
                 ,"facet.range.gap",   "10"
@@ -1421,6 +1512,7 @@ public class SimpleFacetsTest extends So
                 ,"rows", "0"
                 ,"facet", "true"
                 ,"facet.range", f
+                ,"facet.range.method", method.toString()
                 ,"facet.range.start", "10"
                 ,"facet.range.end",   "50"
                 ,"facet.range.gap",   "10"
@@ -1443,6 +1535,7 @@ public class SimpleFacetsTest extends So
                 ,"rows", "0"
                 ,"facet", "true"
                 ,"facet.range", f
+                ,"facet.range.method", method.toString()
                 ,"facet.range.start", "10"
                 ,"facet.range.end",   "50"
                 ,"facet.range.gap",   "10"
@@ -1466,6 +1559,7 @@ public class SimpleFacetsTest extends So
                 ,"rows", "0"
                 ,"facet", "true"
                 ,"facet.range", f
+                ,"facet.range.method", method.toString()
                 ,"facet.range.start", "20"
                 ,"facet.range.end",   "50"
                 ,"facet.range.gap",   "10"
@@ -1488,6 +1582,7 @@ public class SimpleFacetsTest extends So
                 ,"rows", "0"
                 ,"facet", "true"
                 ,"facet.range", f
+                ,"facet.range.method", method.toString()
                 ,"facet.range.start", "10"
                 ,"facet.range.end",   "30"
                 ,"facet.range.gap",   "10"
@@ -1509,6 +1604,7 @@ public class SimpleFacetsTest extends So
                 ,"rows", "0"
                 ,"facet", "true"
                 ,"facet.range", f
+                ,"facet.range.method", method.toString()
                 ,"facet.range.start", "10"
                 ,"facet.range.end",   "30"
                 ,"facet.range.gap",   "10"
@@ -1530,6 +1626,7 @@ public class SimpleFacetsTest extends So
                 ,"rows", "0"
                 ,"facet", "true"
                 ,"facet.range", f
+                ,"facet.range.method", method.toString()
                 ,"facet.range.start", "20"
                 ,"facet.range.end",   "40"
                 ,"facet.range.gap",   "10"
@@ -1551,6 +1648,7 @@ public class SimpleFacetsTest extends So
                 ,"rows", "0"
                 ,"facet", "true"
                 ,"facet.range", f
+                ,"facet.range.method", method.toString()
                 ,"facet.range.start", "20"
                 ,"facet.range.end",   "35.3"
                 ,"facet.range.gap",   "10"
@@ -1574,6 +1672,7 @@ public class SimpleFacetsTest extends So
                 ,"rows", "0"
                 ,"facet", "true"
                 ,"facet.range", f
+                ,"facet.range.method", method.toString()
                 ,"facet.range.start", "20"
                 ,"facet.range.end",   "35.3"
                 ,"facet.range.gap",   "10"
@@ -1599,13 +1698,28 @@ public class SimpleFacetsTest extends So
   public void testNumericRangeFacetsTrieLong() {
     helpTestWholeNumberRangeFacets("range_facet_l");
   }
+  
+  @Test
+  public void testNumericRangeFacetsTrieIntDocValues() {
+    helpTestWholeNumberRangeFacets("id", FacetRangeMethod.DV);
+  }
+  
+  @Test
+  public void testNumericRangeFacetsTrieLongDocValues() {
+    helpTestWholeNumberRangeFacets("range_facet_l", FacetRangeMethod.DV);
+  }
 
   @Test
   public void testNumericRangeFacetsOverflowTrieLong() {
-    helpTestNumericRangeFacetsLongOverflow("range_facet_l");
+    helpTestNumericRangeFacetsLongOverflow("range_facet_l", FacetRangeMethod.FILTER);
+  }
+  
+  @Test
+  public void testNumericRangeFacetsOverflowTrieLongDocValues() {
+    helpTestNumericRangeFacetsLongOverflow("range_facet_l", FacetRangeMethod.DV);
   }
 
-  private void helpTestNumericRangeFacetsLongOverflow(final String fieldName) {
+  private void helpTestNumericRangeFacetsLongOverflow(final String fieldName, final FacetRangeMethod method) {
     final String f = fieldName;
     final String pre = "//lst[@name='facet_ranges']/lst[@name='"+f+"']/lst[@name='counts']";
     final String meta = pre + "/../";
@@ -1620,6 +1734,7 @@ public class SimpleFacetsTest extends So
                 ,"rows", "0"
                 ,"facet", "true"
                 ,"facet.range", f
+                ,"facet.range.method", method.toString()
                 ,"facet.range.start", start
                 ,"facet.range.end",   end
                 ,"facet.range.gap",   gap
@@ -1636,7 +1751,12 @@ public class SimpleFacetsTest extends So
             ,meta+"/int[@name='between'][.='6']"
             );
   }
+  
   private void helpTestWholeNumberRangeFacets(final String fieldName) {
+    helpTestWholeNumberRangeFacets(fieldName, FacetRangeMethod.FILTER);
+  }
+  
+  private void helpTestWholeNumberRangeFacets(final String fieldName, FacetRangeMethod method) {
 
     // the float test covers a lot of the weird edge cases
     // here we just need some basic sanity checking of the parsing
@@ -1650,6 +1770,7 @@ public class SimpleFacetsTest extends So
                 ,"rows", "0"
                 ,"facet", "true"
                 ,"facet.range", f
+                ,"facet.range.method", method.toString()
                 ,"facet.range.start", "35"
                 ,"facet.range.end",   "50"
                 ,"facet.range.gap",   "5"
@@ -1671,6 +1792,7 @@ public class SimpleFacetsTest extends So
                 ,"rows", "0"
                 ,"facet", "true"
                 ,"facet.range", f
+                ,"facet.range.method", method.toString()
                 ,"facet.range.start", "35"
                 ,"facet.range.end",   "50"
                 ,"facet.range.gap",   "5"
@@ -2277,5 +2399,604 @@ public class SimpleFacetsTest extends So
   public void testContainsIgnoreCase() {
     assertTrue(SimpleFacets.contains("FooBar", "bar", true));
   }
+  
+  public void testRangeQueryHardEndParamFilter() {
+    doTestRangeQueryHardEndParam("range_facet_l", FacetRangeMethod.FILTER);
+  }
+  
+  public void testRangeQueryHardEndParamDv() {
+    doTestRangeQueryHardEndParam("range_facet_l", FacetRangeMethod.DV);
+  }
+  
+  private void doTestRangeQueryHardEndParam(String field, FacetRangeMethod method) {
+    assertQ("Test facet.range.hardend",
+        req("q", "id:[42 TO 47]"
+                ,"facet","true"
+                ,"fl","id," + field
+                ,"facet.range", field
+                ,"facet.range.method", method.toString()
+                ,"facet.range.start","43"
+                ,"facet.range.end","45"
+                ,"facet.range.gap","5"
+                ,"facet.range.hardend", "false"
+                ,"facet.range.other", "after"
+        )
+        ,"*[count(//lst[@name='facet_ranges']/lst)=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts']/int)=1]"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts']/int[@name='43'][.='5']"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/long[@name='end'][.='48']"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='after'][.='0']"
+    );
+    
+    assertQ("Test facet.range.hardend",
+        req("q", "id:[42 TO 47]"
+                ,"facet","true"
+                ,"fl","id," + field
+                ,"facet.range", field
+                ,"facet.range.method", method.toString()
+                ,"facet.range.start","43"
+                ,"facet.range.end","45"
+                ,"facet.range.gap","5"
+                ,"facet.range.hardend", "true"
+                ,"facet.range.other", "after"
+        )
+        ,"*[count(//lst[@name='facet_ranges']/lst)=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts']/int)=1]"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts']/int[@name='43'][.='2']"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/long[@name='end'][.='45']"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='after'][.='3']"
+    );
+    
+  }
+  
+  public void testRangeQueryOtherParamFilter() {
+    doTestRangeQueryOtherParam("range_facet_l", FacetRangeMethod.FILTER);
+  }
+  
+  public void testRangeQueryOtherParamDv() {
+    doTestRangeQueryOtherParam("range_facet_l", FacetRangeMethod.DV);
+  }
+  
+  private void doTestRangeQueryOtherParam(String field, FacetRangeMethod method) {
+    
+    assertQ("Test facet.range.other",
+        req("q", "id:[42 TO 47]"
+                ,"facet","true"
+                ,"fl","id," + field
+                ,"facet.range", field
+                ,"facet.range.method", method.toString()
+                ,"facet.range.start","43"
+                ,"facet.range.end","45"
+                ,"facet.range.gap","1"
+                ,"facet.range.other", FacetRangeOther.BEFORE.toString()
+        )
+        ,"*[count(//lst[@name='facet_ranges']/lst)=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts']/int)=2]"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='before'][.='1']"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='after'])=0]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='between'])=0]"
+    );
+    
+    assertQ("Test facet.range.other",
+        req("q", "id:[42 TO 47]"
+                ,"facet","true"
+                ,"fl","id," + field
+                ,"facet.range", field
+                ,"facet.range.method", method.toString()
+                ,"facet.range.start","43"
+                ,"facet.range.end","45"
+                ,"facet.range.gap","1"
+                ,"facet.range.other", FacetRangeOther.AFTER.toString()
+        )
+        ,"*[count(//lst[@name='facet_ranges']/lst)=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts']/int)=2]"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='after'][.='3']"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='between'])=0]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='before'])=0]"
+    );
+    
+    assertQ("Test facet.range.other",
+        req("q", "id:[42 TO 47]"
+                ,"facet","true"
+                ,"fl","id," + field
+                ,"facet.range", field
+                ,"facet.range.method", method.toString()
+                ,"facet.range.start","43"
+                ,"facet.range.end","45"
+                ,"facet.range.gap","1"
+                ,"facet.range.other",FacetRangeOther.BETWEEN.toString()
+        )
+        ,"*[count(//lst[@name='facet_ranges']/lst)=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts']/int)=2]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='after'])=0]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='before'])=0]"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='between'][.='2']"
+    );
+    
+    assertQ("Test facet.range.other",
+        req("q", "id:[42 TO 47]"
+                ,"facet","true"
+                ,"fl","id," + field
+                ,"facet.range", field
+                ,"facet.range.method", method.toString()
+                ,"facet.range.start","43"
+                ,"facet.range.end","45"
+                ,"facet.range.gap","1"
+                ,"facet.range.other",FacetRangeOther.NONE.toString()
+        )
+        ,"*[count(//lst[@name='facet_ranges']/lst)=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts']/int)=2]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='after'])=0]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='before'])=0]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='between'])=0]"
+    );
+    
+    assertQ("Test facet.range.other",
+        req("q", "id:[42 TO 47]"
+                ,"facet","true"
+                ,"fl","id," + field
+                ,"facet.range", field
+                ,"facet.range.method", method.toString()
+                ,"facet.range.start","43"
+                ,"facet.range.end","45"
+                ,"facet.range.gap","1"
+                ,"facet.range.other",FacetRangeOther.BEFORE.toString()
+                ,"facet.range.other",FacetRangeOther.AFTER.toString()
+        )
+        ,"*[count(//lst[@name='facet_ranges']/lst)=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts']/int)=2]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='between'])=0]"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='after'][.='3']"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='before'][.='1']"
+    );
+    
+    assertQ("Test facet.range.other",
+        req("q", "id:[42 TO 47]"
+                ,"facet","true"
+                ,"fl","id," + field
+                ,"facet.range", field
+                ,"facet.range.method", method.toString()
+                ,"facet.range.start","43"
+                ,"facet.range.end","45"
+                ,"facet.range.gap","1"
+                ,"facet.range.other",FacetRangeOther.BEFORE.toString()
+                ,"facet.range.other",FacetRangeOther.AFTER.toString()
+                ,"facet.range.other",FacetRangeOther.NONE.toString()
+        )
+        ,"*[count(//lst[@name='facet_ranges']/lst)=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts']/int)=2]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='between'])=0]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='after'])=0]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='before'])=0]"
+    );
+    
+    assertQ("Test facet.range.other",
+        req("q", "id:[42 TO 47]"
+                ,"facet","true"
+                ,"fl","id," + field
+                ,"facet.range", field
+                ,"facet.range.method", method.toString()
+                ,"facet.range.start","43"
+                ,"facet.range.end","45"
+                ,"facet.range.gap","1"
+                ,"facet.range.other",FacetRangeOther.ALL.toString()
+                ,"facet.range.include", FacetRangeInclude.LOWER.toString()
+        )
+        ,"*[count(//lst[@name='facet_ranges']/lst)=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts']/int)=2]"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='between'][.='2']"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='after'][.='3']"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='before'][.='1']"
+    );
+    
+    assertQ("Test facet.range.other",
+        req("q", "id:[42 TO 47]"
+                ,"facet","true"
+                ,"fl","id," + field
+                ,"facet.range", field
+                ,"facet.range.method", method.toString()
+                ,"facet.range.start","43"
+                ,"facet.range.end","45"
+                ,"facet.range.gap","1"
+                ,"facet.range.other",FacetRangeOther.ALL.toString()
+                ,"facet.range.include", FacetRangeInclude.UPPER.toString()
+        )
+        ,"*[count(//lst[@name='facet_ranges']/lst)=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts']/int)=2]"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='between'][.='2']"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='after'][.='2']"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='before'][.='2']"
+    );
+    
+    assertQ("Test facet.range.other",
+        req("q", "id:[42 TO 47]"
+                ,"facet","true"
+                ,"fl","id," + field
+                ,"facet.range", field
+                ,"facet.range.method", method.toString()
+                ,"facet.range.start","43"
+                ,"facet.range.end","45"
+                ,"facet.range.gap","1"
+                ,"facet.range.other",FacetRangeOther.ALL.toString()
+                ,"facet.range.include", FacetRangeInclude.EDGE.toString()
+        )
+        ,"*[count(//lst[@name='facet_ranges']/lst)=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts']/int)=2]"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='between'][.='3']"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='after'][.='2']"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='before'][.='1']"
+    );
+    
+    assertQ("Test facet.range.other",
+        req("q", "id:[42 TO 47]"
+                ,"facet","true"
+                ,"fl","id," + field
+                ,"facet.range", field
+                ,"facet.range.method", method.toString()
+                ,"facet.range.start","43"
+                ,"facet.range.end","45"
+                ,"facet.range.gap","1"
+                ,"facet.range.other", FacetRangeOther.ALL.toString()
+                ,"facet.range.include", FacetRangeInclude.OUTER.toString()
+        )
+        ,"*[count(//lst[@name='facet_ranges']/lst)=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts']/int)=2]"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='between'][.='1']"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='after'][.='3']"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='before'][.='2']"
+    );
+    
+    assertQ("Test facet.range.other",
+        req("q", "id:[12345 TO 12345]"
+                ,"facet","true"
+                ,"fl","id," + field
+                ,"facet.range", field
+                ,"facet.range.method", method.toString()
+                ,"facet.range.start","43"
+                ,"facet.range.end","45"
+                ,"facet.range.gap","1"
+                ,"facet.range.other", FacetRangeOther.ALL.toString()
+        )
+        ,"*[count(//lst[@name='facet_ranges']/lst)=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts'])=1]"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='between'][.='0']"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='after'][.='0']"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='before'][.='0']"
+    );
+    
+    assertQ("Test facet.range.other",
+        req("q", "id:[42 TO 47]"
+                ,"facet","true"
+                ,"fl","id," + field
+                ,"facet.range", field
+                ,"facet.range.method", method.toString()
+                ,"facet.range.start","43"
+                ,"facet.range.end","45"
+                ,"facet.range.gap","10"
+                ,"facet.range.other", FacetRangeOther.ALL.toString()
+        )
+        ,"*[count(//lst[@name='facet_ranges']/lst)=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts'])=1]"
+        ,"*[count(//lst[@name='facet_ranges']/lst[@name='" + field + "']/lst[@name='counts']/int)=1]"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='between'][.='5']"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='after'][.='0']"
+        ,"//lst[@name='facet_ranges']/lst[@name='" + field + "']/int[@name='before'][.='1']"
+    );
+    
+  }
+  
+  public void testRangeFacetingBadRequest() {
+    String field = "range_facet_l";
+    ignoreException(".");
+    try {
+      for (FacetRangeMethod method:FacetRangeMethod.values()) {
+        assertQEx("Test facet.range bad requests",
+            "range facet 'end' comes before 'start'",
+            req("q", "*:*"
+                    ,"facet","true"
+                    ,"facet.range", field
+                    ,"facet.range.method", method.toString()
+                    ,"facet.range.start","45"
+                    ,"facet.range.end","43"
+                    ,"facet.range.gap","10"
+            ),
+            ErrorCode.BAD_REQUEST
+        );
+        
+        assertQEx("Test facet.range bad requests",
+            "range facet infinite loop (is gap negative? did the math overflow?)",
+            req("q", "*:*"
+                    ,"facet","true"
+                    ,"facet.range", field
+                    ,"facet.range.method", method.toString()
+                    ,"facet.range.start","43"
+                    ,"facet.range.end","45"
+                    ,"facet.range.gap","-1"
+            ),
+            ErrorCode.BAD_REQUEST
+        );
+        
+        assertQEx("Test facet.range bad requests",
+            "range facet infinite loop: gap is either zero, or too small relative start/end and caused underflow",
+            req("q", "*:*"
+                    ,"facet","true"
+                    ,"facet.range", field
+                    ,"facet.range.method", method.toString()
+                    ,"facet.range.start","43"
+                    ,"facet.range.end","45"
+                    ,"facet.range.gap","0"
+            ),
+            ErrorCode.BAD_REQUEST
+        );
+        
+        assertQEx("Test facet.range bad requests",
+            "Missing required parameter",
+            req("q", "*:*"
+                    ,"facet","true"
+                    ,"facet.range", field
+                    ,"facet.range.method", method.toString()
+                    ,"facet.range.end","45"
+                    ,"facet.range.gap","5"
+            ),
+            ErrorCode.BAD_REQUEST
+        );
+        assertQEx("Test facet.range bad requests",
+            "Missing required parameter",
+            req("q", "*:*"
+                    ,"facet","true"
+                    ,"facet.range", field
+                    ,"facet.range.method", method.toString()
+                    ,"facet.range.start","43"
+                    ,"facet.range.gap","5"
+            ),
+            ErrorCode.BAD_REQUEST
+        );
+        assertQEx("Test facet.range bad requests",
+            "Missing required parameter",
+            req("q", "*:*"
+                    ,"facet","true"
+                    ,"facet.range", field
+                    ,"facet.range.method", method.toString()
+                    ,"facet.range.start","43"
+                    ,"facet.range.end","45"
+            ),
+            ErrorCode.BAD_REQUEST
+        );
+        assertQEx("Test facet.range bad requests",
+            "Unable to range facet on field",
+            req("q", "*:*"
+                    ,"facet","true"
+                    ,"facet.range", "contains_s1"
+                    ,"facet.range.method", method.toString()
+                    ,"facet.range.start","43"
+                    ,"facet.range.end","45"
+                    ,"facet.range.gap","5"
+            ),
+            ErrorCode.BAD_REQUEST
+        );
+        assertQEx("Test facet.range bad requests",
+            "foo is not a valid method for range faceting",
+            req("q", "*:*"
+                    ,"facet","true"
+                    ,"facet.range", field
+                    ,"facet.range.method", "foo"
+                    ,"facet.range.start","43"
+                    ,"facet.range.end","45"
+                    ,"facet.range.gap","5"
+            ),
+            ErrorCode.BAD_REQUEST
+        );
+        
+        assertQEx("Test facet.range bad requests",
+            "foo is not a valid type of for range 'include' information",
+            req("q", "*:*"
+                    ,"facet","true"
+                    ,"facet.range", field
+                    ,"facet.range.method", method.toString()
+                    ,"facet.range.start","43"
+                    ,"facet.range.end","45"
+                    ,"facet.range.gap","5"
+                    ,"facet.range.include", "foo"
+            ),
+            ErrorCode.BAD_REQUEST
+        );
+      }
+    } finally {
+      resetExceptionIgnores();
+    }
+    
+  }
+  
+  @SuppressWarnings("unchecked")
+  public void testRangeFacetFilterVsDocValuesRandom() throws Exception {
+    for (int i = 0; i < atLeast(100); i++) {
+      ModifiableSolrParams params = null;
+      int fieldType = i%3;
+      switch (fieldType) {
+        case 0: params = getRandomParamsDate(); break;
+        case 1: params = getRandomParamsInt(); break;
+        case 2: params = getRandomParamsFloat(); break;
+      }
+      String field = params.get("facet.range");
+      params.add("q", getRandomQuery());
+      
+      
+      params.add("facet", "true");
+      if (random().nextBoolean()) {
+        params.add("facet.range.method", FacetRangeMethod.FILTER.toString());
+      }
+      
+      NamedList<Object> rangeFacetsFilter;
+      NamedList<Object> rangeFacetsDv;
+      
+      SolrQueryRequest req = req(params);
+      log.info("Using Params: " + params);
+      try {
+        SolrQueryResponse rsp = h.queryAndResponse("standard", req);
+        rangeFacetsFilter = (NamedList<Object>) ((NamedList<Object>) rsp.getValues().get("facet_counts")).get("facet_ranges");
+      } finally {
+        req.close();
+      }
+      params.add("facet.range.method", FacetRangeMethod.DV.toString());
+      req = req(params);
+      try {
+        SolrQueryResponse rsp = h.queryAndResponse("standard", req);
+        rangeFacetsDv = (NamedList<Object>) ((NamedList<Object>) rsp.getValues().get("facet_counts")).get("facet_ranges");
+      } finally {
+        req.close();
+      }
+      
+      assertNotNull(rangeFacetsFilter.get(field));
+      assertNotNull(rangeFacetsDv.get(field));
+      
+      assertSameResults("Different results obtained when using 'filter' and 'dv' methods for Range Facets using params."
+          + params + "\n" + "Filter:" + rangeFacetsFilter + "\n DV: " + rangeFacetsDv, 
+          (NamedList<Object>)rangeFacetsFilter.get(field), (NamedList<Object>)rangeFacetsDv.get(field));
+    }
+    
+  }
+
+
+  private String getRandomQuery() {
+    if (rarely()) {
+      return "*:*";
+    }
+    Integer[] values = new Integer[2];
+    values[0] = random().nextInt(3000);
+    values[1] = random().nextInt(3000);
+    Arrays.sort(values);
+    return String.format(Locale.ROOT,  "id: [%d TO %d]", values[0], values[1]);
+  }
+
+
+  private void assertSameResults(String message,
+      NamedList<Object> rangeFacetsFilter, NamedList<Object> rangeFacetsDv) {
+    assertEquals(message + " Different number of elements.", rangeFacetsFilter.size(), rangeFacetsDv.size());
+    for (Map.Entry<String, Object> entry:rangeFacetsFilter) {
+      if (entry.getKey().equals("counts")) {
+        continue;
+      }
+      Object value = rangeFacetsDv.get(entry.getKey());
+      if (value == null) {
+        fail(message + " Element not found with 'dv' method: " + entry.getKey());
+      }
+      assertEquals(message + "Different value for key " + entry.getKey(), entry.getValue(), value);
+    }
+    assertNotNull("Null counts: " + rangeFacetsFilter, rangeFacetsFilter.get("counts"));
+    assertNotNull("Null counts: " + rangeFacetsDv, rangeFacetsDv.get("counts"));
+    assertEquals(message + "Different counts", rangeFacetsFilter.get("counts"), rangeFacetsDv.get("counts"));
+  }
+
+  private ModifiableSolrParams getRandomParamsInt() {
+    String field = new String[]{"range_facet_l_dv", "range_facet_i_dv", "range_facet_l", "duration_i1", "id"}[random().nextInt(5)];
+    ModifiableSolrParams params = new ModifiableSolrParams();
+    Integer[] values = new Integer[2];
+    do {
+      values[0] = random().nextInt(3000);
+      values[1] = random().nextInt(3000);
+    } while (values[0].equals(values[1]));
+    Arrays.sort(values);
+    long gapNum = Math.max(1, random().nextInt(3000));
+    
+    params.add(FacetParams.FACET_RANGE_START, String.valueOf(values[0]));
+    params.add(FacetParams.FACET_RANGE_END, String.valueOf(values[1]));
+    params.add(FacetParams.FACET_RANGE_GAP, String.format(Locale.ROOT, "+%d", gapNum));
+    addCommonRandomRangeParams(params);
+    params.add(FacetParams.FACET_RANGE, field);
+    return params;
+  }
+  
+  private ModifiableSolrParams getRandomParamsFloat() {
+    String field = new String[]{"range_facet_d_dv", "range_facet_f_dv", "range_facet_d", "range_facet_f", "range_facet_mv_f", "range_facet_f1", "range_facet_f1_dv"}[random().nextInt(7)];
+    ModifiableSolrParams params = new ModifiableSolrParams();
+    Float[] values = new Float[2];
+    do {
+      values[0] = random().nextFloat() * 3000;
+      values[1] = random().nextFloat() * 3000;
+    } while (values[0].equals(values[1]));
+    Arrays.sort(values);
+    float gapNum = Math.max(1, random().nextFloat() * 3000);
+    
+    params.add(FacetParams.FACET_RANGE_START, String.valueOf(values[0]));
+    params.add(FacetParams.FACET_RANGE_END, String.valueOf(values[1]));
+    params.add(FacetParams.FACET_RANGE_GAP, String.format(Locale.ROOT, "+%f", gapNum));
+    addCommonRandomRangeParams(params);
+    params.add(FacetParams.FACET_RANGE, field);
+    return params;
+  }
+  
+  private final static String[] DATE_GAP_UNITS = new String[]{"SECONDS", "MINUTES", "HOURS", "DAYS", "MONTHS", "YEARS"};
+      
+  private ModifiableSolrParams getRandomParamsDate() {
+    String field = new String[]{"range_facet_dt_dv", "a_tdt", "bday"}[random().nextInt(3)];
+    ModifiableSolrParams params = new ModifiableSolrParams();
+    Date[] dates = new Date[2];
+    do {
+      dates[0] = new Date((long)(random().nextDouble()*(new Date().getTime())));
+      dates[1] = new Date((long)(random().nextDouble()*(new Date().getTime())));
+    } while (dates[0].equals(dates[1]));
+    Arrays.sort(dates);
+    long dateDiff = (dates[1].getTime() - dates[0].getTime())/1000;
+    String gapUnit;
+    if (dateDiff < 1000) {
+      gapUnit = DATE_GAP_UNITS[random().nextInt(DATE_GAP_UNITS.length)];
+    } else if (dateDiff < 10000){
+      gapUnit = DATE_GAP_UNITS[1 + random().nextInt(DATE_GAP_UNITS.length - 1)];
+    } else if (dateDiff < 100000){
+      gapUnit = DATE_GAP_UNITS[2 + random().nextInt(DATE_GAP_UNITS.length - 2)];
+    } else if (dateDiff < 1000000){
+      gapUnit = DATE_GAP_UNITS[3 + random().nextInt(DATE_GAP_UNITS.length - 3)];
+    } else {
+      gapUnit = DATE_GAP_UNITS[4 + random().nextInt(DATE_GAP_UNITS.length - 4)];
+    }
+    int gapNum = random().nextInt(100) + 1;
+    
+    params.add(FacetParams.FACET_RANGE_START, TrieDateField.formatExternal(dates[0]));
+    params.add(FacetParams.FACET_RANGE_END, TrieDateField.formatExternal(dates[1]));
+    params.add(FacetParams.FACET_RANGE_GAP, String.format(Locale.ROOT, "+%d%s", gapNum, gapUnit));
+    addCommonRandomRangeParams(params);
+    params.add(FacetParams.FACET_RANGE, field);
+    return params;
+  }
+
+
+  private void addCommonRandomRangeParams(ModifiableSolrParams params) {
+    for (int i = 0; i < random().nextInt(2); i++) {
+      params.add(FacetParams.FACET_RANGE_OTHER, FacetRangeOther.values()[random().nextInt(FacetRangeOther.values().length)].toString());
+    }
+    if (random().nextBoolean()) {
+      params.add(FacetParams.FACET_RANGE_INCLUDE, FacetRangeInclude.values()[random().nextInt(FacetRangeInclude.values().length)].toString());
+    }
+    if (random().nextBoolean()) {
+      params.add(FacetParams.FACET_MINCOUNT, String.valueOf(random().nextInt(10)));
+    }
+    params.add(FacetParams.FACET_RANGE_HARD_END, String.valueOf(random().nextBoolean()));
+  }
 
 }

Modified: lucene/dev/trunk/solr/solrj/src/java/org/apache/solr/common/params/FacetParams.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/solrj/src/java/org/apache/solr/common/params/FacetParams.java?rev=1675706&r1=1675705&r2=1675706&view=diff
==============================================================================
--- lucene/dev/trunk/solr/solrj/src/java/org/apache/solr/common/params/FacetParams.java (original)
+++ lucene/dev/trunk/solr/solrj/src/java/org/apache/solr/common/params/FacetParams.java Thu Apr 23 19:27:34 2015
@@ -281,7 +281,15 @@ public interface FacetParams {
    * @see FacetRangeInclude
    */
   public static final String FACET_RANGE_INCLUDE = FACET_RANGE + ".include";
-
+  
+  /**
+   * String indicating the method to use to resolve range facets.
+   * <p>
+   * Can be overriden on a per field basis.
+   * @see FacetRangeMethod
+   */
+  public static final String FACET_RANGE_METHOD = FACET_RANGE + ".method";
+  
   /**
    * Any field whose values the user wants to enumerate as explicit intervals of terms.
    */
@@ -410,6 +418,32 @@ public interface FacetParams {
       return include;
     }
   }
+  
+  /**
+   * An enumeration of the legal values for {@link #FACET_RANGE_METHOD}
+   * <ul>
+   * <li>filter = </li>
+   * <li>dv = </li>
+   * </ul>
+   * @see #FACET_RANGE_METHOD
+   */
+  public enum FacetRangeMethod {
+    FILTER, DV;
+    @Override
+    public String toString() { return super.toString().toLowerCase(Locale.ROOT); }
+    public static FacetRangeMethod get(String label) {
+      try {
+        return valueOf(label.toUpperCase(Locale.ROOT));
+      } catch (IllegalArgumentException e) {
+        throw new SolrException
+          (SolrException.ErrorCode.BAD_REQUEST,
+           label+" is not a valid method for range faceting",e);
+      }
+    }
+    public static FacetRangeMethod getDefault() {
+      return FILTER;
+    }
+  }
 
 }