You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ge...@apache.org on 2018/11/14 20:28:19 UTC

[1/2] lucene-solr:branch_7x: SOLR-12965: Add facet support to JsonQueryRequest

Repository: lucene-solr
Updated Branches:
  refs/heads/branch_7x 6faddfe3b -> b502ba288


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b502ba28/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/DirectJsonQueryRequestFacetingIntegrationTest.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/DirectJsonQueryRequestFacetingIntegrationTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/DirectJsonQueryRequestFacetingIntegrationTest.java
new file mode 100644
index 0000000..be4f0cc
--- /dev/null
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/DirectJsonQueryRequestFacetingIntegrationTest.java
@@ -0,0 +1,615 @@
+/*
+ * 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.client.solrj.request.json;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.solr.client.solrj.request.AbstractUpdateRequest;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
+import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.client.solrj.response.UpdateResponse;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.util.ExternalPaths;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class DirectJsonQueryRequestFacetingIntegrationTest extends SolrCloudTestCase {
+
+  private static final String COLLECTION_NAME = "techproducts";
+  private static final String CONFIG_NAME = "techproducts_config";
+  private static final int NUM_TECHPRODUCTS_DOCS = 32;
+  private static final int NUM_IN_STOCK = 17;
+  private static final int NUM_ELECTRONICS = 12;
+  private static final int NUM_CURRENCY = 4;
+  private static final int NUM_MEMORY = 3;
+  private static final int NUM_CORSAIR = 3;
+  private static final int NUM_BELKIN = 2;
+  private static final int NUM_CANON = 2;
+
+
+
+  @BeforeClass
+  public static void setupCluster() throws Exception {
+    configureCluster(1)
+        .addConfig(CONFIG_NAME, new File(ExternalPaths.TECHPRODUCTS_CONFIGSET).toPath())
+        .configure();
+
+    final List<String> solrUrls = new ArrayList<>();
+    solrUrls.add(cluster.getJettySolrRunner(0).getBaseUrl().toString());
+
+    CollectionAdminRequest.createCollection(COLLECTION_NAME, CONFIG_NAME, 1, 1).process(cluster.getSolrClient());
+
+    ContentStreamUpdateRequest up = new ContentStreamUpdateRequest("/update");
+    up.setParam("collection", COLLECTION_NAME);
+    up.addFile(getFile("solrj/techproducts.xml"), "application/xml");
+    up.setAction(AbstractUpdateRequest.ACTION.COMMIT, true, true);
+    UpdateResponse updateResponse = up.process(cluster.getSolrClient());
+    assertEquals(0, updateResponse.getStatus());
+  }
+  @Test
+  public void testSingleTermsFacet() throws Exception {
+    final String jsonBody = String.join("\n","{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'top_cats': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'limit': 3",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    assertHasFacetWithBucketValues(rawResponse,"top_cats", new FacetBucket("electronics",NUM_ELECTRONICS),
+        new FacetBucket("currency", NUM_CURRENCY), new FacetBucket("memory", NUM_MEMORY));
+  }
+
+  @Test
+  public void testMultiTermsFacet() throws Exception {
+    final String jsonBody = String.join("\n","{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'top_cats': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'limit': 3",
+        "    },",
+        "    'top_manufacturers': {",
+        "      'type': 'terms',",
+        "      'field': 'manu_id_s',",
+        "      'limit': 3",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+
+    assertHasFacetWithBucketValues(rawResponse,"top_cats", new FacetBucket("electronics",NUM_ELECTRONICS),
+        new FacetBucket("currency", NUM_CURRENCY), new FacetBucket("memory", NUM_MEMORY));
+    assertHasFacetWithBucketValues(rawResponse,"top_manufacturers", new FacetBucket("corsair",NUM_CORSAIR),
+        new FacetBucket("belkin", NUM_BELKIN), new FacetBucket("canon", NUM_CANON));
+  }
+
+  @Test
+  public void testSingleRangeFacet() throws Exception {
+    final String jsonBody = String.join("\n","{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'prices': {",
+        "      'type': 'range',",
+        "      'field': 'price',",
+        "      'start': 0,",
+        "      'end': 100,",
+        "      'gap': 20",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    assertHasFacetWithBucketValues(rawResponse,"prices",
+        new FacetBucket(0.0f, 5),
+        new FacetBucket(20.0f, 0),
+        new FacetBucket(40.0f, 0),
+        new FacetBucket(60.0f, 1),
+        new FacetBucket(80.0f, 1));
+  }
+
+  @Test
+  public void testMultiRangeFacet() throws Exception {
+    final String jsonBody = String.join("\n","{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'prices': {",
+        "      'type': 'range',",
+        "      'field': 'price',",
+        "      'start': 0,",
+        "      'end': 100,",
+        "      'gap': 20",
+        "    },",
+        "    'shipping_weights': {",
+        "      'type': 'range',",
+        "      'field': 'weight',",
+        "      'start': 0,",
+        "      'end': 200,",
+        "      'gap': 50",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    assertHasFacetWithBucketValues(rawResponse,"prices",
+        new FacetBucket(0.0f, 5),
+        new FacetBucket(20.0f, 0),
+        new FacetBucket(40.0f, 0),
+        new FacetBucket(60.0f, 1),
+        new FacetBucket(80.0f, 1));
+    assertHasFacetWithBucketValues(rawResponse, "shipping_weights",
+        new FacetBucket(0.0f, 6),
+        new FacetBucket(50.0f, 0),
+        new FacetBucket(100.0f, 0),
+        new FacetBucket(150.0f,1));
+  }
+
+  @Test
+  public void testSingleStatFacet() throws Exception {
+    final String jsonBody = String.join("\n","{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'sum_price': 'sum(price)'",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    assertHasStatFacetWithValue(rawResponse,"sum_price", 5251.270030975342);
+  }
+
+  @Test
+  public void testMultiStatFacet() throws Exception {
+    final String jsonBody = String.join("\n","{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'sum_price': 'sum(price)',",
+        "    'avg_price': 'avg(price)'",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    assertHasStatFacetWithValue(rawResponse,"sum_price", 5251.270030975342);
+    assertHasStatFacetWithValue(rawResponse,"avg_price", 328.20437693595886);
+  }
+
+  @Test
+  public void testMultiFacetsMixedTypes() throws Exception {
+    final String jsonBody = String.join("\n","{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'avg_price': 'avg(price)',",
+        "    'top_cats': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'limit': 3",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    assertHasStatFacetWithValue(rawResponse,"avg_price", 328.20437693595886);
+    assertHasFacetWithBucketValues(rawResponse,"top_cats", new FacetBucket("electronics",NUM_ELECTRONICS),
+        new FacetBucket("currency", NUM_CURRENCY), new FacetBucket("memory", NUM_MEMORY));
+  }
+
+  @Test
+  public void testNestedTermsFacet() throws Exception {
+    final String jsonBody = String.join("\n","{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'top_cats': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'limit': 3",
+        "      'facet': {",
+        "        'top_manufacturers_for_cat': {",
+        "          'type': 'terms',",
+        "          'field': 'manu_id_s',",
+        "          'limit': 1",
+        "        }",
+        "      }",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+
+    // Test top level facets
+    assertHasFacetWithBucketValues(rawResponse,"top_cats", new FacetBucket("electronics",NUM_ELECTRONICS),
+        new FacetBucket("currency", NUM_CURRENCY), new FacetBucket("memory", NUM_MEMORY));
+
+    // Test subfacet values for each top-level facet bucket
+    final List<NamedList<Object>> topLevelFacetResponse = (List<NamedList<Object>>) rawResponse.findRecursive("facets", "top_cats", "buckets");
+    final NamedList<Object> electronicsSubFacet = topLevelFacetResponse.get(0);
+    assertFacetResponseHasFacetWithBuckets(electronicsSubFacet, "top_manufacturers_for_cat", new FacetBucket("corsair", 3));
+    final NamedList<Object> currencySubfacet = topLevelFacetResponse.get(1);
+    assertFacetResponseHasFacetWithBuckets(currencySubfacet, "top_manufacturers_for_cat", new FacetBucket("boa", 1));
+    final NamedList<Object> memorySubfacet = topLevelFacetResponse.get(2);
+    assertFacetResponseHasFacetWithBuckets(memorySubfacet, "top_manufacturers_for_cat", new FacetBucket("corsair", 3));
+  }
+
+  @Test
+  public void testNestedFacetsOfMixedTypes() throws Exception {
+    final String subfacetName = "avg_price_for_cat";
+    final String jsonBody = String.join("\n","{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'top_cats': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'limit': 3",
+        "      'facet': {",
+        "        'avg_price_for_cat': 'avg(price)'",
+        "      }",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+
+    // Test top level facets
+    assertHasFacetWithBucketValues(rawResponse,"top_cats", new FacetBucket("electronics",NUM_ELECTRONICS),
+        new FacetBucket("currency", NUM_CURRENCY), new FacetBucket("memory", NUM_MEMORY));
+
+    // Test subfacet values for each top-level facet bucket
+    final List<NamedList<Object>> topLevelFacetResponse = (List<NamedList<Object>>) rawResponse.findRecursive("facets", "top_cats", "buckets");
+    final NamedList<Object> electronicsSubFacet = topLevelFacetResponse.get(0);
+    assertFacetResponseHasStatFacetWithValue(electronicsSubFacet, subfacetName, 252.02909261530095);
+    final NamedList<Object> currencySubfacet = topLevelFacetResponse.get(1);
+    assertFacetResponseHasStatFacetWithValue(currencySubfacet, subfacetName, 0.0);
+    final NamedList<Object> memorySubfacet = topLevelFacetResponse.get(2);
+    assertFacetResponseHasStatFacetWithValue(memorySubfacet, subfacetName, 129.99499893188477);
+  }
+
+  @Test
+  public void testFacetWithDomainFilteredBySimpleQueryString() throws Exception {
+    final String jsonBody = String.join("\n","{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'top_popular_cats': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'limit': 3",
+        "      'domain': {",
+        "        'filter': 'popularity:[5 TO 10]'",
+        "      }",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+
+    // Test top level facets
+    assertHasFacetWithBucketValues(rawResponse,"top_popular_cats", new FacetBucket("electronics",9),
+        new FacetBucket("graphics card", 2), new FacetBucket("hard drive", 2));
+  }
+
+  @Test
+  public void testFacetWithDomainFilteredByLocalParamsQueryString() throws Exception {
+    final String jsonBody = String.join("\n","{",
+        "  'query': '*:*',",
+        "  'facet': {",
+        "    'top_popular_cats': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'limit': 3",
+        "      'domain': {",
+        "        'filter': '{!lucene df=\"popularity\" v=\"[5 TO 10]\"}'",
+        "      }",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+
+    // Test top level facets
+    assertHasFacetWithBucketValues(rawResponse,"top_popular_cats", new FacetBucket("electronics",9),
+        new FacetBucket("graphics card", 2), new FacetBucket("hard drive", 2));
+  }
+
+  @Test
+  public void testFacetWithArbitraryDomainFromQueryString() throws Exception {
+    final String jsonBody = String.join("\n","{",
+        "  'query': 'cat:electronics',",
+        "  'facet': {",
+        "    'top_cats': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'limit': 3",
+        "      'domain': {",
+        "        'query': '*:*'",
+        "      }",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_ELECTRONICS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+
+    // Test top level facets
+    assertHasFacetWithBucketValues(rawResponse,"top_cats", new FacetBucket("electronics",NUM_ELECTRONICS),
+        new FacetBucket("currency", NUM_CURRENCY), new FacetBucket("memory", NUM_MEMORY));
+  }
+
+  @Test
+  public void testFacetWithArbitraryDomainFromLocalParamsQuery() throws Exception {
+    final String jsonBody = String.join("\n","{",
+        "  'query': 'cat:electronics',",
+        "  'facet': {",
+        "    'largest_search_cats': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'domain': {",
+        "        'query': '{!lucene df=\"cat\" v=\"search\"}'",
+        "      }",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_ELECTRONICS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+
+    assertHasFacetWithBucketValues(rawResponse,"largest_search_cats",
+        new FacetBucket("search",2),
+        new FacetBucket("software", 2));
+  }
+
+  /*
+   * Multiple query clauses are effectively AND'd together
+   */
+  public void testFacetWithMultipleSimpleQueryClausesInArbitraryDomain() throws Exception {
+    final String jsonBody = String.join("\n","{",
+        "  'query': 'cat:electronics',",
+        "  'facet': {",
+        "    'cats_matching_solr': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'domain': {",
+        "        'query': ['cat:search', 'name:Solr']",
+        "      }",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_ELECTRONICS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+
+    assertHasFacetWithBucketValues(rawResponse,"cats_matching_solr",
+        new FacetBucket("search",1),
+        new FacetBucket("software", 1));
+  }
+
+  public void testFacetWithMultipleLocalParamsQueryClausesInArbitraryDomain() throws Exception {
+    final String jsonBody = String.join("\n","{",
+        "  'query': 'cat:electronics',",
+        "  'facet': {",
+        "    'cats_matching_solr': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'domain': {",
+        "        'query': ['{!lucene df=\"cat\" v=\"search\"}', '{!lucene df=\"name\" v=\"Solr\"}']",
+        "      }",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_ELECTRONICS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+
+    // Test top level facets
+    assertHasFacetWithBucketValues(rawResponse,"cats_matching_solr",
+        new FacetBucket("search",1),
+        new FacetBucket("software", 1));
+  }
+
+  @Test
+  public void testFacetWithDomainWidenedUsingExcludeTagsToIgnoreFilters() throws Exception {
+    final String jsonBody = String.join("\n","{",
+        "  'query': '*:*',",
+        "  'filter': {'#on_shelf': 'inStock:true'},",
+        "  'facet': {",
+        "    'in_stock_only': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'limit': 2",
+        "    }",
+        "    'all': {",
+        "      'type': 'terms',",
+        "      'field': 'cat',",
+        "      'limit': 2,",
+        "      'domain': {",
+        "        'excludeTags': 'on_shelf'",
+        "      }",
+        "    }",
+        "  }",
+        "}");
+    final DirectJsonQueryRequest request = new DirectJsonQueryRequest(jsonBody);
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_IN_STOCK, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+
+    assertHasFacetWithBucketValues(rawResponse,"in_stock_only",
+        new FacetBucket("electronics",8),
+        new FacetBucket("currency", 4));
+    assertHasFacetWithBucketValues(rawResponse,"all",
+        new FacetBucket("electronics",12),
+        new FacetBucket("currency", 4));
+  }
+
+  private class FacetBucket {
+    private final Object val;
+    private final int count;
+    FacetBucket(Object val, int count) {
+      this.val = val;
+      this.count = count;
+    }
+
+    public Object getVal() { return val; }
+    public int getCount() { return count; }
+  }
+
+  private void assertHasFacetWithBucketValues(NamedList<Object> rawResponse, String expectedFacetName, FacetBucket... expectedBuckets) {
+    final NamedList<Object> facetsTopLevel = assertHasFacetResponse(rawResponse);
+    assertFacetResponseHasFacetWithBuckets(facetsTopLevel, expectedFacetName, expectedBuckets);
+  }
+
+  private void assertHasStatFacetWithValue(NamedList<Object> rawResponse, String expectedFacetName, Double expectedStatValue) {
+    final NamedList<Object> facetsTopLevel = assertHasFacetResponse(rawResponse);
+    assertFacetResponseHasStatFacetWithValue(facetsTopLevel, expectedFacetName, expectedStatValue);
+  }
+
+  private NamedList<Object> assertHasFacetResponse(NamedList<Object> topLevelResponse) {
+    Object o = topLevelResponse.get("facets");
+    if (o == null) fail("Response has no top-level 'facets' property as expected");
+    if (!(o instanceof NamedList)) fail("Response has a top-level 'facets' property, but it is not a NamedList");
+
+    return (NamedList<Object>) o;
+  }
+
+  private void assertFacetResponseHasFacetWithBuckets(NamedList<Object> facetResponse, String expectedFacetName, FacetBucket... expectedBuckets) {
+    Object o = facetResponse.get(expectedFacetName);
+    if (o == null) fail("Response has no top-level facet named '" + expectedFacetName + "'");
+    if (!(o instanceof NamedList)) fail("Response has a property for the expected facet '" + expectedFacetName + "' property, but it is not a NamedList");
+
+    final NamedList<Object> expectedFacetTopLevel = (NamedList<Object>) o;
+    o = expectedFacetTopLevel.get("buckets");
+    if (o == null) fail("Response has no 'buckets' property under 'facets'");
+    if (!(o instanceof List)) fail("Response has no 'buckets' property containing actual facet information.");
+
+    final List<NamedList> bucketList = (List<NamedList>) o;
+    assertEquals("Expected " + expectedBuckets.length + " buckets, but found " + bucketList.size(),
+        expectedBuckets.length, bucketList.size());
+    for (int i = 0; i < expectedBuckets.length; i++) {
+      final FacetBucket expectedBucket = expectedBuckets[i];
+      final NamedList<Object> actualBucket = bucketList.get(i);
+      assertEquals(expectedBucket.getVal(), actualBucket.get("val"));
+      assertEquals(expectedBucket.getCount(), actualBucket.get("count"));
+    }
+  }
+
+  private void assertFacetResponseHasStatFacetWithValue(NamedList<Object> facetResponse, String expectedFacetName, Double expectedStatValue) {
+    Object o = facetResponse.get(expectedFacetName);
+    if (o == null) fail("Response has no top-level facet named '" + expectedFacetName + "'");
+    if (!(o instanceof Number)) fail("Response has a property for the expected facet '" + expectedFacetName + "' property, but it is not a Number");
+
+    final Number actualStatValueAsNumber = (Number) o;
+    final Double actualStatValueAsDouble = ((Number) o).doubleValue();
+    assertEquals(expectedStatValue, actualStatValueAsDouble, 0.5);
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b502ba28/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/DomainMapTest.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/DomainMapTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/DomainMapTest.java
new file mode 100644
index 0000000..d437d14
--- /dev/null
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/DomainMapTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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.client.solrj.request.json;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.junit.Test;
+
+import static org.junit.internal.matchers.StringContains.containsString;
+
+public class DomainMapTest extends SolrTestCaseJ4 {
+
+  @Test
+  public void testRejectsInvalidFilters() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new DomainMap()
+          .withFilter(null);
+    });
+    assertThat(thrown.getMessage(), containsString("must be non-null"));
+  }
+
+  @Test
+  public void testStoresFilterWithCorrectKey() {
+    final DomainMap domain = new DomainMap()
+        .withFilter("name:Solr");
+    final List<String> filterList = (List<String>) domain.get("filter");
+
+    assertTrue("Expected filter list to contain provided filter", filterList.contains("name:Solr"));
+  }
+
+  @Test
+  public void testStoresMultipleFilters() {
+    final DomainMap domain = new DomainMap()
+        .withFilter("name:Solr")
+        .withFilter("cat:search");
+    final List<String> filterList = (List<String>) domain.get("filter");
+
+    assertTrue("Expected filter list to contain 1st provided filter", filterList.contains("name:Solr"));
+    assertTrue("Expected filter list to contain 2nd provided filter", filterList.contains("cat:search"));
+  }
+
+  @Test
+  public void testRejectsInvalidQueries() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new DomainMap()
+          .withQuery(null);
+    });
+    assertThat(thrown.getMessage(), containsString("must be non-null"));
+  }
+
+  @Test
+  public void testStoresQueryWithCorrectKey() {
+    final DomainMap domain = new DomainMap()
+        .withQuery("name:Solr");
+    final List<String> queryList = (List<String>) domain.get("query");
+
+    assertTrue("Expected query list to contain provided query", queryList.contains("name:Solr"));
+  }
+
+  @Test
+  public void testStoresMultipleQueries() {
+    final DomainMap domain = new DomainMap()
+        .withQuery("name:Solr")
+        .withQuery("cat:search");
+    final List<String> queryList = (List<String>) domain.get("query");
+
+    assertTrue("Expected query list to contain 1st provided query", queryList.contains("name:Solr"));
+    assertTrue("Expected query list to contain 2nd provided query", queryList.contains("cat:search"));
+  }
+
+  @Test
+  public void testRejectsInvalidTagsToExclude() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new DomainMap()
+          .withTagsToExclude(null);
+    });
+    assertThat(thrown.getMessage(), containsString("must be non-null"));
+  }
+
+  @Test
+  public void testStoresTagsToExcludeWithCorrectKey() {
+    final DomainMap domain = new DomainMap()
+        .withTagsToExclude("BRAND");
+    final List<String> exclusionList = (List<String>) domain.get("excludeTags");
+
+    assertTrue("Expected tag-exclusion list to contain provided tag", exclusionList.contains("BRAND"));
+  }
+
+  @Test
+  public void testStoresMultipleTagExclusionStrings() {
+    final DomainMap domain = new DomainMap()
+        .withTagsToExclude("BRAND")
+        .withTagsToExclude("COLOR");
+    final List<String> exclusionList = (List<String>) domain.get("excludeTags");
+
+    assertTrue("Expected tag-exclusion list to contain provided 1st tag", exclusionList.contains("BRAND"));
+    assertTrue("Expected tag-exclusion list to contain provided 2nd tag", exclusionList.contains("COLOR"));
+  }
+
+  @Test
+  public void testRejectsInvalidBlockParentQuery() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new DomainMap()
+          .setBlockParentQuery(null);
+    });
+    assertThat(thrown.getMessage(), containsString("must be non-null"));
+  }
+
+  @Test
+  public void testStoresBlockParentQueryWithCorrectKey() {
+    final DomainMap domain = new DomainMap()
+        .setBlockParentQuery("content_type:product");
+    assertEquals("content_type:product", domain.get("blockParent"));
+  }
+
+  @Test
+  public void testRejectsInvalidBlockChildrenQuery() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new DomainMap()
+          .setBlockChildQuery(null);
+    });
+    assertThat(thrown.getMessage(), containsString("must be non-null"));
+  }
+
+  @Test
+  public void testStoresBlockChildrenQueryWithCorrectKey() {
+    final DomainMap domain = new DomainMap()
+        .setBlockChildQuery("content_type:productColors");
+    assertEquals("content_type:productColors", domain.get("blockChildren"));
+  }
+
+  @Test
+  public void testRejectsInvalidJoinFromParam() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new DomainMap()
+          .setJoinTransformation(null, "valid-to-field");
+    });
+    assertThat(thrown.getMessage(), containsString("must be non-null"));
+  }
+
+  @Test
+  public void testRejectsInvalidJoinToParam() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new DomainMap()
+          .setJoinTransformation("valid-from-field", null);
+    });
+    assertThat(thrown.getMessage(), containsString("must be non-null"));
+  }
+
+  @Test
+  public void testStoresJoinValuesWithCorrectKey() {
+    final DomainMap domain = new DomainMap()
+        .setJoinTransformation("any-from-field", "any-to-field");
+
+    assertTrue(domain.containsKey("join"));
+    final Map<String, Object> joinParams = (Map<String, Object>) domain.get("join");
+    assertEquals("any-from-field", joinParams.get("from"));
+    assertEquals("any-to-field", joinParams.get("to"));
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b502ba28/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/HeatmapFacetMapTest.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/HeatmapFacetMapTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/HeatmapFacetMapTest.java
new file mode 100644
index 0000000..9063714
--- /dev/null
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/HeatmapFacetMapTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.client.solrj.request.json;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.junit.Test;
+
+import static org.junit.internal.matchers.StringContains.containsString;
+
+
+public class HeatmapFacetMapTest extends SolrTestCaseJ4 {
+
+  @Test
+  public void testRejectsInvalidFieldName() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new HeatmapFacetMap(null);
+    });
+    assertThat(thrown.getMessage(), containsString("must be non-null"));
+  }
+
+  @Test
+  public void testStoresFieldNameWithCorrectKey() {
+    final HeatmapFacetMap heatmapFacet = new HeatmapFacetMap("ANY_FIELD_NAME");
+    assertEquals("ANY_FIELD_NAME", heatmapFacet.get("field"));
+  }
+
+  @Test
+  public void testDoesntSupportSubfacets() {
+    final Throwable thrown = expectThrows(UnsupportedOperationException.class, () -> {
+      new HeatmapFacetMap("ANY_FIELD_NAME")
+          .withSubFacet("ANY_NAME", new TermsFacetMap("ANY_OTHER_FIELD_NAME"));
+    });
+    assertThat(thrown.getMessage(), containsString("doesn't currently support subfacets"));
+  }
+
+  @Test
+  public void testRejectsInvalidRegionQueries() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new HeatmapFacetMap("ANY_FIELD_NAME")
+          .setRegionQuery(null);
+    });
+    assertThat(thrown.getMessage(), containsString("must be non-null"));
+  }
+
+  @Test
+  public void testStoresRegionQueryWithCorrectKey() {
+    final HeatmapFacetMap heatmapFacet = new HeatmapFacetMap("ANY_FIELD_NAME")
+        .setRegionQuery("[-120,-35 TO 50,60]");
+    assertEquals("[-120,-35 TO 50,60]", heatmapFacet.get("geom"));
+  }
+
+  @Test
+  public void testRejectsInvalidCellSize() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new HeatmapFacetMap("ANY_FIELD_NAME")
+          .setGridLevel(0);
+    });
+    assertThat(thrown.getMessage(), containsString("must be a positive integer"));
+  }
+
+  @Test
+  public void testStoresCellSizeWithCorrectKey() {
+    final HeatmapFacetMap heatmapFacet = new HeatmapFacetMap("ANY_FIELD_NAME")
+        .setGridLevel(42);
+    assertEquals(42, heatmapFacet.get("gridLevel"));
+  }
+
+  @Test
+  public void testRejectsInvalidDistanceError() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new HeatmapFacetMap("ANY_FIELD_NAME")
+          .setDistErr(-1.0);
+    });
+    assertThat(thrown.getMessage(), containsString("must be non-negative"));
+  }
+
+  @Test
+  public void testStoresDistanceErrorWithCorrectKey() {
+    final HeatmapFacetMap heatmapFacet = new HeatmapFacetMap("ANY_FIELD_NAME")
+        .setDistErr(4.5);
+    assertEquals(4.5, heatmapFacet.get("distErr"));
+  }
+
+  @Test
+  public void testRejectsInvalidDistanceErrorPercentageWithCorrectKey() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new HeatmapFacetMap("ANY_FIELD_NAME")
+          .setDistErrPct(2.0);
+    });
+    assertThat(thrown.getMessage(), containsString("must be between 0.0 and 1.0"));
+  }
+
+  @Test
+  public void testStoresDistanceErrorPercentageWithCorrectKey() {
+    final HeatmapFacetMap heatmapFacet = new HeatmapFacetMap("ANY_FIELD_NAME")
+        .setDistErrPct(0.45);
+    assertEquals(0.45, heatmapFacet.get("distErrPct"));
+  }
+
+  @Test
+  public void testRejectsInvalidHeatmapFormat() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new HeatmapFacetMap("ANY_FIELD_NAME")
+          .setHeatmapFormat(null);
+    });
+    assertThat(thrown.getMessage(), containsString("must be non-null"));
+  }
+
+  @Test
+  public void testStoresHeatmapFormatWithCorrectKey() {
+    final HeatmapFacetMap heatmapFacet = new HeatmapFacetMap("ANY_FIELD_NAME")
+        .setHeatmapFormat(HeatmapFacetMap.HeatmapFormat.PNG);
+    assertEquals("png", heatmapFacet.get("format"));
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b502ba28/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestFacetingIntegrationTest.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestFacetingIntegrationTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestFacetingIntegrationTest.java
new file mode 100644
index 0000000..7717f7f
--- /dev/null
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestFacetingIntegrationTest.java
@@ -0,0 +1,530 @@
+/*
+ * 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.client.solrj.request.json;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.solr.client.solrj.request.AbstractUpdateRequest;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
+import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.client.solrj.response.UpdateResponse;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.MapWriter;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.util.ExternalPaths;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class JsonQueryRequestFacetingIntegrationTest extends SolrCloudTestCase {
+
+  private static final String COLLECTION_NAME = "techproducts";
+  private static final String CONFIG_NAME = "techproducts_config";
+  private static final int NUM_TECHPRODUCTS_DOCS = 32;
+  private static final int NUM_IN_STOCK = 17;
+  private static final int NUM_ELECTRONICS = 12;
+  private static final int NUM_CURRENCY = 4;
+  private static final int NUM_MEMORY = 3;
+  private static final int NUM_CORSAIR = 3;
+  private static final int NUM_BELKIN = 2;
+  private static final int NUM_CANON = 2;
+
+  @BeforeClass
+  public static void setupCluster() throws Exception {
+    configureCluster(1)
+        .addConfig(CONFIG_NAME, new File(ExternalPaths.TECHPRODUCTS_CONFIGSET).toPath())
+        .configure();
+
+    final List<String> solrUrls = new ArrayList<>();
+    solrUrls.add(cluster.getJettySolrRunner(0).getBaseUrl().toString());
+
+    CollectionAdminRequest.createCollection(COLLECTION_NAME, CONFIG_NAME, 1, 1).process(cluster.getSolrClient());
+
+    ContentStreamUpdateRequest up = new ContentStreamUpdateRequest("/update");
+    up.setParam("collection", COLLECTION_NAME);
+    up.addFile(getFile("solrj/techproducts.xml"), "application/xml");
+    up.setAction(AbstractUpdateRequest.ACTION.COMMIT, true, true);
+    UpdateResponse updateResponse = up.process(cluster.getSolrClient());
+    assertEquals(0, updateResponse.getStatus());
+  }
+
+  @Test
+  public void testSingleTermsFacet() throws Exception {
+    final TermsFacetMap categoriesFacetMap = new TermsFacetMap("cat")
+        .setLimit(3);
+    final JsonQueryRequest request = new JsonQueryRequest()
+        .setQuery("*:*")
+        .withFacet("top_cats", categoriesFacetMap);
+
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    assertHasFacetWithBucketValues(rawResponse,"top_cats",
+        new FacetBucket("electronics",NUM_ELECTRONICS),
+        new FacetBucket("currency", NUM_CURRENCY),
+        new FacetBucket("memory", NUM_MEMORY));
+  }
+
+  @Test
+  public void testFacetCanBeRepresentedByMapWriter() throws Exception {
+    final MapWriter categoriesFacet = new MapWriter() {
+      @Override
+      public void writeMap(EntryWriter ew) throws IOException {
+        ew.put("type", "terms");
+        ew.put("field", "cat");
+        ew.put("limit", 3);
+      }
+    };
+    final JsonQueryRequest request = new JsonQueryRequest()
+        .setQuery("*:*")
+        .withFacet("top_cats", categoriesFacet);
+
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    assertHasFacetWithBucketValues(rawResponse,"top_cats",
+        new FacetBucket("electronics",NUM_ELECTRONICS),
+        new FacetBucket("currency", NUM_CURRENCY),
+        new FacetBucket("memory", NUM_MEMORY));
+  }
+
+  @Test
+  public void testMultiTermsFacet() throws Exception {
+    final TermsFacetMap categoriesFacetMap = new TermsFacetMap("cat")
+        .setLimit(3);
+    final TermsFacetMap manufacturersFacetMap = new TermsFacetMap("manu_id_s")
+        .setLimit(3);
+    final JsonQueryRequest request = new JsonQueryRequest()
+        .setQuery("*:*")
+        .withFacet("top_cats", categoriesFacetMap)
+        .withFacet("top_manufacturers", manufacturersFacetMap);
+
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    assertHasFacetWithBucketValues(rawResponse,"top_cats",
+        new FacetBucket("electronics",NUM_ELECTRONICS),
+        new FacetBucket("currency", NUM_CURRENCY),
+        new FacetBucket("memory", NUM_MEMORY));
+    assertHasFacetWithBucketValues(rawResponse,"top_manufacturers",
+        new FacetBucket("corsair",NUM_CORSAIR),
+        new FacetBucket("belkin", NUM_BELKIN),
+        new FacetBucket("canon", NUM_CANON));
+  }
+
+  @Test
+  public void testSingleRangeFacet() throws Exception {
+    final RangeFacetMap pricesFacet = new RangeFacetMap("price", 0, 100, 20);
+    final JsonQueryRequest request = new JsonQueryRequest()
+        .setQuery("*:*")
+        .withFacet("prices", pricesFacet);
+
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    assertHasFacetWithBucketValues(rawResponse,"prices",
+        new FacetBucket(0.0f, 5),
+        new FacetBucket(20.0f, 0),
+        new FacetBucket(40.0f, 0),
+        new FacetBucket(60.0f, 1),
+        new FacetBucket(80.0f, 1));
+  }
+
+  @Test
+  public void testMultiRangeFacet() throws Exception {
+    final RangeFacetMap pricesFacet = new RangeFacetMap("price", 0, 100, 20);
+    final RangeFacetMap shippingWeightFacet = new RangeFacetMap("weight", 0, 200, 50);
+    final JsonQueryRequest request = new JsonQueryRequest()
+        .setQuery("*:*")
+        .withFacet("prices", pricesFacet)
+        .withFacet("shipping_weights", shippingWeightFacet);
+
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    assertHasFacetWithBucketValues(rawResponse,"prices",
+        new FacetBucket(0.0f, 5),
+        new FacetBucket(20.0f, 0),
+        new FacetBucket(40.0f, 0),
+        new FacetBucket(60.0f, 1),
+        new FacetBucket(80.0f, 1));
+    assertHasFacetWithBucketValues(rawResponse, "shipping_weights",
+        new FacetBucket(0.0f, 6),
+        new FacetBucket(50.0f, 0),
+        new FacetBucket(100.0f, 0),
+        new FacetBucket(150.0f,1));
+  }
+
+  @Test
+  public void testSingleStatFacet() throws Exception {
+    final JsonQueryRequest request = new JsonQueryRequest()
+        .setQuery("*:*")
+        .withStatFacet("sum_price", "sum(price)");
+
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    assertHasStatFacetWithValue(rawResponse,"sum_price", 5251.270030975342);
+  }
+
+  @Test
+  public void testMultiStatFacet() throws Exception {
+    final JsonQueryRequest request = new JsonQueryRequest()
+        .setQuery("*:*")
+        .withStatFacet("sum_price", "sum(price)")
+        .withStatFacet("avg_price", "avg(price)");
+
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    assertHasStatFacetWithValue(rawResponse,"sum_price", 5251.270030975342);
+    assertHasStatFacetWithValue(rawResponse,"avg_price", 328.20437693595886);
+  }
+
+  @Test
+  public void testMultiFacetsMixedTypes() throws Exception {
+    final TermsFacetMap categoryFacet = new TermsFacetMap("cat")
+        .setLimit(3);
+    final JsonQueryRequest request = new JsonQueryRequest()
+        .setQuery("*:*")
+        .withStatFacet("avg_price", "avg(price)")
+        .withFacet("top_cats", categoryFacet);
+
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    assertHasStatFacetWithValue(rawResponse,"avg_price", 328.20437693595886);
+    assertHasFacetWithBucketValues(rawResponse,"top_cats", new FacetBucket("electronics",NUM_ELECTRONICS),
+        new FacetBucket("currency", NUM_CURRENCY), new FacetBucket("memory", NUM_MEMORY));
+  }
+
+  @Test
+  public void testNestedTermsFacet() throws Exception {
+    final TermsFacetMap categoriesFacet = new TermsFacetMap("cat")
+        .setLimit(3)
+        .withSubFacet("top_manufacturers_for_cat", new TermsFacetMap("manu_id_s").setLimit(1));
+    final JsonQueryRequest request = new JsonQueryRequest()
+        .setQuery("*:*")
+        .withFacet("top_cats", categoriesFacet);
+
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    // Test top level facets
+    assertHasFacetWithBucketValues(rawResponse,"top_cats", new FacetBucket("electronics",NUM_ELECTRONICS),
+        new FacetBucket("currency", NUM_CURRENCY), new FacetBucket("memory", NUM_MEMORY));
+    // Test subfacet values for each top-level facet bucket
+    final List<NamedList<Object>> topLevelFacetResponse = (List<NamedList<Object>>) rawResponse.findRecursive("facets", "top_cats", "buckets");
+    final NamedList<Object> electronicsSubFacet = topLevelFacetResponse.get(0);
+    assertFacetResponseHasFacetWithBuckets(electronicsSubFacet, "top_manufacturers_for_cat", new FacetBucket("corsair", 3));
+    final NamedList<Object> currencySubfacet = topLevelFacetResponse.get(1);
+    assertFacetResponseHasFacetWithBuckets(currencySubfacet, "top_manufacturers_for_cat", new FacetBucket("boa", 1));
+    final NamedList<Object> memorySubfacet = topLevelFacetResponse.get(2);
+    assertFacetResponseHasFacetWithBuckets(memorySubfacet, "top_manufacturers_for_cat", new FacetBucket("corsair", 3));
+  }
+
+  @Test
+  public void testNestedFacetsOfMixedTypes() throws Exception {
+    final String subfacetName = "avg_price_for_cat";
+
+    final TermsFacetMap categoriesFacet = new TermsFacetMap("cat")
+        .setLimit(3)
+        .withStatSubFacet(subfacetName, "avg(price)");
+    final JsonQueryRequest request = new JsonQueryRequest()
+        .setQuery("*:*")
+        .withFacet("top_cats", categoriesFacet);
+
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    // Test top level facets
+    assertHasFacetWithBucketValues(rawResponse,"top_cats", new FacetBucket("electronics",NUM_ELECTRONICS),
+        new FacetBucket("currency", NUM_CURRENCY), new FacetBucket("memory", NUM_MEMORY));
+    // Test subfacet values for each top-level facet bucket
+    final List<NamedList<Object>> topLevelFacetResponse = (List<NamedList<Object>>) rawResponse.findRecursive("facets", "top_cats", "buckets");
+    final NamedList<Object> electronicsSubFacet = topLevelFacetResponse.get(0);
+    assertFacetResponseHasStatFacetWithValue(electronicsSubFacet, subfacetName, 252.02909261530095);
+    final NamedList<Object> currencySubfacet = topLevelFacetResponse.get(1);
+    assertFacetResponseHasStatFacetWithValue(currencySubfacet, subfacetName, 0.0);
+    final NamedList<Object> memorySubfacet = topLevelFacetResponse.get(2);
+    assertFacetResponseHasStatFacetWithValue(memorySubfacet, subfacetName, 129.99499893188477);
+  }
+
+  @Test
+  public void testFacetWithDomainFilteredBySimpleQueryString() throws Exception {
+    final TermsFacetMap popularCategoriesFacet = new TermsFacetMap("cat")
+        .setLimit(3)
+        .withDomain(new DomainMap()
+            .withFilter("popularity:[5 TO 10]"));
+    final JsonQueryRequest request = new JsonQueryRequest()
+        .setQuery("*:*")
+        .withFacet("top_popular_cats", popularCategoriesFacet);
+
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    assertHasFacetWithBucketValues(rawResponse,"top_popular_cats", new FacetBucket("electronics",9),
+        new FacetBucket("graphics card", 2), new FacetBucket("hard drive", 2));
+  }
+
+  @Test
+  public void testFacetWithDomainFilteredByLocalParamsQueryString() throws Exception {
+    final TermsFacetMap popularCategoriesFacet = new TermsFacetMap("cat")
+        .setLimit(3)
+        .withDomain(new DomainMap()
+            .withFilter("{!lucene df=\"popularity\" v=\"[5 TO 10]\"}"));
+
+    JsonQueryRequest request = new JsonQueryRequest()
+        .setQuery("*:*")
+        .withFacet("top_popular_cats", popularCategoriesFacet);
+
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_TECHPRODUCTS_DOCS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    assertHasFacetWithBucketValues(rawResponse,"top_popular_cats", new FacetBucket("electronics",9),
+        new FacetBucket("graphics card", 2), new FacetBucket("hard drive", 2));
+  }
+
+  @Test
+  public void testFacetWithArbitraryDomainFromQueryString() throws Exception {
+    final TermsFacetMap categoriesFacet = new TermsFacetMap("cat")
+        .setLimit(3)
+        .withDomain(new DomainMap()
+            .withQuery("*:*"));
+    final JsonQueryRequest request = new JsonQueryRequest()
+        .setQuery("cat:electronics")
+        .withFacet("top_cats", categoriesFacet);
+
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_ELECTRONICS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    assertHasFacetWithBucketValues(rawResponse,"top_cats", new FacetBucket("electronics",NUM_ELECTRONICS),
+        new FacetBucket("currency", NUM_CURRENCY), new FacetBucket("memory", NUM_MEMORY));
+  }
+
+  @Test
+  public void testFacetWithArbitraryDomainFromLocalParamsQuery() throws Exception {
+    final TermsFacetMap searchCategoriesFacet = new TermsFacetMap("cat")
+        .withDomain(new DomainMap()
+            .withQuery("{!lucene df=\"cat\" v=\"search\"}"));
+    final JsonQueryRequest request = new JsonQueryRequest()
+        .setQuery("cat:electronics")
+        .withFacet("largest_search_cats", searchCategoriesFacet);
+
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_ELECTRONICS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    assertHasFacetWithBucketValues(rawResponse,"largest_search_cats",
+        new FacetBucket("search",2),
+        new FacetBucket("software", 2));
+  }
+
+  public void testFacetWithMultipleSimpleQueryClausesInArbitraryDomain() throws Exception {
+    final TermsFacetMap solrCategoriesFacet = new TermsFacetMap("cat")
+        .withDomain(new DomainMap()
+            .withQuery("cat:search")
+            .withQuery("name:Solr"));
+    final JsonQueryRequest request = new JsonQueryRequest()
+        .setQuery("cat:electronics")
+        .withFacet("cats_matching_solr", solrCategoriesFacet);
+
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_ELECTRONICS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    assertHasFacetWithBucketValues(rawResponse,"cats_matching_solr",
+        new FacetBucket("search",1),
+        new FacetBucket("software", 1));
+  }
+
+  public void testFacetWithMultipleLocalParamsQueryClausesInArbitraryDomain() throws Exception {
+    final TermsFacetMap solrCategoriesFacet = new TermsFacetMap("cat")
+        .withDomain(new DomainMap()
+            .withQuery("{!lucene df=\"cat\" v=\"search\"}")
+            .withQuery("{!lucene df=\"name\" v=\"Solr\"}"));
+    final JsonQueryRequest request = new JsonQueryRequest()
+        .setQuery("cat:electronics")
+        .withFacet("cats_matching_solr", solrCategoriesFacet);
+
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_ELECTRONICS, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    assertHasFacetWithBucketValues(rawResponse,"cats_matching_solr",
+        new FacetBucket("search",1),
+        new FacetBucket("software", 1));
+  }
+
+  @Test
+  public void testFacetWithDomainWidenedUsingExcludeTagsToIgnoreFilters() throws Exception {
+    final TermsFacetMap inStockFacet = new TermsFacetMap("cat")
+        .setLimit(2);
+    final TermsFacetMap allProductsFacet = new TermsFacetMap("cat")
+        .setLimit(2).withDomain(new DomainMap().withTagsToExclude("on_shelf"));
+    final Map<String, Object> taggedFilterMap = new HashMap<>();
+    taggedFilterMap.put("#on_shelf", "inStock:true");
+    final JsonQueryRequest request = new JsonQueryRequest()
+        .setQuery("*:*")
+        .withFilter(taggedFilterMap)
+        .withFacet("in_stock_only", inStockFacet)
+        .withFacet("all", allProductsFacet);
+
+    QueryResponse response = request.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, response.getStatus());
+    final SolrDocumentList returnedDocs = response.getResults();
+    assertEquals(NUM_IN_STOCK, returnedDocs.getNumFound());
+    assertEquals(10, returnedDocs.size());
+    final NamedList<Object> rawResponse = response.getResponse();
+    assertHasFacetWithBucketValues(rawResponse,"in_stock_only",
+        new FacetBucket("electronics",8),
+        new FacetBucket("currency", 4));
+    assertHasFacetWithBucketValues(rawResponse,"all",
+        new FacetBucket("electronics",12),
+        new FacetBucket("currency", 4));
+  }
+
+  private class FacetBucket {
+    private final Object val;
+    private final int count;
+    FacetBucket(Object val, int count) {
+      this.val = val;
+      this.count = count;
+    }
+
+    public Object getVal() { return val; }
+    public int getCount() { return count; }
+  }
+
+  private void assertHasFacetWithBucketValues(NamedList<Object> rawResponse, String expectedFacetName, FacetBucket... expectedBuckets) {
+    final NamedList<Object> facetsTopLevel = assertHasFacetResponse(rawResponse);
+    assertFacetResponseHasFacetWithBuckets(facetsTopLevel, expectedFacetName, expectedBuckets);
+  }
+
+  private void assertHasStatFacetWithValue(NamedList<Object> rawResponse, String expectedFacetName, Double expectedStatValue) {
+    final NamedList<Object> facetsTopLevel = assertHasFacetResponse(rawResponse);
+    assertFacetResponseHasStatFacetWithValue(facetsTopLevel, expectedFacetName, expectedStatValue);
+  }
+
+  private NamedList<Object> assertHasFacetResponse(NamedList<Object> topLevelResponse) {
+    Object o = topLevelResponse.get("facets");
+    if (o == null) fail("Response has no top-level 'facets' property as expected");
+    if (!(o instanceof NamedList)) fail("Response has a top-level 'facets' property, but it is not a NamedList");
+
+    return (NamedList<Object>) o;
+  }
+
+  private void assertFacetResponseHasFacetWithBuckets(NamedList<Object> facetResponse, String expectedFacetName, FacetBucket... expectedBuckets) {
+    Object o = facetResponse.get(expectedFacetName);
+    if (o == null) fail("Response has no top-level facet named '" + expectedFacetName + "'");
+    if (!(o instanceof NamedList)) fail("Response has a property for the expected facet '" + expectedFacetName + "' property, but it is not a NamedList");
+
+    final NamedList<Object> expectedFacetTopLevel = (NamedList<Object>) o;
+    o = expectedFacetTopLevel.get("buckets");
+    if (o == null) fail("Response has no 'buckets' property under 'facets'");
+    if (!(o instanceof List)) fail("Response has no 'buckets' property containing actual facet information.");
+
+    final List<NamedList> bucketList = (List<NamedList>) o;
+    assertEquals("Expected " + expectedBuckets.length + " buckets, but found " + bucketList.size(),
+        expectedBuckets.length, bucketList.size());
+    for (int i = 0; i < expectedBuckets.length; i++) {
+      final FacetBucket expectedBucket = expectedBuckets[i];
+      final NamedList<Object> actualBucket = bucketList.get(i);
+      assertEquals(expectedBucket.getVal(), actualBucket.get("val"));
+      assertEquals(expectedBucket.getCount(), actualBucket.get("count"));
+    }
+  }
+
+  private void assertFacetResponseHasStatFacetWithValue(NamedList<Object> facetResponse, String expectedFacetName, Double expectedStatValue) {
+    Object o = facetResponse.get(expectedFacetName);
+    if (o == null) fail("Response has no top-level facet named '" + expectedFacetName + "'");
+    if (!(o instanceof Number)) fail("Response has a property for the expected facet '" + expectedFacetName + "' property, but it is not a Number");
+
+    final Number actualStatValueAsNumber = (Number) o;
+    final Double actualStatValueAsDouble = ((Number) o).doubleValue();
+    assertEquals(expectedStatValue, actualStatValueAsDouble, 0.5);
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b502ba28/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestUnitTest.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestUnitTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestUnitTest.java
index 84ef956..6e8c647 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestUnitTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestUnitTest.java
@@ -25,7 +25,6 @@ import java.util.Map;
 
 import org.apache.lucene.util.LuceneTestCase;
 import org.apache.solr.client.solrj.request.RequestWriter;
-import org.apache.solr.client.solrj.request.json.JsonQueryRequest;
 import org.apache.solr.client.solrj.util.ClientUtils;
 import org.apache.solr.common.MapWriter;
 import org.junit.Test;
@@ -98,6 +97,91 @@ public class JsonQueryRequestUnitTest extends LuceneTestCase {
   }
 
   @Test
+  public void testRejectsInvalidFacetName() {
+    Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new JsonQueryRequest().withFacet(null, new HashMap<>());
+    });
+    assertThat(thrown.getMessage(),containsString("must be non-null"));
+
+    thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new JsonQueryRequest().withStatFacet(null, "avg(price)");
+    });
+    assertThat(thrown.getMessage(),containsString("must be non-null"));
+  }
+
+  @Test
+  public void testRejectsInvalidFacetMap() {
+    Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new JsonQueryRequest().withFacet("anyFacetName", (Map<String, Object>)null);
+    });
+    assertThat(thrown.getMessage(),containsString("must be non-null"));
+  }
+
+  @Test
+  public void testRejectsNullFacetMapWriter() {
+    Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new JsonQueryRequest().withFacet("anyFacetName", (MapWriter)null);
+    });
+    assertThat(thrown.getMessage(),containsString("must be non-null"));
+  }
+
+  @Test
+  public void testRejectsInvalidStatFacetString() {
+    Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new JsonQueryRequest().withStatFacet("anyFacetName", (String)null);
+    });
+    assertThat(thrown.getMessage(),containsString("must be non-null"));
+  }
+
+  @Test
+  public void testWritesProvidedFacetMapToJsonCorrectly() {
+    final Map<String, Object> categoryFacetMap = new HashMap<>();
+    categoryFacetMap.put("type", "terms");
+    categoryFacetMap.put("field", "category");
+    final JsonQueryRequest request = new JsonQueryRequest().withFacet("top_categories", categoryFacetMap);
+    final String requestBody = writeRequestToJson(request);
+    assertThat(requestBody, containsString("\"facet\":{\"top_categories\":{\"field\":\"category\",\"type\":\"terms\"}}"));
+  }
+
+  @Test
+  public void testWritesProvidedFacetMapWriterToJsonCorrectly() {
+    final MapWriter facetWriter = new MapWriter() {
+      @Override
+      public void writeMap(EntryWriter ew) throws IOException {
+        ew.put("type", "terms");
+        ew.put("field", "category");
+      }
+    };
+    final JsonQueryRequest request = new JsonQueryRequest().withFacet("top_categories", facetWriter);
+    final String requestBody = writeRequestToJson(request);
+    assertThat(requestBody, containsString("\"facet\":{\"top_categories\":{\"type\":\"terms\",\"field\":\"category\"}}"));
+  }
+
+  @Test
+  public void testWritesProvidedStatFacetToJsonCorrectly() {
+    final JsonQueryRequest request = new JsonQueryRequest().withStatFacet("avg_price", "avg(price)");
+    final String requestBody = writeRequestToJson(request);
+    assertThat(requestBody, containsString("\"facet\":{\"avg_price\":\"avg(price)\"}"));
+  }
+
+  @Test
+  public void testWritesMultipleFacetMapsToJsonCorrectly() {
+    final Map<String, Object> facetMap1 = new HashMap<>();
+    facetMap1.put("type", "terms");
+    facetMap1.put("field", "a");
+    final Map<String, Object> facetMap2 = new HashMap<>();
+    facetMap2.put("type", "terms");
+    facetMap2.put("field", "b");
+    final JsonQueryRequest request = new JsonQueryRequest();
+
+    request.withFacet("facet1", facetMap1);
+    request.withFacet("facet2", facetMap2);
+    final String requestBody = writeRequestToJson(request);
+
+    assertThat(requestBody, containsString("\"facet\":{\"facet2\":{\"field\":\"b\",\"type\":\"terms\"},\"facet1\":{\"field\":\"a\",\"type\":\"terms\"}}"));
+  }
+
+  @Test
   public void testRejectsInvalidLimit() {
     Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
       new JsonQueryRequest().setLimit(-1);

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b502ba28/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/QueryFacetMapTest.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/QueryFacetMapTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/QueryFacetMapTest.java
new file mode 100644
index 0000000..8d07a4f
--- /dev/null
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/QueryFacetMapTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.client.solrj.request.json;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.junit.Test;
+
+import static org.junit.internal.matchers.StringContains.containsString;
+
+public class QueryFacetMapTest extends SolrTestCaseJ4 {
+  @Test
+  public void testSetsFacetTypeToQuery() {
+    final QueryFacetMap queryFacet = new QueryFacetMap("any:query");
+    assertEquals("query", queryFacet.get("type"));
+  }
+
+  @Test
+  public void testRejectsInvalidQueryString() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      final QueryFacetMap queryFacet = new QueryFacetMap(null);
+    });
+    assertThat(thrown.getMessage(), containsString("must be non-null"));
+  }
+
+  @Test
+  public void testSetsQueryWithCorrectKey() {
+    final QueryFacetMap queryFacet = new QueryFacetMap("any:query");
+    assertEquals("any:query", queryFacet.get("q"));
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b502ba28/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/RangeFacetMapTest.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/RangeFacetMapTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/RangeFacetMapTest.java
new file mode 100644
index 0000000..43bb5f5
--- /dev/null
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/RangeFacetMapTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.client.solrj.request.json;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.junit.Test;
+
+import static org.junit.internal.matchers.StringContains.containsString;
+
+/**
+ * Unit tests for {@link RangeFacetMap}
+ */
+public class RangeFacetMapTest extends SolrTestCaseJ4 {
+  @Test
+  public void testRejectsInvalidFieldName() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new RangeFacetMap(null, 1, 2, 3);
+    });
+    assertThat(thrown.getMessage(), containsString("must be non-null"));
+  }
+
+  @Test
+  public void testRejectsInvalidStartEndBounds() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new RangeFacetMap("ANY_FIELD_NAME", 1, -1, 3);
+    });
+    assertThat(thrown.getMessage(), containsString("'end' must be greater than parameter 'start'"));
+  }
+
+  @Test
+  public void testRejectsInvalidGap() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new RangeFacetMap("ANY_FIELD_NAME", 1, 2, -1);
+    });
+    assertThat(thrown.getMessage(), containsString("must be a positive integer"));
+  }
+
+  @Test
+  public void testStoresRequiredValuesWithCorrectKeys() {
+    final RangeFacetMap rangeFacet = new RangeFacetMap("ANY_FIELD_NAME", 1, 2, 3);
+    assertEquals("ANY_FIELD_NAME", rangeFacet.get("field"));
+    assertEquals(1L, rangeFacet.get("start"));
+    assertEquals(2L, rangeFacet.get("end"));
+    assertEquals(3L, rangeFacet.get("gap"));
+  }
+
+  @Test
+  public void testStoresHardEndWithCorrectKey() {
+    final RangeFacetMap rangeFacet = new RangeFacetMap("ANY_FIELD_NAME", 1, 2, 3)
+        .setHardEnd(true);
+    assertEquals(true, rangeFacet.get("hardend"));
+  }
+
+  @Test
+  public void testRejectsInvalidOtherBuckets() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new RangeFacetMap("ANY_FIELD_NAME", 1, 2, 3)
+          .setOtherBuckets(null);
+    });
+    assertThat(thrown.getMessage(), containsString("must be non-null"));
+  }
+
+  @Test
+  public void testStoresOtherBucketsValueWithCorrectKey() {
+    final RangeFacetMap rangeFacet = new RangeFacetMap("ANY_FIELD_NAME", 1, 2, 3)
+        .setOtherBuckets(RangeFacetMap.OtherBuckets.BETWEEN);
+    assertEquals("between", rangeFacet.get("other"));
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b502ba28/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/TermsFacetMapTest.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/TermsFacetMapTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/TermsFacetMapTest.java
new file mode 100644
index 0000000..58f807d
--- /dev/null
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/TermsFacetMapTest.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.client.solrj.request.json;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.junit.Test;
+import static org.junit.internal.matchers.StringContains.containsString;
+
+
+public class TermsFacetMapTest extends SolrTestCaseJ4 {
+  private static final String ANY_FIELD_NAME = "ANY_FIELD_NAME";
+
+  @Test
+  public void testSetsFacetTypeToTerm() {
+    final TermsFacetMap termsFacet = new TermsFacetMap(ANY_FIELD_NAME);
+    assertEquals("terms", termsFacet.get("type"));
+  }
+
+  @Test
+  public void testStoresFieldWithCorrectKey() {
+    final TermsFacetMap termsFacet = new TermsFacetMap(ANY_FIELD_NAME);
+    assertEquals(ANY_FIELD_NAME, termsFacet.get("field"));
+  }
+
+  @Test
+  public void testRejectsNegativeBucketOffset() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      final TermsFacetMap termsFacet = new TermsFacetMap(ANY_FIELD_NAME)
+        .setBucketOffset(-1);
+    });
+    assertThat(thrown.getMessage(), containsString("must be non-negative"));
+  }
+
+  @Test
+  public void testStoresBucketOffsetWithCorrectKey() {
+    final TermsFacetMap termsFacet = new TermsFacetMap(ANY_FIELD_NAME)
+        .setBucketOffset(2);
+    assertEquals(2, termsFacet.get("offset"));
+
+  }
+
+  @Test
+  public void testRejectsNegativeBucketLimit() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      final TermsFacetMap termsFacet = new TermsFacetMap(ANY_FIELD_NAME)
+          .setLimit(-1);
+    });
+    assertThat(thrown.getMessage(), containsString("must be non-negative"));
+  }
+
+  @Test
+  public void testStoresBucketLimitWithCorrectKey() {
+    final TermsFacetMap termsFacet = new TermsFacetMap(ANY_FIELD_NAME)
+        .setLimit(3);
+    assertEquals(3, termsFacet.get("limit"));
+  }
+
+  @Test
+  public void testRejectsInvalidSortString() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      final TermsFacetMap termsFacet = new TermsFacetMap(ANY_FIELD_NAME)
+          .setSort(null);
+    });
+    assertThat(thrown.getMessage(), containsString("must be non-null"));
+  }
+
+  @Test
+  public void testStoresSortWithCorrectKey() {
+    final TermsFacetMap termsFacet = new TermsFacetMap(ANY_FIELD_NAME)
+        .setSort("price asc");
+    assertEquals("price asc", termsFacet.get("sort"));
+  }
+
+  @Test
+  public void testRejectInvalidOverRequestBuckets() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      final TermsFacetMap termsFacet = new TermsFacetMap(ANY_FIELD_NAME)
+          .setOverRequest(-2);
+    });
+    assertThat(thrown.getMessage(), containsString("must be >= -1"));
+  }
+
+  @Test
+  public void testStoresOverRequestBucketsWithCorrectKey() {
+    final TermsFacetMap termsFacet = new TermsFacetMap(ANY_FIELD_NAME)
+        .setOverRequest(4);
+    assertEquals(4, termsFacet.get("overrequest"));
+  }
+
+  @Test
+  public void testStoresRefinementFlagWithCorrectKey() {
+    final TermsFacetMap termsFacet = new TermsFacetMap(ANY_FIELD_NAME)
+        .useDistributedFacetRefining(true);
+    assertEquals(true, termsFacet.get("refine"));
+  }
+
+  @Test
+  public void testRejectInvalidOverRefineBuckets() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      final TermsFacetMap termsFacet = new TermsFacetMap(ANY_FIELD_NAME)
+          .setOverRefine(-2);
+    });
+    assertThat(thrown.getMessage(), containsString("must be >= -1"));
+  }
+
+  @Test
+  public void testStoresOverRefineBucketsWithCorrectKey() {
+    final TermsFacetMap termsFacet = new TermsFacetMap(ANY_FIELD_NAME)
+        .setOverRefine(5);
+    assertEquals(5, termsFacet.get("overrefine"));
+  }
+
+  @Test
+  public void testRejectInvalidMinCount() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      final TermsFacetMap termsFacet = new TermsFacetMap(ANY_FIELD_NAME)
+          .setMinCount(0);
+    });
+    assertThat(thrown.getMessage(), containsString("must be a positive integer"));
+  }
+
+  @Test
+  public void testStoresMinCountWithCorrectKey() {
+    final TermsFacetMap termsFacet = new TermsFacetMap(ANY_FIELD_NAME)
+        .setMinCount(6);
+    assertEquals(6, termsFacet.get("mincount"));
+  }
+
+  @Test
+  public void testStoresNumBucketsFlagWithCorrectKey() {
+    final TermsFacetMap termsFacet = new TermsFacetMap(ANY_FIELD_NAME)
+        .includeTotalNumBuckets(true);
+    assertEquals(true, termsFacet.get("numBuckets"));
+  }
+
+  @Test
+  public void testStoresAllBucketsFlagWithCorrectKey() {
+    final TermsFacetMap termsFacet = new TermsFacetMap(ANY_FIELD_NAME)
+        .includeAllBucketsUnionBucket(true);
+    assertEquals(true, termsFacet.get("allBuckets"));
+  }
+
+  @Test
+  public void testRejectInvalidTermPrefix() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      final TermsFacetMap termsFacet = new TermsFacetMap(ANY_FIELD_NAME)
+          .setTermPrefix(null);
+    });
+    assertThat(thrown.getMessage(), containsString("must be non-null"));
+  }
+
+  @Test
+  public void testStoresTermPrefixWithCorrectKey() {
+    final TermsFacetMap termsFacet = new TermsFacetMap(ANY_FIELD_NAME)
+        .setTermPrefix("ANY_PREF");
+    assertEquals("ANY_PREF", termsFacet.get("prefix"));
+  }
+
+  @Test
+  public void testRejectsInvalidMethod() {
+    final Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      final TermsFacetMap termsFacet = new TermsFacetMap(ANY_FIELD_NAME)
+          .setFacetMethod(null);
+    });
+    assertThat(thrown.getMessage(), containsString("must be non-null"));
+  }
+
+  @Test
+  public void testStoresMethodWithCorrectKey() {
+    final TermsFacetMap termsFacet = new TermsFacetMap(ANY_FIELD_NAME)
+        .setFacetMethod(TermsFacetMap.FacetMethod.STREAM);
+    assertEquals("stream", termsFacet.get("method"));
+  }
+}


[2/2] lucene-solr:branch_7x: SOLR-12965: Add facet support to JsonQueryRequest

Posted by ge...@apache.org.
SOLR-12965: Add facet support to JsonQueryRequest


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

Branch: refs/heads/branch_7x
Commit: b502ba2882a86958ef8769c3cb2fd65cf2d9c7e1
Parents: 6faddfe
Author: Jason Gerlowski <ge...@apache.org>
Authored: Sat Nov 10 19:48:50 2018 -0500
Committer: Jason Gerlowski <ge...@apache.org>
Committed: Wed Nov 14 15:19:06 2018 -0500

----------------------------------------------------------------------
 solr/solr-ref-guide/src/json-facet-api.adoc     |  36 ++
 .../client/solrj/request/json/DomainMap.java    | 139 +++++
 .../solrj/request/json/HeatmapFacetMap.java     | 137 +++++
 .../client/solrj/request/json/JsonFacetMap.java |  62 ++
 .../solrj/request/json/JsonQueryRequest.java    | 109 ++++
 .../solrj/request/json/QueryFacetMap.java       |  39 ++
 .../solrj/request/json/RangeFacetMap.java       | 105 ++++
 .../solrj/request/json/TermsFacetMap.java       | 204 ++++++
 .../solrj/src/test-files/solrj/techproducts.xml | 421 +++++++++++++
 .../ref_guide_examples/JsonRequestApiTest.java  |  98 ++-
 ...JsonQueryRequestFacetingIntegrationTest.java | 615 +++++++++++++++++++
 .../solrj/request/json/DomainMapTest.java       | 177 ++++++
 .../solrj/request/json/HeatmapFacetMapTest.java | 130 ++++
 ...JsonQueryRequestFacetingIntegrationTest.java | 530 ++++++++++++++++
 .../request/json/JsonQueryRequestUnitTest.java  |  86 ++-
 .../solrj/request/json/QueryFacetMapTest.java   |  45 ++
 .../solrj/request/json/RangeFacetMapTest.java   |  84 +++
 .../solrj/request/json/TermsFacetMapTest.java   | 189 ++++++
 18 files changed, 3202 insertions(+), 4 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b502ba28/solr/solr-ref-guide/src/json-facet-api.adoc
----------------------------------------------------------------------
diff --git a/solr/solr-ref-guide/src/json-facet-api.adoc b/solr/solr-ref-guide/src/json-facet-api.adoc
index d842517..fd40538 100644
--- a/solr/solr-ref-guide/src/json-facet-api.adoc
+++ b/solr/solr-ref-guide/src/json-facet-api.adoc
@@ -1,5 +1,7 @@
 = JSON Facet API
 :page-tocclass: right
+:solr-root-path: ../../
+:example-source-dir: {solr-root-path}solrj/src/test/org/apache/solr/client/ref_guide_examples/
 // 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
@@ -74,6 +76,11 @@ The response to the facet request above will start with documents matching the r
 
 Here's an example of a bucketing facet, that partitions documents into bucket based on the `cat` field (short for category), and returns the top 3 buckets:
 
+[.dynamic-tabs]
+--
+[example.tab-pane#curljsonsimpletermsfacet]
+====
+[.tab-label]*curl*
 [source,bash]
 ----
 curl http://localhost:8983/solr/techproducts/query -d 'q=*:*&
@@ -85,6 +92,18 @@ json.facet={
   }
 }'
 ----
+====
+
+[example.tab-pane#solrjjsonsimpletermsfacet]
+====
+[.tab-label]*SolrJ*
+
+[source,java,indent=0]
+----
+include::{example-source-dir}JsonRequestApiTest.java[tag=solrj-json-simple-terms-facet]
+----
+====
+--
 
 The response below shows us that 32 documents match the default root domain. and 12 documents have `cat:electronics`, 4 documents have `cat:currency`, etc.
 
@@ -132,6 +151,11 @@ curl http://localhost:8983/solr/techproducts/query -d 'q=*:*&json.facet=
 
 Another option is to use the JSON Request API to provide the entire request in JSON:
 
+[.dynamic-tabs]
+--
+[example.tab-pane#curljsontermsfacet2]
+====
+[.tab-label]*curl*
 [source,bash]
 ----
 curl http://localhost:8983/solr/techproducts/query -d '
@@ -144,6 +168,18 @@ curl http://localhost:8983/solr/techproducts/query -d '
 }
 '
 ----
+====
+
+[example.tab-pane#solrjjsontermsfacet2]
+====
+[.tab-label]*SolrJ*
+
+[source,java,indent=0]
+----
+include::{example-source-dir}JsonRequestApiTest.java[tag=solrj-json-terms-facet2]
+----
+====
+--
 
 === JSON Extensions
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b502ba28/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/DomainMap.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/DomainMap.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/DomainMap.java
new file mode 100644
index 0000000..c23cee9
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/DomainMap.java
@@ -0,0 +1,139 @@
+/*
+ * 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.client.solrj.request.json;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class DomainMap extends HashMap<String, Object> {
+
+  /**
+   * Indicates that the domain should be narrowed by the specified filter
+   *
+   * May be called multiple times.  Each added filter is retained and used to narrow the domain.
+   */
+  public DomainMap withFilter(String filter) {
+    if (filter == null) {
+      throw new IllegalArgumentException("Parameter 'filter' must be non-null");
+    }
+
+    if (! containsKey("filter")) {
+      put("filter", new ArrayList<String>());
+    }
+
+    final List<String> filterList = (List<String>) get("filter");
+    filterList.add(filter);
+    return this;
+  }
+
+  /**
+   * Indicates that the domain should be the following query
+   *
+   * May be called multiple times.  Each specified query is retained and included in the domain.
+   */
+  public DomainMap withQuery(String query) {
+    if (query == null) {
+      throw new IllegalArgumentException("Parameter 'query' must be non-null");
+    }
+
+    if (! containsKey("query")) {
+      put("query", new ArrayList<String>());
+    }
+
+    final List<String> queryList = (List<String>) get("query");
+    queryList.add(query);
+    return this;
+  }
+
+  /**
+   * Provide a tag or tags that correspond to filters or queries to exclude from the domain
+   *
+   * May be called multiple times.  Each exclude-string is retained and used for removing queries/filters from the
+   * domain specification.
+   *
+   * @param excludeTagsValue a comma-delimited String containing filter/query tags to exclude
+   */
+  public DomainMap withTagsToExclude(String excludeTagsValue) {
+    if (excludeTagsValue == null) {
+      throw new IllegalArgumentException("Parameter 'excludeTagValue' must be non-null");
+    }
+
+    if (! containsKey("excludeTags")) {
+      put("excludeTags", new ArrayList<String>());
+    }
+
+    final List<String> excludeTagsList = (List<String>) get("excludeTags");
+    excludeTagsList.add(excludeTagsValue);
+    return this;
+  }
+
+  /**
+   * Indicates that the resulting domain will contain all parent documents of the children in the existing domain
+   *
+   * @param allParentsQuery a query used to identify all parent documents in the collection
+   */
+  public DomainMap setBlockParentQuery(String allParentsQuery) {
+    if (allParentsQuery == null) {
+      throw new IllegalArgumentException("Parameter 'allParentsQuery' must be non-null");
+    }
+
+    put("blockParent", allParentsQuery);
+    return this;
+  }
+
+  /**
+   * Indicates that the resulting domain will contain all child documents of the parents in the current domain
+   *
+   * @param allChildrenQuery a query used to identify all child documents in the collection
+   */
+  public DomainMap setBlockChildQuery(String allChildrenQuery) {
+    if (allChildrenQuery == null) {
+      throw new IllegalArgumentException("Parameter 'allChildrenQuery' must be non-null");
+    }
+
+    put("blockChildren", allChildrenQuery);
+    return this;
+  }
+
+  /**
+   * Transforms the domain by running a join query with the provided {@code from} and {@code to} parameters
+   *
+   * Join modifies the current domain by selecting the documents whose values in field {@code to} match values for the
+   * field {@code from} in the current domain.
+   *
+   * @param from a field-name whose values are matched against {@code to} by the join
+   * @param to a field name whose values should match values specified by the {@code from} field
+   */
+  public DomainMap setJoinTransformation(String from, String to) {
+    if (from == null) {
+      throw new IllegalArgumentException("Parameter 'from' must be non-null");
+    }
+    if (to == null) {
+      throw new IllegalArgumentException("Parameter 'to' must be non-null");
+    }
+
+    final Map<String, Object> joinParameters = new HashMap<>();
+    joinParameters.put("from", from);
+    joinParameters.put("to", to);
+    put("join", joinParameters);
+
+    return this;
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b502ba28/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/HeatmapFacetMap.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/HeatmapFacetMap.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/HeatmapFacetMap.java
new file mode 100644
index 0000000..ed64e08
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/HeatmapFacetMap.java
@@ -0,0 +1,137 @@
+/*
+ * 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.client.solrj.request.json;
+
+import java.util.Map;
+
+/**
+ * Represents a "heatmap" facet in a JSON request query.
+ *
+ * Ready for use with {@link JsonQueryRequest#withFacet(String, Map)}
+ */
+public class HeatmapFacetMap extends JsonFacetMap<HeatmapFacetMap> {
+  public HeatmapFacetMap(String fieldName) {
+    super("heatmap");
+
+    if (fieldName == null) {
+      throw new IllegalArgumentException("Parameter 'fieldName' must be non-null");
+    }
+
+    put("field", fieldName);
+  }
+
+  @Override
+  public HeatmapFacetMap getThis() { return this; }
+
+  @Override
+  public HeatmapFacetMap withSubFacet(String facetName, JsonFacetMap map) {
+    throw new UnsupportedOperationException(getClass().getName() + " doesn't currently support subfacets");
+  }
+
+  /**
+   * Indicate the region to compute the heatmap facet on.
+   *
+   * Defaults to the "world" ("[-180,-90 TO 180,90]")
+   */
+  public HeatmapFacetMap setRegionQuery(String queryString) {
+    if (queryString == null) {
+      throw new IllegalArgumentException("Parameter 'queryString' must be non-null");
+    }
+
+    put("geom", queryString);
+    return this;
+  }
+
+  /**
+   * Indicates the size of each cell in the computed heatmap grid
+   *
+   * If not set, defaults to being computed by {@code distErrPct} or {@code distErr}
+   *
+   * @param individualCellSize the forced size of each cell in the heatmap grid
+   *
+   * @see #setDistErr(double)
+   * @see #setDistErrPct(double)
+   */
+  public HeatmapFacetMap setGridLevel(int individualCellSize) {
+    if (individualCellSize <= 0) {
+      throw new IllegalArgumentException("Parameter 'individualCellSize' must be a positive integer");
+    }
+    put("gridLevel", individualCellSize);
+    return this;
+  }
+
+  /**
+   * A fraction of the heatmap region that is used to compute the cell size.
+   *
+   * Defaults to 0.15 if not specified.
+   *
+   * @see #setGridLevel(int)
+   * @see #setDistErr(double)
+   */
+  public HeatmapFacetMap setDistErrPct(double distErrPct) {
+    if (distErrPct < 0 || distErrPct > 1) {
+      throw new IllegalArgumentException("Parameter 'distErrPct' must be between 0.0 and 1.0");
+    }
+    put("distErrPct", distErrPct);
+    return this;
+  }
+
+  /**
+   * Indicates the maximum acceptable cell error distance.
+   *
+   * Used to compute the size of each cell in the heatmap grid rather than specifying {@link #setGridLevel(int)}
+   *
+   * @param distErr a positive value representing the maximum acceptable cell error.
+   *
+   * @see #setGridLevel(int)
+   * @see #setDistErrPct(double)
+   */
+  public HeatmapFacetMap setDistErr(double distErr) {
+    if (distErr < 0) {
+      throw new IllegalArgumentException("Parameter 'distErr' must be non-negative");
+    }
+    put("distErr", distErr);
+    return this;
+  }
+
+  public enum HeatmapFormat {
+    INTS2D("ints2D"), PNG("png");
+
+    private final String value;
+
+    HeatmapFormat(String value) {
+      this.value = value;
+    }
+
+    @Override
+    public String toString() { return value; }
+  }
+
+  /**
+   * Sets the format that the computed heatmap should be returned in.
+   *
+   * Defaults to 'ints2D' if not specified.
+   */
+  public HeatmapFacetMap setHeatmapFormat(HeatmapFormat format) {
+    if (format == null) {
+      throw new IllegalArgumentException("Parameter 'format' must be non-null");
+    }
+    put("format", format.toString());
+    return this;
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b502ba28/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/JsonFacetMap.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/JsonFacetMap.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/JsonFacetMap.java
new file mode 100644
index 0000000..3d3e6de
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/JsonFacetMap.java
@@ -0,0 +1,62 @@
+/*
+ * 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.client.solrj.request.json;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A common parent for a small set of classes that allow easier composition of JSON facet objects.
+ *
+ * Designed for use with {@link JsonQueryRequest#withFacet(String, Map)}
+ */
+public abstract class JsonFacetMap<B extends JsonFacetMap<B>> extends HashMap<String, Object> {
+
+  public abstract B getThis(); // Allows methods shared here to return subclass type
+
+  public JsonFacetMap(String facetType) {
+    super();
+
+    put("type", facetType);
+  }
+
+  public B withDomain(DomainMap domain) {
+    put("domain", domain);
+    return getThis();
+  }
+
+  public B withSubFacet(String facetName, JsonFacetMap map) {
+    if (! containsKey("facet")) {
+      put("facet", new HashMap<String, Object>());
+    }
+
+    final Map<String, Object> subFacetMap = (Map<String, Object>) get("facet");
+    subFacetMap.put(facetName, map);
+    return getThis();
+  }
+
+  public B withStatSubFacet(String facetName, String statFacet) {
+    if (! containsKey("facet")) {
+      put("facet", new HashMap<String, Object>());
+    }
+
+    final Map<String, Object> subFacetMap = (Map<String, Object>) get("facet");
+    subFacetMap.put(facetName, statFacet);
+    return getThis();
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b502ba28/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/JsonQueryRequest.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/JsonQueryRequest.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/JsonQueryRequest.java
index 781d9c3..1c7b071 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/JsonQueryRequest.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/JsonQueryRequest.java
@@ -133,6 +133,115 @@ public class JsonQueryRequest extends QueryRequest {
   }
 
   /**
+   * Specify a facet sent as a part of this JSON request.
+   *
+   * This method may be called multiple times.  Each call made with a different {@code facetName} value will add a new
+   * top-level facet.  Repeating {@code facetName} values will cause previous facets with that {@code facetName} to be
+   * overwritten.
+   * <p>
+   * <b>Example:</b> You wish to send the JSON request: {"query": "*:*", "facet": { "top_cats":{"type": "terms", "field":"cat"}}}.  You
+   * would represent (and attach) the facet in this request as follows:
+   * <pre>{@code
+   *     final Map<String, Object> catFacetMap = new HashMap<>();
+   *     catFacetMap.put("type", "terms");
+   *     catFacetMap.put("field", "cat");
+   *
+   *     jsonQueryRequest.withStatFacet("top_cats", catFacetMap);
+   * }</pre>
+   *
+   * @param facetName the name of the top-level facet you'd like to add.
+   * @param facetJson a Map of values representing the facet you wish to add to the request
+   */
+  public JsonQueryRequest withFacet(String facetName, Map<String, Object> facetJson) {
+    if (facetName == null) {
+      throw new IllegalArgumentException("'facetName' parameter must be non-null");
+    }
+    if (facetJson == null) {
+      throw new IllegalArgumentException("'facetMap' parameter must be non-null");
+    }
+
+    if (! jsonRequestMap.containsKey("facet")) {
+      jsonRequestMap.put("facet", new HashMap<String, Object>());
+    }
+
+    final Map<String, Object> facetMap = (Map<String, Object>) jsonRequestMap.get("facet");
+    facetMap.put(facetName, facetJson);
+    return this;
+  }
+
+  /**
+   * Specify a facet sent as a part of this JSON request.
+   *
+   * This method may be called multiple times.  Each call made with a different {@code facetName} value will add a new
+   * top-level facet.  Repeating {@code facetName} values will cause previous facets with that {@code facetName} to be
+   * overwritten.
+   * <p>
+   * <b>Example:</b> You wish to send the JSON request: {"query": "*:*", "facet": { "top_cats":{"type": "terms", "field":"cat"}}}.  You
+   * would represent the facet in this request as follows:
+   * <pre>
+   *     final MapWriter facetWriter = new MapWriter() {
+   *         &#64;Override
+   *         public void writeMap(EntryWriter ew) throws IOException {
+   *             ew.put("type", "terms");
+   *             ew.put("field", "cat");
+   *         }
+   *     };
+   * </pre>
+   *
+   * @param facetName the name of the top-level facet you'd like to add.
+   * @param facetWriter a MapWriter representing the facet you wish to add to the request
+   */
+  public JsonQueryRequest withFacet(String facetName, MapWriter facetWriter) {
+    if (facetName == null) {
+      throw new IllegalArgumentException("'facetName' parameter must be non-null");
+    }
+    if (facetWriter == null) {
+      throw new IllegalArgumentException("'facetWriter' parameter must be non-null");
+    }
+
+    if (! jsonRequestMap.containsKey("facet")) {
+      jsonRequestMap.put("facet", new HashMap<String, Object>());
+    }
+
+    final Map<String, Object> facetMap = (Map<String, Object>) jsonRequestMap.get("facet");
+    facetMap.put(facetName, facetWriter);
+    return this;
+  }
+
+  /**
+   * Specify a simple stat or aggregation facet to be sent as a part of this JSON request.
+   *
+   * This method may be called multiple times.  Each call made with a different {@code facetName} value will add a new
+   * top-level facet.  Repeating {@code facetName} values will cause previous facets with that {@code facetName} to be
+   * overwritten.
+   * <p>
+   * <b>Example:</b>  You wish to send the JSON request: {"query": "*:*", "facet": {"avg_price": "avg(price)"}}.  You
+   * would represent the facet in this request as follows:
+   * <pre>{@code
+   *     jsonQueryRequest.withStatFacet("avg_price", "avg(price)");
+   * }</pre>
+   *
+   * @param facetName the name of the top-level stat/agg facet you'd like to add.
+   * @param facetValue a String representing the stat/agg facet computation to perform.
+   */
+  public JsonQueryRequest withStatFacet(String facetName, String facetValue) {
+    if (facetName == null) {
+      throw new IllegalArgumentException("'facetName' parameter must be non-null");
+    }
+    if (facetValue == null) {
+      throw new IllegalArgumentException("'facetValue' parameter must be non-null");
+    }
+
+    if (! jsonRequestMap.containsKey("facet")) {
+      jsonRequestMap.put("facet", new HashMap<String, Object>());
+    }
+
+    final Map<String, Object> facetMap = (Map<String, Object>) jsonRequestMap.get("facet");
+    facetMap.put(facetName, facetValue);
+    return this;
+  }
+
+  /**
    * Specify whether results should be fetched starting from a particular offset (or 'start').
    *
    * Defaults to 0 if not set.

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b502ba28/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/QueryFacetMap.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/QueryFacetMap.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/QueryFacetMap.java
new file mode 100644
index 0000000..7613183
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/QueryFacetMap.java
@@ -0,0 +1,39 @@
+/*
+ * 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.client.solrj.request.json;
+
+import java.util.Map;
+
+/**
+ * Represents a "query" facet in a JSON query request.
+ *
+ * Ready for use in {@link JsonQueryRequest#withFacet(String, Map)}
+ */
+public class QueryFacetMap extends JsonFacetMap<QueryFacetMap> {
+  public QueryFacetMap(String queryString) {
+    super("query");
+
+    if (queryString == null) {
+      throw new IllegalArgumentException("Parameter 'queryString' must be non-null");
+    }
+    put("q", queryString);
+  }
+
+  @Override
+  public QueryFacetMap getThis() { return this; }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b502ba28/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/RangeFacetMap.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/RangeFacetMap.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/RangeFacetMap.java
new file mode 100644
index 0000000..24d5123
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/RangeFacetMap.java
@@ -0,0 +1,105 @@
+/*
+ * 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.client.solrj.request.json;
+
+import java.util.Map;
+
+/**
+ * Represents a "range" facet in a JSON request query.
+ *
+ * Ready for use with {@link JsonQueryRequest#withFacet(String, Map)}
+ */
+public class RangeFacetMap extends JsonFacetMap<RangeFacetMap> {
+  public RangeFacetMap(String field, long start, long end, long gap) {
+    super("range");
+
+    if (field == null) {
+      throw new IllegalArgumentException("Parameter 'field' must be non-null");
+    }
+    if (end < start) {
+      throw new IllegalArgumentException("Parameter 'end' must be greater than parameter 'start'");
+    }
+    if (gap <= 0) {
+      throw new IllegalArgumentException("Parameter 'gap' must be a positive integer");
+    }
+
+    put("field", field);
+    put("start", start);
+    put("end", end);
+    put("gap", gap);
+  }
+
+  public RangeFacetMap(String field, double start, double end, double gap) {
+    super("range");
+
+    if (field == null) {
+      throw new IllegalArgumentException("Parameter 'field' must be non-null");
+    }
+    if (end < start) {
+      throw new IllegalArgumentException("Parameter 'end' must be greater than parameter 'start'");
+    }
+    if (gap <= 0) {
+      throw new IllegalArgumentException("Parameter 'gap' must be a positive value");
+    }
+
+    put("field", field);
+    put("start", start);
+    put("end", end);
+    put("gap", gap);
+  }
+
+  @Override
+  public RangeFacetMap getThis() { return this; }
+
+  /**
+   * Indicates whether the facet's last bucket should stop exactly at {@code end}, or be extended to be {@code gap} wide
+   *
+   * Defaults to false if not specified.
+   *
+   * @param hardEnd true if the final bucket should be truncated at {@code end}; false otherwise
+   */
+  public RangeFacetMap setHardEnd(boolean hardEnd) {
+    put("hardend", hardEnd);
+    return this;
+  }
+
+  public enum OtherBuckets {
+    BEFORE("before"), AFTER("after"), BETWEEN("between"), NONE("none"), ALL("all");
+
+    private final String value;
+
+    OtherBuckets(String value) {
+      this.value = value;
+    }
+
+    public String toString() { return value; }
+  }
+
+  /**
+   * Indicates that an additional range bucket(s) should be computed and added to those computed for {@code start} and {@code end}
+   *
+   * See {@link OtherBuckets} for possible options.
+   */
+  public RangeFacetMap setOtherBuckets(OtherBuckets bucketSpecifier) {
+    if (bucketSpecifier == null) {
+      throw new IllegalArgumentException("Parameter 'bucketSpecifier' must be non-null");
+    }
+    put("other", bucketSpecifier.toString());
+    return this;
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b502ba28/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/TermsFacetMap.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/TermsFacetMap.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/TermsFacetMap.java
new file mode 100644
index 0000000..e28f8a8
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/TermsFacetMap.java
@@ -0,0 +1,204 @@
+/*
+ * 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.client.solrj.request.json;
+
+import java.util.Map;
+
+/**
+ * Represents a "terms" facet in a JSON query request.
+ *
+ * Ready for use in {@link JsonQueryRequest#withFacet(String, Map)}
+ */
+public class TermsFacetMap extends JsonFacetMap<TermsFacetMap> {
+  public TermsFacetMap(String fieldName) {
+    super("terms");
+
+    put("field", fieldName);
+  }
+
+  @Override
+  public TermsFacetMap getThis() { return this; }
+
+  /**
+   * Indicates that Solr should skip over the N buckets for this facet.
+   *
+   * Used for "paging" in facet results.  Defaults to 0 if not provided.
+   *
+   * @param numToSkip the number of buckets to skip over before selecting the buckets to return
+   */
+  public TermsFacetMap setBucketOffset(int numToSkip) {
+    if (numToSkip < 0) {
+      throw new IllegalArgumentException("Parameter 'numToSkip' must be non-negative");
+    }
+    put("offset", numToSkip);
+    return this;
+  }
+
+  /**
+   * Indicates the maximum number of buckets to be returned by this facet.
+   *
+   * Defaults to 10 if not specified.
+   */
+  public TermsFacetMap setLimit(int maximumBuckets) {
+    if (maximumBuckets < 0) {
+      throw new IllegalArgumentException("Parameter 'maximumBuckets' must be non-negative");
+    }
+    put("limit", maximumBuckets);
+    return this;
+  }
+
+  /**
+   * Indicates the desired ordering for the returned buckets.
+   *
+   * Values can be based on 'count' (the number of results in each bucket), 'index' (the natural order of bucket values),
+   * or on any stat facet that occurs in the bucket.  Defaults to "count desc" if not specified.
+   */
+  public TermsFacetMap setSort(String sortString) {
+    if (sortString == null) {
+      throw new IllegalArgumentException("Parameter 'sortString' must be non-null");
+    }
+    put("sort", sortString);
+    return this;
+  }
+
+  /**
+   * Indicates the number of additional buckets to request internally beyond those required by {@link #setLimit(int)}.
+   *
+   * Defaults to -1 if not specified, which triggers some heuristic guessing based on other settings.
+   */
+  public TermsFacetMap setOverRequest(int numExtraBuckets) {
+    if (numExtraBuckets < -1) {
+      throw new IllegalArgumentException("Parameter 'numExtraBuckets' must be >= -1");
+    }
+    put("overrequest", numExtraBuckets);
+    return this;
+  }
+
+  /**
+   * Indicates whether this facet should use distributed facet refining.
+   *
+   * "Distributed facet refining" is a second, optional stage in the facet process that ensures that counts for the
+   * returned buckets are exact.  Enabling it is a tradeoff between precision and speed/performance.  Defaults to false
+   * if not specified.
+   * @param useRefining true if distributed facet refining should be used; false otherwise
+   */
+  public TermsFacetMap useDistributedFacetRefining(boolean useRefining) {
+    put("refine", useRefining);
+    return this;
+  }
+
+  /**
+   * Indicates how many extra buckets to request during distributed-facet-refining beyond those required by {@link #setLimit(int)}
+   *
+   * Defaults to -1 if not specified, which triggers some heuristic guessing based on other settings.
+   */
+  public TermsFacetMap setOverRefine(int numExtraBuckets) {
+    if (numExtraBuckets < -1) {
+      throw new IllegalArgumentException("Parameter 'numExtraBuckets' must be >= -1");
+    }
+    put("overrefine", numExtraBuckets);
+    return this;
+  }
+
+  /**
+   * Indicates that the facet results should not include any buckets with a count less than {@code minCount}.
+   *
+   * Defaults to 1 if not specified.
+   */
+  public TermsFacetMap setMinCount(int minCount) {
+    if (minCount < 1) {
+      throw new IllegalArgumentException("Parameter 'minCount' must be a positive integer");
+    }
+    put("mincount", minCount);
+    return this;
+  }
+
+  /**
+   * Indicates that Solr should create a bucket corresponding to documents missing the field used by this facet.
+   *
+   * Defaults to false if not specified.
+   *
+   * @param missingBucket true if the special "missing" bucket should be created; false otherwise
+   */
+  public TermsFacetMap includeMissingBucket(boolean missingBucket) {
+    put("missing", missingBucket);
+    return this;
+  }
+
+  /**
+   * Indicates that Solr should include the total number of buckets for this facet.
+   *
+   * Note that this is different than the number of buckets returned.  Defaults to false if not specified
+   *
+   * @param numBuckets true if the "numBuckets" field should be computed; false otherwise
+   */
+  public TermsFacetMap includeTotalNumBuckets(boolean numBuckets) {
+    put("numBuckets", numBuckets);
+    return this;
+  }
+
+  /**
+   * Creates a bucket representing the union of all other buckets.
+   *
+   * For multi-valued fields this is different than a bucket for the entire domain, since documents can belong to
+   * multiple buckets.  Defaults to false if not specified.
+   *
+   * @param shouldInclude true if the union bucket "allBuckets" should be computed; false otherwise
+   */
+  public TermsFacetMap includeAllBucketsUnionBucket(boolean shouldInclude) {
+    put("allBuckets", shouldInclude);
+    return this;
+  }
+
+  /**
+   * Indicates that the facet should only produce buckets for terms that start with the specified prefix.
+   */
+  public TermsFacetMap setTermPrefix(String termPrefix) {
+    if (termPrefix == null) {
+      throw new IllegalArgumentException("Parameter 'termPrefix' must be non-null");
+    }
+    put("prefix", termPrefix);
+    return this;
+  }
+
+  public enum FacetMethod {
+    DV("dv"), UIF("uif"), DVHASH("dvhash"), ENUM("enum"), STREAM("stream"), SMART("smart");
+
+    private final String value;
+    FacetMethod(String value) {
+      this.value = value;
+    }
+
+    public String toString() {
+      return value;
+    }
+  }
+
+  /**
+   * Indicate which method should be used to compute the facet.
+   *
+   * Defaults to "smart" if not specified, which has Solr guess which computation method will be most efficient.
+   */
+  public TermsFacetMap setFacetMethod(FacetMethod method) {
+    if (method == null) {
+      throw new IllegalArgumentException("Parameter 'method' must be non-null");
+    }
+    put("method", method.toString());
+    return this;
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b502ba28/solr/solrj/src/test-files/solrj/techproducts.xml
----------------------------------------------------------------------
diff --git a/solr/solrj/src/test-files/solrj/techproducts.xml b/solr/solrj/src/test-files/solrj/techproducts.xml
new file mode 100644
index 0000000..15650fa
--- /dev/null
+++ b/solr/solrj/src/test-files/solrj/techproducts.xml
@@ -0,0 +1,421 @@
+<!--
+ 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.
+-->
+<add>
+  <doc>
+    <field name="id">TWINX2048-3200PRO</field>
+    <field name="name">CORSAIR  XMS 2GB (2 x 1GB) 184-Pin DDR SDRAM Unbuffered DDR 400 (PC 3200) Dual Channel Kit System Memory - Retail</field>
+    <field name="manu">Corsair Microsystems Inc.</field>
+    <!-- Join -->
+    <field name="manu_id_s">corsair</field>
+    <field name="cat">electronics</field>
+    <field name="cat">memory</field>
+    <field name="features">CAS latency 2,  2-3-3-6 timing, 2.75v, unbuffered, heat-spreader</field>
+    <field name="price">185.00</field>
+    <field name="popularity">5</field>
+    <field name="inStock">true</field>
+    <!-- San Francisco store -->
+    <field name="store">37.7752,-122.4232</field>
+    <field name="manufacturedate_dt">2006-02-13T15:26:37Z</field>
+    <!-- a field for testing payload tagged text via DelimitedPayloadTokenFilter -->
+    <field name="payloads">electronics|6.0 memory|3.0</field>
+  </doc>
+  <doc>
+    <field name="id">VS1GB400C3</field>
+    <field name="name">CORSAIR ValueSelect 1GB 184-Pin DDR SDRAM Unbuffered DDR 400 (PC 3200) System Memory - Retail</field>
+    <field name="manu">Corsair Microsystems Inc.</field>
+    <!-- Join -->
+    <field name="manu_id_s">corsair</field>
+    <field name="cat">electronics</field>
+    <field name="cat">memory</field>
+    <field name="price">74.99</field>
+    <field name="popularity">7</field>
+    <field name="inStock">true</field>
+    <!-- Dodge City store -->
+    <field name="store">37.7752,-100.0232</field>
+    <field name="manufacturedate_dt">2006-02-13T15:26:37Z</field>
+    <field name="payloads">electronics|4.0 memory|2.0</field>
+  </doc>
+  <doc>
+    <field name="id">VDBDB1A16</field>
+    <field name="name">A-DATA V-Series 1GB 184-Pin DDR SDRAM Unbuffered DDR 400 (PC 3200) System Memory - OEM</field>
+    <field name="manu">A-DATA Technology Inc.</field>
+    <!-- Join -->
+    <field name="manu_id_s">corsair</field>
+    <field name="cat">electronics</field>
+    <field name="cat">memory</field>
+    <field name="features">CAS latency 3,   2.7v</field>
+    <!-- note: price & popularity is missing on this one -->
+    <field name="popularity">0</field>
+    <field name="inStock">true</field>
+    <!-- Buffalo store -->
+    <field name="store">45.18414,-93.88141</field>
+    <field name="manufacturedate_dt">2006-02-13T15:26:37Z</field>
+    <field name="payloads">electronics|0.9 memory|0.1</field>
+  </doc>
+  <doc>
+    <field name="id">MA147LL/A</field>
+    <field name="name">Apple 60 GB iPod with Video Playback Black</field>
+    <field name="manu">Apple Computer Inc.</field>
+    <!-- Join -->
+    <field name="manu_id_s">apple</field>
+    <field name="cat">electronics</field>
+    <field name="cat">music</field>
+    <field name="features">iTunes, Podcasts, Audiobooks</field>
+    <field name="features">Stores up to 15,000 songs, 25,000 photos, or 150 hours of video</field>
+    <field name="features">2.5-inch, 320x240 color TFT LCD display with LED backlight</field>
+    <field name="features">Up to 20 hours of battery life</field>
+    <field name="features">Plays AAC, MP3, WAV, AIFF, Audible, Apple Lossless, H.264 video</field>
+    <field name="features">Notes, Calendar, Phone book, Hold button, Date display, Photo wallet, Built-in games, JPEG photo playback, Upgradeable firmware, USB 2.0 compatibility, Playback speed control, Rechargeable capability, Battery level indication</field>
+    <field name="includes">earbud headphones, USB cable</field>
+    <field name="weight">5.5</field>
+    <field name="price">399.00</field>
+    <field name="popularity">10</field>
+    <field name="inStock">true</field>
+    <!-- Dodge City store -->
+    <field name="store">37.7752,-100.0232</field>
+    <field name="manufacturedate_dt">2005-10-12T08:00:00Z</field>
+  </doc>
+  <doc>
+    <field name="id">F8V7067-APL-KIT</field>
+    <field name="name">Belkin Mobile Power Cord for iPod w/ Dock</field>
+    <field name="manu">Belkin</field>
+    <!-- Join -->
+    <field name="manu_id_s">belkin</field>
+    <field name="cat">electronics</field>
+    <field name="cat">connector</field>
+    <field name="features">car power adapter, white</field>
+    <field name="weight">4.0</field>
+    <field name="price">19.95</field>
+    <field name="popularity">1</field>
+    <field name="inStock">false</field>
+    <!-- Buffalo store -->
+    <field name="store">45.18014,-93.87741</field>
+    <field name="manufacturedate_dt">2005-08-01T16:30:25Z</field>
+  </doc>
+  <doc>
+    <field name="id">IW-02</field>
+    <field name="name">iPod &amp; iPod Mini USB 2.0 Cable</field>
+    <field name="manu">Belkin</field>
+    <!-- Join -->
+    <field name="manu_id_s">belkin</field>
+    <field name="cat">electronics</field>
+    <field name="cat">connector</field>
+    <field name="features">car power adapter for iPod, white</field>
+    <field name="weight">2.0</field>
+    <field name="price">11.50</field>
+    <field name="popularity">1</field>
+    <field name="inStock">false</field>
+    <!-- San Francisco store -->
+    <field name="store">37.7752,-122.4232</field>
+    <field name="manufacturedate_dt">2006-02-14T23:55:59Z</field>
+  </doc>
+  <doc>
+    <field name="id">9885A004</field>
+    <field name="name">Canon PowerShot SD500</field>
+    <field name="manu">Canon Inc.</field>
+    <!-- Join -->
+    <field name="manu_id_s">canon</field>
+    <field name="cat">electronics</field>
+    <field name="cat">camera</field>
+    <field name="features">3x zoop, 7.1 megapixel Digital ELPH</field>
+    <field name="features">movie clips up to 640x480 @30 fps</field>
+    <field name="features">2.0" TFT LCD, 118,000 pixels</field>
+    <field name="features">built in flash, red-eye reduction</field>
+    <field name="includes">32MB SD card, USB cable, AV cable, battery</field>
+    <field name="weight">6.4</field>
+    <field name="price">329.95</field>
+    <field name="popularity">7</field>
+    <field name="inStock">true</field>
+    <field name="manufacturedate_dt">2006-02-13T15:26:37Z</field>
+    <!-- Buffalo store -->
+    <field name="store">45.19614,-93.90341</field>
+  </doc>
+  <doc>
+    <field name="id">VA902B</field>
+    <field name="name">ViewSonic VA902B - flat panel display - TFT - 19"</field>
+    <field name="manu">ViewSonic Corp.</field>
+    <!-- Join -->
+    <field name="manu_id_s">viewsonic</field>
+    <field name="cat">electronics and stuff2</field>
+    <field name="features">19" TFT active matrix LCD, 8ms response time, 1280 x 1024 native resolution</field>
+    <field name="weight">190.4</field>
+    <field name="price">279.95</field>
+    <field name="popularity">6</field>
+    <field name="inStock">true</field>
+    <!-- Buffalo store -->
+    <field name="store">45.18814,-93.88541</field>
+  </doc>
+  <doc>
+    <field name="id">EN7800GTX/2DHTV/256M</field>
+    <field name="name">ASUS Extreme N7800GTX/2DHTV (256 MB)</field>
+    <!-- Denormalized -->
+    <field name="manu">ASUS Computer Inc.</field>
+    <!-- Join -->
+    <field name="manu_id_s">asus</field>
+    <field name="cat">electronics</field>
+    <field name="cat">graphics card</field>
+    <field name="features">NVIDIA GeForce 7800 GTX GPU/VPU clocked at 486MHz</field>
+    <field name="features">256MB GDDR3 Memory clocked at 1.35GHz</field>
+    <field name="features">PCI Express x16</field>
+    <field name="features">Dual DVI connectors, HDTV out, video input</field>
+    <field name="features">OpenGL 2.0, DirectX 9.0</field>
+    <field name="weight">16.0</field>
+    <field name="price">479.95</field>
+    <field name="popularity">7</field>
+    <field name="store">40.7143,-74.006</field>
+    <field name="inStock">false</field>
+    <field name="manufacturedate_dt">2006-02-13T15:26:37Z/DAY</field>
+  </doc>
+  <!-- yes, you can add more than one document at a time -->
+  <doc>
+    <field name="id">100-435805</field>
+    <field name="name">ATI Radeon X1900 XTX 512 MB PCIE Video Card</field>
+    <field name="manu">ATI Technologies</field>
+    <!-- Join -->
+    <field name="manu_id_s">ati</field>
+    <field name="cat">electronics</field>
+    <field name="cat">graphics card</field>
+    <field name="features">ATI RADEON X1900 GPU/VPU clocked at 650MHz</field>
+    <field name="features">512MB GDDR3 SDRAM clocked at 1.55GHz</field>
+    <field name="features">PCI Express x16</field>
+    <field name="features">dual DVI, HDTV, svideo, composite out</field>
+    <field name="features">OpenGL 2.0, DirectX 9.0</field>
+    <field name="weight">48.0</field>
+    <field name="price">649.99</field>
+    <field name="popularity">7</field>
+    <field name="inStock">false</field>
+    <field name="manufacturedate_dt">2006-02-13T15:26:37Z/DAY</field>
+    <!-- NYC store -->
+    <field name="store">40.7143,-74.006</field>
+  </doc>
+  <doc>
+    <field name="id">0579B002</field>
+    <field name="name">Canon PIXMA MP500 All-In-One Photo Printer</field>
+    <field name="manu">Canon Inc.</field>
+    <!-- Join -->
+    <field name="manu_id_s">canon</field>
+    <field name="cat">electronics</field>
+    <field name="cat">multifunction printer</field>
+    <field name="cat">printer</field>
+    <field name="cat">scanner</field>
+    <field name="cat">copier</field>
+    <field name="features">Multifunction ink-jet color photo printer</field>
+    <field name="features">Flatbed scanner, optical scan resolution of 1,200 x 2,400 dpi</field>
+    <field name="features">2.5" color LCD preview screen</field>
+    <field name="features">Duplex Copying</field>
+    <field name="features">Printing speed up to 29ppm black, 19ppm color</field>
+    <field name="features">Hi-Speed USB</field>
+    <field name="features">memory card: CompactFlash, Micro Drive, SmartMedia, Memory Stick, Memory Stick Pro, SD Card, and MultiMediaCard</field>
+    <field name="weight">352.0</field>
+    <field name="price">179.99</field>
+    <field name="popularity">6</field>
+    <field name="inStock">true</field>
+    <!-- Buffalo store -->
+    <field name="store">45.19214,-93.89941</field>
+  </doc>
+  <doc>
+    <field name="id">3007WFP</field>
+    <field name="name">Dell Widescreen UltraSharp 3007WFP</field>
+    <field name="manu">Dell, Inc.</field>
+    <!-- Join -->
+    <field name="manu_id_s">dell</field>
+    <field name="cat">electronics and computer1</field>
+    <field name="features">30" TFT active matrix LCD, 2560 x 1600, .25mm dot pitch, 700:1 contrast</field>
+    <field name="includes">USB cable</field>
+    <field name="weight">401.6</field>
+    <field name="price">2199.0</field>
+    <field name="popularity">6</field>
+    <field name="inStock">true</field>
+    <!-- Buffalo store -->
+    <field name="store">43.17614,-90.57341</field>
+  </doc>
+  <doc>
+    <field name="id">adata</field>
+    <field name="compName_s">A-Data Technology</field>
+    <field name="address_s">46221 Landing Parkway Fremont, CA 94538</field>
+  </doc>
+  <doc>
+    <field name="id">apple</field>
+    <field name="compName_s">Apple</field>
+    <field name="address_s">1 Infinite Way, Cupertino CA</field>
+  </doc>
+  <doc>
+    <field name="id">asus</field>
+    <field name="compName_s">ASUS Computer</field>
+    <field name="address_s">800 Corporate Way Fremont, CA 94539</field>
+  </doc>
+  <doc>
+    <field name="id">ati</field>
+    <field name="compName_s">ATI Technologies</field>
+    <field name="address_s">33 Commerce Valley Drive East Thornhill, ON L3T 7N6 Canada</field>
+  </doc>
+  <doc>
+    <field name="id">belkin</field>
+    <field name="compName_s">Belkin</field>
+    <field name="address_s">12045 E. Waterfront Drive Playa Vista, CA 90094</field>
+  </doc>
+  <doc>
+    <field name="id">canon</field>
+    <field name="compName_s">Canon, Inc.</field>
+    <field name="address_s">One Canon Plaza Lake Success, NY 11042</field>
+  </doc>
+  <doc>
+    <field name="id">corsair</field>
+    <field name="compName_s">Corsair Microsystems</field>
+    <field name="address_s">46221 Landing Parkway Fremont, CA 94538</field>
+  </doc>
+  <doc>
+    <field name="id">dell</field>
+    <field name="compName_s">Dell, Inc.</field>
+    <field name="address_s">One Dell Way Round Rock, Texas 78682</field>
+  </doc>
+  <doc>
+    <field name="id">maxtor</field>
+    <field name="compName_s">Maxtor Corporation</field>
+    <field name="address_s">920 Disc Drive Scotts Valley, CA 95066</field>
+  </doc>
+  <doc>
+    <field name="id">samsung</field>
+    <field name="compName_s">Samsung Electronics Co. Ltd.</field>
+    <field name="address_s">105 Challenger Rd. Ridgefield Park, NJ 07660-0511</field>
+  </doc>
+  <doc>
+    <field name="id">viewsonic</field>
+    <field name="compName_s">ViewSonic Corp</field>
+    <field name="address_s">381 Brea Canyon Road Walnut, CA 91789-0708</field>
+  </doc>
+  <doc>
+    <field name="id">SP2514N</field>
+    <field name="name">Samsung SpinPoint P120 SP2514N - hard drive - 250 GB - ATA-133</field>
+    <field name="manu">Samsung Electronics Co. Ltd.</field>
+    <!-- Join -->
+    <field name="manu_id_s">samsung</field>
+    <field name="cat">electronics</field>
+    <field name="cat">hard drive</field>
+    <field name="features">7200RPM, 8MB cache, IDE Ultra ATA-133</field>
+    <field name="features">NoiseGuard, SilentSeek technology, Fluid Dynamic Bearing (FDB) motor</field>
+    <field name="price">92.0</field>
+    <field name="popularity">6</field>
+    <field name="inStock">true</field>
+    <field name="manufacturedate_dt">2006-02-13T15:26:37Z</field>
+    <!-- Near Oklahoma city -->
+    <field name="store">35.0752,-97.032</field>
+  </doc>
+  <doc>
+    <field name="id">6H500F0</field>
+    <field name="name">Maxtor DiamondMax 11 - hard drive - 500 GB - SATA-300</field>
+    <field name="manu">Maxtor Corp.</field>
+    <!-- Join -->
+    <field name="manu_id_s">maxtor</field>
+    <field name="cat">electronics</field>
+    <field name="cat">hard drive</field>
+    <field name="features">SATA 3.0Gb/s, NCQ</field>
+    <field name="features">8.5ms seek</field>
+    <field name="features">16MB cache</field>
+    <field name="price">350.0</field>
+    <field name="popularity">6</field>
+    <field name="inStock">true</field>
+    <!-- Buffalo store -->
+    <field name="store">45.17614,-93.87341</field>
+    <field name="manufacturedate_dt">2006-02-13T15:26:37Z</field>
+  </doc>
+  <doc>
+    <field name="id">USD</field>
+    <field name="name">One Dollar</field>
+    <field name="manu">Bank of America</field>
+    <field name="manu_id_s">boa</field>
+    <field name="cat">currency</field>
+    <field name="features">Coins and notes</field>
+    <field name="price_c">1,USD</field>
+    <field name="inStock">true</field>
+  </doc>
+  <doc>
+    <field name="id">EUR</field>
+    <field name="name">One Euro</field>
+    <field name="manu">European Union</field>
+    <field name="manu_id_s">eu</field>
+    <field name="cat">currency</field>
+    <field name="features">Coins and notes</field>
+    <field name="price_c">1,EUR</field>
+    <field name="inStock">true</field>
+  </doc>
+  <doc>
+    <field name="id">GBP</field>
+    <field name="name">One British Pound</field>
+    <field name="manu">U.K.</field>
+    <field name="manu_id_s">uk</field>
+    <field name="cat">currency</field>
+    <field name="features">Coins and notes</field>
+    <field name="price_c">1,GBP</field>
+    <field name="inStock">true</field>
+  </doc>
+  <doc>
+    <field name="id">NOK</field>
+    <field name="name">One Krone</field>
+    <field name="manu">Bank of Norway</field>
+    <field name="manu_id_s">nor</field>
+    <field name="cat">currency</field>
+    <field name="features">Coins and notes</field>
+    <field name="price_c">1,NOK</field>
+    <field name="inStock">true</field>
+  </doc>
+  <doc>
+    <field name="id">UTF8TEST</field>
+    <field name="name">Test with some UTF-8 encoded characters</field>
+    <field name="manu">Apache Software Foundation</field>
+    <field name="cat">software</field>
+    <field name="cat">search</field>
+    <field name="features">No accents here</field>
+    <field name="features">This is an e acute: &#xE9;</field>
+    <field name="features">eaiou with circumflexes: &#xEA;&#xE2;&#xEE;&#xF4;&#xFB;</field>
+    <field name="features">eaiou with umlauts: &#xEB;&#xE4;&#xEF;&#xF6;&#xFC;</field>
+    <field name="features">tag with escaped chars: &lt;nicetag/&gt;</field>
+    <field name="features">escaped ampersand: Bonnie &amp; Clyde</field>
+    <field name="features">Outside the BMP:&#x10308; codepoint=10308, a circle with an x inside. UTF8=f0908c88 UTF16=d800 df08</field>
+    <field name="price">0.0</field>
+    <field name="inStock">true</field>
+  </doc>
+  <doc>
+    <field name="id">GB18030TEST</field>
+    <field name="name">Test with some GB18030 encoded characters</field>
+    <field name="features">No accents here</field>
+    <field name="features">&#xD5;&#xE2;&#xCA;&#xC7;&#xD2;&#xBB;&#xB8;&#xF6;&#xB9;&#xA6;&#xC4;&#xDC;</field>
+    <field name="features">This is a feature (translated)</field>
+    <field name="features">&#xD5;&#xE2;&#xB7;&#xDD;&#xCE;&#xC4;&#xBC;&#xFE;&#xCA;&#xC7;&#xBA;&#xDC;&#xD3;&#xD0;&#xB9;&#xE2;&#xD4;&#xF3;</field>
+    <field name="features">This document is very shiny (translated)</field>
+    <field name="price">0.0</field>
+    <field name="inStock">true</field>
+  </doc>
+  <doc>
+    <field name="id">SOLR1000</field>
+    <field name="name">Solr, the Enterprise Search Server</field>
+    <field name="manu">Apache Software Foundation</field>
+    <field name="cat">software</field>
+    <field name="cat">search</field>
+    <field name="features">Advanced Full-Text Search Capabilities using Lucene</field>
+    <field name="features">Optimized for High Volume Web Traffic</field>
+    <field name="features">Standards Based Open Interfaces - XML and HTTP</field>
+    <field name="features">Comprehensive HTML Administration Interfaces</field>
+    <field name="features">Scalability - Efficient Replication to other Solr Search Servers</field>
+    <field name="features">Flexible and Adaptable with XML configuration and Schema</field>
+    <field name="features">Good unicode support: h&#xE9;llo (hello with an accent over the e)</field>
+    <field name="price">0.0</field>
+    <field name="popularity">10</field>
+    <field name="inStock">true</field>
+    <field name="incubationdate_dt">2006-01-17T00:00:00.000Z</field>
+  </doc>
+</add>

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/b502ba28/solr/solrj/src/test/org/apache/solr/client/ref_guide_examples/JsonRequestApiTest.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/test/org/apache/solr/client/ref_guide_examples/JsonRequestApiTest.java b/solr/solrj/src/test/org/apache/solr/client/ref_guide_examples/JsonRequestApiTest.java
index b941f2d..5fd3876 100644
--- a/solr/solrj/src/test/org/apache/solr/client/ref_guide_examples/JsonRequestApiTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/ref_guide_examples/JsonRequestApiTest.java
@@ -27,11 +27,13 @@ import org.apache.solr.client.solrj.request.AbstractUpdateRequest;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
 import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
 import org.apache.solr.client.solrj.request.json.JsonQueryRequest;
+import org.apache.solr.client.solrj.request.json.TermsFacetMap;
 import org.apache.solr.client.solrj.response.QueryResponse;
 import org.apache.solr.client.solrj.response.UpdateResponse;
 import org.apache.solr.cloud.SolrCloudTestCase;
 import org.apache.solr.common.SolrDocument;
 import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.util.NamedList;
 import org.apache.solr.util.ExternalPaths;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -58,7 +60,7 @@ public class JsonRequestApiTest extends SolrCloudTestCase {
 
     ContentStreamUpdateRequest up = new ContentStreamUpdateRequest("/update");
     up.setParam("collection", COLLECTION_NAME);
-    up.addFile(getFile("solrj/docs2.xml"), "application/xml"); // A subset of the 'techproducts' documents
+    up.addFile(getFile("solrj/techproducts.xml"), "application/xml");
     up.setAction(AbstractUpdateRequest.ACTION.COMMIT, true, true);
     UpdateResponse updateResponse = up.process(cluster.getSolrClient());
     assertEquals(0, updateResponse.getStatus());
@@ -67,7 +69,7 @@ public class JsonRequestApiTest extends SolrCloudTestCase {
   @Test
   public void testSimpleJsonQuery() throws Exception {
     SolrClient solrClient = cluster.getSolrClient();
-    final int expectedResults = 3;
+    final int expectedResults = 4;
 
     // tag::solrj-json-query-simple[]
     final JsonQueryRequest simpleQuery = new JsonQueryRequest()
@@ -116,7 +118,97 @@ public class JsonRequestApiTest extends SolrCloudTestCase {
     // end::solrj-json-query-macro-expansion[]
 
     assertEquals(0, queryResponse.getStatus());
-    assertEquals(3, queryResponse.getResults().size());
+    assertEquals(5, queryResponse.getResults().size());
+  }
+
+  @Test
+  public void testSimpleJsonTermsFacet() throws Exception {
+    SolrClient solrClient = cluster.getSolrClient();
+
+    //tag::solrj-json-simple-terms-facet[]
+    final TermsFacetMap categoryFacet = new TermsFacetMap("cat").setLimit(3);
+    final JsonQueryRequest request = new JsonQueryRequest()
+        .setQuery("*:*")
+        .withFacet("categories", categoryFacet);
+    QueryResponse queryResponse = request.process(solrClient, COLLECTION_NAME);
+    //end::solrj-json-simple-terms-facet[]
+
+    assertEquals(0, queryResponse.getStatus());
+    assertEquals(32, queryResponse.getResults().getNumFound());
+    assertEquals(10, queryResponse.getResults().size());
+    assertHasFacetWithBucketValues(queryResponse.getResponse(),"categories",
+        new FacetBucket("electronics",12),
+        new FacetBucket("currency", 4),
+        new FacetBucket("memory", 3));
+  }
+
+  @Test
+  public void testTermsFacet2() throws Exception {
+    SolrClient solrClient = cluster.getSolrClient();
+
+    //tag::solrj-json-terms-facet2[]
+    final TermsFacetMap categoryFacet = new TermsFacetMap("cat").setLimit(5);
+    final JsonQueryRequest request = new JsonQueryRequest()
+        .setQuery("*:*")
+        .withFacet("categories", categoryFacet);
+    QueryResponse queryResponse = request.process(solrClient, COLLECTION_NAME);
+    //end::solrj-json-terms-facet2[]
+
+    assertEquals(0, queryResponse.getStatus());
+    assertEquals(32, queryResponse.getResults().getNumFound());
+    assertEquals(10, queryResponse.getResults().size());
+    assertHasFacetWithBucketValues(queryResponse.getResponse(),"categories",
+        new FacetBucket("electronics",12),
+        new FacetBucket("currency", 4),
+        new FacetBucket("memory", 3),
+        new FacetBucket("connector", 2),
+        new FacetBucket("graphics card", 2));
+  }
+
+  private class FacetBucket {
+    private final Object val;
+    private final int count;
+    FacetBucket(Object val, int count) {
+      this.val = val;
+      this.count = count;
+    }
+
+    public Object getVal() { return val; }
+    public int getCount() { return count; }
+  }
+
+  private void assertHasFacetWithBucketValues(NamedList<Object> rawResponse, String expectedFacetName, FacetBucket... expectedBuckets) {
+    final NamedList<Object> facetsTopLevel = assertHasFacetResponse(rawResponse);
+    assertFacetResponseHasFacetWithBuckets(facetsTopLevel, expectedFacetName, expectedBuckets);
+  }
+
+  private NamedList<Object> assertHasFacetResponse(NamedList<Object> topLevelResponse) {
+    Object o = topLevelResponse.get("facets");
+    if (o == null) fail("Response has no top-level 'facets' property as expected");
+    if (!(o instanceof NamedList)) fail("Response has a top-level 'facets' property, but it is not a NamedList");
+
+    return (NamedList<Object>) o;
+  }
+
+  private void assertFacetResponseHasFacetWithBuckets(NamedList<Object> facetResponse, String expectedFacetName, FacetBucket... expectedBuckets) {
+    Object o = facetResponse.get(expectedFacetName);
+    if (o == null) fail("Response has no top-level facet named '" + expectedFacetName + "'");
+    if (!(o instanceof NamedList)) fail("Response has a property for the expected facet '" + expectedFacetName + "' property, but it is not a NamedList");
+
+    final NamedList<Object> expectedFacetTopLevel = (NamedList<Object>) o;
+    o = expectedFacetTopLevel.get("buckets");
+    if (o == null) fail("Response has no 'buckets' property under 'facets'");
+    if (!(o instanceof List)) fail("Response has no 'buckets' property containing actual facet information.");
+
+    final List<NamedList> bucketList = (List<NamedList>) o;
+    assertEquals("Expected " + expectedBuckets.length + " buckets, but found " + bucketList.size(),
+        expectedBuckets.length, bucketList.size());
+    for (int i = 0; i < expectedBuckets.length; i++) {
+      final FacetBucket expectedBucket = expectedBuckets[i];
+      final NamedList<Object> actualBucket = bucketList.get(i);
+      assertEquals(expectedBucket.getVal(), actualBucket.get("val"));
+      assertEquals(expectedBucket.getCount(), actualBucket.get("count"));
+    }
   }
 
 }