Searchkick Aggregation for has_many_through association
Hey guys,
I have an ecommerce product catalogue, where I store products, product options and option values in this kind of fashion:
# models/product.rb
has_many :option_values
has_many :option_types, through: :option_values
# models/option_value.rb
belongs_to :option_type
belongs_to :product
#models/option_type.rb
has_many :option_values
has_many :products
I came up with this structure so I can have for example a lamp Product
that can be connected to OptionType
titled "Power supply included" via value OptionValue
set to true
. Data in database for this product are saved as follows:
#products
id: 22
title: "A lamp"
#option_values
id: 1
option_type_id: 5
product_id: 22
value: true
#option_types
id: 5
title: "Power supply included"
Every ProductCategory
has its own specific set of option values, that's why I came up with this structure, it is inspired by Spotify architecture and I will need this in search/aggregates/filters, what leads me to my question:
How can i create aggregate search so I can select all products, that have "Power supply included" set to true? Do I need nested aggregates, or do I setup search_data
in some fashion so I can access them via aggs
?
Hi, I am solving very similar problem. And I think I crack it =)
For your product model you need add to search_data this
# product.rb
def search_data
{
option_value_id: option_values.map(&:id),
option_type_option_value_id: option_values.map{ |option_value| option_value.option_type.id.to_s + ";" + option_value.id.to_s }
}
end
option_type_option_value_id its a little hacky way that will help us to aggregate.
We take option_type id (like 5) and combine it with option_value id (like 10) and delimiter ; so we get something like this '5;10'
Elasticsearch will aggregate that string.
In your controller where you want to display catalog add to method
option_value_ids = array_of_option_value_ids_from_params
search_params = {}
search_params = search_params.merge(option_value_id: {all: option_value_ids}) if option_value_ids.present?
# Here we aggregate option_type_option_value_id and search with sended option values, I have pagination too
@products = Product.search "*", aggs: { option_type_option_value_id: {} }, where: search_params, page: params[:page], per_page: 25
# Here we are creating an object with option_type_id, option_value_id and count, we use this in our view
@aggs_products = @products.aggs["option_type_option_value_id"]["buckets"].map { |bucket| { option_type_id: bucket["key"].split(';')[0].to_i, option_value_id: bucket["key"].split(';')[1].to_i, count: bucket["doc_count"] } }
Now last piece the view.
Here we iterate all option types and control if we have it in our @aggs_products hash
If is there we then iterate over option type option values to test if the value is in our @aggs_products hash
If value is there we print title and count for this option (you could make it as link_to a send it as parameter to our method from above)
<% OptionType.all.each do |option_type| %>
<% if @aggs_products.any? { |h| h[:option_type_id] == option_type.id } %>
<div class="option-type">
<h3 class="option-type__title"><%= option_type.title %></h3>
<ul>
<% option_type.option_values.each do |option_value| %>
<% if @aggs_products.any? { |h| h[:option_value_id] == option_value.id } %>
<li>
<%= option_value.title %> <span class="option-type__option-count">(<%= @aggs_products.detect {|c| c[:option_value_id] == option_value.id }[:count] %>)</span>
</li>
<% end %>
<% end %>
</ul>
</div>
<% end %>
<% end %>