Drupal7 AJAXified Solr Search with facets. Search API, Panels and some custom code.

In this article I would like to show how to build very nice user experience search page with facets. For this we will use Apache Solr as our search engine and some custom code to make it fully AJAX (with full non-javascript support).

Here is demo screencast of what we want to do.




Link http://www.youtube.com/watch?v=LhKWmWRqeJc


In Drupal 7 there are several ways to integrate Apache Solr to our system. I have chosen to use Search API as it has already built integration with Views and we can create several indexes for our site. This is very effecient in way of performance and scalability.


So lets get started!



Installed modules:

- Chaos tools 7.x-1.0-alpha3

- Page manager 7.x-1.0-alpha3

- Entity API 7.x-1.0-beta7

- Panels 7.x-3.0-alpha3

- Search API 7.x-1.0-beta7 (latest dev version)

- Search facets 7.x-1.0-beta7

- Search views 7.x-1.0-beta7

- Solr search 7.x-1.0-beta7 (http://drupal.org/project/search_api_solr)

- Views 7.x-3.0-alpha1 (latest dev version)

- Views UI 7.x-3.0-alpha1

- Devel 7.x-1.0

- Devel generate 7.x-1.0



There is great documentation about installing Solr search in the INSTALL.txt of the module. In brief you need to download Solr PHP client and put it to module folder (http://code.google.com/p/solr-php-client/downloads/list). Download Apache Solr itself from http://www.apache.org/dyn/closer.cgi/lucene/solr/ (it is Java application and you will need to install Sun Java for it. If you do it on Ubuntu 10 here are good instructions http://www.clickonf5.org/linux/how-install-sun-java-ubuntu-1004-lts/7777). Copy schema.xml and solrconfig.xml files to _where you extracted solr_/example/solr/conf. Then go to folder _where you extracted solr_/example and run java -jar start.jar. Receive a lot of different letters in output and open in your web browser http://localhost:8983/solr/admin/ and if you see following interface, you have installed apache solr correctly.






In my demo is have extracted solr to /srv/apachesolr folder as you can see on screenshot. Also very important to make sure the version of schema. It should be search-api-1.0.



Next step is to create in Search API server and index. You can view screencast by creator of Search API how to do that http://vimeo.com/15556855. Server should be your local Apache Solr. For index with use node entity.



I have named server "Apache Solr" and index "Apache Solr index".



In index lets have following fields selected to be indexed:

Content type as String

Title as Fulltext

Published as Boolean



I have added Related Entity Author and Main Body so we can select

Author » Name as string

The main body text » Text as Fulltext



On the facets page please enable facet Content type and Author Name.



Now in order to have some test content lets generate it with Devel module. First we generate users (lets do 5 users). Second we generate content (50 nodes both Article and Basic page). After we generated content we should index it. Please go to settings of your index Status tab. Clear index and then Index now.



Next is to create a View (new Views UI was great surprise to me as I have made initially this post with old UI and had to rebuild it with new).



In show selectbox we choose Apache Solr index (or how you named your index). We don't create a page, but create block with table display.






On view settings page:

Fields

Node: Title,

Node: Content type

Author: Name



Filter criteria

Node: Published = 1

Search: Fulltext search -- Exposed filter. Searched fields both TItle and Main body.



Advanced settings

Use AJAX: Yes

Exposed form in block: Yes



So view settings page should look like following screenshot.





Now lets create panel that will be the page with search and facets.



Structure -> Panels. Create new Panel page. For path lets use example. I used in my example simple Two column layout. Everything else is standard.



On the Panel content lets add to left column we add views block we created (Miscellaneous: test_view: Block)

To the right column we add our exposed filter and facets (Miscellaneous: Exposed form: test_view-block_1, Apache Solr index: Author » Name, Apache Solr index: Content type).



Here is screenshot for panels settings.





Now we can go to panel page and see our filter working plus exposed filters. Filter and pager work without page reload but filters don't. For that we need to write some custom code. So lets create custom module.



First we need to add click behavior for facets. As example we take javascript from views module (js/ajax_views.js). If you compare to original example I just removed all unneeded parts and changed selector to find proper links (var view = '.search-api-facets';).


/**
 * @file example.js
 *
 * Handles AJAX facets reactions.
 * 
 * @see views/js/ajax_views.js
 */
(function ($) {
 
/**
 * Attaches the AJAX behavior to Views exposed filter forms and key View links.
 */
Drupal.behaviors.ExampleFacets = {};
Drupal.behaviors.ExampleFacets.attach = function() {
  if (Drupal.settings && Drupal.settings.views && Drupal.settings.views.ajaxViews) {
    // Retrieve the path to use for views' ajax.
    var ajax_path = Drupal.settings.views.ajax_path;
 
    $.each(Drupal.settings.views.ajaxViews, function(i, settings) {
      var view = '.search-api-facets';
      var element_settings = {
        url: ajax_path,
        submit: settings,
        setClick: true,
        event: 'click',
        selector: view,
        progress: {type: 'throbber'}
      };
 
      $(view).filter(':not(.views-processed)')
         .each(function() {
          // Set a reference that will work in subsequent calls.
          var target = this;
          $(this)
            .addClass('views-processed')
            // Process facet links.
            .find('li > a')
            .each(function () {
              var viewData = {};
              // Construct an object using the settings defaults and then overriding
              // with data specific to the link.
              $.extend(
                viewData,
                settings,
                Drupal.Views.parseQueryString($(this).attr('href')),
                // Extract argument data from the URL.
                Drupal.Views.parseViewArgs($(this).attr('href'), settings.view_base_path)
              );
              // For anchor tags, these will go to the target of the anchor rather
              // than the usual location.
              $.extend(viewData, Drupal.Views.parseViewArgs($(this).attr('href'), settings.view_base_path));
 
              element_settings.submit = viewData;
              var ajax = new Drupal.ajax(false, this, element_settings);
            }); // .each function () {
      }); // $view.filter().each
    }); // .each Drupal.settings.views.ajaxViews
  } // if
};
 
})(jQuery);

After we have enabled javascript event handling on our links we need to rebuild facets HTML on answer from server. We can hook into response on hook_ajax_render_alter with adding our own commands. This can be done with following code in module:
// Path of our panel.
define('EXMPLE_PATH', 'example');
 
/**
 * Implements hook_ajax_render_alter().
 */
function example_ajax_render_alter(&$commands) {
  if ($_GET['q'] != EXMPLE_PATH) {
    return;
  }
 
  // Facets that present on the page.
  $facet_delta = array('apache_solr_index_author_name', 'apache_solr_index_type');
  foreach ($facet_delta as $delta) {
    // Load block for facet.
    $block_array = search_api_facets_block_view($delta);
    $delta_class = str_replace('_', '-', $delta);
    // Render block.
    $output = drupal_render($block_array);
    // Show or hide block.
    if (!empty($output)) {
      $commands[] = ajax_command_invoke('.pane-search-api-facets-' . $delta_class, 'show');
      $commands[] = ajax_command_replace('.pane-search-api-facets-' . $delta_class . ' .pane-content .item-list', $output);
    }
    else {
      $commands[] = ajax_command_invoke('.pane-search-api-facets-' . $delta_class, 'hide');
    }
  }
}
 
/**
 * Implements hook_form_FORM_ID_alter().
 */
function example_form_views_exposed_form_alter(&$form, &$form_state) {
  if ($_GET['q'] != EXMPLE_PATH) {
    return;
  }
 
  // Add action to form so it will work without javascript.
  $form['#action'] = EXMPLE_PATH;
  // Add javascript for facets.
  drupal_add_js(drupal_get_path('module', 'example') . '/example.js');
}

We add javascript on form alter for exposed filter. Also we change #action property of the form so it can work without javascript.

Here we go. Now our page is completely AJAXified and also can work without javascript enabled.

So if you need to add any other facets you will need to change the code in custom javascript. As we are dealing with Views we are in full control of theming, adding new fields, sorting etc.

I am sure there are a lot of possibilities to improve this code (at least not to have so much settings hardcoded :).

Module code is attached.

Thank you for reading.

Comments

Just wondering, any reason u didn't use this module in the tut?

Truly to say by the time this article has been written there were no module search_api_ajax. Anyhow here I implement ajax functionality differently with using Views. Main advantage is that we can add as many exposed filters as we want. Disadvantage of this method is that we don't have browser history (YUI3 History) like implemented in search_api_ajax.

This is so so weird. I followed your instructions, built the module (it's still disabled though in my install) but I'm just trying a different layout - I'm putting the facets on the left instead of right. And they don't show up. Any idea what could be causing this? Thanks!!

facets will be shown only if there are different values in search results. So please make sure you have at least two different nodes in results and they have different terms attached (if we are talking about taxonomy terms facets). Also try to reindex your content to make sure ters got attached to nodes in index.

I have a site that has over 18,000 data items in a catalog. I am using Drupal 7 and the latest Views module to display the data. The data items contain an auto make, model, year, cv axle. My search query needs to be able to search this catalog in the following matter: If the EU click in an input box for "make," they can put in the search term (e.g. Ford) and all Ford models pull up. In addition, there is another search field next to the "Make" field for "Model" and another one for "Year." I want Drupal to add all the search terms together to alter the view. Right now, If the user types in one term and searches, it works; however, if the EU types in another term in the "Model" field say, the view that outputs will only include the last term searched. For example, I type in "Ford," and all Fords come up; if I add a term to the "Year" field (e.g. 1999), all 1999 autos come up, not just the Ford models of that year. How do I create a search engine that spans terms across multiple fields (Make+Model+Year: OR however many search term the EU wishes to include) to filter what is output through Views? Any help would be greatly appreciated!! www.sutrack.biz=> cv axle catalog link

Hi Ken, I think your question is little bit out of the topic of the article. I will drop you email. I believe the problem is in how you index your content in Solr or something. But I might get the question wrong.

Hi Yuriy I'm trying out your tutorial again. Thanks for this. One step I'm stuck with is when you say to change the option 'Exposed form in block:' to 'Yes'. This is no longer available in views. Here's where this is mentioned on d.o http://drupal.org/node/1128480#comment-4352264 Is there a workaround? cheers

Here's an updated function which uses the facetapi which search_api now uses for facets. Should work automatically with any facet block. The only issue is settings the path to trigger on (i.e. EXAMPLE_PATH) /** * Implements hook_ajax_render_alter(). */ function example_ajax_render_alter(&$commands) { if ($_GET['q'] != EXAMPLE_PATH) { return; }   // Load the include to gain access to facetapi block functions module_load_include('inc', 'facetapi', 'facetapi.block');   // Facets that present on the page. $facet_delta = array_keys(facetapi_get_delta_map());   foreach ($facet_delta as $delta) { // Load block for facet. $block_array = facetapi_block_view($delta); $delta_class = drupal_strtolower(str_replace('_', '-', $delta));   // Render block. $output = drupal_render($block_array);   // Show or hide block. if (!empty($output)) { $commands[] = ajax_command_invoke('.pane-facetapi-' . $delta_class, 'show'); $commands[] = ajax_command_replace('.pane-facetapi-' . $delta_class . ' .pane-content .item-list', $output); } else { $commands[] = ajax_command_invoke('.pane-facetapi-' . $delta_class, 'hide'); } } }

Drupal is a great solution for building websites fast - i think. I am using drupal now for over 5 years in webdesign. The search extensions is working perfect, i was looking for a good solution incl. tutorial - thanks. http://www.seodesigner.de

are the mentioned search plugins repectable for google and other search engines? we tried a lot of ajax extensions but having problems with seo. http://www.seomatrix.de

I’m impressed. Very informative and trustworthy blog does exactly what it sets out to do. I’ll bookmark your weblog for future use. Joseph www.joeydavila.com