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"
]
}
}