2019-12-22 22:52:31 +05:30
|
|
|
# ActiveRecord associations and Elasticsearch
|
|
|
|
# ===========================================
|
|
|
|
#
|
|
|
|
# https://github.com/rails/rails/tree/master/activerecord
|
|
|
|
# http://guides.rubyonrails.org/association_basics.html
|
|
|
|
#
|
|
|
|
# Run me with:
|
|
|
|
#
|
|
|
|
# ruby -I lib examples/activerecord_associations.rb
|
|
|
|
#
|
|
|
|
|
|
|
|
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
|
|
|
|
|
|
|
require 'pry'
|
|
|
|
|
|
|
|
require 'logger'
|
|
|
|
require 'ansi/core'
|
|
|
|
require 'active_record'
|
|
|
|
|
2020-03-09 13:42:32 +05:30
|
|
|
require 'json'
|
2019-12-22 22:52:31 +05:30
|
|
|
require 'elasticsearch/model'
|
|
|
|
|
|
|
|
ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT)
|
|
|
|
ActiveRecord::Base.establish_connection( adapter: 'sqlite3', database: ":memory:" )
|
|
|
|
|
|
|
|
# ----- Schema definition -------------------------------------------------------------------------
|
|
|
|
|
|
|
|
ActiveRecord::Schema.define(version: 1) do
|
|
|
|
create_table :categories do |t|
|
|
|
|
t.string :title
|
2020-03-09 13:42:32 +05:30
|
|
|
t.timestamps null: false
|
2019-12-22 22:52:31 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
create_table :authors do |t|
|
|
|
|
t.string :first_name, :last_name
|
2020-03-09 13:42:32 +05:30
|
|
|
t.string :department
|
|
|
|
t.timestamps null: false
|
2019-12-22 22:52:31 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
create_table :authorships do |t|
|
|
|
|
t.references :article
|
|
|
|
t.references :author
|
2020-03-09 13:42:32 +05:30
|
|
|
t.timestamps null: false
|
2019-12-22 22:52:31 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
create_table :articles do |t|
|
|
|
|
t.string :title
|
2020-03-09 13:42:32 +05:30
|
|
|
t.timestamps null: false
|
2019-12-22 22:52:31 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
create_table :articles_categories, id: false do |t|
|
|
|
|
t.references :article, :category
|
|
|
|
end
|
|
|
|
|
|
|
|
create_table :comments do |t|
|
|
|
|
t.string :text
|
|
|
|
t.references :article
|
2020-03-09 13:42:32 +05:30
|
|
|
t.timestamps null: false
|
2019-12-22 22:52:31 +05:30
|
|
|
end
|
2020-03-09 13:42:32 +05:30
|
|
|
|
|
|
|
add_index(:comments, :article_id) unless index_exists?(:comments, :article_id)
|
2019-12-22 22:52:31 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
# ----- Elasticsearch client setup ----------------------------------------------------------------
|
|
|
|
|
|
|
|
Elasticsearch::Model.client = Elasticsearch::Client.new log: true
|
2020-03-09 13:42:32 +05:30
|
|
|
Elasticsearch::Model.client.transport.logger.formatter = proc { |s, d, p, m| "\e[2m#{m}\n\e[0m" }
|
2019-12-22 22:52:31 +05:30
|
|
|
|
|
|
|
# ----- Search integration ------------------------------------------------------------------------
|
|
|
|
|
|
|
|
module Searchable
|
|
|
|
extend ActiveSupport::Concern
|
|
|
|
|
|
|
|
included do
|
|
|
|
include Elasticsearch::Model
|
|
|
|
include Elasticsearch::Model::Callbacks
|
|
|
|
|
|
|
|
include Indexing
|
|
|
|
after_touch() { __elasticsearch__.index_document }
|
|
|
|
end
|
|
|
|
|
|
|
|
module Indexing
|
|
|
|
|
|
|
|
# Customize the JSON serialization for Elasticsearch
|
|
|
|
def as_indexed_json(options={})
|
|
|
|
self.as_json(
|
|
|
|
include: { categories: { only: :title},
|
2020-03-09 13:42:32 +05:30
|
|
|
authors: { methods: [:full_name, :department], only: [:full_name, :department] },
|
2019-12-22 22:52:31 +05:30
|
|
|
comments: { only: :text }
|
|
|
|
})
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# ----- Model definitions -------------------------------------------------------------------------
|
|
|
|
|
|
|
|
class Category < ActiveRecord::Base
|
|
|
|
include Elasticsearch::Model
|
|
|
|
include Elasticsearch::Model::Callbacks
|
|
|
|
|
|
|
|
has_and_belongs_to_many :articles
|
|
|
|
end
|
|
|
|
|
|
|
|
class Author < ActiveRecord::Base
|
|
|
|
has_many :authorships
|
|
|
|
|
|
|
|
after_update { self.authorships.each(&:touch) }
|
|
|
|
|
|
|
|
def full_name
|
|
|
|
[first_name, last_name].compact.join(' ')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class Authorship < ActiveRecord::Base
|
|
|
|
belongs_to :author
|
|
|
|
belongs_to :article, touch: true
|
|
|
|
end
|
|
|
|
|
|
|
|
class Article < ActiveRecord::Base
|
|
|
|
include Searchable
|
|
|
|
|
|
|
|
has_and_belongs_to_many :categories, after_add: [ lambda { |a,c| a.__elasticsearch__.index_document } ],
|
|
|
|
after_remove: [ lambda { |a,c| a.__elasticsearch__.index_document } ]
|
|
|
|
has_many :authorships
|
|
|
|
has_many :authors, through: :authorships
|
|
|
|
has_many :comments
|
|
|
|
end
|
|
|
|
|
|
|
|
class Comment < ActiveRecord::Base
|
|
|
|
include Elasticsearch::Model
|
|
|
|
include Elasticsearch::Model::Callbacks
|
|
|
|
|
|
|
|
belongs_to :article, touch: true
|
|
|
|
end
|
|
|
|
|
|
|
|
# ----- Insert data -------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
# Create category
|
|
|
|
#
|
|
|
|
category = Category.create title: 'One'
|
|
|
|
|
|
|
|
# Create author
|
|
|
|
#
|
2020-03-09 13:42:32 +05:30
|
|
|
author = Author.create first_name: 'John', last_name: 'Smith', department: 'Business'
|
2019-12-22 22:52:31 +05:30
|
|
|
|
|
|
|
# Create article
|
|
|
|
|
|
|
|
article = Article.create title: 'First Article'
|
|
|
|
|
|
|
|
# Assign category
|
|
|
|
#
|
|
|
|
article.categories << category
|
|
|
|
|
|
|
|
# Assign author
|
|
|
|
#
|
|
|
|
article.authors << author
|
|
|
|
|
|
|
|
# Add comment
|
|
|
|
#
|
|
|
|
article.comments.create text: 'First comment for article One'
|
|
|
|
article.comments.create text: 'Second comment for article One'
|
|
|
|
|
|
|
|
Elasticsearch::Model.client.indices.refresh index: Elasticsearch::Model::Registry.all.map(&:index_name)
|
|
|
|
|
2020-03-09 13:42:32 +05:30
|
|
|
# Search for a term and return records
|
|
|
|
#
|
|
|
|
puts "",
|
|
|
|
"Articles containing 'one':".ansi(:bold),
|
|
|
|
Article.search('one').records.to_a.map(&:inspect),
|
|
|
|
""
|
2019-12-22 22:52:31 +05:30
|
|
|
|
2020-03-09 13:42:32 +05:30
|
|
|
puts "",
|
|
|
|
"All Models containing 'one':".ansi(:bold),
|
|
|
|
Elasticsearch::Model.search('one').records.to_a.map(&:inspect),
|
|
|
|
""
|
2019-12-22 22:52:31 +05:30
|
|
|
|
2020-03-09 13:42:32 +05:30
|
|
|
# Difference between `records` and `results`
|
2019-12-22 22:52:31 +05:30
|
|
|
#
|
2020-03-09 13:42:32 +05:30
|
|
|
response = Article.search query: { match: { title: 'first' } }
|
2019-12-22 22:52:31 +05:30
|
|
|
|
2020-03-09 13:42:32 +05:30
|
|
|
puts "",
|
|
|
|
"Search results are wrapped in the <#{response.class}> class",
|
|
|
|
""
|
|
|
|
|
|
|
|
puts "",
|
|
|
|
"Access the <ActiveRecord> instances with the `#records` method:".ansi(:bold),
|
|
|
|
response.records.map { |r| "* #{r.title} | Authors: #{r.authors.map(&:full_name) } | Comment count: #{r.comments.size}" }.join("\n"),
|
|
|
|
""
|
|
|
|
|
|
|
|
puts "",
|
|
|
|
"Access the Elasticsearch documents with the `#results` method (without touching the database):".ansi(:bold),
|
|
|
|
response.results.map { |r| "* #{r.title} | Authors: #{r.authors.map(&:full_name) } | Comment count: #{r.comments.size}" }.join("\n"),
|
|
|
|
""
|
2019-12-22 22:52:31 +05:30
|
|
|
|
2020-03-09 13:42:32 +05:30
|
|
|
puts "",
|
|
|
|
"The whole indexed document (according to `Article#as_indexed_json`):".ansi(:bold),
|
|
|
|
JSON.pretty_generate(response.results.first._source.to_hash),
|
|
|
|
""
|
|
|
|
|
|
|
|
# Retrieve only selected fields from Elasticsearch
|
|
|
|
#
|
|
|
|
response = Article.search query: { match: { title: 'first' } }, _source: ['title', 'authors.full_name']
|
|
|
|
|
|
|
|
puts "",
|
|
|
|
"Retrieve only selected fields from Elasticsearch:".ansi(:bold),
|
|
|
|
JSON.pretty_generate(response.results.first._source.to_hash),
|
|
|
|
""
|
|
|
|
|
|
|
|
# ----- Pry ---------------------------------------------------------------------------------------
|
2019-12-22 22:52:31 +05:30
|
|
|
|
|
|
|
Pry.start(binding, prompt: lambda { |obj, nest_level, _| '> ' },
|
2020-03-09 13:42:32 +05:30
|
|
|
input: StringIO.new('response.records.first'),
|
2019-12-22 22:52:31 +05:30
|
|
|
quiet: true)
|