{"id":2442,"date":"2012-08-01T12:23:25","date_gmt":"2012-08-01T18:23:25","guid":{"rendered":"http:\/\/bililite.com\/blog\/?p=2442"},"modified":"2012-08-05T06:44:28","modified_gmt":"2012-08-05T12:44:28","slug":"contextual-search-results-in-wordpress","status":"publish","type":"post","link":"https:\/\/bililite.com\/blog\/2012\/08\/01\/contextual-search-results-in-wordpress\/","title":{"rendered":"Contextual Search Results in WordPress"},"content":{"rendered":"<p>Michael Tyson had <a href=\"http:\/\/atastypixel.com\/blog\/keeping-blog-visitors-by-showing-meaningful-search-results-in-wordpress\/\">a cool idea<\/a>: instead of the search results page showing an excerpt of the first words of the post, show an excerpt that contains the search terms and highlight them (say, by making them bold). I thought his method was too complex&mdash;it requires replacing your theme's <code>search.php<\/code> with a custom page, and it shows the context of <em>every<\/em> occurrence of the search terms. I thought it would be more straightforward to use the existing search page, which should be using <a href=\"http:\/\/codex.wordpress.org\/Function_Reference\/the_excerpt\"><code>the_excerpt<\/code><\/a> to show an extract of the found page, and use the existing filters to change the text. Also, there's no need to show every occurrence of the search terms; the first ones should be fine.<\/p>\r\n<!--more-->\r\n<p>There are two parts to displaying the excerpt. The first is generating the text of the excerpt, with (possibly) a suffix that indicates that it was truncated. This is done by the function <a href=\"http:\/\/codex.wordpress.org\/Function_Reference\/get_the_excerpt\"><code>get_the_excerpt<\/code><\/a>. That uses the <a href=\"http:\/\/codex.wordpress.org\/Plugin_API\/Filter_Reference\/excerpt_length\"><code>excerpt_length<\/code><\/a> filter to determine the length of the excerpt in words, and the <a href=\"http:\/\/codex.wordpress.org\/Plugin_API\/Filter_Reference\/excerpt_more\"><code>excerpt_more<\/code><\/a> filter to determine the suffix. It then calls the <code>get_the_extract<\/code> filter to return the final results. We'll hook that filter to return our own text, one that contains the search terms.<\/p>\r\n<p>The second part is displaying the excerpt, done by <a href=\"http:\/\/codex.wordpress.org\/Function_Reference\/the_excerpt\"><code>the_excerpt<\/code><\/a>. All that does is embed the text in a <code>p<\/code> element, call the <code>the_extract<\/code> filter, and echo the result. We'll hook that to highlight the search terms.<\/p>\r\n<p>First, we create a regular expression that looks for the search terms (basically, same as that used by Michael Tyson):<\/p>\r\n<pre><code class=\"language-php\">function query_re(){\r\n\tglobal $wp_query;\r\n\t$terms = $wp_query->query_vars['search_terms'];\r\n\tforeach ($terms as &$term) $term = preg_quote($term, '\/');\r\n\treturn '\/'.implode('|', $terms).'\/iu';\r\n}<\/code><\/pre>\r\n<p>Then, we use my <a href=\"\/blog\/2012\/07\/31\/replace-text-not-in-tags\/\" title=\"Replace Text not in Tags\">preg_replace_text<\/a> to highlight the terms when the excerpt is displayed (the <code>the_excerpt<\/code> filter):<\/p>\r\n<pre><code class=\"language-php\">add_filter('the_excerpt', function ($text){\r\n\treturn preg_replace_text (query_re(), '&lt;span class=&quot;searchterm&quot;&gt;$0&lt;\/span&gt;', $text);\r\n});<\/code><\/pre>\r\n<p>And now make sure our stylesheet does something pretty with <code class=\"language-css\">span.searchterm<\/code>.<\/p>\r\n<p>Selecting the correct excerpt is slightly more complicated. First choice is the author-composed excerpt (if it matches the search terms), which is passed to the filter directly. Second choice is the default excerpt (if it matches the search terms), which is the first <code>excerpt_length<\/code> words. Third choice is the <code>excerpt_length<\/code> words surrounding the first search term; I'll arbitrarily take one-third of the length before and two thirds after. Fourth choice, which means that neither the excerpt or the whole text contain the search term, is the original excerpt (this can happen if the search term matched the title). Fifth choice is the first <code>excerpt_length<\/code> words.<\/p>\r\n<p>WordPress has a function <a href=\"http:\/\/codex.wordpress.org\/Function_Reference\/wp_trim_words\"><code>wp_trim_words<\/code><\/a> that we can use to limit the excerpt size, but it only trims on the end. To trim the start of the text, we use a little hack: trim the reverse of the text. The definition of a word doesn't depend on spelling. Since we may well want Unicode text and <a href=\"http:\/\/php.net\/manual\/en\/function.strrev.php\">strrev<\/a> can't handle that, we use <a href=\"http:\/\/php.net\/manual\/en\/function.strrev.php#107664\">this cute function<\/a>:\r\n<code class=\"language-php\">function strrev_utf8($str) {\r\n    return join(\"\", array_reverse(preg_split(\"##u\", $str)));\r\n}<\/code>:<\/p>\r\n<pre><code class=\"language-php\">remove_all_filters('get_the_excerpt'); \/\/ we want to take over handling the excerpt\r\nadd_filter( 'get_the_excerpt', function($excerpt){\r\n\tglobal $post;\r\n\t$excerpt_length = apply_filters('excerpt_length', 55);\r\n\t$excerpt_more = apply_filters('excerpt_more', '\u2026');\r\n\t$query_re =  query_re();\r\n\t\r\n\t\/\/ First choice: the author-composed excerpt\r\n\tif ($excerpt &amp;&amp; preg_match($query_re, $excerpt)) return $excerpt;\r\n\r\n\t\/\/ Second choice: the start of the text\r\n\t\/\/ get the actual text of the post\r\n\t$text = wp_strip_all_tags(apply_filters('the_content', $post-&gt;post_content));\r\n\t\/\/ Create the default excerpt\r\n\t$excerpted_text = wp_trim_words($text, $excerpt_length, $excerpt_more);\r\n\tif (preg_match($query_re, $excerpted_text)) return $excerpted_text;\r\n\t\r\n\t\/\/ Third choice: context of the search term\r\n\t$text_matched = preg_match ($query_re, $text, $matches, PREG_OFFSET_CAPTURE); \/\/ save the matched terms with their offsets\r\n\tif ($text_matched){\r\n\t\t$offset = $matches[0][1]+strlen($matches[0][0]); \/\/ the offset into the end of the text where the term was found\r\n\t\t\/\/ hack: we want to add context for where the term was found, but we want it to use whole words. wp_trim_words will trim the end,\r\n\t\t\/\/ but we want so many words (empirically, one third the excerpt length) in the beginning. So we reverse the text and use that.\r\n\t\t$len = $excerpt_length\/3;\r\n\t\t\/\/ need to use a single character to indicate truncation since we are reversing the text\r\n\t\t$reversetext = strrev_utf8(wp_trim_words(strrev_utf8(substr($text, 0, $offset)), $len, '\u2026'));\r\n\t\t$context = $reversetext.substr($text, $offset); \/\/ rebuild it\r\n\t\treturn wp_trim_words($context, $excerpt_length, $excerpt_more);\r\n\t}\r\n\t\r\n\t\/\/ No matches. Just use the usual excerpt \r\n\treturn $excerpt ? $excerpt : $excerpted_text;\r\n});<\/code><\/pre>\r\n<p>This seems to work well, and should work with any theme that uses <code>the_excerpt()<\/code> to display search results. One note is that the <a href=\"http:\/\/codex.wordpress.org\/Function_Reference\/wp_trim_excerpt\">wp_trim_excerpt<\/a> function, which this replaces (it is the original <code>get_the_excerpt<\/code> filter) does a <a href=\"http:\/\/codex.wordpress.org\/Function_Reference\/strip_shortcodes\">strip_shortcodes<\/a> on the content, which I specifically left out. I want to include the text of my shortcodes. Also, it does <a href=\"http:\/\/core.trac.wordpress.org\/browser\/tags\/3.4.1\/wp-includes\/formatting.php#L2119\"><code class=\"language-php\">$text = str_replace(']]&gt;', ']]&amp;gt;', $text);<\/code><\/a>, for reasons I don't understand. Where would <code>]]&gt;<\/code> come from? So I left it out.<\/p>","protected":false},"excerpt":{"rendered":"Michael Tyson had a cool idea: instead of the search results page showing an excerpt of the first words of the post, show an excerpt that contains the search terms and highlight them (say, by making them bold). I thought his method was too complex&mdash;it requires replacing your theme's search.php with a custom page, and [&hellip;]","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[9,17],"tags":[],"_links":{"self":[{"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/posts\/2442"}],"collection":[{"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/comments?post=2442"}],"version-history":[{"count":23,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/posts\/2442\/revisions"}],"predecessor-version":[{"id":2521,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/posts\/2442\/revisions\/2521"}],"wp:attachment":[{"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/media?parent=2442"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/categories?post=2442"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/tags?post=2442"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}