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
Install Sunspot
Add Sunspot to your Gemfile:
1 2 |
|
Bundle:
1
|
|
Generate a default configuration file at config/sunspot.yml
:
1
|
|
Run the Solr server
If sunspot_solr
was installed, start the packaged Solr distribution with:
1
|
|
For your test environment, start Solr like this:
1
|
|
Make your models searchable
Let’s say your application has a User
model that has_one :profile
.
In your User
class:
1 2 3 4 5 |
|
All the above symbols are simply methods on a User
. The name
and age
methods actually live in Profile
, but they’re defined on User
by doing:
1
|
|
By using 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.
Index associations with touch and after_touch
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:
1 2 |
|
To this:
1 2 |
|
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 User
:
1 2 |
|
You could also pass in a block if you wanted to do other things in your after_touch
callback.
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:
1 2 |
|
Unit tests with sunspot_matchers
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.
Creating a wrapper for searching for users
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 lat
and 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!
Creating a JSON API for searching users using active_model_serializers
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.
Add 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 User
indexes.
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 |
|
The serializer: UsersSerializer
part needs some explaining. As you may recall
earlier in the tutorial when we defined UserSearch.search
, it returns
SunspotSearchDecorator.new(s)
. s
is actually an instance of
Sunspot::Search
. 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
draper.
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 ActiveRecord
or
include Mongoid::Document
, then your object must include
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 |
|
The users
JSON attribute will be an array of users from the search results
that are serialized into JSON using UserSerializer
.
1 2 3 4 |
|
Conclusion
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.