Live Search

Live search has been one of the hottest features. I decided to give it a try on my site, which is Drupal-powered. And since it's powered by Drupal, there are obviously some little problems that need to be sorted out.

This article is on Drupal 4.7. For live search on Drupal 5.0, see my live search module for Drupal 5.

Get Results Fast

The basic goal for me is that after the user types some text to search, a little box should appear shortly, containing the search result for their keyword. If their keyword returned no results, a message should inform them that.

It was obvious to implement it in the so-called "Ajax" fashion. As the user types, a request is sent in the background, and when it's completed, the client-side JavaScript injects the results directly on the current page.

There are also several points that needs to be achieved, including...

  • falling back gracefully in case JavaScript isn't enabled
  • the existing Search module should be reused and modifications should be minimized
  • adjusts with the locale
  • not to increase server burden

With these goals in mind, a vague idea of what's needed appears.

The Tools

Investigation

First of all, since we want to reuse the existing Search module for Drupal, I go to the Search Results page for "ajax" and examine the DOM structure.
Here are some simple facts about it:

  • The URL for searching keyword is http://www.kourge.net/search/node/keyword
  • The result is in a <dl> definition list element with the class search-results
  • If no results were found then the <dl> tag won't exist, as demonstrated in the Search Results page for "abracadabra"
  • If nothing's found, the message "your search yielded no results" is in an <h2> element with the class title

Sum that all up, and you have a basic process:

  1. Fire an XMLHttpRequest for http://www.kourge.net/search/node/keyword
  2. When the request is complete, transverse through the responseXML object of the request to look for all <dl> elements and choose only the one with the class search-results
  3. If such an element is found, inject it to a prearranged <div> element
  4. If no such element exists, look for all <h2> elements and choose only the one with the class title
  5. If such an element is found, inject the content of it to the prearranged <div> element to inform the user that nothing's found
  6. If even that doesn't work, fallback to the default and hard-coded English message and inject it

The Problem

After writing experimental code, you'll notice that Mozilla-based browsers return null when you try to access the responseXML object of the completed request. Why?

The AJAX:Getting Started article on the Mozilla Developer Center states that:

Some versions of some Mozilla browsers won't work properly if the response from the server doesn't have an XML mime-type header. To satisfy this, you can use an extra method call to override the header sent by the server, just in case it's not text/xml.

http_request = new XMLHttpRequest();
http_request.overrideMimeType('text/xml');

AJAX:Getting Started - Step 1 – say "Please!" or How to Make an HTTP Request - MDC

The bottom line is that we are left with two options to deal with this.

  • Utilize the overrideMimeType() method in your client-side JavaScript code
  • Modify the Drupal Search module so that it will send back Content-Type: text/xml in the response header

The first way would require you to modify Prototype: not a very good idea. The second way is a bit easier, but requires a trick. You won't want to send back the header Content-Type: text/xml for every single search request, because that'll break the normal search feature for standard-aware browsers, causing them to display a parsed XML tree instead of rendering the page as (X)HTML.

A quick glimpse of the Prototype code will reveal this:

/* ... */
setRequestHeaders: function() {
    var requestHeaders =
      ['X-Requested-With', 'XMLHttpRequest',
       'X-Prototype-Version', Prototype.Version,
       'Accept', 'text/javascript, text/html, application/xml, text/xml, */*'];
/* ... */

Prototype sends these three custom headers when initiating an XHR request, assuming my Prototype version is 1.5.0_rc0:

X-Requested-With: XMLHttpRequest
X-Prototype-Version: 1.5.0_rc0
Accept: text/javascript, text/html, application/xml, text/xml, */*

Which header is the best for determining whether not to respond with the HTTP header Content-Type: text/xml?

  • Accept? Maybe some other browsers might send the exact same string. Not a good idea.
  • X-Prototype-Version? What if you use a different version of Prototype? Not a good idea either.
  • X-Requested-With? Right on spot, because every version of Prototype sends this header.

I opened up modules/search.module on my server and looked for the function search_view():

/**
 * Menu callback; presents the search form and/or search results.
 */
function search_view() {
  $type = arg(1);

  // Search form submits with POST but redirects to GET. This way we can keep
  // the search query URL clean as a whistle:
  // search/type/keyword+keyword
  if (!isset($_POST['edit']['form_id'])) {
    if ($type == '') {
      // Note: search/node can not be a default tab because it would take on the
      // path of its parent (search). It would prevent remembering keywords when
      // switching tabs. This is why we drupal_goto to it from the parent instead.
      drupal_goto('search/node');
    }

After the code:

    if ($type == '') {
      drupal_goto('search/node');
    }

Add the following code:

    if ($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') header('Content-Type: text/xml');

That's one line of code for the server-side PHP script.

Now we need a slight modification for the theme template. My Drupal theme is Typography Paramount, powered by the PHPTemplate theme engine.

Again, I opened up themes/typography_paramount/search-box.tpl.php on my server, which is responsible for generating the markup for the existing search box on the right.

<input maxlength="128" name="edit[search_theme_form_keys]" id="search-box" size="15" value="<?php print t('Search') ?>" onclick="if(value=='<?php print t('Search') ?>')value=''" onblur="if(value=='')value='<?php print t('Search') ?>'" class="form-text" type="text" autocomplete="off"  />
      <input name="edit[form_id]" id="edit-form_id" value="search_theme_form" type="hidden" />
      <input name="op" value="search" class="form-submit" type="hidden" alt="" />
<div id="search-result"></div>
<br /><br />

Notice that:

  • The ID of the search box is search-box
  • <?php print t('Search') ?> is used to print out the localized string of "Search"
  • onclick and onblur handlers move the "Search" text out of the way when the user wants to search and grabs it back when the user leaves it empty
  • The prearranged <div> element with the ID search-result is for the injection of content

Then I modify themes/typeography_paramound/page.tpl.php to include the JavaScript files:

  <script type="text/javascript" src="/js/prototype.js"></script>
  <script type="text/javascript" src="/js/eventselectors.js"></script>
  <script type="text/javascript" src="/js/domready.js"></script>
  <script type="text/javascript" src="/js/cursor.js"></script>
  <script type="text/javascript" src="/js/search.js"></script>

The last search.js is the custom code written. Search.js consists of several parts...

  • $E() function for ease of creating elements
  • The function search() for getting the search box value and firing off the actual Ajax request.
  • window.searchTimer for delaying the Ajax request to avoid firing a request on every keystroke
  • Event.onDOMReady() to immediately attach event listeners to the search box using event:Selector when the DOM is ready

It is also aware of several things.

  • It encodes the search keyword
  • If the user presses the Escape key the search box is immediately cleared

In conclusion, it fits with Drupal's search feature nicely. Try typing "ajax" in the search box and try it out.

This article is on Drupal 4.7. For live search on Drupal 5.0, see my live search module for Drupal 5.

Great article. Well written

Great article. Well written and very useful. Thanks alot!

Hi, Sorry, my last message

Hi,

Sorry, my last message was typed wrong. I meant hi elements in addition
to h2 elements in the response XML object parsing.

Thanks Again

Steve

Hi, I aplogize for this

Hi,

I aplogize for this edited reply. I was wondering why the h1 elements aren't
also searched for in the response XML object in addition to the h 2 elements.
This is a great insight into the trade offs involved in implementing this
search function. I surely couldn't have filled your shoes on this one!

Thanks Again

Steve

Great article. Well written

Great article. Well written and very useful. Thanks alot!

It would be nice if you have this module for Drupal 6 :d

Thanks

It's the lack of feedback

It's the lack of feedback that's holding me back. In fact, if I don't get any more feedback by the time summer breaks, I'll just go ahead and port it to Drupal 6, since it's already available for Drupal 5.

Hi, You were going to decide

Hi,

You were going to decide whether to port it to drupal 6. Have you decided yet?

Mike

This is good information on

This is good information on adding live search to drupal. Thanks for all the detail, it really helps.

Kandk

It's so great that sites

It's so great that sites like this exist so that we can learn how to add live search to our drupal sites. I would never have know before.

Very nice job. I like how

Very nice job. I like how the "ajax" was manipulated for the search bar.

Drupal is much more

Drupal is much more complicated to work with but once you master it a lot can be done with it. Live search module would be good but not many people would like live search besides ading google search is easier.

Donna

Does anyone know if the AJAX

Does anyone know if the AJAX for the search can be deactivated or not? Thanks.

For the module or for the

For the module or for the hack in this post?

Hi, thank you for your hard

Hi, thank you for your hard work. it seems you always make a revisions for your code. Any way, thanks again. I am a newbie in Drupal actually

Im with Donna, once you get

Im with Donna, once you get past the learning curve, you will find that Drupal is just wonderful. Sometimes you gotta pay up front to reap the benefits later!

:)

This is another vote to port

This is another vote to port it to 6. Pretty please Kourge!

Drupal 6! +1 :)

Drupal 6!
+1
:)

It is difficult Drupal for

It is difficult Drupal for beginners. Thanks were learnt a lot of by the new.

What a great project.

What a great project. Nicely done, Thanks for sharing it.

Live search can really do

Live search can really do wonders in giving the visitors to a site the best possible experience. Thanks for posting this. I am going to work on it on my Drupal site.

Will this still work for

Will this still work for Drupal 6? Have not upgraded yet, but curious if it will break. I'm factoring in my "luck." :)

Hi! Have you got a search

Hi!
Have you got a search module for Drupal 6?

Hey, thanks for the hack.

Hey, thanks for the hack. Really solid information. Cheers. Shaw.

Thanks to all drupal

Thanks to all drupal comunity.
Just installed it! New changes are very impressive.

I'm with the others, a

I'm with the others, a drupal 6 workup would be awesome!

Thanks for sharing. I'm

Thanks for sharing.

I'm getting into my first drupal install and have been looking for a search functionality similar to this. Appreciate it.