Ask A Question

Notifications

You’re not receiving notifications from this thread.

Search Filters with ElasticSearch Aggregations Discussion

This has been very helpful for a project I'm working on. Thank you. On mine, I wanted to add checkboxes to be able to select multiple keys. For anyone else looking to do the same, the solution I came up with is:

<%= form_with(method: :get, local: true, html: { class: 'query'}) do |form| %>
  <h6>Brand</h6>
  <% @televisions.aggs["brand"]["buckets"].sort_by{ |b| b["key"] }.each do |bucket| %>
    <%= check_box_tag "brand[]",
                      bucket["key"],
                      params["brand"].try(:include?, bucket["key"]),
                      id: "brand_#{bucket["key"].to_s.parameterize.underscore}" %>
    <%= label_tag "brand_#{bucket["key"].to_s.parameterize.underscore}", bucket["key"].to_s %>
    (<%= bucket["doc_count"] %>)
    <br>
  <% end %>

  <%= form.submit %>
<% end %>

In the check_box_tag, I added '[]' to the name so "brand[]" establishes an Array. Searchkick can use this to filter for any of the values in the array without changing anything Chris's controller code.

For the id value in the check_box_tag and for value in the label_tag, I used .to_s.parameterize.underscore. This removes any spaces that might be in the bucket key and then standardizes it to my other code with underscores.

In my example I added a submit button but I'll be removing that in production and adding a Stimulus controller to submit the form on click or do a little AJAX to just load the results.

I hope this helps anyone else diving into it and I'm definitely open to any suggestions to make the code better.

edit: I changed the code that states whether the check box should be selected to use the .try method. Before, an error would be raised if no values were selected. This way, it just provides a nil value and doesn't check that box if the params do not exist.

Reply

Hi Ken. Did you manage to keep the checkboxes checked after form submission??

Reply

There was a mistake in my code. I had abstracted it out into a partial and when I rewrote the partial to match Chris's example, I wrote it wrong. My original example was looking into params["bucket"] and it should have been params["brand"].

Starting from the beginning, the third argument of the check_box_tag determines whether the check_box is selected or not. It'll take a true/false value, but you can substitute a nil value for the false. Orignally, I began with params["brand"].include? bucket["key"]. This will return a true/false value but will fail when the params["brand"] is not set, for example when first hitting the search page and no params are set because there is no .include? method on a nil class.

From there, I integrated the .try method, so if the params["brand"] supports the .include? method, it'll use it and return a true/false value. If the params["brand"] method doesn't support .include? then it'll return a nil value to the check_box_tag and the check box will not be selected.

The final code should read as: params["brand"].try(:include?, bucket["key"]). Note, on the .try method, the first argument is the method I'm calling and the second argument is the value I'm passing to the .include? method.

https://api.rubyonrails.org/v4.2/classes/Object.html#method-i-try

Reply

This is gold! Great video.
I have two questions.
Is there a way, for better SEO, to move filter params to URL?
Instead of /televisions?brand=Brick to /televisions/brand/brick ?
And second question, how to handle multiple values for attribute?
For example, if I have a color attribute for a television and the tv comes in three colors options 'Black', 'White' and 'Silver'.
Should I make three different televisions or save it as array? Or make another table or use something like hstore?
What is the best approach and how to use this filter with it?

Reply

This is best! Great video.
Is there a way, for better SEO, to move filter params to URL?

Reply

Great video thank you! I'm currently implementing aggregations in my apps :)
Aggregations sort could actually be made by elastisearch itself.
See in searchkick docs :

Product.search "wingtips", aggs: {color: {order: {"_key" => "asc"}}} # alphabetically

Reply

Sweet, thanks for sharing that! Makes a lot of sense to use any built-in functionality that you can with ElasticSearch. This is a good improvement.

Reply

Is it possible to still use Aggregations when searching multiple models? Especially if each model doesn't have that particular value?

Reply

Looks like you can do that: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-global-aggregation.html

Not sure if you can do it with Searchkick directly, so you'd have to ask them.

Reply

I know I am reaching a little bit with my questions, but if all models DID have the same attributes, would I be able to setup aggregations just like in the video?

Reply

Hi Guys/Chris.

1.I am strungling to have this work in parallel with a search input. not sure how to write the query to avoid intereference with the buckets.

2.Also, similar question but this time with Pundit. Where to incorporate the policy_scope(Model) to allow restriction based on usage rights?

3) Last but not least, in the checkbox_tag used by Ken Beegle (above), how to you retain the checkbox value after submission...so that the user can understand what he/her has selected.

many thks in advance ;-)

Reply

How do you do this to aggregate on related models? Eg: IRL brand would probably be a model, not just a label in the Television model. Also the original video included searching acts-as-taggable-on : which is a really cool feature, but how do you include that in your search filtering?

Reply

Question on this. Say I have a join to another via an integer ID. I want to display in a bucket the description of the ID vale from the other table. How do you do that?

Project joins to project_statuses with proj_status_id. I can return the proj_status_ids in the bucket but what I really want is to display the statusdescr field I get from the secondary table.

How would I do this?

Reply

Hi,

I am late to the party - the way I managed to display description rather than ID is by modifying search_data method like this:

def search_data
    {
      name: name,
      description: description,
      sup_name: supplier.name,
      cat_name: category.name,
      status: active
    }

I can use cat_name in my aggs.

Hope it helps.

Reply

Hi everyone,

Great video, I've seen it a few times now as I have the following problem:

Every time I click on any value, it filters out the main table and the sidebar at the same time so the <strong></strong> is a bit helpless at the moment.

any ideas?

here is my controller:

  def index
    query = params[:q].presence || "*"
    args = {status: true}
    args["cat_name"] = params[:cat_name] if params[:cat_name]
    @products = Product.search(query, where: args,  aggs: {cat_name: {order: {"_key" => "asc"}}}, includes: [:supplier, :category])
  end

and partial view:

    <h4>Compra por Especialidad</h4>
         <ul class="list-group">
         <% @products.aggs["cat_name"]["buckets"].each do |bucket| %>
            <%if params[:cat_name] == bucket["key"] %>
              <strong>
               <%= link_to "#{bucket["key"].capitalize}<span class='badge badge-primary badge-pill'>#{bucket["doc_count"]}</span>".html_safe, request.params.merge(cat_name: bucket["key"]), class:"list-group-item  list-group-item-action d-flex justify-content-between align-items-center"%>
              </strong>
            <% else %>
               <%= link_to "#{bucket["key"].capitalize}<span class='badge badge-primary badge-pill'>#{bucket["doc_count"]}</span>".html_safe, request.params.merge(cat_name: bucket["key"]), class:"list-group-item  list-group-item-action d-flex justify-content-between align-items-center"%>
            <%end%>

      <% end %>

thanks

Reply

sorted with smart_aggs: false

Reply

To anyone looking for more info about how to work with associated models, here is:

Item.rb (model):

  # Relations
  belongs_to :brand

    ## many-to-many relations between Items and Textures
    has_many   :item_attribute_for_textures
    has_many   :textures, through: :item_attribute_for_textures, :class_name => 'ItemAttribute::Texture'


    # Searchkick
  searchkick

  def search_data
    {
      full_item_title:  full_item_title,
      brand:            brand.try(:title),
      texture:          textures.map(&:title)
    }
  end   

ItemController.rb:

  def index
    args = {}
    args[:brand]          = params[:brand]          if params[:brand].present?
    args[:texture]        = params[:texture]        if params[:texture].present?

    query = params[:busca].presence || "*"
    @items  = Item.search query, where: args, 
      aggs: { 
          full_item_title:  {},
          brand:            {},
          texture:          {}
          }
  end

and view index.html.erb:

<div class="row">
  <div class="col-sm-2" style="font-size: 0.75rem">

    <h5>Brands</h5>
    <div>
      <% @items.aggs["brand"]["buckets"].sort_by{ |b| b["key"] }.each do |bucket| %>
        <div>
          <% if params[:brand] == bucket["key"].to_s %>
            <strong><%= bucket["key"] %></strong>
          <% else %>
            <%= link_to bucket["key"], request.params.merge(brand: bucket["key"]) %>
          <% end %>
          (<%= bucket["doc_count"] %>)
        </div>
      <% end %>
    </div>
    <hr>

    <h5>Texture</h5>
    <div>
      <% @items.aggs["texture"]["buckets"].sort_by{ |b| b["key"] }.each do |bucket| %>
        <div>
          <% if params[:texture] == bucket["key"].to_s %>
            <strong><%= bucket["key"] %></strong>
          <% else %>
            <%= link_to bucket["key"], request.params.merge(texture: bucket["key"]) %>
          <% end %>
          (<%= bucket["doc_count"] %>)
        </div>
      <% end %>
    </div>
    <hr>

  </div>
</div>
Reply

i like this hack - thanks for sharing.

Reply

you need to reindex after including search_data in the model, for more: https://github.com/ankane/searchkick#indexing

Reply

Thanks!

Reply

Chris Oliver, thanks a lot.

Is it possible to use (aggregation) with rails enum?
I tried but it show nothing.

Reply

Thank you for this! I am currently on the part where you add <% if params[:price]==bucket["key"].to_s%>
<%= link_to bucket["key"], request.params.except(:price)%>

    <br>
    <% else %>
      <%= link_to bucket["key"], request.params.merge(price: bucket["key"]) %>
    <%end%> but I get  an error:

undefined method `merge' for []:Array
Extracted source (around line #28):
@producs = Product.search "*", where: args, aggs: {category_id: {}, price: {}, condition: {}, created_at:{}}

end

I've looked online but I don't see any sources for it. Did anyone else get this error?

Reply
Join the discussion
Create an account Log in

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

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

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