faking faceted search with views and views fastsearch

I was responding to a post on Drupal4Lib about modifying Drupal search results to show facets for content types and was writing just how easy I thought it would be, when I decided that if it was really easy I could have some fun trying to make it work.

Instead of using the Search module, I used the Views module with the addition of Views FastSearch. I'm on Drupal 5, so this might not work with the new version of Views, but you'll get the idea.

I created a view named 'faceted_search' that:
- uses the view type 'search results' - this might be
- uses the field 'Search: score'
- uses a filter for 'Node: published' = true
- uses an exposed filter for 'Search: Fast Index'
- uses an exposed filter for 'Node: Type'
- uses 'Lock Operator' on both exposed filters.
- sorts by 'Search: score'

I put this view at http://www.oregonlibaries.net/faceted_search.

Two code snippets make this work.

The first is in my theme's template.php file. I added a function that modifies the exposed filters for this view.

function phptemplate_views_filters($form) {
  if ($form['#view_name'] == 'faceted_search') {
// Make the search box (filter1) as big as we want.
    $form['filter1']['#size']=60;
// Hide the content type filter (filter0).
    unset($form['filter0']);
  }
  return theme_views_filters ($form);
}

The second snippet is copied largely from how-to on drupal.org for Theming your Views. I copied the part about 'Display a view'.

// lnet_views_view_faceted_search displays themes this view to show facets by content type.
//
// In this case, the module is called 'lnet' and the view is called 'faceted_search'.
//
// Call yours MODULE_views_view_VIEWNAME
//
// Based on the code snippet at http://drupal.org/node/42597 for 'Display a view'.
function lnet_views_view_faceted_search ($view,$type,$nodes,$level,$args) {
  // The beginning of the function sets up the facets by modifying the Views query.
  $my_view = views_build_view('queries',$view,$args);
  $my_query = $my_view['query'];

  // Instead of asking for nodes, we're asking for a count of each node type.
  $my_query = str_replace('DISTINCT(node.nid)','node.type, COUNT(node.nid) as co',$my_query);

  // Remove the ORDER BY stuff and replace it with GROUP BY stuff.
  $my_query = preg_replace('/ORDER BY .*$/','GROUP BY (node.type)',$my_query);

  // Last, remove the content type filter if one exists.
  $my_query = preg_replace('/AND \(node\.type.*$/','GROUP BY (node.type)',$my_query);

  $facet_query = db_query($my_query);
  $facets = array();
  while ($ref = db_fetch_object($facet_query)) {
    $facets[$ref->type] = $ref->co;
  }
  $facet_output .="<div class='facet_tabs'>\n";
  foreach ($facets as $ntype => $count) {
    // Get the URL - there's got to be a more Drupal-ly way to do this.
    $link =  $_SERVER['REQUEST_URI'];

    // Remove the content type filter (in this case, filter0) from the end of the URL, if it exists
    $link = preg_replace('/\&filter0=.*/','',$link);

    // Add a new content type filter at the end of the URL
    $link .= '&filter0=' . $ntype;
    $facet_output .= "<a href='http://www.oregonlibraries.net" . $link . "'>$ntype</a>" . ' (' . $count . ') ';
  }
  $facet_output .= "</div>\n";

  $num_nodes = count($nodes);

  if ($type == 'page') {
    drupal_set_title(filter_xss_admin(views_get_title($view, 'page')));
    views_set_breadcrumb($view);
  }

  if ($num_nodes) {
    $output .= views_get_textarea($view, $type, 'header');
  }

  if ($type != 'block' && $view->exposed_filter) {
    $output .= views_theme('views_display_filters', $view);
  }

  // Here is where we display the facets!
  $output .= $facet_output;    

  $plugins = _views_get_style_plugins();
  $view_type = ($type == 'block') ? $view->block_type : $view->page_type;
  if ($num_nodes || $plugins[$view_type]['even_empty']) {
    if ($level !== NULL) {
      $output .= "<div class='view-summary ". views_css_safe('view-summary-'. $view->name) ."'>".
views_theme($plugins[$view_type]['summary_theme'], $view, $type, $level, $nodes, $args) . '</div>';
    }
    else {
      $output .= "<div class='view-content ". views_css_safe('view-content-'. $view->name) ."'>".
views_theme($plugins[$view_type]['theme'], $view, $nodes, $type) . '</div>';
    }
    $output .= views_get_textarea($view, $type, 'footer');
    if ($type == 'block' && $view->block_more && $num_nodes >= $view->nodes_per_block) {
      $output .= theme('views_more', $view->real_url);
    }
  }
  else {
    $output .= views_get_textarea($view, $type, 'empty');
  }

  if ($view->use_pager) {
    $output .= theme('pager', '', $view->pager_limit, $view->use_pager - 1);
  }

  if ($output) {
    $output = "<div class='view ". views_css_safe('view-'. $view->name) ."'>$output</div>\n";
  }
  return $output;

}

The results aren't perfect - I lost my pager, and I hacked some things I don't know how to do in "The Drupal Way", but I think this is a good whack at faceted search in Drupal.

Update: I just noticed all of the faceted search modules at http://drupal.org/project/faceted_search, including under 'See Also'. Wee!

Comments

Hi Caleb, still digesting the

Hi Caleb, still digesting the actual content of the post, but you've got a typo in your example URL (mispeeled libary) :-)