Implement faceted navigation - BloomReach Experience - Open Source CMS

This article covers a Hippo CMS version 10. There's an updated version available that covers our most recent release.

12-05-2015

Implement faceted navigation

Solr support faceted navigation out-of-the-box. You need nothing special to achieve this. The only thing you should realize, is that if you want to do faceted navigation on the field as it IS (thus not tokenized), you should index the field as type="string". When you use this, the field is treated more like a keyword, and not tokenized at all. When you also want to tokenize the field to make is searchable, you should use copyField and copy the field to a field that has type="text_general". Either way, you can also do faceted navigation on tokenized fields, but the facet values are then the tokens in the field instead of the entire field.

How to:

For a working version of this, see TESTSUITE at Solr Faceted menu item

Assume we have the following sitemap item structure to support a faceted navigation URL (facet/value/facet/value/...etc kind of path)

HST Sitemap structure:

<sv:node sv:name="solrfaceting">
  <sv:property sv:name="jcr:primaryType" sv:type="Name">
    <sv:value>hst:sitemapitem</sv:value>
  </sv:property>
  <sv:property sv:name="hst:componentconfigurationid" sv:type="String">
    <sv:value>hst:pages/solrfacetingpage</sv:value>
  </sv:property>
  <sv:node sv:name="_any_">
    <sv:property sv:name="jcr:primaryType" sv:type="Name">
      <sv:value>hst:sitemapitem</sv:value>
    </sv:property>
    <sv:property sv:name="hst:componentconfigurationid" sv:type="String">
      <sv:value>hst:pages/solrfacetingpage</sv:value>
    </sv:property>
    <sv:property sv:multiple="true" sv:name="hst:parameternames"
                sv:type="String">
      <sv:value>facetpath</sv:value>
    </sv:property>
    <sv:property sv:multiple="true" sv:name="hst:parametervalues"
                 sv:type="String">
      <sv:value>${1}</sv:value>
    </sv:property>
  </sv:node>
</sv:node>

Faceted navigation java code:

@Override
public void doBeforeRender(final HstRequest request,
                           final HstResponse response)
                                          throws HstComponentException {

    HippoSolrClient solrClient =
          HstServices.getComponentManager().getComponent(
                HippoSolrClient.class.getName(), "org.hippoecm.hst.solr");
    SolrSearchParams params = new SolrSearchParams(request);

    try {
        // set the free text query if present
        String userQuery = ....
        HippoQuery hippoQuery = solrClient.createQuery(query);

        SolrQuery solrQuery = hippoQuery.getSolrQuery();
        // see the sitemap item config : There we store on the 'facetpath'
        // param
        // the facetpath ${1}
        String facetPath = request.getRequestContext()
                       .getResolvedSiteMapItem().getParameter("facetpath");
        if (facetPath != null) {
            // there is a facet path : Account for the constraints
            // the facet path looks like key/value/key/value

            String[] constraints = facetPath.split("/");
            if (constraints.length % 2 != 0) {
                log.warn("Invalid constraints because not equal number
                                                     of keys and values");
            } else {
                int i = 0;
                while (i < constraints.length) {
                    String facetField = constraints[i];
                    String facetValue = constraints[i+1];
                    // Add the filter query for every facet/value
                    // combination.
                    // Note the facetValue must be escaped
                    // Also note that Solr caches FilterQuery's separately,
                    // so performance
                    // will increase after more and more faceting
                    solrQuery.addFilterQuery(facetField + ":"
                          + solrManager.getQueryParser().escape(facetValue));
                    i+=2;
                }
            }
        }

        // set rows to 0 : we are not interested to get results, but to get
        // faceted navigation
        solrQuery.setRows(0);
        // we want faceting now for "brand", "categories" and "title"
        solrQuery.addFacetField("brand", "categories", "title");
        solrQuery.setFacetLimit(10);

        // example of included data range faceting
        Calendar startCal = Calendar.getInstance();
        startCal.add(Calendar.YEAR, -5);

        Date startDate = startCal.getTime();
        Date endDate = Calendar.getInstance().getTime();
        solrQuery.addDateRangeFacet("date", startDate , endDate, "+1YEAR");

        // From SOLR 3.6 and higher you can also use :
        // solrQuery.addDateRangeFacet("date", startDate , endDate, "+1YEAR,
        //                             +1MONTH, +1DAY");
        // This way you can create multiple buckets for 1 date field

        final HippoQueryResult result = hippoQuery.execute();

        // we do not need to bind the beans with their providers for
        // faceting, so no need for
        // DO NOT result.bindHits()

        request.setAttribute("result", result);
        request.setAttribute("query", params.getQuery());

    } catch (SolrServerException e) {
        throw new HstComponentException(e);
    }
}

And the JSP code to show the facets:

  <hst:link var="currentLink"/>

  <c:forEach var="facetField" items="${result.queryResponse.facetFields}">
    <br/>
    ${facetField.name}<br/>
    <ul style="padding-left:20px;">
      <c:forEach var="facet" items="${facetField.values}">
        <li>
            <c:set var="facetLink"
                   value="${currentLink}/${facet.facetField.name}/
                                                          ${facet.name}" />
            <c:choose>
              <c:when test="${query eq null}">
                <a href="${facetLink}">${facet.name} (${facet.count})</a>
              </c:when>
              <c:otherwise>
                <a href="${facetLink}?query=${query}">
                         ${facet.name} (${facet.count})
                </a>
              </c:otherwise>
            </c:choose>

        </li>
      </c:forEach>
    </ul>
  </c:forEach>
  <c:forEach var="facetRange" items="${result.queryResponse.facetRanges}">
    <br/>
    ${facetRange.name}<br/>
    <ul style="padding-left:20px;">
      <c:forEach var="facet" items="${facetRange.counts}">
        <li>
            ${facet.value} (${facet.count})
        </li>
      </c:forEach>
    </ul>
  </c:forEach> 

Did you find this page helpful?
How could this documentation serve you better?
On this page
    Did you find this page helpful?
    How could this documentation serve you better?