Searching with Marionette.js + Rails + PostgreSQL

In this post, I’m going to cover a basic implementation of search functionality using Backbone.js, Marionette.js, and PostgreSQL in a Rails app. Essentially, we’re going to be building an application that lists bookmarks and allows us to easily search them. See a video of what we’re building here.

Installing PostgreSQL

If you don’t already have PostgreSQL installed, the easiest way to install it is with Homebrew:

$ brew install postgres

I’m not going to go over the major details of getting it installed and set up, there are plenty of resources for that (the Railscast on migrating to PostgreSQL is a good resource). A few notes, though: First, you’ll need to run the following command to create your initial database:

$ initdb /usr/local/var/postgres

Once you’ve got it set up, you can start it with a command like this:

$ pg_ctl -D /usr/local/var/postgres start

You’ll also want to make sure that in your environment PATH, that /usr/local/bin comes before /usr/bin. If necessary, you can do this in your shell setup somewhere:

export PATH=/usr/local/bin:$PATH

Creating a Rails app

We’re going to create a Rails app and use RSpec for testing (-T says not to create the test directory). I typically use RVM, so I never let Rails call bundle when I create a new app (--skip-bundle). We’re also going to tell Rails that we’re using PostgreSQL as our database (-d postgresql):

$ rails new marks -T --skip-bundle -d postgresql

Don’t forget to remove public/index.html, and to set up your RVM gemset, etc. for this app if you’re using RVM. Also, it’s a good idea to set this up as a Git repository:

$ git init
$ git add .
$ git commit -m 'Initial commit'

You can view the progress at this point on Github here.

Setting up PostgreSQL for our app

Using PostgreSQL with Rails relies on the pg gem. This was automatically added to our Gemfile when we added the -d postgresql flag to our rails new command. One thing we’ll need to do is edit the config/database.yml file a little bit. By default, Rails assumes that the username for our PostgreSQL database will be the name of our app. For our purposes—in testing and development—we need it to be changed to our username:

development:
  adapter: postgresql
  encoding: unicode
  database: marks_development
  pool: 5
  username: YOUR_USERNAME
  password:

test:
  adapter: postgresql
  encoding: unicode
  database: marks_test
  pool: 5
  username: YOUR_USERNAME
  password:

You can safely remove the production section of this file, as we’re not going to be using it. Also note that if you’re deploying with Heroku, the “production” section is unnecessary there, as well.

Now, we create our PostgreSQL databases for our app:

$ rake db:create:all

Now we’re at this commit.

Creating the model

For the purpose of this tutorial, I’m going to assume that you know how to create a model called Mark with title, description, and url attributes.

For reference, here is the git diff for when I created the Mark model. In reality, you’ll obviously want much better test coverage, but that’s not the point of this tutorial. Note that from this point forward, I won’t be covering tests in the tutorial, but basic test coverage will be in the git commits that I’ll link to along the way.

Building the controller

We’re going to put the controller behind an Api namespace, so we’ll want to update our routes.rb file with something like the following:

namespace :api do
  resources :marks, only: [:index]
end

And for now we’ll implement a very basic controller:

class Api::MarksController < ApplicationController
  respond_to :json

  def index
    @marks = Mark.all
    respond_with @marks
  end
end

We’re now up to this commit.

Implementing search (on the server)

We’re going to use a gem called pg_search in our app, so you’ll want to add that to your Gemfile. Then, we’ll need to include the PgSearch module in our Mark model:

class Mark < ActiveRecord::Base
  include PgSearch
  # ...
end

Then, we’re going to tell it what fields we want to search against, and with what dictionary:

class Mark < ActiveRecord::Base
  include PgSearch

  pg_search_scope :pg_search, against: [:url, :title, :description],
    using: { tsearch: { dictionary: 'english' } }

  # ...
end

Finally, we need to implement a search method on the Mark class that actually leverages the pg_search gem:

class Mark < ActiveRecord::Base
  # ...

  def self.search(query)
    if query.present?
      pg_search(query)
    else
      scoped.order('created_at DESC')
    end
  end

  # ...
end

As this tutorial is ultimately about getting this hooked into Marionette, I won’t go into great detail about this, so again: the PostgreSQL Railscast is a great resource.

Now that we’ve got search set up on our model, it’s trivial to get the controller ready for search:

class Api::MarksController < ApplicationController
  respond_to :json

  def index
    @marks = Mark.search(params[:query])
    respond_with @marks
  end
end

If there’s a query present, the model will use pg_search to perform the search. Otherwise, it’ll just list the marks in reverse chronological order.

Browse the current state of the app on Github here.

Listing our marks in the browser

Building the basic view

For Rails + Backbone.js apps, I typically route the entire client-side application through a single controller action, generally called MainController#main. We’ll update our routes.rb file accordingly:

root to: 'main#main'

And create the MainController:

class MainController < ApplicationController
  layout false

  def main
    render '/main'
  end
end

Pretty straightforward. It renders a single view template called ‘main’. This view is essentially just responsible for getting us a starter-DOM and giving Backbone.js/Marionette.js a container to render into:

<!doctype html>

<html lang='en'>
<head>
  <title>Marks</title>
  <%= stylesheet_link_tag    "application", :media => "all" %>
  <%= javascript_include_tag "application" %>
  <%= csrf_meta_tags %>
</head>

<body>
</body>
</html>

At this point, it’s also a good idea to add a copy each of Underscore, Backbone, and Marionette to the vendor/assets/javascripts directory, and to update application.js accordingly:

// ...
//= require underscore
//= require backbone
//= require marionette
// ...

Setting up the Marionette app and listing marks

Typically, I like to define the Marionette app in a file called app/assets/javascripts/backbone/app.js:

window.App = new Backbone.Marionette.Application();
_.extend(App, {
  Controllers: {},
  Routers: {},
  Layouts: {}
});

This gives us a Marionette.Application to work with, and also provides some organizational structure.

Then, we actually take care of starting the application in a file called app/assets/javascripts/backbone/init.js, where we set up some initial controllers, routers, and a layout:

$(function () {
  App.addInitializer(function () {
    this.controllers = {
      marksController: new App.Controllers.MarksController()
    };

    this.routers = {
      marksRouter: new App.Routers.MarksRouter({ controller: App.controllers.marksController })
    };

    App.layout = new App.Layouts.Layout();
    $('body').prepend(App.layout.render());
    Backbone.history.start({ pushState: true });
  });

  App.start();
});

For now, we’re just going to implement a single route in our MarksRouter, so it should be in app/assets/javascripts/backbone/routers/marks_router.js and look like the following:

(function () {
  App.Routers.MarksRouter = Backbone.Marionette.AppRouter.extend({
    appRoutes: {
      '': 'list'
    }
  });
})();

Then, we’ll need a controller in app/assets/javascripts/backbone/controllers/marks_controller.js to respond to the router:

(function () {
  App.Controllers.MarksController = function () {
    this.list = function () {
    };
  };
})();

Next, we’ll create the basic layout we defined before in app/assets/javascripts/backbone/layouts/layout.js:

(function () {
  App.Layouts.Layout = Backbone.Marionette.Layout.extend({
    template: JST['layouts/layout'],

    render: function () {
      this.$el.html(this.template());
      return this.$el;
    },

    regions: function () {
      content: '#content'
    }
  });
})();

Finally, we’ll build a basic template for the app layout in app/assets/templates/layouts/layout.jst.ejs (remember to add the ejs gem to your Gemfile and to bundle):

<h1>Marks</h1>

<div id='content'></div>

And make sure that everything is in order in your application.js file, as well:

//= require jquery
//= require jquery_ujs
//= require underscore
//= require backbone
//= require marionette
//= require_tree ../templates
//= require backbone/app
//= require backbone/init
//= require_tree ./backbone
//= require_tree .

So now when you start up your server and visit localhost:3000, you should just see the header “Marks” that’s rendered by App.layout. Note that our current commit is now here, and we’re ready to move on to listing our marks.

Listing marks

In order to list marks, we’ll need a Backbone.Collection to fetch them. We’ll build that first, and for now it’ll be very simple:

(function () {
  App.Collections.Marks = Backbone.Collection.extend({
    url: '/api/marks'
  });
})();

We tell Backbone to fetch marks from the /api/marks url. Also note that you’ll want to update app.js so that App has a Collections object in it:

window.App = new Backbone.Marionette.Application();
_.extend(App, {
  Collections: {},
  Controllers: {},
  Routers: {},
  Layouts: {}
});

As we set up before, the App.controllers.marksController object is going to be responsible for fetching the marks and rendering them inside a view using a function called list. We can now build that out a little bit to look like the following:

// ...
this.list = function () {
  var collection = new App.Collections.Marks();

  collection.fetch().done(function () {
    var view = new App.Views.Marks.List({ collection: collection });
    App.layout.content.show(view);
  });
};
// ...

We tell the controller to fetch the collection. Once it’s fetched, create a view (which we’ll write momentarily) called App.Views.Marks.List and show it inside of the content region of App.layout.

Next we’ll create the App.Views.Marks.List view, but first, we’ll update app.js so that the view objects are available:

window.App = new Backbone.Marionette.Application();
_.extend(App, {
  Collections: {},
  Controllers: {},
  Routers: {},
  Layouts: {}
  Views: {
    Marks: {}
  }
});

For our list view, we’re going to use Backbone.Marionette.CompositeView. This will allow us to render a collection, but also have some extra stuff inside of the list view itself outside of the collection (i.e. our search field).

(function () {
  App.Views.Marks.List = Backbone.Marionette.CompositeView.extend({
    initialize: function () {
      this.itemView = App.Views.Marks.Show
    },

    template: JST['marks/list'],

    itemViewContainer: '#marks'
  });
})();

Note that you’ll want to update application.js so that your views are loaded before your controllers, otherwise the MarksController won’t be aware of App.Views.Marks.List. I won’t include that here, but see this git commit diff for reference.

Our App.Views.Marks.Show view will be fairly straightforward, as well:

(function () {
  App.Views.Marks.Show = Backbone.Marionette.ItemView.extend({
    template: JST['marks/show'],
    render: function () {
      this.$el.html(this.template({ model: this.model }));
    }
  });
})();

It’ll have a very basic template:

Title: <%= model.get('title') %><br>
URL: <a href="<%= model.get('url') %>"><%= model.get('url') %></a><br>
Description: <%= model.get('description') %>

By this point, we should have a working list view! Create some marks in the rails console and visit localhost:3000. You should see them listed, and our current commit at this point can be found here.

Searching for marks on the client side

So now we’ve got our marks listed and we’re ready to implement search. It’s already configured on the server side, so it’s just a matter of finding a way to pass a search parameter to the marks endpoint. First, we’ll update the App.Collections.Marks so that it can pass a query along to the endpoint:

(function () {
  App.Collections.Marks = Backbone.Collection.extend({
    url: function () {
      if (this.query) {
        return '/api/marks?query=' + encodeURIComponent(this.query);
      } else {
        return '/api/marks';
      }
    }
  });
})();

Now url is a function. If the collection has a query present, it encodes the query and passes it along to the marks endpoint. Otherwise, it just hits the marks endpoint and behaves like a normal list.

Next, we’ll update the marks/list view template with a search input:

<form id='search'>
  <input type='search' id='query'>
  <input type='submit' value='Search'>
</form>

<div id='marks'>
</div>

In order to perform the search, all we need to do is add an event and a handler to App.Views.Marks.List:

// ...
events: {
  'click #search input[type=submit]': 'search'
},

search: function (e) {
  e.preventDefault();
  this.collection.query = this.$('#query').val();
  this.collection.fetch();
}
// ...

Whenever the user clicks the submit button, the search will be performed. Marionette is awesome enough to re-render our view whenever the collection is fetched, so there’s very little work involved, compared to what would be necessary with vanilla Backbone.js.

The final little feature we’ll implement will be incremental searching. First, add the incremental attribute to our search tag:

<input type='search' id='query' incremental>

With the incremental attribute, newer browsers (however, notably not Firefox) will fire a search event on this input whenever the user pauses in typing, or when the input is cleared. Now, we just add a new event to our list view:

events: {
  'click #search input[type=submit]': 'search',
  'search #query': 'search'
}

And that’s it! Use Safari or Chrome, and use the search field. You’ll notice that results update automatically as you type. The final commit is here.

Anyway, I hope that was a helpful overview of implementing search in a Marionette-powered Rails app. For any questions, I’m always available on Twitter (@_clem), or you can find my email address on the about page.

Previous article: From Ruby to Shell.