Using Elasticsearch with Ruby and Rails

Mar 30th, 2014

At work I’ve been working on replacing our current product search (currently powered by a SaaS) with Elasticsearch. When I first started I set out to find what Ruby gem I should use on this project.

I found a couple of very impressive gems, such as Search Kick, but unfortunately Search Kick currently doesn’t support Mongoid, so I kept looking.

After burning through a couple of other gems, all I’m left with now is the Elasticsearch Ruby Gem

Here are some tricks I used that I think made using the Elasticsearch gem easy.

Most methods are called against an Elasticsearch object. So in my model, I decided to store that object on initialization.

1
2
3
4
5
6
class Search
  attr_accessor :connection
  def initialize
    @connection = Elasticsearch::Client.new hosts: ["myhost1","myhost2" ], randomize_hosts: true
  end
end

The randomized_hosts: true allows you the load balance your Elasticsearch hosts

With the above code I can make queries in my model like so

1
2
3
4
5
6
7
8
9
10
11
12
13
def look_for(query)
self.connection.search index: "index_name", body: {
    query: {
      terms: {
        FIELD: [
          "VALUE1",
          "VALUE2"
         ]
      }
    },
    size: 25
  }
end

I found building queries in my ruby code to be quite annoying. Mainly because it took too long for me to get error messages from my Elasticsearch server and tests.

Then I found Sense. Sense is a chrome extension that allows you to build JSON based queries, that can be converted into the Ruby query DSL rather easily.

Here is the JSON query

1
2
3
4
5
6
7
8
"query": {
    "terms": {
       "FIELD": [
          "VALUE1",
          "VALUE2"
       ]
    }
}

You can see how it translates to the ruby DSL above. The feedback is quick, and it’s a great way to test out your queries without doing a ton of modifications to your models.

One of the last roadblocks I ran into was dealing with Facets. I wanted to facet the products based on what Category they were in. Thing is, categories aren’t always one word. By default Elasticsearch breaks up all of the words you want to facet. So a category like Category Name would come back with results something like this

1
2
3
4
5
6
7
8
9
10
"terms": [
            {
               "term": "Category",
               "count": 1
            },
            {
               "term": "Name",
               "count": 1
            },
]

What I wanted is this

1
2
3
4
5
6
"terms": [
            {
               "term": "Category Name",
               "count": 1
            }
]

To get there I found out I have to make sure anything I want to facet on (that will have more than 1 word) need to have their indexing set to not_analyzed. Now if you try to index something to an Elasticsearch index that doesn’t exist, it will create it for you. But to get a column to be set as not_analyzed you need to create it yourself.

You can do it pretty easily like this

1
2
3
4
5
6
7
8
9
10
11
12
  def create_index
    self.connection.indices.create index: MYINDEXNAME,
      body: {
        mappings: {
          TYPE: {
            properties: {
              field: { type: 'string', index: 'not_analyzed'}
            }
          }
        }
      }
  end

My current roadblock is I can’t put any sort of Ruby logic into the middle of the Elasticsearch query DSL, and I can’t query for blank as it returns no results.

We’ll see if something like that is possible, if not I may end up moving to another gem all together.

Comments