Searching for and Highlighting Text Dynamically

Problem

You want to let users search a body of text on a page while highlighting matches for the search term as they type it.

Solution

Use the observe_field Prototype helper to send continuous Ajax search terms to the server for processing. For example, suppose you have an application that stores articles that you want users to be able to search. Assuming you have a Rails application created and configured to connect to a database, create an Article model with:

$ ruby script/generate model Article 

Then create and load a migration to instantiate the articles table:

db/migrate/001_create_articles.rb:

class CreateArticles < ActiveRecord::Migration
 def self.up
 create_table :articles do |t|
 t.column :title, :string
 t.column :body, :text
 end
 end
 def self.down
 drop_table :articles
 end end

You'll also need to include the Prototype JavaScript library. Do that by creating the following layout template:

app/views/layouts/search.rhtml:

<html>
<head>
 <title>Search</title>
 <%= javascript_include_tag :defaults %>
 <style >
 #results {
 font-weight: bold;
 font-size: large;
 position: relative;
 background-color: #ffc;
 margin-top: 4px;
 padding: 2px;
 }
 </style> 
</head>
<body>
 <%= yield %>
</body>
</html>

The index view of the application defines an observed field with the Prototype JavaScript helper function observe_field. This template also contains a div tag where search results are rendered.

app/views/search/index.rhtml:

<h1>Search</h1>
<input type="text" id="search">
<%= observe_field("search", :frequency => 1,
 :update => "content",
 :url => { :action => "highlight"}) %>
<div id="content">
 <%= render :partial => "search_results", 
 :locals => { :search_text => @article } %>
</div>

As with all Ajax interactions, you need to define code on the server to handle each XMLHttpRequest. The highlight action of the following Search Controller contains that code, taking in search terms and then rendering a partial to display results:

app/controllers/search_controller.rb:

class SearchController < ApplicationController
 def index
 end
 def highlight
 @search_text = request.raw_post || request.query_string
 @article = Article.find :first,
 :conditions => ["body like ?", "%#{@search_text}%"]
 render :partial => "search_results", 
 :locals => { :search_text => @search_text, 
 :article_body => @article.respond_to?('body') ?
 @article.body : "" }
 end end

Finally, the search results partial simply calls to the highlight helper, passing it local variables containing the contents of the article body (if any) along with the search text that should be highlighted.

app/views/search/_search_results.rhtml:

<p>
 <%= highlight(article_body, search_text, 
 '<a href="http://en.wikipedia.org?search=\1" id="results" 
 title="Search Wikipedia for \1">\1</a>') %>
</p>

The partial not only highlights each occurrence of the search text, but it creates a link to Wikipedia's search, passing the same search text.

Discussion

The solution demonstrates a cool effect called live search. Making it work is really a combination of a number of components, all working together to provide real-time, visual feedback about the search.

Here's how it works: a user navigates to the index view of the Search Controller. There, she finds a search box waiting for input. That text input field is configured to observe itself. As the user enters text, an Ajax call is sent to the server ever second (the interval is specified by the :frequency option).

For each one of these Ajax requests, the highlight action of the Search Controller is invoked. This action takes the text from the raw post and looks up the first article in the database that contains the text being searched for. Next, the search_results partial is rendered by the highlight action, with the search text and article body being passed in.

Finally, the partial _search_results.rhtml expects to receive the body text of the article found by Search#highlight along with the same search text that the user is in the process of entering. The partial processes the search text along with the search results using the view helper, highlight.

The highlight view helper takes a body of text as its first argument, and a phrase as the second. Each occurrence of the phrase within the body of text is surrounded with <strong> tags (by default). To treat the matched text differently (as the solution does) you pass a third argument to highlight, which is called the highlighter. The highlighter is just a string with an occurrence of "\1" somewhere in it. "\1" is substituted for the matched text. This way you can create whatever kind of treatment you like. The solution wraps the occurrences of the search terms in a hyperlink that points to Wikipedia.

shows the results of the solution's search form, highlighting words within the text that match the search term.

Figure 8-9. A search form that dynamically highlights matched words

See Also