This is an archive site. For the recent posts, visit orderedlist.com.

Ordered List

OrderedList

Jun 23 2008

Live Search with Quicksilver Style

Live search? C’mon guys, this has been done plenty of times before, you say. Yes, it has but I promise you we’ve added a bit of a twist to it.

When working on the latest incarnation of orderedlist.com, Steve mentioned that the typical year and month layout of archives was overkill. He thought a better way to present them would be to list them all and let live search sort ’em out. That simple thought fired a neuron in my brain. I am a fan of QuickSilver and I remembered seeing a port of the QuickSilver string ranking algorithm in JavaScript. A side note: anytime I say “algorithm” in a sentence I feel really smart. The fact that in the previous sentence “algorithm” was preceded by “string ranking” made me feel doubly smart. Ahem…back to the point at hand.

Demos

Typically, when I am reading a tutorial of any sort, I want to see the end product because if that is not impressive, I do not want to waste my time reading it. Feel free to open up a new tab with the archives page or the simplified demo I created just for this post and try a few searches.

Type This, Get That

Sure you can search ‘sidebar creative’ to find posts titled that, but I am lazy. I’d rather type ‘sdbcv’ because it is shorter. Plus, vowels are so 2007. You know who else is cool? Yeah, that guy Nunemaker, but holy crap is that name a keyboardful. Why don’t we just hit a few of the important characters like ‘nnmkr’. Bing, bang, boom. Welcome John Nunemaker is right there at the top. Cool, eh?

The How

First things first, we need a JS library to take away the hurt of cross-browserness. For this exercise, we used Prototype.

Second, we drop in the JS QS string ranking algorithm (I still feel smart).

Third, I made the simple prototype class featured below. It is commented pretty well with what is going on. Also, note that this is not the live version on Ordered List but a slightly simplified one that I made for the demo. Go ahead and give it a read through.

var QuicksilverLiveSearch = Class.create({
        /**
         * Sets up the caches and adds observers
         */
        initialize: function(field, list) {
                this.field = $(field);
                this.list  = $(list);
                
                if (this.field && this.list) {
                        this.rows  = $A([]);
                        this.cache = $A([]);
                        this.setupCache();
                        
                        // kill normal submit of form since it's live
                        this.form = this.field.up('form');
                        this.form.observe('submit', function(e) { e.stop(); });
                        
                        // setup observer on the search field to run the filter when typing
                        this.field.observe('keyup', this.filter.bindAsEventListener(this));
                        
                        // run the filter initially for any text that may be in it
                        this.filter();
                }
        },
        
        /**
         * Caches inner html of children in array for later manipulation. 
         */
        setupCache: function() {
                // loop through immediate descendents (in this case li's) and push
                // their lowercase text to the cache and the li to the rows
                this.list.immediateDescendants().each(function(child) {
                        this.cache.push(child.innerHTML.toLowerCase());
                        this.rows.push(child);
                }.bind(this));
                this.cache_length = this.cache.length;
        },
        
        /**
         * Runs the filter that only shows the rows 
         * that have a score based on the search term.
         */
        filter: function() {
                // if nothing is in the field show all the rows
                if (!this.field.present()) { this.rows.invoke('show'); return; }
                
                // get the scores and hide the low scoring items
                this.displayResults(this.getScores($F(this.field).toLowerCase()));
        },
        
        /**
         * Hides all the rows and shows on the ones with a score over 0
         */
        displayResults: function(scores) {
                // hide all rows default
                this.rows.invoke('hide');
                
                // show each row that had a score
                scores.each(function(score) { this.rows[score[1]].show(); }.bind(this))
        },
        
        /**
         * Get the score of each row in the cache and return sorted 
         * result set of [score, index of row in this.rows]
        */
        getScores: function(term) {
                var scores = $A([]);
                
                // loop through the cache and get the score for each item 
                // appending them to the return set if they have a score
                // greater than 0; basically building an array like this:
                // [[0.69, 2], [0.33, 34], ...] where the first element is
                // the string score and the second is the index of the item
                // that scored that
                for (var i=0; i < this.cache_length; i++) {
                        var score = this.cache[i].score(term);
                        if (score > 0) { scores.push([score, i]); }
                }
                
                // sort the scores descending by the algorithm score (the first element in the array)
                return scores.sort(function(a, b) { return b[0] - a[0]; });
        }
});

Now all that is left is to create a new instance of this class once the dom has loaded.

document.observe('dom:loaded', function() { 
        new QuicksilverLiveSearch('q', 'posts');
});

As you can see, most of the heavy lifting is done by prototype and the quicksilver string ranker. All I do is build a cache of all the strings I want to search through and then show only the rows that have strings with a score greater than 0. Simple implementation with pretty cool results so I thought I would share it here.