Using Elastic Search and Rails for a Compound Query with Text Strings and User ID
I'm writing this blog post, my first in quite a while, because I recently had to implement ElasticSearch for Rails for a new application I'm building and I find life in the Elastic world a bit different than I had expected. If you're an Elastic Search veteran then you should definitely move along because:
- This is fairly basic
- I'm writing this mostly to cement this in my own brain
Curiously I found very few examples about how to do this type of compound query in Rails and that's also part of my motivation for writing it. The closest example I found was in a four year old Stack Overflow post.
The Problem: Everyone Should See Only Their Own Data
I have a series of ActiveRecord models that I want to be able to search using Elastic Search. This can easily be done with this code fragment:
Job.search_user(params[:q])
Given that I was initially the only user on this code base, I didn't even notice the issue until I thought about deployment. At which point there was the obligatory light bulb / I'm an idiot moment. The problem here is that this code searches everyone's jobs, not just the jobs that you created. Now since every bit of data encompasses a user_id attribute, this should boil down to two problems:
- Getting user_id into the index
- Constructing a JSON query for ElasticSearch to execute
Sidebar: But What About SearchKick
I'm sure a number of people are shouting out "Use SearchKick (dummy)" but whenever I tried to use SearchKick, I got odd errors and I eventually just pulled it from Gemfile. Given that I'm a long time believer in Ankane's ChartKick, I'm sure it is me but I still couldn't make use of it.
Step 1: Getting user_id into the Index
I've opted to setup each of my searchable models as follows:
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
settings do
mappings dynamic: false do
indexes :company^5, type: :text, analyzer: :english
indexes :title^5, type: :text, analyzer: :english
indexes :why_rejected, type: :text, analyzer: :english
indexes :location, type: :text, analyzer: :english
indexes :name, type: :text, analyzer: :english
indexes :domain, type: :text, analyzer: :english
indexes :created_at, type: :date
indexes :user_id, type: :text
end
end
The :title^5 bumps the ranking of search results in the title or company attributes by a factor of 5.
Step 2: Constructing the JSON query
I ended up with a class method on each of my searchable objects like this:
def self.search_user(query, user)
self.search({
query: {
bool: {
must: [
{
multi_match: {
query: query,
fields: [:company, :title, :description, :name]
}
},
{
match: {
user_id: user.id.to_s
}
}]
}
}
})
end
I can call this from my search controller like this:
@jobs = Job.search_user(params[:q], current_user)
and get results bound only to the logged in user – exactly what I was looking for.
Step 3: Re-indexing Everything
After making changes to your settings / mappings, you need to re-index everything. I handle this with a simple rake task like this:
namespace :search do
# bundle exec rake search:index_all --trace
task :index_all => :environment do
klasses = [Job, Note, CoverLetter, Task]
klasses.each do |klass|
klass.send(:import, :force => true)
end
end
end
This re-indexes every model in full. You don't always need this but it is very convenient to have for development when you're playing with schema changes and the like.
Recommended Reading
I made heavy use of Iridakos's excellent tutorial and I recommend you do too.
Posted In: #rails #ruby #elastic_search