Ask A Question

Notifications

You’re not receiving notifications from this thread.

Searchkick Aggregation for has_many_through association

Lubomir Herko asked in Gems / Libraries

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?

Reply

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 %>
Reply
Join the discussion
Create an account Log in

Want to stay up-to-date with Ruby on Rails?

Join 83,038+ developers who get early access to new tutorials, screencasts, articles, and more.

    We care about the protection of your data. Read our Privacy Policy.