Blog

Keeping blog visitors by showing meaningful search results in WordPress

I recently became disgruntled with the way my blogs displayed search results. By default, WordPress blogs will show searched posts exactly as they might appear on an index or archives page: Typically as an extract, or perhaps even as the full entry.

This doesn’t help at all if you’re looking for something in particular – It’s a much better idea to show the post within the context of the search query, as real search engines do.

See it in practice here.

This is a fairly easy thing to actually get working in WordPress. It’ll take just a couple of minutes, and will make a big difference to blog visitors. Here’s how I did it.

Creating a search result page

If your theme doesn’t already have one, you’ll need to construct a template within your theme that WordPress will use for search results. By default, WordPress will use your index.php template, so that’s usually a good place to start with, for normal themes.

Duplicate index.php, and call it search.php.

If you already have a search.php, you’re all set.

A note about theme engines

A special case here is for theme engines like Thematic (which I use for this blog). For Thematic, it’s a matter of un-hooking the provided search ‘loop’ from within your child theme, and replacing it with your own.

In my case, with a Thematic child theme, this takes place within functions.php. First, one needs an ‘init’ action, to remove the existing hooks.

function mytheme_init() {
   remove_action('thematic_searchloop', 'thematic_search_loop');
}
...
add_action('init', 'mytheme_init', 10);

Then, it needs a replacement function to perform the search result loop:

function mytheme_search_loop() {
  while ( have_posts() ) : the_post(); ?>
 
    <div id="post-<?php the_ID() ?>" class="<?php thematic_post_class() ?>">
      <?php thematic_postheader(); ?>
      <div class="entry-content">
        <?php thematic_content(); /* We will replace this next */ ?>
 
      </div>
      <?php thematic_postfooter(); ?>
  </div><!-- .post -->
 
  <?php endwhile;
}
...
add_action('thematic_searchloop', 'mytheme_search_loop');

Some smarts to show context

What I did was replace the content of each post displayed with some code that constructs and displays some context around the search terms found in the post.

In your search.php (or your search loop function, if you’re using a theme engine), look for the line that inserts the post content. Chances are, it’ll look something like <?php the_content('Keep reading'); ?>. In the case of the Thematic child theme above, it’s <php thematic_content(); ?>.

Delete that line, and replace it with the following (here’s a plain-text version, if that’s easier):

<?php
// Configuration
$max_length = 400; // Max length in characters
$min_padding = 30; // Min length in characters of the context to place around found search terms
 
// Load content as plain text
global $wp_query, $post;
$content = (!post_password_required($post) ? strip_tags(preg_replace(array("/\r?\n/", '@<\s*(p|br\s*/?)\s*>@'), array(' ', "\n"), apply_filters('the_content', $post->post_content))) : '');
 
// Search content for terms
$terms = $wp_query->query_vars['search_terms'];
if ( preg_match_all('/'.str_replace('/', '\/', join('|', $terms)).'/i', $content, $matches, PREG_OFFSET_CAPTURE) ) {
    $padding = max($min_padding, $max_length / (2*count($matches[0])));
 
  // Construct extract containing context for each term
  $output = '';
  $last_offset = 0;
  foreach ( $matches[0] as $match ) {
    list($string, $offset) = $match;
    $start  = $offset-$padding;
    $end = $offset+strlen($string)+$padding;
    // Preserve whole words
    while ( $start > 1 && preg_match('/[A-Za-z0-9\'"-]/', $content{$start-1}) ) $start--;
    while ( $end < strlen($content)-1 && preg_match('/[A-Za-z0-9\'"-]/', $content{$end}) ) $end++;
    $start = max($start, $last_offset);
    $context = substr($content, $start, $end-$start);
    if ( $start > $last_offset ) $context = '...'.$context;
    $output .= $context;
    $last_offset = $end;
  }
 
  if ( $last_offset != strlen($content)-1 ) $output .= '...';
} else {
  $output = $content;
}
 
if ( strlen($output) > $max_length ) {
  $end = $max_length-3;
  while ( $end > 1 && preg_match('/[A-Za-z0-9\'"-]/', $output{$end-1}) ) $end--;
  $output = substr($output, 0, $end) . '...';
}
 
// Highlight matches
$context = nl2br(preg_replace('/'.str_replace('/', '\/', join('|', $terms)).'/i', '<strong>$0</strong>', $output));
?>
 
<p class="search_result_context">
  <?php echo $context ?>
</p>
<p>
  <a href="<?php the_permalink() ?>" rel="bookmark">Read this entry</a>
</p>

Save, and search for something on your blog — you should see contextual search results, now.

One final tweak: Results per page

WordPress has a setting for the number of posts to show per page. You may want to use a different number of search results per page, given that each result is now shorter than a full post.

To override this ‘posts per page’ setting, you’ll want to find the line just before the search loop. It’ll probably look like <?php if (have_posts()) : ?>, or, if your theme doesn’t bother with that part, <?php while ( have_posts() ) : the_post(); ?>.

Before that line, insert the following:

<?php global $wp_query; $v = $wp_query->query_vars; $v['posts_per_page'] = 10; query_posts($v); ?>

This will take the current query (including the search phrase, page number, etc.), add a ‘posts per page’ parameter, then pass it back to WordPress’s query engine.

Tagged , , , . Bookmark the permalink. Both comments and trackbacks are currently closed.

10 Comments

  1. Mark
    Posted September 14, 2010 at 6:59 pm | Permalink

    Great thoughts Michael. This is helpful for me. I will save your URL and of course your post for future reference.

  2. B
    Posted January 14, 2011 at 6:49 pm | Permalink

    Might you consider making a plugin to do this? I would cheerfully donate to own such a helpful thing without having to get my fingers into code (at which I purely suck).

  3. Posted February 15, 2011 at 6:02 pm | Permalink

    Mike, awesome thoughts, did you decide if you will make a plugin to help do so?

  4. Posted August 1, 2012 at 9:01 pm | Permalink

    Great idea. I re-implemented it using filter hooks (the_excerpt and get_the_excerpt) so this works with any theme that uses the_excerpt() to display post excerpts, without replacing search.com. You can see it at bililite.com/blog/2012/08/01/contextual-search-results-in-wordpress. That would be trivial to put into a plugin.

  5. Posted August 20, 2012 at 4:55 pm | Permalink

    This came handy. With your and Danny’s examples I was able to achieve just what I wanted for one of my sites (as php still remains pretty much a mystery for me :] ). Thanks for sharing.

  6. P
    Posted February 9, 2013 at 1:09 am | Permalink

    this is way late but… I’m getting HTML char codes instead of certain characters in my excerpt: ’

    is there a quick work around?

    • Posted February 9, 2013 at 1:28 am | Permalink

      Hmm, I’m not sure, actually – I think this is a feature of the WordPress excerpt system, unless I’m mistaken. You could try adding, just before the “// Search content for terms” comment:

      $content = str_replace("&", "&amp;", $content);

      That would undo the escaping of “&” characters that is being applied.

  7. P
    Posted February 9, 2013 at 1:58 am | Permalink

    yea… no. actually, it converts apostrophes and quotes into HTML chars but that IS probably part of WP.

    thanks anyway!

    • P
      Posted February 9, 2013 at 2:01 am | Permalink

      so this line: $content = htmlspecialchars_decode($content, ENT_NOQUOTES); did the trick