Highlighting Search Results with jQuery
19 Mar 2015Example on Page: Skip to the code
Make your search user-friendly
Searching is key a function of almost every site you use, and every site you will ever build. For some sites, the search functionality can be the difference between keeping a user or losing them to a competitor. Google’s only function is search, and they’re worth over $300 Billion. It’s important to make your search as user-friendly as possible.
A great example of extremely user-friendly search is Product Hunt. They use Algolia, which provides simple fuzzy matching, real-time results, and highlights the matching words on the page. Their pricing is steep for low-budget projects, but it’s simple to implement your own matching word highlighting!
Implementation
First, we need to grab what the user searched and break it up into individual words. This helps with basic fuzzy matching. Otherwise, it would have to find an exact match of what the user typed in the search box. This is easy in JavaScript:
// Maps "Test Search" to ["Test", "Search"]
var words = $('#search-field').val().trim().split(/\s+/);
Then, we need to find the search terms on the page. The jQuery :contains
selector is great for this, so we need to map
our words
array into a jQuery selector.
// Maps ["Test", "Search"] to ":contains(Test), :contains(Search)"
var query_string = words.map(function(word) {
return ":contains(" + word + ")";
}).join(', ');
Now we have everything setup to grab the DOM elements that match our query. I ran a jsPerf benchmark to see the most effecient way of running this query and this was the best by far. This assumes you have your search results as a child of a li
element, but this can easily be rewritten with td
, or whatever you have it in.
var all_items = $('li');
var matching_items = all_items.children(query_string);
Almost there! We have all of our matching search results, we just need to do the highlighting! Unfortunately, this is the hardest part. We can use a regex replace
to wrap the search terms with a span that will highlight them on the page.
The regex will be generated from words
, the array we had above that held the words the user searched for. We will use the g
(global), and i
(case-insensitve) flags so that the regex will replace all matches in the string, regardless of case.
// Maps ["Test", "Search"] to /Test|Search/gi
var regex = RegExp(words.join('|'), 'gi');
Then, we use this regex to find our search terms, and wrap them with a span with the class highlight
, which we will define to give them a background color. The $&
will be replaced with the matched text.
// Maps "Test" in DOM to "<span class='highlight'>Test</span>"
matching_items.each(function(_, item) {
var j_item = $(item);
j_item.html(j_item.html().replace(
regex, "<span class='highlight'>$&</span>"
));
});
For the highlight
class:
.highlight {
background-color: #2ecc71;
}
The final code:
// Maps "Test Search" to ["Test", "Search"]
var words = $('#search-field').val().trim().split(/\s+/);
// Maps ["Test", "Search"] to ":contains(Test), :contains(Search)"
var query_string = words.map(function(word) {
return ":contains(" + word + ")";
}).join(', ');
var all_items = $('li');
var matching_items = all_items.children(query_string);
var regex = RegExp(words.join('|'), 'gi');
// Maps "Test" in DOM to "<span class='highlight'>Test</span>"
matching_items.each(function(_, item) {
var j_item = $(item);
j_item.html(j_item.html().replace(
regex, "<span class='highlight'>$&</span>"
));
});
Gotchas
You might notice if you try to use this, it is case-sensitive. This is because :contains
is case-sensitive. To get around this, we have to write our own case-insensitive :containsi
. I borrowed this code from this StackOverflow post.
$.extend($.expr[':'], {
'containsi': function(elem, i, match, array)
{
return (elem.textContent || elem.innerText || '').toLowerCase()
.indexOf((match[3] || "").toLowerCase()) >= 0;
}
});
Then just replace the :contains
above with :containsi
.