Creating Custom Path For Solr Search With Custom Filters

One of the most commonly used features in Drupal is path aliases; that is, the ability to define a custom, clean URL that accurately represents the content contained in that page.  A basic example is creating a mysite.com/about-us URL for an About Us page.  This can also be done in Apache Solr with search result pages.  It's a bit more work than setting up an alias with the Pathauto module, but the end result is a page of customized search results listed at a custom URL.  In this post, I'm going to take the URL of mysite.com/search/apachesolr_search/?filters=type%3Avisualization and create an alias of mysite.com/visualizations.

Before continuing reading, you need to familiarize yourself with this post from James McKinney on creating custom search paths with keywords.  However, what I'm going to show is different, in that instead of using keys (i.e. search terms entered in the search box), I'm going to apply a custom filter, or facet in Solr-speak.

To start, we first define our custom paths in hook_menu():

/**
* Implementation of hook_menu().
*/
function mymodule_solr_menu() {
  $items = array();
  $items['partners'] = array(
    'page callback' => 'mymodule_solr_custom_search',
    'access arguments' => array('search content'),
    'type' => MENU_CALLBACK,
  );
  $items['visualizations'] = array(
    'page callback' => 'mymodule_solr_custom_search',
    'access arguments' => array('search content'),
    'type' => MENU_CALLBACK,
  );
  return $items;
}

These menu items are defined in mymodule_solr_menu(), which is part of a custom module mymodule_solr.module.  It defines two URLs, mysite.com/visualizations and mysite.com/partners, that both use mymodule_solr_custom_search() as a page callback.  mymodule_solr_custom_search() is a modified copy of apachesolr_search_view() from apachesolr_search.module.

/**
* Create a custom search path.  This is a slightly modified copy of
* apachesolr_search_view from apachesolr_search.module
*/
function mymodule_solr_custom_search($type = 'apachesolr_search') {
  // Search form submits with POST but redirects to GET.
  $results = '';
  if (!isset($_POST['form_id'])) {
    if (empty($type)) {
      // Note: search/X can not be a default tab because it would take on the
      // path of its parent (search). It would prevent remembering keywords when
      // switching tabs. This is why we drupal_goto to it from the parent instead.
      drupal_goto('search/apachesolr_search');
    }
    $keys = trim(mymodule_solr_get_keys());
    $filters = '';
    if ($type == 'apachesolr_search' && isset($_GET['filters'])) {
      $filters = trim($_GET['filters']);
    }
    // Collect the search results.
    //  In this custom function we are searching regardless of whether or not there are $keys
    // which there won't be whenever this function is used
    $results = search_data($keys, 'mymodule_solr');

    if ($results) {
      $results = theme('box', t('Search results'), $results);
    }
    else {
      $results = theme('box', t('Your search yielded no results'), variable_get('apachesolr_search_noresults', apachesolr_search_noresults()));
    }

  }
  // Construct the search form.
  return drupal_get_form('search_form', NULL, $keys, $type) . $results;
}

If you compare this module to apachesolr_search_view(), you'll see there are a few differences.

    Replaced the call to search_get_keys() with a call to mymodule_solr_get_keys().  This is a custom version of search_get_keys() that gets the value from the URL (covered in detail below). 
    Remove the conditions that only call apachesolr_search_execute() if there are values for $keys or $filters, since we don't want to use keywords for this search and we can't apply filters at this point in the process (they are applied later in hook_apachesolr_prepare_query()).
    Replaced the second parameter of $type in the call to search_data() with 'mymodule_solr'.  This will call our custom function mymodule_solr_search() (detailed below), which is an implementation of hook_search().

The next function is mymodule_solr_get_keys(). 

function mymodule_solr_get_keys() {
  static $return;
  if (!isset($return)) {
    $parts = explode('/', $_GET['q']);
    if (count($parts) == 1) {
      $return = array_pop($parts);
    }
    else {
      $return = empty($_REQUEST['keys']) ? '' : $_REQUEST['keys'];
    }
  }
  return $return;
}

The purpose of search_get_keys() is to get the search terms from the URL, which is normally assumed to be /search/apachesolr_search/$keys.  These terms are then passed to solr via search_data() and eventually apachesolr_search_execute().  However, in this case, we are hijacking this function to get the URL, but instead of using the value as a key, we will be using it to apply a filter.  Since this will only get called for specific paths which only have one value in addition to the base path, we get that value and return it to mymodule_solr_search_view().

Our third custom function is mymodule_solr_search().

/**
* Implementation of hook_search().
* Copy of apachesolr_search_search with slight modification
*/
function seedge_solr_search($op = 'search', $keys = NULL) {
  // We're using the $keys variable to store the base path that is passed to apachesolr_search_execute
  // so we need to blank it out since it's not needed as $keys any more
  $custom_path = $keys;
  $keys = '';

  switch ($op) {
    case 'name':
      return t('Search');

    case 'reset':
      apachesolr_clear_last_index('apachesolr_search');
      return;

    case 'status':
      return apachesolr_index_status('apachesolr_search');

    case 'search':
      $filters = isset($_GET['filters']) ? $_GET['filters'] : '';
      $solrsort = isset($_GET['solrsort']) ? $_GET['solrsort'] : '';
      $page = isset($_GET['page']) ? $_GET['page'] : 0;
      try {
        $results = apachesolr_search_execute($keys, $filters, $solrsort, $custom_path, $page);
        return $results;
      }
      catch (Exception $e) {
        watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR);
        apachesolr_failure(t('Solr search'), $keys);
      }
      break;
  } // switch
}

As I mentioned above, I'm using the $keys variable to pass my filter value from mymodule_solr_search_view() onward, and here is where we take it out of $keys.  Since the $keys variable is actually passed on to apachesolr_search_execute(), we store the value in $custom_path and set $keys to an empty string.  Next, we alter the call to apachesolr_search_execute() from

$results = apachesolr_search_execute($keys, $filters, $solrsort, 'search/' . arg(1), $page);

to

$results = apachesolr_search_execute($keys, $filters, $solrsort, $custom_path, $page);

The final function is an implementation of hook_apachesolr_prepare_query():

/**
* Implementation of hook_apachesolr_prepare_query
*/
function mymodule_solr_apachesolr_prepare_query(&$query, &$params) {
  // Get the $base_path value, since that will tell us what type of content type
  // the filter needs to use
  $base_path = trim($query->get_path(''), "/"); 
  // Add filter for data_sets
  switch ($base_path) {
    case 'visualizations':
      $type = 'visualization';
      break;
    case 'partners':
      $type = 'knowledge_partners';
      break;   
  } 
  if ($type) {
    $query->add_filter('type', $type);
  } 
}

The value stored in $query->base_path is the $custom_path value that was passed to apachesolr_search_execute() in mymodule_solr_search_search() above.  We pass an empty string to get_path so that  only the base path value is returned (see the get_path() function in Solr_Base_Query.php to see why).  Once we have that value, we use the $query->add_filter() method to the $query object.  The first parameter is the type of filter ('type' is the node type), and the second parameter is the value of the filter.

One point that is worth making is the difference between hook_apachesolr_prepare_query() and hook_apachesolr_modify_query().  As I mentioned in my previous post, which one you use is determined by whether or not you want the user to see the change.  Anything modified in prepare_query will be visible to your users, and anything modified in modify_query will not be visible to your users.  Here is the code from apachesolr.module that calls the two hooks to help illustrate why:

// Allow modules to alter the query prior to statically caching it.
  // This can e.g. be used to add available sorts.
  foreach (module_implements('apachesolr_prepare_query') as $module) {
    $function_name = $module . '_apachesolr_prepare_query';
    $function_name($query, $params, $caller);
  }

  // Cache the built query. Since all the built queries go through
  // this process, all the hook_invocations will happen later
  $current_query = apachesolr_current_query($query);

  // This hook allows modules to modify the query and params objects.
  apachesolr_modify_query($query, $params, $caller);
  $params['start'] = $page * $params['rows'];

The only line between the calls to the two hooks is the caching of the query.

This is illustrated in this case by the display of the Apache Solr Search: Current search block; when the code to add the filter with $query->add_filter() is placed in modify_query, the block is not displayed, but when it is placed in prepare_query, it is displayed on the search results page (assuming you have it enabled in your Blocks admin, of course).  I won't go into detail on all the ways I tried to get that block to display, including directly modifying $_GET['q'] before I finally remembered that distinction...

And that's it.  Now, when you navigate to mysite.com/visualizations, you get a page of Solr results with only the content type filter of the visualizations content type applied.  It's a bit of a hack in that I'm using the functionality for passing keywords to pass filter values, but it shows the customization possibilities that are possible with Solr.

Share This