Rails 4: searching for related models with Elasticsearch and tagged content via acts-as-taggable-on

In this code snippet I’ll show how to integrate a Rails 4 model with Elasticsearch and find related models via matching tags.

Create a new Rails project:

# create directory and RVM files
mkdir rails-elasticsearch-related
echo ruby-2.2.2 > rails-elasticsearch-related/.ruby-version
echo rails-elasticsearch-related > rails-elasticsearch-related/.ruby-gemset
cd rails-elasticsearch-related

# install Rails gem
gem install rails

# create new Rails project
rails new . --database=postgresql --skip-javascript --skip-turbolinks --skip-test-unit

# setup postgresql database
rake db:create
rake db:migrate

# create Rails model (ex: User)
rails g model User first_name:string last_name:string
rake db:migrate

Add the acts-as-taggable-on gem for tagging models. edit file: Gemfile, add: gem 'acts-as-taggable-on'

# install gem and migrate database
bundle install
rake acts_as_taggable_on_engine:install:migrations
rake db:migrate

Add (interests) tags to User model. edit file: app/models/user.rb

class User < ActiveRecord::Base
  acts_as_taggable_on :interests
end

Add Elasticsearch gems. Edit file: Gemfile, add the following, and execute: bundle install.

gem 'elasticsearch-model', git: 'git://github.com/elasticsearch/elasticsearch-rails.git'
gem 'elasticsearch-rails', git: 'git://github.com/elasticsearch/elasticsearch-rails.git'

Integrate User model with Elasticsearch. edit file: app/models/user.rb

class User < ActiveRecord::Base
  acts_as_taggable_on :interests

  #
  # Elasticsearch integration - start
  #

  include Elasticsearch::Model
  include Elasticsearch::Model::Callbacks
  include Elasticsearch::Model::Indexing

  # determine how to index model as JSON, and add tag list for interests
  def as_indexed_json(_options = {})
    as_json.merge(
      'interests' => interest_list
    )
  end

  # instance method to search elasticsearch and execute more_like_this query:
  def search_more_like_this(how_many = nil)
    how_many = 5 unless how_many.is_a?(Integer)

    search_definition = {
      query: {
        more_like_this: {
          fields: tag_types,
          docs: [
            {
              _index: self.class.index_name,
              _type: self.class.document_type,
              _id: id
            }
          ],
          min_term_freq: 1
        }
      },
      size: how_many
    }

    self.class.__elasticsearch__.search(search_definition)
  end

  #
  # Elasticsearch integration - end
  #
end

Time to populate the user model. For this tutorial I used the faker gem. Edit file: Gemfile, add: gem 'faker'. Execute: bundle install to install.

Edit file: db/seeds.rb, add:

# select random content from faker (via i18n translation)
creatures = I18n.t('faker.team.creature').sample(25)

# add 100 users
100.times do |i|
  user = User.create!({
    first_name: Faker::Name.first_name,
    last_name: Faker::Name.last_name,
    interest_list: creatures.sample(10), # select 10 random interests
  })
end

Via rails console, create the Elasticsearch index for the User model.

rails c
> User.__elasticsearch__.create_index! force: true
> exit

Populate the User model by executing: rake db:seed.

Check data structure in Elasticsearch via cURL:

curl 'http://127.0.0.1:9200/users/_search?size=1&sort=id' 2>/dev/null | python -m json.tool
{
    "_shards": {
        "failed": 0,
        "successful": 5,
        "total": 5
    },
    "hits": {
        "hits": [
            {
                "_id": "1",
                "_index": "users",
                "_score": null,
                "_source": {
                    "created_at": "2015-07-16T01:22:27.882Z",
                    "first_name": "Lavon",
                    "id": 1,
                    "interests": [
                        "vampires",
                        "sons",
                        "fishes",
                        "goats",
                        "zebras",
                        "dogs",
                        "horses",
                        "spirits",
                        "giants",
                        "sorcerors"
                    ],
                    "last_name": "Wuckert",
                    "updated_at": "2015-07-16T01:22:27.882Z"
                },
                "_type": "user",
                "sort": [
                    1
                ]
            }
        ],
        "max_score": null,
        "total": 100
    },
    "timed_out": false,
    "took": 1
}

Via Rails console, find related Users via tags:

rails c

# get first user's interests
pry(main)> User.first.interest_list
  User Load (0.6ms)  SELECT  "users".* FROM "users"  ORDER BY "users"."id" ASC LIMIT 1
  ActsAsTaggableOn::Tag Load (1.0ms)  SELECT "tags".* FROM "tags" INNER JOIN "taggings" ON "tags"."id" = "taggings"."tag_id" WHERE "taggings"."taggable_id" = $1 AND "taggings"."taggable_type" = $2 AND (taggings.context = 'interests' AND taggings.tagger_id IS NULL)  [["taggable_id", 1], ["taggable_type", "User"]]
[
    [0] "vampires",
    [1] "sons",
    [2] "fishes",
    [3] "goats",
    [4] "zebras",
    [5] "dogs",
    [6] "horses",
    [7] "spirits",
    [8] "giants",
    [9] "sorcerors"
]

# search for related users by matching tags
pry(main)> User.first.search_more_like_this.first
  User Load (0.4ms)  SELECT  "users".* FROM "users"  ORDER BY "users"."id" ASC LIMIT 1
{
     "_index" => "users",
      "_type" => "user",
        "_id" => "5",
     "_score" => 0.63467145,
    "_source" => {
                "id" => 5,
        "first_name" => "Jacynthe",
         "last_name" => "Russel",
        "created_at" => "2015-07-16T01:22:28.232Z",
        "updated_at" => "2015-07-16T01:22:28.232Z",
         "interests" => [
            [0] "zebras",
            [1] "giants",
            [2] "werewolves",
            [3] "spirits",
            [4] "horses",
            [5] "witches",
            [6] "dogs",
            [7] "fishes",
            [8] "elves",
            [9] "penguins"
        ]
    }
}

Source code on GitHub

Updated: