You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by yo...@apache.org on 2016/10/11 22:30:46 UTC

lucene-solr:branch_6x: SOLR-9432: json facet refinement progress, test refinement info going to shards

Repository: lucene-solr
Updated Branches:
  refs/heads/branch_6x 98827c5ca -> 690a78780


SOLR-9432: json facet refinement progress, test refinement info going to shards


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/690a7878
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/690a7878
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/690a7878

Branch: refs/heads/branch_6x
Commit: 690a78780ddb2b2a0e925136dac72f93bf1b9aad
Parents: 98827c5
Author: yonik <yo...@apache.org>
Authored: Tue Oct 11 17:13:36 2016 -0400
Committer: yonik <yo...@apache.org>
Committed: Tue Oct 11 17:14:26 2016 -0400

----------------------------------------------------------------------
 .../apache/solr/search/facet/FacetBucket.java   | 189 ++++++++++++++++
 .../apache/solr/search/facet/FacetModule.java   | 166 --------------
 .../search/facet/FacetRequestSortedMerger.java  |  25 ++-
 .../search/facet/TestJsonFacetRefinement.java   | 218 +++++++++++++++++++
 4 files changed, 426 insertions(+), 172 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/690a7878/solr/core/src/java/org/apache/solr/search/facet/FacetBucket.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/search/facet/FacetBucket.java b/solr/core/src/java/org/apache/solr/search/facet/FacetBucket.java
new file mode 100644
index 0000000..ae1eba6
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/search/facet/FacetBucket.java
@@ -0,0 +1,189 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.search.facet;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.solr.common.util.SimpleOrderedMap;
+
+public class FacetBucket {
+  final FacetBucketMerger parent;
+  final Comparable bucketValue;
+  final int bucketNumber;  // this is just for internal correlation (the first bucket created is bucket 0, the next bucket 1, across all field buckets)
+
+  long count;
+  Map<String, FacetMerger> subs;
+
+  public FacetBucket(FacetBucketMerger parent, Comparable bucketValue, FacetMerger.Context mcontext) {
+    this.parent = parent;
+    this.bucketValue = bucketValue;
+    this.bucketNumber = mcontext.getNewBucketNumber(); // TODO: we don't need bucket numbers for all buckets...
+  }
+
+  public long getCount() {
+    return count;
+  }
+
+  /** returns the existing merger for the given key, or null if none yet exists */
+  FacetMerger getExistingMerger(String key) {
+    if (subs == null) return null;
+    return subs.get(key);
+  }
+
+  private FacetMerger getMerger(String key, Object prototype) {
+    FacetMerger merger = null;
+    if (subs != null) {
+      merger = subs.get(key);
+      if (merger != null) return merger;
+    }
+
+    merger = parent.createFacetMerger(key, prototype);
+
+    if (merger != null) {
+      if (subs == null) {
+        subs = new HashMap<>();
+      }
+      subs.put(key, merger);
+    }
+
+    return merger;
+  }
+
+  public void mergeBucket(SimpleOrderedMap bucket, FacetMerger.Context mcontext) {
+    // todo: for refinements, we want to recurse, but not re-do stats for intermediate buckets
+
+    mcontext.setShardFlag(bucketNumber);
+
+    // drive merging off the received bucket?
+    for (int i=0; i<bucket.size(); i++) {
+      String key = bucket.getName(i);
+      Object val = bucket.getVal(i);
+      if ("count".equals(key)) {
+        count += ((Number)val).longValue();
+        continue;
+      }
+      if ("val".equals(key)) {
+        // this is taken care of at a higher level...
+        continue;
+      }
+
+      FacetMerger merger = getMerger(key, val);
+
+      if (merger != null) {
+        merger.merge( val , mcontext );
+      }
+    }
+  }
+
+
+  public SimpleOrderedMap getMergedBucket() {
+    SimpleOrderedMap out = new SimpleOrderedMap( (subs == null ? 0 : subs.size()) + 2 );
+    if (bucketValue != null) {
+      out.add("val", bucketValue);
+    }
+    out.add("count", count);
+    if (subs != null) {
+      for (Map.Entry<String,FacetMerger> mergerEntry : subs.entrySet()) {
+        FacetMerger subMerger = mergerEntry.getValue();
+        out.add(mergerEntry.getKey(), subMerger.getMergedResult());
+      }
+    }
+
+    return out;
+  }
+
+  public Map<String, Object> getRefinement(FacetMerger.Context mcontext, Collection<String> refineTags) {
+    if (subs == null) {
+      return null;
+    }
+    Map<String,Object> refinement = null;
+    for (String tag : refineTags) {
+      FacetMerger subMerger = subs.get(tag);
+      if (subMerger != null) {
+        Map<String,Object> subRef = subMerger.getRefinement(mcontext);
+        if (subRef != null) {
+          if (refinement == null) {
+            refinement = new HashMap<>(refineTags.size());
+          }
+          refinement.put(tag, subRef);
+        }
+      }
+    }
+    return refinement;
+  }
+
+  public Map<String, Object> getRefinement2(FacetMerger.Context mcontext, Collection<String> refineTags) {
+    // TODO - partial results should turn off refining!!!
+
+    boolean parentMissing = mcontext.bucketWasMissing();
+
+    // TODO: this is a redundant check for many types of facets... only do on field faceting
+    if (!parentMissing) {
+      // if parent bucket wasn't missing, check if this bucket was.
+      // this really only needs checking on certain buckets... (like terms facet)
+      boolean sawThisBucket = mcontext.getShardFlag(bucketNumber);
+      if (!sawThisBucket) {
+        mcontext.setBucketWasMissing(true);
+      }
+    } else {
+      // if parent bucket was missing, then we should be too
+      assert !mcontext.getShardFlag(bucketNumber);
+    }
+
+    Map<String,Object> refinement = null;
+
+    if (!mcontext.bucketWasMissing()) {
+      // this is just a pass-through bucket... see if there is anything to do at all
+      if (subs == null || refineTags.isEmpty()) {
+        return null;
+      }
+    } else {
+      // for missing bucket, go over all sub-facts
+      refineTags = null;
+      refinement = new HashMap<>(4);
+      if (bucketValue != null) {
+        refinement.put("_v", bucketValue);
+      }
+      refinement.put("_m",1);
+    }
+
+    // TODO: listing things like sub-facets that have no field facets are redundant
+    // (we only need facet that have variable values)
+
+    for (Map.Entry<String,FacetMerger> sub : subs.entrySet()) {
+      if (refineTags != null && !refineTags.contains(sub.getKey())) {
+        continue;
+      }
+      Map<String,Object> subRef = sub.getValue().getRefinement(mcontext);
+      if (subRef != null) {
+        if (refinement == null) {
+          refinement = new HashMap<>(4);
+        }
+        refinement.put(sub.getKey(), subRef);
+      }
+    }
+
+
+    // reset the "bucketMissing" flag on the way back out.
+    mcontext.setBucketWasMissing(parentMissing);
+    return refinement;
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/690a7878/solr/core/src/java/org/apache/solr/search/facet/FacetModule.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/search/facet/FacetModule.java b/solr/core/src/java/org/apache/solr/search/facet/FacetModule.java
index 8767e5b..f8d677a 100644
--- a/solr/core/src/java/org/apache/solr/search/facet/FacetModule.java
+++ b/solr/core/src/java/org/apache/solr/search/facet/FacetModule.java
@@ -473,169 +473,3 @@ class FacetQueryMerger extends FacetBucketMerger<FacetQuery> {
 
 
 
-class FacetBucket {
-  final FacetBucketMerger parent;
-  final Comparable bucketValue;
-  final int bucketNumber;  // this is just for internal correlation (the first bucket created is bucket 0, the next bucket 1, across all field buckets)
-
-  long count;
-  Map<String, FacetMerger> subs;
-
-  public FacetBucket(FacetBucketMerger parent, Comparable bucketValue, FacetMerger.Context mcontext) {
-    this.parent = parent;
-    this.bucketValue = bucketValue;
-    this.bucketNumber = mcontext.getNewBucketNumber(); // TODO: we don't need bucket numbers for all buckets...
-  }
-
-  public long getCount() {
-    return count;
-  }
-
-  /** returns the existing merger for the given key, or null if none yet exists */
-  FacetMerger getExistingMerger(String key) {
-    if (subs == null) return null;
-    return subs.get(key);
-  }
-
-  private FacetMerger getMerger(String key, Object prototype) {
-    FacetMerger merger = null;
-    if (subs != null) {
-      merger = subs.get(key);
-      if (merger != null) return merger;
-    }
-
-    merger = parent.createFacetMerger(key, prototype);
-
-    if (merger != null) {
-      if (subs == null) {
-        subs = new HashMap<>();
-      }
-      subs.put(key, merger);
-    }
-
-    return merger;
-  }
-
-  public void mergeBucket(SimpleOrderedMap bucket, FacetMerger.Context mcontext) {
-    // todo: for refinements, we want to recurse, but not re-do stats for intermediate buckets
-
-    mcontext.setShardFlag(bucketNumber);
-
-    // drive merging off the received bucket?
-    for (int i=0; i<bucket.size(); i++) {
-      String key = bucket.getName(i);
-      Object val = bucket.getVal(i);
-      if ("count".equals(key)) {
-        count += ((Number)val).longValue();
-        continue;
-      }
-      if ("val".equals(key)) {
-        // this is taken care of at a higher level...
-        continue;
-      }
-
-      FacetMerger merger = getMerger(key, val);
-
-      if (merger != null) {
-        merger.merge( val , mcontext );
-      }
-    }
-  }
-
-
-  public SimpleOrderedMap getMergedBucket() {
-    SimpleOrderedMap out = new SimpleOrderedMap( (subs == null ? 0 : subs.size()) + 2 );
-    if (bucketValue != null) {
-      out.add("val", bucketValue);
-    }
-    out.add("count", count);
-    if (subs != null) {
-      for (Map.Entry<String,FacetMerger> mergerEntry : subs.entrySet()) {
-        FacetMerger subMerger = mergerEntry.getValue();
-        out.add(mergerEntry.getKey(), subMerger.getMergedResult());
-      }
-    }
-
-    return out;
-  }
-
-  public Map<String, Object> getRefinement(FacetMerger.Context mcontext, Collection<String> refineTags) {
-    if (subs == null) {
-      return null;
-    }
-    Map<String,Object> refinement = null;
-    for (String tag : refineTags) {
-      FacetMerger subMerger = subs.get(tag);
-      if (subMerger != null) {
-        Map<String,Object> subRef = subMerger.getRefinement(mcontext);
-        if (subRef != null) {
-          if (refinement == null) {
-            refinement = new HashMap<>(refineTags.size());
-          }
-          refinement.put(tag, subRef);
-        }
-      }
-    }
-    return refinement;
-  }
-
-  public Map<String, Object> getRefinement2(FacetMerger.Context mcontext, Collection<String> refineTags) {
-    // TODO - partial results should turn off refining!!!
-
-    boolean parentMissing = mcontext.bucketWasMissing();
-
-    // TODO: this is a redundant check for many types of facets... only do on field faceting
-    if (!parentMissing) {
-      // if parent bucket wasn't missing, check if this bucket was.
-      // this really only needs checking on certain buckets... (like terms facet)
-      boolean sawThisBucket = mcontext.getShardFlag(bucketNumber);
-      if (!sawThisBucket) {
-        mcontext.setBucketWasMissing(true);
-      }
-    } else {
-      // if parent bucket was missing, then we should be too
-      assert !mcontext.getShardFlag(bucketNumber);
-    }
-
-    Map<String,Object> refinement = null;
-
-    if (!mcontext.bucketWasMissing()) {
-      // this is just a pass-through bucket... see if there is anything to do at all
-      if (subs == null || refineTags.isEmpty()) {
-        return null;
-      }
-    } else {
-      // for missing bucket, go over all sub-facts
-      refineTags = null;
-      refinement = new HashMap<>(4);
-      if (bucketValue != null) {
-        refinement.put("_v", bucketValue);
-      }
-      refinement.put("_m",1);
-    }
-
-    // TODO: listing things like sub-facets that have no field facets are redundant
-    // (we only need facet that have variable values)
-
-    for (Map.Entry<String,FacetMerger> sub : subs.entrySet()) {
-      if (refineTags != null && !refineTags.contains(sub.getKey())) {
-        continue;
-      }
-      Map<String,Object> subRef = sub.getValue().getRefinement(mcontext);
-      if (subRef != null) {
-        if (refinement == null) {
-          refinement = new HashMap<>(4);
-        }
-        refinement.put(sub.getKey(), subRef);
-      }
-    }
-
-
-    // reset the "bucketMissing" flag on the way back out.
-    mcontext.setBucketWasMissing(parentMissing);
-    return refinement;
-  }
-
-}
-
-

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/690a7878/solr/core/src/java/org/apache/solr/search/facet/FacetRequestSortedMerger.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/search/facet/FacetRequestSortedMerger.java b/solr/core/src/java/org/apache/solr/search/facet/FacetRequestSortedMerger.java
index a981006..f55fc0f 100644
--- a/solr/core/src/java/org/apache/solr/search/facet/FacetRequestSortedMerger.java
+++ b/solr/core/src/java/org/apache/solr/search/facet/FacetRequestSortedMerger.java
@@ -18,6 +18,7 @@
 package org.apache.solr.search.facet;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
@@ -157,9 +158,21 @@ abstract class FacetRequestSortedMerger<FacetRequestT extends FacetRequestSorted
     // If we are missing a bucket for this shard, we'll need to get the specific buckets that need refining.
     Collection<String> tagsWithPartial = mcontext.getSubsWithPartial(freq);
 
-    boolean thisMissing = mcontext.bucketWasMissing();
+    boolean thisMissing = mcontext.bucketWasMissing(); // Was this whole facet missing (i.e. inside a bucket that was missing)?
 
-    int num = (int)(freq.offset + freq.limit);
+    // TODO: add information in sub-shard response about dropped buckets (i.e. not all returned due to limit)
+    // If we know we've seen all the buckets from a shard, then we don't have to add to leafBuckets or missingBuckets, only skipBuckets
+    boolean isCommandPartial = freq.returnsPartial();
+    boolean returnedAllBuckets = !isCommandPartial && !thisMissing;  // did the shard return all of the possible buckets?
+
+    if (returnedAllBuckets && tags.isEmpty() && tagsWithPartial.isEmpty()) {
+      // this facet returned all possible buckets, and there were no sub-facets with partial results
+      // and sub-facets that require refining
+      return null;
+    }
+
+
+    int num = freq.limit < 0 ? Integer.MAX_VALUE : (int)(freq.offset + freq.limit);
     int numBucketsToCheck = Math.min(buckets.size(), num);
 
     Collection<FacetBucket> bucketList;
@@ -176,8 +189,8 @@ abstract class FacetRequestSortedMerger<FacetRequestT extends FacetRequestSorted
     }
 
     ArrayList<Object> leafBuckets = null;    // "_l" missing buckets specified by bucket value only (no need to specify anything further)
-    ArrayList<Object> missingBuckets = null; // "_m" missing buckets that need to specify values for partial facets
-    ArrayList<Object> skipBuckets = null;    // "_s" present buckets that we need to recurse into because children facets have refinement requirements
+    ArrayList<Object> missingBuckets = null; // "_m" missing buckets that need to specify values for partial facets.. each entry is [bucketval, subs]
+    ArrayList<Object> skipBuckets = null;    // "_s" present buckets that we need to recurse into because children facets have refinement requirements. each entry is [bucketval, subs]
 
     for (FacetBucket bucket : bucketList) {
       if (numBucketsToCheck-- <= 0) break;
@@ -196,7 +209,7 @@ abstract class FacetRequestSortedMerger<FacetRequestT extends FacetRequestSorted
 
           if (bucketRefinement != null) {
             if (missingBuckets==null) missingBuckets = new ArrayList<>();
-            missingBuckets.add(bucketRefinement);
+            missingBuckets.add( Arrays.asList(bucket.bucketValue, bucketRefinement) );
           }
         }
 
@@ -211,7 +224,7 @@ abstract class FacetRequestSortedMerger<FacetRequestT extends FacetRequestSorted
         Map<String,Object> bucketRefinement = bucket.getRefinement(mcontext, tagsWithPartial);
         if (bucketRefinement != null) {
           if (skipBuckets == null) skipBuckets = new ArrayList<>();
-          skipBuckets.add(bucketRefinement);
+          skipBuckets.add( Arrays.asList(bucket.bucketValue, bucketRefinement) );
         }
       }
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/690a7878/solr/core/src/test/org/apache/solr/search/facet/TestJsonFacetRefinement.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/search/facet/TestJsonFacetRefinement.java b/solr/core/src/test/org/apache/solr/search/facet/TestJsonFacetRefinement.java
new file mode 100644
index 0000000..7c510ea
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/search/facet/TestJsonFacetRefinement.java
@@ -0,0 +1,218 @@
+package org.apache.solr.search.facet;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.apache.solr.JSONTestUtil;
+import org.apache.solr.SolrTestCaseHS;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.request.SolrQueryRequest;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.noggit.JSONParser;
+import org.noggit.JSONUtil;
+import org.noggit.ObjectBuilder;
+
+
+public class TestJsonFacetRefinement extends SolrTestCaseHS {
+
+  private static SolrInstances servers;  // for distributed testing
+  private static int origTableSize;
+
+  @BeforeClass
+  public static void beforeTests() throws Exception {
+    JSONTestUtil.failRepeatedKeys = true;
+    initCore("solrconfig-tlog.xml","schema_latest.xml");
+  }
+
+  public static void initServers() throws Exception {
+    if (servers == null) {
+      servers = new SolrInstances(3, "solrconfig-tlog.xml", "schema_latest.xml");
+    }
+  }
+
+  @AfterClass
+  public static void afterTests() throws Exception {
+    JSONTestUtil.failRepeatedKeys = false;
+    if (servers != null) {
+      servers.stop();
+      servers = null;
+    }
+  }
+
+
+  // todo - pull up to test base class?
+  public void matchJSON(String json, double delta, String... tests) throws Exception {
+    for (String test : tests) {
+      if (test == null) {
+        assertNull(json);
+        continue;
+      }
+      if (test.length()==0) continue;
+
+      String err = JSONTestUtil.match(json, test, delta);
+
+      if (err != null) {
+        throw new RuntimeException("JSON failed validation. error=" + err +
+            "\n expected =" + test +
+            "\n got = " + json
+        );
+      }
+    }
+  }
+
+
+  public void match(Object input, double delta, String... tests) throws Exception {
+    for (String test : tests) {
+      String err = null;
+      if (test == null) {
+        if (input != null) {
+          err = "expected null";
+        }
+      } else if (input == null) {
+        err = "got null";
+      } else {
+        err = JSONTestUtil.matchObj(input, test, delta);
+      }
+
+      if (err != null) {
+        throw new RuntimeException("JSON failed validation. error=" + err +
+            "\n expected =" + test +
+            "\n got = " + input
+        );
+      }
+    }
+  }
+
+
+  /** Use SimpleOrderedMap rather than Map to match responses from shards */
+  public static Object fromJSON(String json) throws IOException {
+    JSONParser parser = new JSONParser(json);
+    ObjectBuilder ob = new ObjectBuilder(parser) {
+      @Override
+      public Object newObject() throws IOException {
+        return new SimpleOrderedMap();
+      }
+
+      @Override
+      public void addKeyVal(Object map, Object key, Object val) throws IOException {
+        ((SimpleOrderedMap)map).add(key.toString(), val);
+      }
+    };
+
+    return ob.getObject();
+  }
+
+  void doTestRefine(String facet, String... responsesAndTests) throws Exception {
+    SolrQueryRequest req = req();
+    try {
+      int nShards = responsesAndTests.length / 2;
+      Object jsonFacet = ObjectBuilder.fromJSON(facet);
+      FacetParser parser = new FacetTopParser(req);
+      FacetRequest facetRequest = parser.parse(jsonFacet);
+
+      FacetMerger merger = null;
+      FacetMerger.Context ctx = new FacetMerger.Context(nShards);
+      for (int i=0; i<nShards; i++) {
+        Object response = fromJSON(responsesAndTests[i]);
+        if (i==0) {
+          merger = facetRequest.createFacetMerger(response);
+        }
+        ctx.newShard("s"+i);
+        merger.merge(response, ctx);
+      }
+
+      for (int i=0; i<nShards; i++) {
+        ctx.setShard("s"+i);
+        Object refinement = merger.getRefinement(ctx);
+        String tests = responsesAndTests[nShards+i];
+        match(refinement, 1e-5, tests);
+      }
+
+    } finally {
+      req.close();
+    }
+
+  }
+
+  @Test
+  public void testMerge() throws Exception {
+    doTestRefine("{x : {type:terms, field:X, limit:2, refine:true} }",  // the facet request
+        "{x: {buckets:[{val:x1, count:5}, {val:x2, count:3}] } }",  // shard0 response
+        "{x: {buckets:[{val:x2, count:4}, {val:x3, count:2}] } }",  // shard1 response
+        null,              // shard0 expected refinement info
+        "=={x:{_l:[x1]}}"  // shard1 expected refinement info
+        );
+
+    // same test w/o refinement turned on
+    doTestRefine("{x : {type:terms, field:X, limit:2} }",  // the facet request
+        "{x: {buckets:[{val:x1, count:5}, {val:x2, count:3}] } }",  // shard0 response
+        "{x: {buckets:[{val:x2, count:4}, {val:x3, count:2}] } }",  // shard1 response
+        null, // shard0 expected refinement info
+        null  // shard1 expected refinement info
+    );
+
+    // same test, but nested in query facet
+    doTestRefine("{top:{type:query, q:'foo_s:myquery', facet:{x : {type:terms, field:X, limit:2, refine:true} } } }",  // the facet request
+        "{top: {x: {buckets:[{val:x1, count:5}, {val:x2, count:3}] } } }",  // shard0 response
+        "{top: {x: {buckets:[{val:x2, count:4}, {val:x3, count:2}] } } }",  // shard1 response
+        null,              // shard0 expected refinement info
+        "=={top:{x:{_l:[x1]}}}"  // shard1 expected refinement info
+    );
+
+    // same test w/o refinement turned on
+    doTestRefine("{top:{type:query, q:'foo_s:myquery', facet:{x : {type:terms, field:X, limit:2, refine:false} } } }",
+        "{top: {x: {buckets:[{val:x1, count:5}, {val:x2, count:3}] } } }",  // shard0 response
+        "{top: {x: {buckets:[{val:x2, count:4}, {val:x3, count:2}] } } }",  // shard1 response
+        null,
+        null
+    );
+
+    // same test, but nested in a terms facet
+    doTestRefine("{top:{type:terms, field:Afield, facet:{x : {type:terms, field:X, limit:2, refine:true} } } }",
+        "{top: {buckets:[{val:'A', count:2, x:{buckets:[{val:x1, count:5},{val:x2, count:3}]} } ] } }",
+        "{top: {buckets:[{val:'A', count:1, x:{buckets:[{val:x2, count:4},{val:x3, count:2}]} } ] } }",
+        null,
+        "=={top: {" +
+            "_s:[  ['A' , {x:{_l:[x1]}} ]  ]" +
+            "    }  " +
+            "}"
+    );
+
+    // same test, but nested in range facet
+    doTestRefine("{top:{type:range, field:R, start:0, end:1, gap:1, facet:{x : {type:terms, field:X, limit:2, refine:true} } } }",
+        "{top: {buckets:[{val:0, count:2, x:{buckets:[{val:x1, count:5},{val:x2, count:3}]} } ] } }",
+        "{top: {buckets:[{val:0, count:1, x:{buckets:[{val:x2, count:4},{val:x3, count:2}]} } ] } }",
+        null,
+        "=={top: {" +
+            "_s:[  [0 , {x:{_l:[x1]}} ]  ]" +
+            "    }  " +
+            "}"
+    );
+
+  }
+
+
+
+
+}