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.

You could also use Views 3 +

You could also use Views 3 + ApacheSolr Views and use an exposed filter for search terms. The path of the view can be set within views and the exposed keywords are added in the query. Example: [views-path]?[exposed-filter-name]=keywords I'm not sure whether you can pass on the keywords as argument, but it's already a nice way to solve this issue without custom coding.

That sounds like a cool idea,

That sounds like a cool idea, but I don't think exposed filters would work in my particular use case since the search is generated from a menu item.

If it's generated from a menu

If it's generated from a menu item, you can still use arguments as standard in views to pass on the data to the apachesolr view.

Great article Steve! This

Great article Steve!

This covers almost what I was looking for. However, when searching one has to check the "Retain current filters" checkbox.
Also after submission of the search form, the user is forwarded to example.com/search/apache_solr/my_keyword?filters=type:my_type&retain-filters=1. Is there a way to keep the user on the custom path example.com/my_type/my_keyword, also after the form is submitted?

In addition, it would be nice to disable the search/apache_solr path and to hide or disallow the removal of the type filter.
Any ideas? on how to achieve these things?

Hey, I'm exactly facing the

Hey, I'm exactly facing the same issues as you did. I need to have the filter remained and would like to stay on the very same page. Furthermore after submission the search form is not loaded properly even the address is right. very strange.

Did you find any solutions for having the filter on all the time?

Thank you, Nina

Great article! Thank you! I'm

Great article! Thank you! I'm having one issue however. Whenever I pull up the page that calls this callback function, i must clear the theme cache every time to be able to see the search results. When I first go to the page, it shows the filter options, the line 'search results', but not the search results templates. But like I said, I have to clear the cache before it will show, everytime. Does anyone know why this could be? Thanks

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><img>
  • Lines and paragraphs break automatically.

More information about formatting options

By submitting this form, you accept the Mollom privacy policy.