background image
HomeRecent PostsDrupalSearchTagsRSSContactAboutAccount

In this tutorial I'll show how you to create a custom Lucene Search facet from taxonomy and integrate into the search form block. A great first step would be to review the Drupal Lucene API documentation (see luceneapi.api.php PHP file in the luceneapi module folder).

I assigned the search form block to a region in my theme (admin/build/block), which generates this form:

Search form block

I added a new vocabulary called "Topics", and added a few terms (admin/content/taxonomy).

Topic and terms

The goal of this code is to integrate the Topic vocabulary into the search form block to allow the user to select a taxonomy term as they search. To start, you need to implement a hook_luceneapi_facet_realm() and a callback function in your custom module.

<?php
/**
* Implements hook_luceneapi_facet_realm()
*/
function MYMODULE_luceneapi_facet_realm() {

 
$realms = array();

 
$realms['form'] = array(
   
'title' => t('Search form block'),
   
'callback' => 'MYMODULE_luceneapi_facet_realm_callback_search_form_block',
   
'callback arguments' => array(),
   
'allow empty' => TRUE,
   
'description' => t('Displays facets in the search form block.'),
  );
 
  return
$realms;
 
}

/**
* Implements hook_luceneapi_facet_realm() callback function
*/
function MYMODULE_luceneapi_facet_realm_callback_search_form_block($facets, $realm, $module) {

 
$form = array();

 
// loop through facets
 
foreach ($facets as $name => $facet) {

   
// NOTE: luceneapi_facet_to_fapi_convert() converts a Lucene facet to Drupal Form API data
   
$form = array_merge_recursive($form, luceneapi_facet_to_fapi_convert($facet));

  }

  return
$form;

}
?>

At this point, if you go to the facets admin page (admin/settings/luceneapi_node/facets), you can see the newly created realm. I assigned the taxonomy vocabulary "Topic" to this realm.

Assigning facets to realms

Up next is implementing a hook_form_alter() to integrate the facet into the search form.

<?php
/**
* Implementation of hook_form_FORM_ID_alter().
*/
function MYMODULE_form_search_block_form_alter(&$form, &$form_state) {
 
 
// get default search module (IE: luceneapi_node) 
 
$module = luceneapi_setting_get('default_search');

 
// check if default search module is defined in lucene searchable module list
 
if (array_key_exists($module, luceneapi_searchable_module_list())) {

   
// get index type (IE: node)
   
$type = luceneapi_index_type_get($module);

   
// fetch realm facets
   
$elements = luceneapi_facet_realm_render('form', $module, $type);

   
// if facet form elements exist, recursively merge with current form object
   
if (!empty($elements)) {
     
$form = array_merge_recursive($form, $elements);
    }

  }

}
?>

The search from block should now show an empty "Topic" facet.

Search form block, empty topic

The last piece of code implements hook_luceneapi_facet_postrender_alter() which gives you the opportunity to modify the facet, and in the this case, add its options.

Immediately after implementing this hook, if you krumo() or dsm() the $items argument, you'll see the form element has no options.

Using krumo to see empty form element

The next section of code copied the contrib module code in "Lucene Node". [See file: luceneapi/contrib/luceneapi_node/luceneapi_node.module; function: function luceneapi_node_luceneapi_facet_postrender_alter()]

<?php
/**
* Implements hook_luceneapi_facet_postrender_alter()
*/
function MYMODULE_luceneapi_facet_postrender_alter(&$items, $realm, $module, $type = NULL) {

  if (
$realm == 'form' && $module == 'luceneapi_node' && $type == 'node' && is_array($items['category'])) {
   
   
// get taxonomy form data
   
$taxonomy = module_invoke('taxonomy', 'form_all', 1);
   
   
// get enabled facets
   
$facets_enabled = luceneapi_facet_enabled_facets_get($module, $realm);
   
   
// loop through enabled facets, validate, and fetch weight
   
$weights = array();
    foreach (
$facets_enabled as $name => $value) {
   
     
// check for "category" facet
      // FORMAT: category_{VOCABID}
     
if (preg_match('/^category_(\d+)$/', $name, $match)) {
     
       
// load taxonomy vocabulary     
       
if ($vocabulary = taxonomy_vocabulary_load($match[1])) {
       
         
// ensure category and vocab id is enabled for this module and realm
         
if (luceneapi_facet_enabled($match[0], $module, 'form')) {
                    
           
// fetch weight
           
$variable = sprintf('luceneapi_facet:%s:%s:%s:weight', $module, $realm, $name);
           
$weights[$vocabulary->name] = variable_get($variable, 0);
         
          }
       
        }
     
      }
   
   
// end foreach
   
}
   
   
// gets weighted taxonomy array
   
asort($weights);
   
$taxonomy_weighted = array();
    foreach (
$weights as $vocab_name => $weight) {
     
$taxonomy_weighted[$vocab_name] = $taxonomy[$vocab_name];
    }

   
// create array of fapi data to override
   
$category_data = array(
     
'#prefix' => '<div class="criterion">',
     
'#suffix' => '</div>',
     
//'#size' => 10,
     
'#options' => $taxonomy_weighted,
     
'#multiple' => TRUE,
     
'#default_value' => luceneapi_facet_value_get('category', array()),
     
'#title' => NULL,
     
'#description' => NULL,
    );

   
// merge data
   
$items['category'] = array_merge($items['category'], $category_data);
   
   
// sets weight as the lowest weight of all taxonomy facets
   
if (is_array($items['category']['#weight'])) {
     
$items['category']['#weight'] = min($items['category']['#weight']);
    }   
         
 
// end if
 
}

}
?>

Reloading the page will now show the search form with a completed facet.

Search form with facet

Submitting the search form block with a selected taxonomy term will now take the user to the search results page with the taxonomy facet pre-selected!

Special thanks to Chris Pliakas for all of his great Lucene API work! (I miss working with you Chris)

For some time now I've wanted to write a blog entry about using AHAH to create dynamically generated form elements. After a recent conversation at work regarding usability, I now had a real world example to create: how to use tiered taxonomy to dynamically generate a form. This code snippet will show you how to create a form that creates child select dropdowns based on the parent taxonomy term the user selects.

First I established a multi-tier taxonomy called "AHAH":

For this example I created a menu callback to display my initial form:

<?php
function helper_menu() {
 
 
$items = array();
 
 
$items['ahah-form'] = array(
   
'title' => 'AHAH Form',
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('_helper_callback_ahah_form'),
   
'type' => MENU_CALLBACK,
   
'access callback' => 'user_access',
   
'access arguments' => array('access content'),
  );
 
  return
$items;
 
}
?>

I then defined the page callback to show the initial form:

<?php
function _helper_callback_ahah_form() {

 
// define an array to contain form elements
 
$form = array();
 
 
// define the top level vid
 
$vid = 2;
 
 
// fetch a tree of taxonomy elements
 
$tree = taxonomy_get_tree($vid, 0, -1, 1);
 
 
// loop though taxonomy and collect elements
 
$options = array();
  foreach (
$tree as $key => $value) {
   
$options[$value->tid] = $value->name;
  }
 
 
// create the first select dropdown input
 
$form['select_1'] = array(
   
'#type' => 'select',
   
'#options' => $options,
   
'#title' => t('Select 1'),
   
'#size' => 5,
   
'#multiple' => false,
   
'#ahah' => array(
     
'event' => 'change',
     
'path' => 'ahah-form-callback',
     
'wrapper' => 'wrapper-1',
     
'method' => 'replace',
    ),
  );
 
 
// pass the top level vid in the form
 
$form['ahah_vid'] = array(
   
'#type' => 'hidden',
   
'#value' => $vid,
  );
 
 
// create an empty form element to contain the second taxonomy dropdown
 
$form['wrapper_1'] = array(
   
'#prefix' => '<div id="wrapper-1">',
   
'#suffix' => '</div>',
   
'#value' => '&nbsp;',
  );
 
 
// add a form submit button
 
$form['submit'] = array(
   
'#value' => 'Submit',
   
'#type' => 'submit'
 
);
 
  return
$form;
 
}
?>

The above form callback produces the following:

Next, I defined a callback to handle the AHAH page request:

<?php
// new menu item:
function helper_menu() {
 
 
// ...
 
 
$items['ahah-form-callback'] = array(
   
'title' => 'AHAH Form Callback',
   
'page callback' => '_helper_callback_ahah_form_callback',
   
'type' => MENU_CALLBACK,
   
'access callback' => 'user_access',
   
'access arguments' => array('access content'),
  );
 
 
// ...

 
return $items;
 
}

// and, here's the AHAH callback used to create the new form elements:
function _helper_callback_ahah_form_callback() {
 
 
// define a string variable to contain callback output
 
$output = "";
 
 
// pull the top level vid from the $_POST data
 
$vid = $_POST['ahah_vid'];
 
 
// pull the selected dropdown from the $_PODT data
 
$parentVid = $_POST['select_1'];
 
 
// loop through the taxonomy tree and fetch child taxonomies
 
$options = array();
 
$tree = taxonomy_get_tree($vid, $parentVid, -1, 1);  
  foreach (
$tree as $key => $value) {
   
$options[$value->tid] = $value->name;
  }
 
 
// define the second tier select dropdown element
 
$form['select_2'] = array(
   
'#type' => 'select',
   
'#options' => $options,
   
'#title' => t('Select 2'),
   
'#size' => 5,
   
'#multiple' => false,
  );
 
 
// rebuild form object and output new form elements
 
$output .= ahah_render($form, 'select_2');
 
 
// render form output as JSON
 
print drupal_to_js(array('data' => $output, 'status' => true));
 
 
// exit to avoid rendering the theme layer
 
exit();
 
}

// Lastly, here's a help function pulled from Nick Lewis's blog to alter the form
// see: http://www.nicklewis.org/node/967
// NOTE: based on poll module, see: poll_choice_js() function in poll.module
function ahah_render($fields, $name) {
 
$form_state = array('submitted' => FALSE);
 
$form_build_id = $_POST['form_build_id'];
 
// Add the new element to the stored form. Without adding the element to the
  // form, Drupal is not aware of this new elements existence and will not
  // process it. We retreive the cached form, add the element, and resave.
 
$form = form_get_cache($form_build_id, $form_state);
 
$form[$name] = $fields;
 
form_set_cache($form_build_id, $form, $form_state);
 
$form += array(
   
'#post' => $_POST,
   
'#programmed' => FALSE,
  );
 
// Rebuild the form.
 
$form = form_builder($_POST['form_id'], $form, $form_state);

 
// Render the new output.
 
$new_form = $form[$name];
  return
drupal_render($new_form);
}
?>

The above code allows the user to select an option from the top level tier of taxonomy and the AHAH callback will generate the a select dropdown of the child taxonomies as shown below:

On form submission, you'll see that the options the user selected as stored in $form_state['values']['select_1'] and $form_state['values']['select_2']

Eric.London's picture

If someone clicks on a taxonomy term they land on a page showing all the content tagged with that term. The title on those pages is simply the taxonomy term. I thought it would be more usable if I changed the verbiage of the title to let the user know what they are viewing. This code snippet shows how you can change the title for the taxonomy term landing pages:

<?php
function MYTHEME_preprocess_page(&$variables) {
  if (
arg(0)=='taxonomy' && arg(1)=='term') {
   
$variables['title'] = "Content tagged with: " . $variables['title'];
  }
}
?>

Most SEO tutorials claim that meta keywords are not very important to search engines. But, if you insist on inserting meta keywords for each node's taxonomy terms, this tutorial will show you how to accomplish this. I added the following function to my template.php file in my theme.

<?php
function _phptemplate_variables($hook, $vars) {
 
// check for page scope
 
if ($hook == 'page') {
   
// ensure this page is a node view
   
if (is_object($vars['node'])) {
     
// get a list of taxonomy terms for this node
     
$terms = taxonomy_node_get_terms($vars['node']->nid);
           
     
// ensure terms exist
     
if (is_array($terms)) {

       
// loop through terms, and collect the names
       
$t = array();
        foreach (
$terms as $k => $v) {
         
$t[] = $v->name;   
        }
               
       
// ensure names exist
       
if (is_array($t)) {
         
// implode the terms into a string and add to the head variable
         
$vars['head'] .= "<meta name='keywords' content='" . implode(",", $t) . "'>";
        }

      }

    }
       
  }
  return
$vars;
}
?>

To verify this code is working, check out the tags I used on this page and then view the source. Hopefully, they match!

(NOTE: my homepage is a view, so there show not be any meta keywords.)


NOTE: when I upgraded my theme to Drupal 6, I had to update this above code:

<?php
function MYTHEME_preprocess_page(&$variables) {
  if (
is_object($variables['node']) && is_array($variables['node']->taxonomy)) {
   
$tags = array();
    foreach (
$variables['node']->taxonomy as $tid => $term) {
      if (!
in_array($term->name, $tags)) $tags[] = $term->name;
    }
       
    if (
count($tags)) {
     
sort($tags);
     
$variables['head'] .= "<meta name='keywords' content='" . implode(",", $tags) . "'>";
    }
  }
}
?>

Eric.London's picture

Here's a quick code snippet that shows you how you can create custom breadcrumbs for a certain node type. This code would reside in your template.php theme file.

<?php
function MYTHEME_preprocess_page(&$variables) {
  if (
$variables['node']->type == 'MYNODETYPE') {
       
   
$links = array();

   
// creating a link to the home page
   
$links[] = l('Home', '<front>');

   
// here's how you could add a link to a taxonomy page
   
$vid = 2;
    foreach (
$variables['node']->taxonomy as $k => $v) {
      if (
$v->vid = $vid) {  
       
$links[] = l($v->name, 'taxonomy/term/' . $v->tid);
        break;
      }
    }
       
   
// yet another link
   
$links[] = l('Some Other Link', 'SOMEOTHERLINK');

   
// lastly, overwrite the contents of the breadcrumbs variable in the page scope
   
$variables['breadcrumb'] = theme('breadcrumb', $links);
  }
}
?>

NOTE: if you were adding this code to a module, you could use the drupal_set_breadcrumb() function to do the same functionality.

Syndicate content