joestelmach.com

Live Search With Ruby On Rails

Gone are the days of actually submitting your search query and waiting for an entirely new page to be rendered. The 'Live Search' era is upon us, and I'm here to welcome it with open arms. My first encounter with a Live Search type of form was some time in 2005 when Google launched Google Suggest. I was so blown away that I figured only a giant company full of PhD's could figure out how to implement this. Boy, was I wrong.

I'm here to discuss a step-by-step procedure for adding a live search feature to your Ruby On Rails application, and some of the peculiarities you may encounter while walking through the steps. The wiki on the Rails site is the only real resource I could find regarding live search, and it will be the basis of our discussion here.

Implementation Steps

The steps to adding live search to your Ruby On Rails app are as follows:

  1. Include the default Javascript files in your page
    <%= javascript_include_tag :defaults %>
    OR
    <%= define_javascript_functions %>
    Both of these will give you access to the ActionPack Javascript libraries. The difference is that define_javascript_functions will place ALL of the ActionPack Javascript within a <script> tag on your page, where the javascript_include_tag will simply link the external ActionPack Javascript files to your page. (HINT: use javascript_include_tag)
  2. Create a text field for your users to enter their search criteria. We'll use 'search' as our id and name attributes, but feel free to use any value you want.
    <input type="text" id="search" name="search" />
  3. Create an image to be used as a status indicator while the search is being performed. Be sure to set an id and add an inline style rule to force the image to not show initially.
    <img id="busy" src="/images/spinner.gif" style="display:none" />
    If you're looking for some images to use on your site, some are available here. Also feel free to use the modified spinner indicator I'm using on this site.
  4. Create an empty div where you would like the results of the search to be rendered. Be sure to give the div an id.
    <div id="searchResults"></div>
  5. Create an Ajax observer to observe the text field while the user is typing. The value of the first parameter should be whatever you chose as an id in step 2.
    <%= observe_field 'search',
      :frequency => 0.5,
      :update => 'searchResults',
      :url => { :controller => 'blog', :action=> 'search' },
      :with => "'criteria=' + escape(value)",
      :loading => "document.getElementById('busy').style.display='inline'",
      :loaded => "document.getElementById('busy').style.display='none'" %>
    This is where we get a first glimpse of the power inherent in the Ajax helpers. To implement this directly through the XMLHttpRequest object would be much more difficult, error-prone, and messy. Let's take a look at each of the expressions we're using here
    • :frequency - The frequency, in seconds, that you would like the field to be observed
    • :update - The id of the element where the search results should be rendered. This is the id you chose in step 4
    • :url - The controller/action that implements your search functionality (see step 6)
    • :with - Used to pass a parameter called 'criteria' which will be set to the value of the text field at the time of the observation. The call to escape ensures that any special characters in the input are escaped. Note that we are breaking out of the traditional form-submission process where a request parameter is automatically created for a form element when the form is submitted. We technically don't have a form in this case (more on this later.)
    • :loading - used to display the busy indicator image you created in step 3. The Javascript placed here will be executed when the search results element is being loaded with data
    • :loaded - used to hide the busy indicator once the search results element is done loading.
    You should also be aware that any options available in link_to_remote are also available to you here.
  6. Create an action in the controller of your choice that will provide the search facility. Let's assume we have a model called 'Blog' with the blog's contents being stored in the 'content' column.
    def search
      if 0 == @params['criteria'].length
        @items = nil
      else
        @items = Blog.find(:all, :order_by => 'title',
          :conditions => [ 'LOWER(content) LIKE ?',
          '%' + @params['criteria'].downcase + '%' ])
        @mark_term = @params['criteria']
      end
      render_without_layout
    end
    This search function will perform a simple sql like query on the blog's criteria column. Depending on the size of your site and the volume of content you have, this may or may not be a sufficient way to search. For our purposes here, this method works just fine. Once the results have been gathered, we render the search results (using no layout) with the page you will create in the next step. Also note that we have set an instance variable, @mark_term, that we can use to highlight our search criteria within the rendered search results.
  7. Create a search.rhtml view page to display the search results.
    <% if @items && @items.length > 0 %>
      <ul id="searchResults">
        <% for blog in @items %>
          <li>
            <%= link_to
              @mark_term ? highlight(blog.title, @mark_term) : h(blog.title),
              :controller => "blog",
              :action => "show", :id => blog.id %>
          </li>
        <% end %>
      </ul>
      <% elsif @mark_term && @mark_term.length > 0 %>
        No Results
      <% else %>
        &nbsp;
    <% end %>
    If @items is not nil or empty, then we display our search results as a list of links to each blog entry's 'show' action, highlighting the given search term within each link. If you would like to change the appearance of the highlighting, simply add a style rule for the class 'highlight' in your style sheet. If the @mark_term is not nil or 0 characters, then the search must not have returned any results, so we simply display 'No Results'. Otherwise, we show nothing since this would indicate that the search field has been cleared.
In just 7 painless steps we were able to create a piece of functionality that I wouldn't have dreamed possible back in 1999. I have to admit that my initial reaction to such a cool feature (given so little code) was nothing less than a giddy feeling of amazement. However, there are some things I do feel uncomfortable with here, and this being my blog, I'm gonna go ahead and discuss them.

We use an <input> tag that isn't wrapped with a <form> tag.
I'm not sure if I should be as bothered by this as I am, but it just doesn't feel right to have form elements without a form. Let's take a look at the W3C's definition of form elements:
Form elements are elements that allow the user to enter information (like text fields, textarea fields, drop-down menus, radio buttons, checkboxes, etc.) in a form.
The last part is what gets me: in a form. Form elements allow you to capture data in a form. But we have no form here. I suppose you could argue that wrapping the input tag with a form element will do no harm here and will make the page more semantically correct, however; the action attribute would be left empty (or useless, since we have no submit button,) and that would open up a whole new basket of worries.

It is at this point I realized that Ajax functionality may come at a cost.
Rails provides a text_field html helper that seems unusable
There is built in support for generating html form elements, but I find them to be unusable in this case. The html form helpers seem to work only when the form is backed by an actual model object. If I am missing something here, please let me know.
The observe_field kind of just hangs out on the page
I find it to have no semantic meaning or proper place. Perhaps all of the observers should be registered in a single place, but I'm not sure if that makes sense either. I suppose you could argue that the <script> or <style> tag is just as semantically incorrect. In any event, it bothers me.
The search is not persisted in the session
I'm assuming this can be fixed. Perhaps this will be the topic of a later post.
No JavaScript For You!

I personally feel very strongly against limiting a site's core functionality when the user has Javascript disabled. I can see this issue becoming less and less prominent as more mainstream sites are depending on Javascript for core functionality, however; I still see the need to support browsers sans Javascript (Certainly in the case of search functionality.)

If Javascript is disabled in our example here, the user has no way of knowing how to search. They'll enter some text, hit enter, wait, and then get frustrated and leave. Therefore, a button should be displayed for the user when Javascript is disabled (or when the XMLHttpRequest object is not available on the user's browser.) At first glance this problem does not seem to be trivial. There are many factors to consider here (one being the previously stated absence of a form tag and submit button.) This will be the topic of a later post.

Browser Support
I tested this code on all of the browsers I had laying around. I realize this is in no way an exhaustive list, but I found the following browsers to be supported:
  • Safari 2.0
  • Firefox 1.5 Mac
  • Firefox 1.0.4 Linux
  • Firefox 1.0.4 Windows
  • Mozilla 1.7.8 Linux
  • Konqueror 3.5
  • Epiphany 1.6.0
  • Opera 8.51 Mac
  • IE 5.5 Windows
  • IE 6 Windows
and these browsers to be unsupported:
  • IE 5.2.3 Mac
  • Opera 7.5.4 Mac
Update - 3/13/2006: I've added a followup to this post here.

Update - 5/1/2006: I've added another followup here.

Comments
blog comments powered by Disqus