diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 814a85fda2a..dc45c6dacd4 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -265,6 +265,8 @@ New Features
* SOLR-2112: Solrj API now supports streaming results. (ryan)
+* SOLR-792: Adding PivotFacetComponent for Hierarchical faceting
+ (erik, Jeremy Hinegardner, Thibaut Lassalle, ryan)
Optimizations
----------------------
diff --git a/solr/example/solr/conf/solrconfig.xml b/solr/example/solr/conf/solrconfig.xml
index 6e11651d0d0..318d0b52a26 100755
--- a/solr/example/solr/conf/solrconfig.xml
+++ b/solr/example/solr/conf/solrconfig.xml
@@ -498,6 +498,9 @@
2.1
-->
+
+ pivot
+
-
+
diff --git a/solr/src/common/org/apache/solr/common/params/FacetParams.java b/solr/src/common/org/apache/solr/common/params/FacetParams.java
index d119c0ea256..d7a20cd11ea 100644
--- a/solr/src/common/org/apache/solr/common/params/FacetParams.java
+++ b/solr/src/common/org/apache/solr/common/params/FacetParams.java
@@ -93,6 +93,22 @@ public interface FacetParams {
*/
public static final String FACET_MISSING = FACET + ".missing";
+
+ /**
+ * Comma separated list of fields to pivot
+ *
+ * example: author,type (for types by author / types within author)
+ */
+ public static final String FACET_PIVOT = FACET + ".pivot";
+
+ /**
+ * Minimum number of docs that need to match to be included in the sublist
+ *
+ * default value is 1
+ */
+ public static final String FACET_PIVOT_MINCOUNT = FACET_PIVOT + ".mincount";
+
+
/**
* String option: "count" causes facets to be sorted
* by the count, "index" results in index order.
diff --git a/solr/src/java/org/apache/solr/core/SolrCore.java b/solr/src/java/org/apache/solr/core/SolrCore.java
index 378a59aa3d7..2cb1b44bcb8 100644
--- a/solr/src/java/org/apache/solr/core/SolrCore.java
+++ b/solr/src/java/org/apache/solr/core/SolrCore.java
@@ -833,6 +833,7 @@ public final class SolrCore implements SolrInfoMBean {
addIfNotPresent(components,HighlightComponent.COMPONENT_NAME,HighlightComponent.class);
addIfNotPresent(components,QueryComponent.COMPONENT_NAME,QueryComponent.class);
addIfNotPresent(components,FacetComponent.COMPONENT_NAME,FacetComponent.class);
+ addIfNotPresent(components,PivotFacetComponent.COMPONENT_NAME,PivotFacetComponent.class);
addIfNotPresent(components,MoreLikeThisComponent.COMPONENT_NAME,MoreLikeThisComponent.class);
addIfNotPresent(components,StatsComponent.COMPONENT_NAME,StatsComponent.class);
addIfNotPresent(components,DebugComponent.COMPONENT_NAME,DebugComponent.class);
diff --git a/solr/src/java/org/apache/solr/handler/component/PivotFacetComponent.java b/solr/src/java/org/apache/solr/handler/component/PivotFacetComponent.java
new file mode 100644
index 00000000000..9f8a714678b
--- /dev/null
+++ b/solr/src/java/org/apache/solr/handler/component/PivotFacetComponent.java
@@ -0,0 +1,278 @@
+/**
+ * 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.handler.component;
+
+import org.apache.solr.search.SolrIndexSearcher;
+import org.apache.solr.search.DocSet;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.params.FacetParams;
+import org.apache.solr.request.SimpleFacets;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.schema.FieldType;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.index.Term;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ *
+ * @since solr 4.0
+ */
+public class PivotFacetComponent extends SearchComponent
+{
+ public static final String COMPONENT_NAME = "pivot";
+
+ static final String PIVOT_KEY = "facet_pivot";
+
+
+ /**
+ * Designed to be overridden by subclasses that provide different faceting implementations.
+ * TODO: Currently this is returning a SimpleFacets object, but those capabilities would
+ * be better as an extracted abstract class or interface.
+ */
+ protected SimpleFacets getFacetImplementation(SolrQueryRequest req,
+ DocSet docs,
+ SolrParams params) {
+ return new SimpleFacets(req, docs, params);
+ }
+
+ @Override
+ public void prepare(ResponseBuilder rb) throws IOException
+ {
+ if (rb.req.getParams().getBool(FacetParams.FACET,false)) {
+ rb.setNeedDocSet( true );
+ rb.doFacets = true;
+ }
+ }
+
+ public void process(ResponseBuilder rb) throws IOException {
+ if (!rb.doFacets) return;
+
+ SolrParams params = rb.req.getParams();
+ String[] pivots = params.getParams(FacetParams.FACET_PIVOT); // example: author,type (for types by author / types within author)
+ if (pivots == null) return;
+
+ int minMatch = params.getInt( FacetParams.FACET_PIVOT_MINCOUNT, 1 );
+
+ SimpleOrderedMap>> pivotResponse = new SimpleOrderedMap>>();
+ for (String pivot : pivots) {
+ String[] fields = pivot.split(","); // only support two levels for now
+
+ if( fields.length < 2 ) {
+ throw new SolrException( ErrorCode.BAD_REQUEST,
+ "Pivot Facet needs at least two fields: "+pivot );
+ }
+
+ DocSet docs = rb.getResults().docSet;
+ String field = fields[0];
+ String subField = fields[1];
+ Deque fnames = new LinkedList();
+ for( int i=fields.length-1; i>1; i-- ) {
+ fnames.push( fields[i] );
+ }
+
+ SimpleFacets sf = getFacetImplementation(rb.req, rb.getResults().docSet, rb.req.getParams());
+ NamedList superFacets = sf.getTermCounts(field);
+
+ pivotResponse.add(pivot, doPivots(superFacets, field, subField, fnames, rb, docs, minMatch));
+ }
+ NamedList facetCounts = (NamedList) rb.rsp.getValues().get("facet_counts");
+ if (facetCounts == null) {
+ facetCounts = new NamedList();
+ rb.rsp.add("facet_counts", facetCounts);
+ }
+ facetCounts.add( PIVOT_KEY, pivotResponse);
+ }
+
+ /**
+ * Recursive function to do all the pivots
+ */
+ protected List> doPivots( NamedList superFacets, String field, String subField, Deque fnames, ResponseBuilder rb, DocSet docs, int minMatch ) throws IOException
+ {
+ SolrIndexSearcher searcher = rb.req.getSearcher();
+ // TODO: optimize to avoid converting to an external string and then having to convert back to internal below
+ FieldType ftype = null;
+
+ // SimpleFacets sf = getFacetImplementation(rb.req, docs, rb.req.getParams());
+ String nextField = fnames.poll();
+
+ List> values = new ArrayList>( superFacets.size() );
+ for (Map.Entry kv : superFacets) {
+ // Only sub-facet if parent facet has positive count - still may not be any values for the sub-field though
+ if (kv.getValue() > minMatch ) {
+ SimpleOrderedMap