05 April 2015

Fuzzy search with Backbone collections

While bulding an autocomplete feature for a search form, I discovered the following technique.

The goal is to perform a fuzzy search on a pre-populated backbone.js collection rather than deffering to the server. I’m going to be using Fuse.js to do the actual searching as it’s a robust and fast library that fits the bill perfectly.

We start by setting up our collection to respond to change. Then calling a function to rebuild the Fuse index whenever the collection is changed. I’ve included debounce as to not create too much overhead as it might be regenerated on keypress:

App.SchoolsCollection = Backbone.Collection.extend({
  //...
  initialize: function(){
    this.on "add remove reset", function(){ this.trigger "change" }
    this.on "change", function(){ _.debounce ( this.rebuildIndex, 1000 ) }
  }
  //...

)};

Then we implement the rebuildIndex function to set this.fuse to an new Fuse object. We pass Fuse our current collection in JSON form and a set of options (which I think I’ve left as the defaults). The keys option is the only important one below. It should be an array of key names you want to search on and it can be set to search nested elements (eg: ['address.street']).

App.SchoolsCollection = Backbone.Collection.extend({
  //...
  searchOptions: {
    caseSensitive: false,
    includeScore: false,
    shouldSort: true,
    threshold: 0.6,
    location: 0,
    distance: 100,
    maxPatternLength: 32,
    keys: ['name']
  }

  rebuildIndex: function(options = {}){
    this.fuse = new Fuse(this.toJSON(), this.searchOptions);
  }
  //.. 
})  

Last thing to do within our collection is to implement a search method that we’ll call in our view to execute the search.

App.SchoolsCollection = Backbone.Collection.extend({
  //...
  search: function(query){
    return this.fuse.search(query);
  }
  //.. 
})  

Then to call it we just need to fetch the collection and call search on it.

App.SchoolsIndex = Backbone.View.extend({
  //...
  initialize: function(){
    this.schools = new App.SchoolsCollection();
    this.schools.fetch({reset: true});
  }

  someFunction: function(){
    var schools = this.schools.search(<searchTerm>);
    // 'schools' will be an array of JSON objects 
    // that can be passed to a new collection if needed.
  }

  //.. 
})