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.
Post and Author Info.
Published June 23, 2008 by:
Commenting is currently off for this post.
So far there are 32 comments.
Nice work! Now all you need to do is have it live search within the text of all the articles… ;)
June 24th, 2008
@Matthew: It does search through the tags, as they’re in a hidden cell for each article. I figured that would be enough for now. :)
June 24th, 2008
Wow, that works nicely, very nicely in fact. Thanks for this, I may have to steal it (or find a jQuery port, because my JavaScript skills are not as sharp as required).
That’s another cool thing to add to the list of things I want to implement on my site redesign.
June 24th, 2008
@Matt – The original author of the js port created an example in jquery though it is a bit more complicated than the one we made.
June 24th, 2008
Simple and beautiful. If something was ever worthy of stealing, it is this. =)
June 24th, 2008
Nice script – I am thinking of adding this feature to my own live search implementation.
June 24th, 2008
Very cool.
Added to my short list of “things to implement.”
June 24th, 2008
Pure hotness. I’ll have to give the jQuery version a try for my own site (if I ever get the archives section done that is). One thing I noticed is that there’s no hat tip to the user that the live search is doing anything except that the list of results keeps changing. Sure, that’s enough, but it would be nice to see some sort of UI indicator as you type.
June 24th, 2008
@Patrick – Glad you like it. I’m thinking about throwing a jQuery example together as I’ve been using it of late anyway.
Per the UI indicator: It’s so fast and instantaneous that some other UI change seems almost overkill. If it was doing an Ajax lookup I’d definitely agree with you, but in this case I’m not sure. Did you have any specific ideas that you thought would do the trick, other than the standard spinner?
June 24th, 2008
Nice work, I forgot how wonderful it feels to not be doing laggy AJAX calls when the result set is small.
Also, kudos to the person who made this form live preview. Except for the continously updating gravatar in FF3 it’s perfect.
June 24th, 2008
@John – That’s a fantastic approach. Timely for me too as I’ve been scratching my head trying to come up with a decent way to present our archives. I quite like the way Veerle Pieters handled hers, but being a huge Quicksilver and jQuery fan myself, yours has captured my heart <3>
Shout if you’d like some help with your jQuery version.
June 24th, 2008
John: I’m not really sure what I’d put in there as an indicator, or to smooth out the quickly changing results. I agree with you that things happen so fast, it’s not completely necessary, but the updating results seem to move so fast that it’s almost too much. I’d have to think about that one for a while…
June 24th, 2008
Pretty cool, but when you search using caps it stops working (no articles returned).
June 24th, 2008
@Elijah – Live comment is Steve’s idea and I did the implementation for this version of the site.
@Patrick – Yeah, I know what you mean.
@Sahil – Fixed. Thanks for pointing that out. Updated the post and the example.
June 24th, 2008
Great ide and implementation! What’s the license? Can i use it in my own projects?..
June 25th, 2008
@Rouss – Yeah, feel free to use the live search stuff. That main reason I created the demo is so it wouldn’t be so specific to what we are doing but more generic so anyone can use and tweak it.
June 25th, 2008
Wow! Damn is this cool. Maybe there’s a nice solution for the space below the input-field. Because there has to be al lot of space for all articles. On the other hand the user looks only at the first results.
June 25th, 2008
Pretty cool! Thanks a lot for sharing, guys!
:)
June 26th, 2008
Nice idea. I`d like to implement it in my projects ;-)
June 26th, 2008
That is sick…I mean slick…well, it’s both and thanks. Adding to list as well.
June 26th, 2008
Very Nice. I would like to see it done with jQuery.
June 26th, 2008
Nice, nice, nice. I’ve just bookmarked your site! I will write a article in my blog about Quicksilver!
June 27th, 2008
Hi there. Went ahead and did some porting of my own which ended up in a MooToolifyied version – http://blog.olicio.us/2008/06/27/live-search-with-quicksilver-style-mootools-12-port/
June 27th, 2008
I’ve been fiddling around with this approach on my site, and I noticed that when filtering, I naturally tend to hit the up and down arrow keys expecting that I can select from the search results. This might work well for your site. I’m guessing that hitting Enter after selecting a result would make that link click through…
Thanks again for sharing :)
June 29th, 2008
Wow! Great! Will try to recode this with jQuery. Thanks for sharing with us.
July 1st, 2008
very neat idea. thanks a lot! i will give it a try
July 6th, 2008
Nice, works great!
It would be interesting to see a version that used tabindex and focus styles so one could tab down from the search field through the list.
July 7th, 2008
@Brian – Ask and you may receive. :) I think that is a good idea too. We’ll see when I get time to play with it.
July 7th, 2008
Very cool, John. What interests me about this, apart from it’s obvious usability advantages, is that I’m noticing a slight performance difference between the jQuery and Prototype implementations. Is it just me or is there a very small lag in the jQuery version? I would be interested to test against Resig’s jQuery version, too.
July 8th, 2008
If it is any quicker, it’s because I’m more familiar with Prototype. Only way to tell would be to profile them probably.
July 8th, 2008
I like the idea about it, but one thing i wan’t if it would be possible if you can create such a something like on the google suggest which shows a dropdown thing. That would be impressive to look at.
July 10th, 2008
How do you hide the list before a key is pressed down? This would be good to know since that would lead to something that looks more like a search engine, instead of a list that gets sorted. Great script!
July 13th, 2008