RESTful search with Backbone

The auto-complete box is the todo list of search in Backbone. There are hundreds of examples out there which cover searching through a collection. Almost all of these, however, are just searching the already fetched collection on the client side.

While working on a new project at Uber, I needed to implement a search against a RESTful API where the collection could contain tens of thousands of resources. Clearly, fetching the entire collection and searching is going to be a large waste of a time and memory.

RESTful searching

First of all, we need to decide how the REST api should support searching. The guys at apigee put together an extremely helpful video detailing the things to consider when designing an API. After watching the video, I decided that the Google style search query was the best way to go. A search query looks like this:

http://api.service.com/resource/search?q=parameter[,parameter2[,...]]

Now that we know the format of the search URL, we can design a simple Backbone Collection which can build this URL and fetch the results as models.

Backbone Searchable

I've seen a few implentations of this pattern, some of them using Models to encapsulate the logic (which makes no sense to me) and some defining a collection just used for searching. The latter is OK, but I wanted a way to make any collection searchble following our REST convention, without having to define a new type for each collection.

The key here is to use one of Backbone's infrequently used[CITATION NEEDED] and under-documented features; passing a second object parameter to extend:

extendBackbone.Model.extend(properties, [classProperties])

To create a Model class of your own, you extend Backbone.Model and provide instance properties, as well as optional classProperties to be attached directly to the constructor function.

I'd always been attaching functions to the prototype before, but this lets us use class methods in Backbone which are compatible with its inheritance chain. Neat.

So, we can take advantage of this by defining a new SearchableCollection which extends Backbone.Collection and adds a new search class property:

var SearchableCollection = Backbone.Collection.extend({},{  
  search: function(query){
    console.log(query);
  }
});

Note that the first paramter to extend is just an empty object, we're not adding anything to the SearchableCollection itself, we're just adding a class method.

Now, we go to our collection definition and we extend our SearchableCollection and check that things are working as expected:

var Rums = SearchableCollection.extend({  
  url: '/api/rums/'
});

Rums.search('havana club');  
> 'havana club'

Good stuff. Notice that we're not making an instance of Rums here, we're just calling search on the 'class'. The next thing to do is to adapt our URL to our search specific endpoint:

var SearchableCollection = Backbone.Collection.extend({},{  
  search: function(query, options){
    options = options || {};
    var collection = new this([], options);
    collection.url = _.result(collection, 'url') + 'search';
    return collection;
  }
});

Here, we use new this to make a new instance of the collection that the method was invoked with. If you don't quite understand why that works, consider reading this book which should bring you up to speed on some of the more interesting JavaScript techniques and neuances.

Now we have a factory method for creating a new instance of our collection with the correct REST search URL. At this point, we could be done by simply wiring up our code to listen to the sync event for the collection which is triggered once the fetch finishes. I wanted to go a little bit further, because I feel like it is not obvious that calling search should give you a collection object back. To me, search is an operation, just like fetch, so they should share a common interface.

To achieve this, we can wrap the search implementation in another deferred object and resolve it with the collection once the fetch is completed (wiring up some custom search events along the way):

var SearchableCollection = Backbone.Collection.extend({},{

  search: function(query, options){
    var search = $.Deferred();
    options = options || {};
    var collection = new this([], options);
    collection.url = _.result(collection, 'url') + 'search';
    var fetch = collection.fetch({
      data: {
        q: query
      }
    });
    fetch.done(_.bind(function(){
      Backbone.Events.trigger('search:done');
      search.resolveWith(this, [collection]);
    }, this));
    fetch.fail(function(){
      Backbone.Events.trigger('search:fail');
      search.reject();
    });
    return search.promise();
  }

});

We can now use this in the following way with our previously defined Rums collection:

var findRums = Rums.search('angostura');  
findRums.done(function(rums){  
  var rumsView = new RumsView({
    collection: rums
  });
  rumsView.render();
});

And there you have it. Easy searching for any Backbone collection.