I’m going to show you how to set up search using Sunspot in your Rails application. Here’s what we’ll cover:
- Install Sunspot
- Run the Solr server
- Make your models searchable
- Index associations with touch and after_touch
- Unit tests with sunspot_matchers
- Creating a wrapper for searching for users
- Setting up a JSON search API with pagination using ActiveModel::Serializers
Add Sunspot to your Gemfile:
Generate a default configuration file at
sunspot_solr was installed, start the packaged Solr distribution with:
For your test environment, start Solr like this:
Let’s say your application has a
User model that
1 2 3 4 5
All the above symbols are simply methods on a
age methods actually live in
Profile, but they’re defined on
User by doing:
delegate, you avoid breaking Law of Demeter and doing something silly like this:
1 2 3 4 5 6 7 8 9
See the Sunspot docs for more ways to index your data.
You’re probably wondering how to index your associations. At first, you might think to open
Profile and put a
searchable block in there, but that’s not what you want to do.
Instead, you want to denormalize the data from User’s associated models into the User index. That means when a user edits their profile, we need a way to tell the user object to index itself.
That can be accomplished using touch on your
belongs_to associations. In our example, in the
Project class, change this:
Now, when a profile gets saved, its associated user will get its timestamps updated, and its after_touch callback will be executed, so let’s define that in
You could also pass in a block if you wanted to do other things in your
1 2 3 4 5
With these changes, when a profile gets updated, its user will re-index itself.
You should modify your
profile_spec.rb to ensure that the
touch: true never gets removed. If you’re using shoulda_matchers, it’s as easy as:
sunspot_matchers gives you an easy way to unit test your search functionality.
After adding the gem to your
Gemfile and bundling, in your
spec_helper.rb, you’ll need:
1 2 3 4 5 6 7
Now let’s test that
User is indexing the fields that we expect it to.
1 2 3 4 5 6
We’ll add some more unit tests in the next section.
At this point, you could just search for users by throwing something like this
in your controller:
User.search name: params[:name], but that’s quickly going
to get out of hand once you want to support more fields, pagination, and
add other logic like search by location if
lon are provided. That
would be too much responsibility for the controller and it would make it very
difficult to test. A better idea is to assign the controller the responsibility of calling
some class and simply returning its results as JSON, and then that class has
the big responsibility of searching – we’re trying to follow the single
responsibility principle here.
Let’s make a class called
UserSearch. Its responsibility will be to search
users so the controller can stay nice and slim.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
Now that we have a
UserSearch class that is fully tested, our controller
specs will just have to test that they’re calling
UserSearch with the correct
parameters. Easy, and fast!
The first thing we’ll do is define our routes.
1 2 3 4
For the JSON views, we’re using active_model_serializers (AMS) instead of something
like jbuilder or rabl.
Using AMS is dead simple and makes testing your JSON views trivial.
gem 'active_model_serializers' to your
Gemfile, create this initializer file, and restart your server.
1 2 3 4 5 6 7 8 9
Next, in your
UsersController, let’s define an
index action that simply passes parameters to
UserSearch and returns a paginated list of
User objects in JSON. If you’re using Rails 4 and/or using the
strong_parameters gem, your controller will need a
private method to permit all params. That’s okay in this instance because
UserSearch handles sanitization for us; it only accepts parameters that
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
serializer: UsersSerializer part needs some explaining. As you may recall
earlier in the tutorial when we defined
UserSearch.search, it returns
s is actually an instance of
UserSearch.search could have returned
s.results which would
simply return an array of user objects, but using a decorator means we can
add some additional methods and it allows us to better
customize our JSON output. This assumes you’re using
1 2 3 4 5 6 7 8
According to the active_model_serializers
docs, if the object you’re serializing doesn’t descend from
Mongoid::Document, then your object must
ActiveModel::SerializerSupport, which we’ve done above.
We also do some delegation to get some useful methods that our
UsersSerializer will use.
1 2 3 4 5 6 7 8 9
users JSON attribute will be an array of users from the search results
that are serialized into JSON using
1 2 3 4
And that’s it! You could use something like Postman
to fire off GET requests to
/api/users?name=John&age=20 and you should see
some nicely formatted results. Hope that helps! Feel free to ask questions in
the comments below.