Why and how to denormalize indexing with elasticsearch-rails

federico chavez
Apr 4 · 6 min read
# models/client.rb
class Client < ApplicationRecord
has_many :sales
end
# models/branch.rb
class Branch < ApplicationRecord
has_many :sales
belongs_to :state
end
# models/state.rb
class State < ApplicationRecord
has_many :branches
belongs_to :country
end
# models/country.rb
class Country < ApplicationRecord
has_many :states
end
# models/sale.rb
class Sale < ApplicationRecord
belongs_to :client
belongs_to :payment_type
belongs_to :campaign
belongs_to :branch
end
# models/payment_type.rb
class PaymentType < ApplicationRecord
has_many :sales
end
# models/campaign_type.rb
class CampaignType < ApplicationRecord
has_many :campaigns
end
# models/campaign.rb
class Campaign < ApplicationRecord
has_many :sales
belongs_to :campaign_type
end
# models/sale.rb
class Sale < ApplicationRecord
belongs_to :client
belongs_to :payment_type
belongs_to :campaign
belongs_to :branch
def as_indexed_json(_options = nil)
as_json(
only: [:id, :date, :amount],
include: {
state: {
only: [:id, :name, :code]
},
payment_type: {
only: [:id, :description]
},
campaign: {
only: [:budget, :description],
include: {
campaign_type: { only: [:id, :description] }
}
}
}
)
end
mapping dynamic: :strict do
indexes :id, type: ‘integer’
indexes :date, type: ‘date’
indexes :amount, type: ‘float’
indexes :state do
indexes :id, type: ‘integer’
indexes :name, type: ‘text’
indexes :code, type: ‘text’
end
indexes :payment_type do
indexes :id, type: ‘integer’
indexes :description, type: ‘text’
end
indexes :campaign do
indexes :budget, type: ‘float’
indexes :description, type: ‘text’
indexes :campaign_type do
indexes :id, type: ‘integer’
indexes :description, type: ‘text’
end
end
end
end
end
# models/state.rb
class State < ApplicationRecord
…… after_commit :update_sales

private
def update_sales
Sale.update_states(self)
end
end
# models/sale.rb
class Sale < ApplicationRecord
…… def self.update_states(state, options = {})
options[:index] ||= index_name
options[:type] ||= document_type
options[:wait_for_completion] ||= false
options[:body] = {
conflicts: :proceed,
query: {
match: {
‘state.id’: state.id
}
},
script: {
lang: :painless
source: ‘ctx._source.state.name = params.state.name’,
params: { state: { name: state.name } }
}
}
__elasticsearch__.client.update_by_query(options) end
end
query: {
match: {
‘state.id’: state.id
}
}
script: {
lang: :painless
source: ‘ctx._source.state.name = params.state.name’,
params: { state: { name: state.name } }
}
GET sales/_search
{
“query”: {
“bool”: {
“must”: [
{
“range”: {
“date”: {
“gte”: “2014–01–01”,
“lte”: “2019–01–01”
}
}
},
{ “match”: { “payment_type.id”: 1 } },
{ “match”: { “campaign.campaign_type.id”: 3 } },
{ “range”: { “campaign.budget”: { “gte”: 1000 } } }
],
“should”: [
{ “match”: { “state.id”: 30 } },
{ “match”: { “state.id”: 22 } }
]
}
}
}
{
“took”: 4,
“timed_out”: false,
“_shards”: {
“total”: …,
“successful”: 1,
“skipped”: 0,
“failed”: 0
},
“hits”: {
“total”: …,
“max_score”: …,
“hits”: [
{
“_index”: “sales”,
“_type”: “sales”,
“_id”: “2”,
“_score”: …,
“_source”: {
“id”: 2,
“date”: “2018–09–21”,
“amount”: 2000,
“state”: {
“id”: 22,
“name”: “Washington, D.C.”,
“code”: “WA”
},
“payment_type”: {
“id”: 1,
“description”: “Credit card”
},
“campaign”: {
“budget”: 3000,
“description”: “Facebook”,
“campaign_type”: {
“id”: 3,
“description”: “social media”
}
}
},
}
]
}
}

Wolox

Wolox stands for innovation, engineering and working culture that transforms problems into solutions and ideas into products. www.wolox.com.ar

Thanks to Romina Blasucci.

federico chavez

Written by

Wolox

Wolox

Wolox stands for innovation, engineering and working culture that transforms problems into solutions and ideas into products. www.wolox.com.ar