Customizing Apachesolr facets built by FacetAPI with the power of CTools


The Apachesolr module (7.x) allows us to build sites that use powerful search technology. Besides being blazingly fast, Apachesolr has another advantage: faceted search.

Faceted search allows our search results to be filtered by defined criteria like category, date, author, location, or anything else that can come out of a field. We call these criteria facets. With facets, you can narrow down your search more and more until you get to the desired results.

Sometimes, however, the standard functionality is not enough. You might want or need to customize the way that facets work. This is controlled by Facet API. Unfortunately there is not much documentation for Facet API, and the API can be difficult to understand.

This post will change that!

In this post, you will learn how to use FacetAPI to create powerful custom faceted searches in Drupal 7. I will take a look at the UI of the module as well as its internals, so that we can define our own widgets, filters, and dependencies using some cool ctools features.


For an example of powerful facets, see:

This is a site that we built recently. It uses facets from SearchAPI instead of Apachesolr, but the general principle is the same.

As we move forward, I plan to answer the following questions:

  • How can I set the sort order of facet links? How can I create my own sort order?
  • How can I show/hide some facets depending on custom conditions? For example, how can I show a facet only if there are more than 100 items in the search results
  • How can I exclude some of the links from the facets?
  • How can I change the facets from a list of links to form elements with autosubmit behavior?

I will start with showing the general settings that can be configured out of the box and then dive into the code to show how those settings can be extended.

Block settings options

Let's take a look at the contextual links of the facet block. We can see there are three special options:

  • Configure facet display
  • Configure facet dependencies
  • Configure facet filters

Let's take a closer look at each of these.

Facets UI

On the facet display configuration page we can choose the widget used to display the facet and its soft limit, the setting of the widget, which controls how many facets are displayed. We can also use different sort options to set the links to display in the order we prefer.

The next settings page is for Facet dependencies. Here we can choose different options that will make Drupal understand when to show or hide the facet block.

The third settings page is Facet filters. Here we can filter what facet items to show (or not show) in the block.

As you can see, there are plenty of options in the UI that we can use when we set up our facets. We can filter some options, change sort order, control visibility, and even change the facet widget to something completely different than a list of links.

Now let's take a look at the internals of Facet API and learn how we can define our own facet widgets, facet dependencies, and facet filters.

Facet API is built on the ctools plugins system, which is a very, very flexible instrument.

Build your own facet widget

Imagine we want to create a facet that will consist of a select list of options and a submit button. Better yet, let's hide the submit button and just autosubmit the form when we change the value by selecting an item from the list.

In order to define our own widget we need to implement the following hook:

* Implements hook_facetapi_widgets()
function example_facetapi_widgets() {
  return array(
'example_select' => array(
'handler' => array(
'label' => t('Select List'),
'class' => 'ExampleFacetapiWidgetSelect',
'query types' => array('term', 'date'),

With this we define a new widget called "example_select" which is bound to a class called "ExampleFacetapiWidgetSelect".

All our logic will be in this ExampleFacetapiWidgetSelect plugin class:

class ExampleFacetapiWidgetSelect extends FacetapiWidget {
   * Renders the form.
public function execute() {
$elements = &$this->build[$this->facet['field alias']];

    $elements = drupal_get_form('example_facetapi_select', $elements);

Instead of rendering the links directly, we will load a form to show our select element.

Now let's see how to build the form:

* Generate form for facet.
function example_facetapi_select($form, &$form_state, $elements) {

  // Build options from facet elements.
$options = array('' => t('- Select -'));
  foreach (
$elements as $element) {
    if (
$element['#active']) {
$options[serialize($element['#query'])] = $element['#markup'] . '(' . $element['#count'] . ')';

  $form['select'] = array(
'#type' => 'select',
'#options' => $options,
'#attributes' => array('class' => array('ctools-auto-submit')),
'default_value' => '',
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Filter'),
'#attributes' => array('class' => array('ctools-use-ajax', 'ctools-auto-submit-click')),

  // Lets add autosubmit js functionality from ctools.
$form['#attached']['js'][] = drupal_get_path('module', 'ctools') . '/js/auto-submit.js';
// Add javascript that hides Filter button.
$form['#attached']['js'][] = drupal_get_path('module', 'example') . '/js/example-hide-submit.js';

  $form['#attributes']['class'][] = 'example-select-facet';

  return $form;

* Submit handler for facet form.
function example_facetapi_select_submit($form, &$form_state) {
$form_state['redirect'] = array($_GET['q'], array('query' => unserialize($form_state['values']['select'])));

In this form, the value of each select box option is the url where the page should be redirected. So, on the submit handler we simply redirect the user to the proper page. We add autosubmit functionality via ctools' auto-submit javascript. Also, we add our own javascript example-hide-submit to hide the Filter button. This enables our facet to work even if javascript is disabled. In such a case, the user will just need to manually submit the form.

Each element that we retrieved from FacetAPI has the following properties:

  • #active - Determines if this facet link is active
  • #query - The url to the page that represents the query when this facet is active
  • #markup - The text of the facet
  • #count - The number of search results that will be returned when this facet is active.

Please note how easy it is to add the autosubmit functionality via ctools. It is just a matter of setting up the right attributes class on the form elements, where one is used as sender (.ctools-autosubmit) and the other one is the receiver (.ctools-autosubmit-click). Then we just need to add the javascript in order to get the autosubmit functionality working.

Please use minimum beta8 version of the FacetAPI module.

Sorting order

All options in facets are sorted. On the settings page above, we saw different kinds of sorts. We can also define our own type of sorting if none of the options on the settings page meet our needs. As an example, I will show you how to implement a random sort order.

To reach this goal we need to implement hook_facetapi_sort_info():

* Implements hook_facetapi_sort_info().
function example_facetapi_sort_info() {
$sorts = array();

  $sorts['random'] = array(
'label' => t('Random'),
'callback' => 'example_facetapi_sort_random',
'description' => t('Random sorting.'),
'weight' => -50,

  return $sorts;

* Sort randomly.
function example_facetapi_sort_random(array $a, array $b) {
rand(-1, 1);

The sort callback is passed to the uasort() function, so we need to return -1, 0, or 1. Note that you again get a facetapi element, which has the same properties as outlined above, so you could, for example, compare $a['#count'] and $b['#count'].

In order to see the result we just need to disable all the other sort order plugins and enable our own.


When facet items are generated they are passed on to filters. If we want to exclude some of the items, we should look at the filters settings. We can also, of course, implement our own filter.

As an example, we will create a filter where we can specify what items (by labels) we want to exclude on the settings page.

* Implements hook_facetapi_filters().
function example_facetapi_filters() {
  return array(
'exclude_items' => array(
'handler' => array(
'label' => t('Exclude specified items'),
'class' => 'ExampleFacetapiFilterExcludeItems',
'query types' => array('term', 'date'),

Like with the widgets, we define a filter plugin and bind it to the ExampleFacetapiFilterExcludeItems class.

Now lets take a look at the ExampleFacetapiFilterExcludeItems class:


* Plugin that filters active items.
class ExampleFacetapiFilterExcludeItems extends FacetapiFilter {

   * Filters facet items.
public function execute(array $build) {
$exclude_string = $this->settings->settings['exclude'];
$exclude_array = explode(',', $exclude_string);
// Exclude item if its markup is one of excluded items.
$filtered_build = array();
    foreach (
$build as $key => $item) {
      if (
in_array($item['#markup'], $exclude_array)) {
$filtered_build[$key] = $item;

    return $filtered_build;

   * Adds settings to the filter form.
public function settingsForm(&$form, &$form_state) {
$form['exclude'] = array(
'#title' => t('Exclude items'),
'#type' => 'textfield',
'#description' => t('Comma separated list of titles that should be excluded'),
'#default_value' => $this->settings->settings['exclude'],

   * Returns an array of default settings
public function getDefaultSettings() {
    return array(
'exclude' => '');

In our example, we define settingsForm to hold information about what items we want to exclude. In the execute method we parse our settings value and remove the items we don't need.

Again please note how easy it is to enhance a plugin to expose settings in a form: All that is needed is to define the functions settingsForm and getDefaultSettings in the class.


In order to create our own dependencies we need to implement hook_facetapi_dependencies() and define our own class. This is very similar to the implementation of creating a custom filter, so I am not going to go into great detail here. The main idea of dependencies is that it allows you to show facet blocks based on a specific condition. The main difference between using dependencies and using context to control the visibility of these blocks is that facets whose dependencies are not matched are not even processed by FacetAPI.

For example, by default there is a Bundle dependency that will show a field facet only if we have selected the bundle that has the field attached. This is very handy, for example, in the situation when we have an electronics shop search. Facets related to monitor size should be shown only when the user is looking for monitors (when he has selected the bundle monitor in the bundle facet). We could create our own dependency to show some facet blocks only when a specific term of another facet is selected. There are many potential use cases here. For example you can think of a Facet that really is only interesting to users interested in content from a specific country. So you could process and show this facet only if this particular country is selected. A practical example would be showing the state field facet only when the country "United States" is selected as you know that for other countries filtering by the state field is not useful. Being able to tweak this yourself gives you endless possibilities!

Here is a shorted code excerpt from FacetAPI that can be used as a sample. It displays facet if the user has one of the selected roles. The main logic is in execute() method.

class FacetapiDependencyRole extends FacetapiDependency {

   * Executes the dependency check.
public function execute() {
$roles = array_filter($this->settings['roles']);
    if (
$roles && !array_intersect_key($user->roles, $roles)) {

   * Adds dependency settings to the form.
public function settingsForm(&$form, &$form_state) {
$form[$this->id]['roles'] = array(
'#type' => 'checkboxes',
'#title' => t('Show block for specific roles'),
'#default_value' => $this->settings['roles'],
'#options' => array_map('check_plain', user_roles()),
'#description' => t('Show this facet only for the selected role(s). If you select no roles, the facet will be visible to all users.'),

   * Returns defaults for settings.
public function getDefaultSettings() {
    return array(
'roles' => array(),


As you can see, FacetAPI gives us the option to change anything we want about facets. We can change the display, alter the order, filter some facets out, and control facet blocks visibility and content based on dependencies we define.

I would like to thank the maintainers of this module, Chris Pliakas and Peter Wolanin, for their great work!

An example module is attached to this article. Thank you for reading, and if you have any further questions please let us know in the comments!

(Author: Yuriy Gerasimov, Co-Author: Fabian Franz, Editor: Stuart Broz)

example_facetapi.tar.gz1.99 KB


To say this post is excellent

To say this post is excellent is a vast understatement. You hit all of the major concepts perfectly, and this is an invaluable resource I will be referring everyone I can to. Thanks for this great contribution, better than any patch that could be written. Thanks for your patches, too :-).

I agree! Excellent and

I agree! Excellent and extremely informative article about FacetAPI. Thank you.

Thanks for the post. ¿Do you

Thanks for the post. ¿Do you know if there are some way to transform the ugly search url's into a clean and friendly url's?

I've see some tutorial to do this but it didn't consistent.

For example, in your home web you have a block with category links. Don't you think that instead of having this type of links

would be better this type


But not only this, in the search too, if i filter the search by people and location

¿What do you think?

The URL system in Facet API

The URL system in Facet API is pluggable, however the things you are pointing to are more filters than facets. This is best accomplished by utilizing the search pages functionality on Apache Solr Search Integration / Search API, where you can set alternate search paths (i.e. that have pre-filtered results. Note that facet calculations are much more expensive from a resource standpoint than plain filters.YcCfm

Thanks por de info ;)

Thanks por de info ;)

Thanks for your post. I

Thanks for your post.
I downloaded the example module and tried to use it, but when I select the display widget I get the following error:

Notice: Undefined index: query types in facetapi_facet_display_form_validate() (line 690 of /var/www/styrdokument/sites/all/modules/facetapi/
The widget does not support the term query type

My version of Facet API is 7.x-1.0-beta8. Do you have any idea what could be wrong?

Yes. I have updated example

Yes. I have updated example module. Please download it again and try.

The problem was about definition of Select widget. It should be changed to

function example_facetapi_widgets() {
  return array(
'example_facetapi_select' => array(
'handler' => array(
'label' => t('Select'),
'class' => 'ExampleFacetapiSelect',
'query types' => array('term', 'date'),

Key 'query types' is required.

Ok, thanks for quick reply.

Ok, thanks for quick reply.

The example module works fine

The example module works fine as long as you use a single facet, but try to use more than one and you get problems: "An illegal choice has been detected. Please contact the site administrator."

Very good point. Thank you

Very good point. Thank you for testing. It doesn't work because we show several forms with same id. I have changed the code to use hook_forms to generate facet forms. Now it works with several facets showed at the same time. Please retest.

I'm now getting fatal errors

I'm now getting fatal errors in my (non-Solr) dev environment. Could the following line be changed to something more generic?

$searcher = 'apachesolr@solr';

After all, the power of FacetAPI/Search API is that it is backend agnostic...

Notice: Undefined index: apachesolr@solr in facetapi_get_facet_info() (line 525 of /var/www/drupal-dev/sites/all/modules/facetapi/facetapi.module).
Notice: Undefined index: apachesolr@solr in facetapi_get_facet_info() (line 525 of /var/www/drupal-dev/sites/all/modules/facetapi/facetapi.module).
Recoverable fatal error: Argument 1 passed to search_api_facetapi_facetapi_facet_info() must be an array, null given in search_api_facetapi_facetapi_facet_info() (line 76 of /var/www/drupal-dev/sites/all/modules/search_api/contrib/search_api_facetapi/search_api_facetapi.module).

I have updated the code. Now

I have updated the code. Now searchers retrieved from facetapi_get_searcher_info() so it should work for any backend.

foreach (facetapi_get_searcher_info() as $searcher => $searcher_info) {
    foreach (
facetapi_get_facet_info($searcher) as $facet) {
$forms['example_facetapi_select_' $facet['name']]['callback'] = 'example_facetapi_select';

Could you please test and advise is there anything else that should be fixed.

Works great, thanks!

Works great, thanks!

I notice on the

I notice on the link - the Facet blocks do not have bullets? I have been looking for a way to get rid of them and just have the text links (and checkbox) - can you share how you did it?

The theme css declaration li

The theme css declaration

li {
list-style: none;

and browser default
ul, menu, dir {
list-style-type: disc;

are overridden by a more specific declaration
.region-sidebar-first .block ul li {
list-style: none;

This post is the best

This post is the best explatanion about the facet api stuff. Tanks so much for that. I have a question:

What about not hiding the submit button and make the selectlist multiselect, and put a checkbox on every item so the enduser can make multiple selections.

Is this possible somehow ?

Agreed, this is by far the

Agreed, this is by far the best Facet API doc out there at the moment, really helpful. Thanks Yuriy!

One of our customers noticed

One of our customers noticed that facets that are not relevant are disappearing. Can we change this behaviour? Or is this exactly how facetsearch works?


John Verbeek

Hi John, I would say that

Hi John, I would say that this is how faceted search works. If there are no options in facet it is simply not shown. Meanwhile I had a case when I had to show empty facets for one of our client. If this is requirement for you please contact me privately I will share some advice.

Excellent post, thank! I have

Excellent post, thank!
I have a question, I have noticed that on the site you have linked you have a date filter that exposes the year, I am trying to do something similar, but for all I try I get only to see the facets in the format "Month, YYYY (count)" and after clicking on them a subfilter is shown as in "Month DD, YYYY(count)" and so on by hour and minutes! All I want is actually to have a structure like YYYY at top level and Month as sub level. Is this possible? How did you get to show just the year in your example?
Thanks Yuriy!

On Photoshare site the year

On Photoshare site the year is separate string type year. So there is nothing special there. If you would like to "split" date field to year and months, I believe you will need to index your content in custom way. I would suggest to take a look how hierarchical taxonomy terms got indexed. In your custom code you can create separate field that for each date will create two "terms" year and month that will be connected.

Hi, thank you very much for

thank you very much for this posting! I install the example Module but i did see nothing concerning that! Please how can i see someting happened?

Thanks a lot!


Looking at your example site,

Looking at your example site, I was expecting to be able to select more than one option from a category -- eg. "children" and "adolescents" from the "People" category -- and get a boolean OR set of results, not a reduced set via boolean AND. Only between categories would I expect the AND combination. Is there any way to allow for ORing two or more choices from within one category?

I know this article is not

I know this article is not about displaying results but I would be really greatful if you could explain how did you set up the results page on the
I can see this is a view (from the output html) and I guess it's made with apachesolr_views module? I can't figure out how to set up this module and how to make facets appear (which I have working with default solr search page).

Here is the code I wrote to

Here is the code I wrote to add a new index field, and then add a new facet. Till here it works fine. The facet shows up in admin/config/search/apachesolr/settings/solr/facets

But the relevant block is not showing up at admin/structure/block

function jem_search_apachesolr_index_document_build($document, $entity, $entity_type, $env_id) {
$collection_id = (int) $entity->field_address_collection['und'][0]['value'];
$address_collection = entity_load('field_collection_item', array($collection_id));

$fieldCollection_entity = $address_collection[$collection_id];

$country = $fieldCollection_entity->field_country['und'][0]['value'];
$state_province = $fieldCollection_entity->field_state_province['und'][0]['value'];
$city = $fieldCollection_entity->field_city['und'][0]['value'];

$document->ss_field_country = $country;
$document->ss_field_state_provience = $state_province;
$document->ss_field_city = $city;

jem_search_facetapi_facet_info($searcher_info) {


$facets = array();

// Facets are usually associated with the type of content stored in the index.
if (isset($searcher_info['types']['node'])) {

$facets['ss_field_country'] = array(
'name' => 'my country',
'label' => t('My country'),
'field api name' => 'ss_field_country',
'description' => t('Custom facet by amrit for countries.'),
'map callback' => 'jem_search_map_ss_field_country',
'query types' => array('term', 'date'),
'dependency plugins' => array('role'),
'default widget' => 'links',


jem_search_map_ss_field_country(array $values) {
$map = array();
$values as $val) {
$map[$val] = $val;

/* I added this add got the block listed.. but it doesn't comes up in the search page..
function jem_search_facetapi_force_delta_mapping() {
  return array(
// The machine-readable name of the searcher.
'apachesolr@solr' => array(
// The realm we are mapping, usually block.
'block' => array(
// Machine readable names of facets whose mappping are being forced.
        // Regardless of whether they are enabled via the Facet API interface,
        // their blocks will be available to enable and position via
        // admin/structure/block.

Can anybody help? I want to write a complete Drupal 7 documentation after I figure this out.


Oops! my bad.. put wrong

Oops! my bad..

put wrong value in 'name' of facet_info()

Thank you guys! awesome module :p Spent 2 days to figure everything out! Now will create a widget and I am done!

Where do I go to find this

Where do I go to find this facet UI cos I don't see it anywhere in my config! This documentation is incomplete.

When you have added facet to

When you have added facet to the page, in the block contextual links you will see facet UI links. Like on screenshot

How can I create fivestar

How can I create fivestar facets with stars as images?

Hi JO, I haven't tried it,

Hi JO,

I haven't tried it, but I'm sure you could use the same approach to build a custom facet to replicate the fivestar widget. Probably not going to be easy, but sounds awesome. Definitely take a look at the fivestar module widget implementation and try to re-use what you can from there.

Good luck!

Great Post ! Please Could you

Great Post ! Please Could you tell us how to deal with hierarchy and build like the category block as you mad in this site : ??

Hi Can you give me a working

Can you give me a working example of how facet api is to be used for custom facets with drupal 6 and apachesolr. Please provide coded example. I am unable to find a working example for drupal 6.

I want to show facets widget

I want to show facets widget on all pages right below the search form. And I want all facets to be there. I've found "Show facets on non-search pages." checkbox and checked it and used "*" as filter to show facet block everywhere. Now it messes up with my search results. After hours of tracing I found out that in order to populate facets block the empty search request is performed (see: apachesolr_search_init() there is a call of apachesolr_search_run_empty()).
Here is what happens:
- Say I search for "hello". In its submit handler search block form redirects me to "site/search/hello" or to "site/search/hello?f[0]=im_bla_bla_bla%3A23"
- Drupal catches that redirect and does two things:
first it makes a notorious emty query to solr to get all facets
second it tries to make an actual search query and sees that query was done (the empty one) and uses result of empty query to populate results.
- Drupal presents a search result that does not make sence as there is all indexed content in there.
Hope my explanation makes sence.

I'd like to know how does one trick solr search module into making a search request even if empty request was done?

Hi Many thanks! What I would

Many thanks! What I would like to know if what to stop the date filter drilling down to day and time. I would like it to stop at the month - as the filter by date works on this site.
Is that possible?

Greetings, Quick question


Quick question regarding facet link order. How do you set a specific order to the facets? Or better yet, how do you set the raw value --> "Sort by the raw value stored in the index." I have searched everywhere and cannot find this. I know it can be done as I have done it before, just forgot how.

Thanks for any help!

- James

Sorting the $build array in

Sorting the $build array in execute() should give the desired order.

Hi everyone! Thanks for this

Hi everyone!
Thanks for this post :)
So, I have one question:
How I can display my custom facet on the page?
I can see other facets on the page of blocks
But I don't see my custom facet on the page of blocks and
I don't see it on the admin page:

I hope somebody can help me...

Hi there, thanks for your

Hi there, thanks for your documentation
This is a good plugin...
Im trying to use it in order to display a taxonomy navigation
It works but not as much as I want ... let me explain

In my test, Ive got a bunch of product pages. Each product is associated with a term, from a 2 level vocabulary. The term associated with is from the level 2 of this vocabulary.

Ive created an index on the term-reference field and Ive created a facet with this field.

My facet is displayed on a page view, which is listing my products.
There the facet lists all my level 1 vocabulary terms, and each term is a link to display the associated products.

The problem is : when I click a term, a new page is loaded and the facet is then only displaying the clicked term and all of its children.

But the other level 1 terms are not displayed anymore ...

How can I manage to always display the level 1 terms ?
Note that I do want to display the level 2 term related to a level 1 term I have clicked, but I would simply be better to always see other level 1 terms so that I do not have to go backward

Ive tried a lot of options, facet api bonus, facetapi plugins but can't find a way to have this functionnality

Keep in mind that I do not want to cumulate the selected terms ... just want to always display the top terms from my vocabulary

Hope someone will understand my situation :)

Hi, When I tried to save the


When I tried to save the facets configuration i've a fatal error :

Fatal error: facetapi_save_facet_status() [function.facetapi-save-facet-status]: The script tried to execute a method or access a property of an incomplete object. Please ensure that the class definition "ApacheSolrFacetapiAdapter" of the object you are trying to operate on was loaded _before_ unserialize() gets called or provide a __autoload() function to load the class definition in facetapi.module on line 1159

Did someone have this error before ?

I'm tryin to build my own

I'm tryin to build my own sorting function by hooking facet api sort (hook_facetapi_sort), sorted by weight of several data in table.

here, I've table called animal with data
name, weight
dog, -2
cat, 1
zebra, -5
dolphin, 3
bee, 5

function zoo_facetapi_sort_animal(array $a, array$b) {
$a = db_query("select weight from {animal} where name = :a", array(':a' => $a['#markup']))->fetchAssoc();
$b = db_query("select weight from {animal} where name = :b", array(':b' => $b['#markup']))->fetchAssoc();

$a_value = (isset($a['weight'])) ? $a['weight']:0;
$b_value = (isset($b['weight'])) ? $b['weight']:0;

strnatcmp($b_value, $a_value);

result is like what I expected:

but I've got error message in log
Warning: uasort(): Array was modified by the user comparison function in FacetapiWidget->applySorts() (line 230 of /var/www/

how to avoid that message, is code wrong?

Hello there, I've installed

Hello there,

I've installed this module and no facet or block coming up.

Can you anyone help?

Hi, I am using Kickstart


I am using Kickstart (Drupal Commerce) and using search Facets, I see that as soon as I select a check box in a category, it disable selecting any other in order to add one more logical OR value.

Is there any way to allow more than one value in each category?


I am stuck on same

I am stuck on same point.

I've installed this module and no facet or block coming up.

This page is linked as official documentation of FacetAPI.

It is desperately needed to be updated easy enough to understand for new people.

In order to enable Facet

In order to enable Facet block , download and install Search Facets module.

Great post on facetapi.

Great post on facetapi. Thanks
Is there a way that we can have checkbox in front of each item so that user can make multiple selections like newegg?


Post new comment

The content of this field is kept private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.
  • You may post code using <code>...</code> (generic) or <?php ... ?> (highlighted PHP) tags.

More information about formatting options