Related Items Block Using Drupal 6 Search

A related items block looks at the current content of the page and tries to present the user with a list of items that relate to the current content. Creating a related items block is quite easy, and is a good way of introducing the search module api without having to get to involved in the search module.

Before starting I should point out that there are other related items modules available. These are modules like Related Nodes or Related Items but these modules either aren't released for Drupal 6, or simply don't work in the way I wanted the block to work. I wanted a module that would act with little or no user input.

The module we create will contain 4 files, the .info file, the .module file and two template files. I will go through each file in turn here, but I will also add a download link at the bottom of the post where you can download all the files as a module.

The first thing to do (after creating a folder in the module folder) is create a file called relateditems.info. This file tells Drupal what the name and description of the module are, and also adds a dependency clause that tells Drupal that the search module is required for this function to work.

name = Related Items
description = Provides the functionality needed to create a related items block.
core = 6.x

dependencies[] = search

Next, create a file called relateditems.module. This is the main module file that Drupal will look for when running the module. Rather than split up the file into lots of bits I have added lots of comments to the code so that it is clear what is going on. The relateditems_block() function hooks into Drupal's block making code and allows the definition of one or more blocks. The searching is done in the relateditems_query() function, which gets the title and teaser from the current node, extracts the most commonly used keywords and then does a search using them. We only use the title and teaser as these will generally have a higher percentage of good keywords and will cut down on the amount of string processing that PHP has to do.

Here is the relateditems.module file in full.

<?php
/**
 * Hook for block. This allows us to define and integrate our block into Drupal.
 *
 * @param string $op Current operation.
 * @param int $delta What delta is being called right now.
 * @param array $edit Edit options for block
 *
 * @return array Information about blocks (list) or the block itself (view).
 */
function relateditems_block($op = 'list', $delta = 0, $edit = array())
{
    switch ($op) {
        case 'list':
            /*
             * The list operation allows us to define our blocks. We only want
             * to define one block here, but you can create more by adding items
             * to the $blocks array.
             */
            $blocks[0]['info']  = t('Related Items');
            $blocks[0]['cache'] = BLOCK_NO_CACHE;

            return $blocks;
        case 'view':
            /*
             * For every block you define the hook_block function will be run
             * with a different $delta being passed for each block.
             * The $delta parameter will correspond to the item in the $blocks
             * array that we defined in the list operation.
             */
            if ($delta == 0) {
                /*
                 * A $delta value of 0 corresponds to the block we created
                 * previously. All we do here is get and render our list of
                 * related items and add it to the content item in the block
                 * array.
                 */
                $list = relateditems_render();
                $block['content'] = $list;
                $block['subject'] = t('Related Items');
            }
            // Always return the block.
            return $block;
    }
}

/**
 * Implementation of hook _theme. This allows us to either define functions or
 * files that will allow us to render the block output in a hookable way.
 *
 * @param array  $existing An array of existing implementations that may be used
 *                         for override purposes. This is primarily useful for
 *                         themes that may wish to examine existing
 *                         implementations to extract data (such as arguments)
 *                         so that it may properly register its own, higher
 *                         priority implementations.
 * @param string $type     What 'type' is being processed. This is primarily
 *                         useful so that themes tell if they are the actual
 *                         theme being called or a parent theme.
 * @param string $theme    The actual name of theme that is being being checked
 *                         (mostly only useful for theme engine).
 * @param string $path     The directory path of the theme or module, so that it
 *                         doesn't need to be looked up.
 *
 * @return array A keyed array of theme hooks.
 */
function relateditems_theme($existing, $type, $theme, $path)
{
    /*
     * We need to create two theme items. The first is to theme each item and
     * the second is to refine the list wrapper.
     */
    return array(
            // Define theme for items.
            'relateditems_item' => array(
                    'arguments' => array(
                            'item' => NULL),
                    'template' => 'relateditems-item',
            ),
            // Define theme for list wrapper.
            'relateditems_block' => array(
                    'arguments' => array(
                            'items' => NULL),
                    'template' => 'relateditems-content',
            ),
    );
}

/**
 * Take a text string and extract the most commonly used keywords. The keywords
 * are returned in a format that is compatable with Drupal search. The function
 * also contains a list of stop words that can be used to remove junk words.
 *
 * @param string  $content The content that the keywords are to be extracted from.
 * @param integer $count   The number of keywords to be extracted.
 *
 * @return string The
 */
function relateditems_node_content_keywords($content, $count = 5)
{
    // Strip HTML tags from content and convert to lowercase.
    $content = strtolower(strip_tags($content));

    // List of stop words
    $stopWords = array('i','a','about','an','are','as','at','be','by','com',
        'for','from','has','how','in','is','it','of','on','or','nbsp','that',
        'the','this','to','was','what','when','where','who','will','with',
        'the','www','were',);

    // Filter out stop words.
    $filterCount = count($stopWords);
    for($i=0; $i < $filterCount; $i++){
        $content = preg_replace('/\b'.$stopWords[$i].'\b/i', "", $content);
    }

    // Count the number of words.
    $words = array_count_values(str_word_count($content, 1));
    // Sort the words so that the most popular words are at the top.
    arsort($words);
    // Slice the array and then convert the words array into a string.
    return implode(' OR ', array_keys(array_slice($words, 0, $count)));
}

/**
 * This function is called by the relateditems_render() function. It finds the
 * content of the current node and uses this to search for nodes that match
 * this content.
 *
 * @return array The array of nodes found through the search.
 */
function relateditems_query()
{
    if (arg(0)=='node' && is_numeric(arg(1))) {
        // We are looking at a node page so load the node content.
        $current_node = node_load(arg(1));
        // Combine the title and teaser into a single variable and extract keywords.
        $content = $current_node->title . ' ' . $current_node->teaser;
        $words = relateditems_node_content_keywords($content);
        // Run a search on the keywords.
        $searched_nodes = node_search('search', $words);
        // Return the nodes found.
        return $searched_nodes;
    }
}

/**
 * This function calls relateditems_query() and renders the results as a list.
 *
 * @return string The rendered output.
 */
function relateditems_render()
{
    // Find the nodes to render.
    $nodes = relateditems_query();

    $items = array();
    $output = '';
    if (count($nodes) > 0) {
        // If we have more than one node then start rendering.
        // Render nodes.
        foreach ($nodes as $node) {
            $items[] = theme('relateditems_item', $node);
        }
        // Render overall output.
        $output .= theme('relateditems_block', $items);
    } else {
        // If no items found then return simple message.
        $output .= '<p>No Related Items Found</p>';
    }
    // Return output.
    return $output;
}

One thing to note from all this is that I did initially try using a function called do_search(), however in order for this function to work correctly it has to be passed 10 parameters, which can take a bit of working out. I then came across a simpler and more effective way of searching nodes by using the appropriately named node_search() function. The first parameter of the node_search() function is the type of operation we want to perform, in this case we want to search so we use the "search" operation.

Using the node_search() function makes things like filtering possible and quite straightforward. For example, lets say that we wanted to restrict the nodes found to just those of the type "blog", this can be done with the following change to the node_search() function.

$searched_nodes = node_search('search', 'type:blog ' . $words);

We can also use the keyword "category" to restrict the search term to a specific taxonomy term.

Finally, we need to create the two template files that the module uses to theme the output. The first file is called relateditems-content.tpl.php and contains the following:

<?php
/**
 * $items = A list of rendered items.
 */
 
<ul><?php foreach ($items as $item) {
    print $item;
} ?>
</ul>

The final file needed is called relateditems-item.tpl.php and contains the following:

<?php
/**
 * $item = The search result, containing information about the node.
 */
?><li>
<a href="<?php print url($item['link']); ?>" title="<?php print check_plain($item['title']); ?>"<?php print $item['title']; ?></a>
</li>

If you want to you can download the entire module right here.

Comments

Hi

I used you given module and it works nicely. thanks for this posting........

Permalink

Did you know that there is a very similar module, called the "Similar Entries" module? -> http://drupal.org/project/similar

It would be nice to know where the differences between these two are.

Permalink

How can i  have results of google search as  related items?

Permalink

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
3 + 0 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.