User login

Highlight search words in Drupal autocomplete results

We've done this for SCF in the biblioreference module. Not the cleanest or most universal, but our autocomplete was messy and it's easy to make your own autocomplete function.

Here's the code and a lot of it surrounding it. The biblioreference_match_nodes function immediately below calls the biblioreference_autocomplete_highlight several times, each time before putting any HTML spans around the text, so we don't insert strong highlighting tags into our own markup.

<?php
/**
 * Match biblio nodes based on index of words from title.
 *
 * Used in autocomplete function.
 *
 * The index could be extended to include words from the abstract or a keywords
 * field.
 */
function biblioreference_match_nodes($string, $match = 'starts_with', $limit = 10) {
  $words = search_index_split($string);
  // the difference with these match operators is that they apply per word
  $match_operators = array(
    'contains' => "LIKE '%%%s%%'",
    'equals' => "= '%s'",
    'starts_with' => "LIKE '%s%%'",
  );
 
  $where_clause = "WHERE n.type = 'biblio'";

  $count = count($words);
  if ($count) {
    $where_clause .= ' AND ';
  }
  for ($i = 0; $i < $count; $i++) {
    $where[] = 'bk.word ' . $match_operators[$match];
  }
  $args = $words;
  $where_clause .= $where ? ' ('. implode(' OR ', $where) . ')' : '';

// @todo: put back in db_rewrite_sql if it won't break anything
// @TODO optimization: we may want to put title, authors, and journal title
// into a separate table so that we can do it without joins-- especially if
// the same table can be reused for theming purposes.
  $sql = "SELECT COUNT(bk.nid) AS matchcount, bk.nid AS nid, b.vid AS vid, n.title AS node_title, b.biblio_secondary_title AS journal_title, b.biblio_alternate_title AS journal_abbr, b.biblio_year AS year
          FROM {biblioreference_keyword} bk LEFT JOIN {node} n ON n.nid=bk.nid
          LEFT JOIN {biblio} b ON n.vid=b.vid " .
          $where_clause . " GROUP BY nid ORDER BY matchcount DESC";
  $result = $limit ? db_query_range($sql, $args, 0, $limit) : db_query($sql, $args);

  $references = array();
// $debug = $sql;
// $references[666] = array(
//  'title' => $debug,
//  'rendered' => $debug,
// );
  while ($node = db_fetch_object($result)) {
    $value = '';  // plain text for form field
    $rendered = '';  // HTML for drop-down selection
    $journal = ($node->journal_abbr) ? $node->journal_abbr : $node->journal_title;
    $authors =  biblioreference_authors_string($node->vid);
    $year = $node->year;
    $article = $node->node_title;
    $journal_r = biblioreference_autocomplete_highlight($journal, $words);
    $authors_r = biblioreference_autocomplete_highlight($authors, $words);
    $article_r = biblioreference_autocomplete_highlight($article, $words);
    $value .= $authors . '. ' . $node->year;
    $rendered .= '<span class="nodereference-authors">';
    $rendered .= $authors_r . '.</span> ';
    if ($year) {
      $year_r = biblioreference_autocomplete_highlight($year, $words);
      $rendered .= '<span class="nodereference-year">' . $year_r . '.</span> ';
    }
    $value .= $journal . '. ' . $article . '.';
    // added later.  $title .= ' [nid:' . $node->nid . ']';  //@TODO replace with hidden field
    $rendered .= '<span class="nodereference-journal">' . $journal_r . '</span> ';
    $rendered .= '<span class="description">' . $article_r . '</span>';
    // @TOREVIEW show match count?  '(' . $node->matchcount . ')',
    $references[$node->nid] = array(
      'title' => $value,
      'rendered' => $rendered,
    );
  }
  return $references;
}

function biblioreference_autocomplete_form_value($nid) {
  $value = '';
  $sql = "SELECT b.vid AS vid, n.title AS node_title, b.biblio_secondary_title AS journal_title, b.biblio_alternate_title AS journal_abbr, b.biblio_year AS year
          FROM {node} n LEFT JOIN {biblio} b ON n.vid=b.vid 
          WHERE n.nid = %d";
  $node = db_fetch_object(db_query($sql, $nid));
  $journal = ($node->journal_abbr) ? $node->journal_abbr : $node->journal_title;
  $authors = biblioreference_authors_string($node->vid);
  $value .= $authors . '. ' . $node->year;
  $value .= $journal . '. ' . $node->node_title . '.';
  // $value .= ' [nid:' . $node->nid . ']';  //@TODO replace with hidden field
  return $value;
}

/**
 * In the autocomplete results, highlight words searched for.
 */
function biblioreference_autocomplete_highlight($rendered, $words) {
  foreach($words as $word) {
    //@TODO don't think this can be done with str_replace array functionality
    // but that would be nice
    $rendered = str_replace($word, '<strong>' . $word . '</strong>', $rendered);
  }
  return $rendered;  // not passing by reference since used to do assignment
  // @TODO maybe do this once with a regexp to prevent highlighting within HTML
}
?>

Reference

old issue posted against core:
Improved styling of autocomplete matches

Newer issue marked duplicate, and then work went back over to the previous issue, but this one has more clearly working code:
Usability: highlight found string in autofill

Update: Case-respecting, case-insensitive highlighting

To make this a bit better we have to replace str_replace with a smarter function.

String replace should be case insensitive, str_ireplace rather than str_replace, so that all occurrences are highlighted, but even that then lowercases the result when highlighted, so we really want PHP string replacement that is case insensitive yet keeps the case.

A commenter on the str_ireplace function PHP manual page provides that:

<?php
function ext_str_ireplace($findme, $replacewith, $subject)
{
  // Replaces $findme in $subject with $replacewith
  // Ignores the case and do keep the original capitalization by using $1 in $replacewith
  // Required: PHP 5
 
  return substr($subject, 0, stripos($subject, $findme)).
         str_replace('$1', substr($subject, stripos($subject, $findme), strlen($findme)), $replacewith).
         substr($subject, stripos($subject, $findme)+strlen($findme));
}
?>

Another couple commenters provide a better one:

<?php
 ewen dot cumming at gmail dot com
12-Jan-2009 03:38
Thanks for the highlightStr function sawdust - so that it also copes with special characters in the needle I added the following line before the preg_match_all line:

<?php $needle = preg_quote($needle);
?>

sawdust
04-Dec-2008 03:28
Here's a different approach to search result keyword highlighting that will match all keyword sub strings in a case insensitive manner and preserve case in the returned text. This solution first grabs all matches within $haystack in a case insensitive manner, and the secondly loops through each of those matched sub strings and applies a case sensitive replace in $haystack. This way each unique (in terms of case) instance of $needle is operated on individually allowing a case sensitive replace to be done in order to preserve the original case of each unique instance of $needle.

<?php
function highlightStr($haystack, $needle, $highlightColorValue) {
     // return $haystack if there is no highlight color or strings given, nothing to do.
    if (strlen($highlightColorValue) < 1 || strlen($haystack) < 1 || strlen($needle) < 1) {
        return $haystack;
    }
    preg_match_all("/$needle+/i", $haystack, $matches);
    if (is_array($matches[0]) && count($matches[0]) >= 1) {
        foreach ($matches[0] as $match) {
            $haystack = str_replace($match, '<span style="background-color:'.$highlightColorValue.';">'.$match.'</span>', $haystack);
        }
    }
    return $haystack;
}
?>

But I think the one that uses stripos instead of a regular expression is probably best:

Michael dot Bond at mail dot wvu dot edu
14-Nov-2008 02:44
This function will highlight search terms (Key Words in Context).

The difference between this one and the ones below is that it will preserve the original case of the search term as well. So, if you search for "american" but in the original string it is "American" it will retain the capital "A" as well as the correct case for the rest of the string.

<?php
function kwic($str1,$str2) {
  
    $kwicLen = strlen($str1);

    $kwicArray = array();
    $pos          = 0;
    $count       = 0;

    while($pos !== FALSE) {
        $pos = stripos($str2,$str1,$pos);
        if($pos !== FALSE) {
            $kwicArray[$count]['kwic'] = substr($str2,$pos,$kwicLen);
            $kwicArray[$count++]['pos']  = $pos;
            $pos++;
        }
    }

    for($I=count($kwicArray)-1;$I>=0;$I--) {
        $kwic = '<span class="kwic">'.$kwicArray[$I]['kwic'].'</span>';
        $str2 = substr_replace($str2,$kwic,$kwicArray[$I]['pos'],$kwicLen);
    }
      
    return($str2);
}
?>

Resolution

Searched words: 
drupal autocomplete with highlighting bold characters from search string in drop-down completion rendering

Comments

Very good.

Very good.

Tnk.

Post new comment

The content of this field is kept private and will not be shown publicly.
  • You may post code using <code>...</code> (generic) or <?php ... ?> (highlighted PHP) tags.
  • You can use Markdown syntax to format and style the text. Also see Markdown Extra for tables, footnotes, and more.
  • 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> <blockquote> <small> <h2> <h3> <h4> <h5> <h6> <sub> <sup> <p> <br> <strike> <table> <tr> <td> <thead> <th> <tbody> <tt> <output>
  • Lines and paragraphs break automatically.

More information about formatting options

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