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.
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
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?
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
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.
Is it possible to still use Aggregations when searching multiple models? Especially if each model doesn't have that particular value?
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.
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?
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 ;-)
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?
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?
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.
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
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>
you need to reindex after including search_data in the model, for more: https://github.com/ankane/searchkick#indexing
Chris Oliver, thanks a lot.
Is it possible to use (aggregation) with rails enum?
I tried but it show nothing.
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?