Build a Filtered Search From Scratch for Your Rails 5 Application
When your search algorithm is good, no one will notice, but when a search algorithm is bad… it’s really bad
Adding a search to any application is always harder than it looks. The thing is, when your search algorithm is good, no one will notice, but when a search algorithm is bad… it’s really bad. I’m thinking of this particular restaurant app in Shanghai, and how it’s impossible to find a good brunch near you.
There are several ways to build search and filters in a Rails 5 application. I’m in the process of writing an article that integrates filters with named scopes, but I wanted to create an article using
acts-as-taggable first. That’s because to me, this is really the most important part of the gem: how do you find completely unrelated information using a specific search criteria?
Using tags is a powerful way to connect unrelated data models in your Rails app. In this example, we will compare both the type of cocktail (do you serve it before dinner, with dinner, after, etc.) and the taste notes (is the cocktail sweet, sour, fruity?). Both of those things are related to the overall description of the cocktail, but they are independent of each other.
1. Add Tags Into Your Application
I’ll start by installing the gem and runnings its migrations.
gem 'acts-as-taggable-on', '~> 6.0'#Terminal
2. Add Tags to Cocktails
Once the tags are in the application, I can add them to my
Cocktail model. Typically, Acts As Taggable adds the tags into a generic ‘tag_list,’ but for this app I’m creating two filters — one for the general category of the cocktail (“After Dinner,” “Before Dinner,” etc.) and one for the taste (“sweet,” “sour,” “citrusy”).
#app/models/cocktail.rb acts_as_taggable_on :tags (the typical way of adding tags, you don’t need this line if you are adding the next two lines)acts_as_taggable_on :categories
Now, instead of adding tags to the
tag_list, I will be adding two sets of tags — to the
category_list and the
Note that there are no tagging columns in the cocktails table: the data lives inside the migrations generated by the gem.
3. Add Cocktail Seeds With Tags
I am seeding my database with cocktails from a JSON list I used for the search tutorial. In order to add the categories, I am adding the drink ‘category’ from the JSON list and randomly attributing flavors to the cocktails to save time.
require 'open-uri'url = "https://raw.githubusercontent.com/maltyeva/iba-cocktails/master/recipes.json"Cocktail.delete_all if Rails.env.development?flavors = ['sweet', 'citrusy’, 'sour', 'strong', 'bitter']cocktails = JSON.parse(open(url).read)cocktails.each do |cocktail|
For this example, there is only one tag per cocktail, but the great thing about
acts-as-taggable is that you can add as many items to the list as you’d like.
4. Add the Filter Form
First, let’s understand the form from the front end. I’m using the basic form from the Simple Form tutorial, but let’s break down the input step-by-step:
:flavors— this is the name of the field I will use on the back end. It will correspond to
params[:search][:flavors]inside the controller
label: “Category—the label field will use a custom tag, but leaving it out will just default to the field name (in this case Flavors)
collection: $categories— this collection will pull from the
Cocktailmodel and return a global variable containing an array of categories corresponding to my database.
as: :check_boxes— this is totally optional, but it sets my filter as a multi-select checkbox, as opposed to the default dropdown menu
#app/views/cocktails/index.html.erb<%= simple_form_for :search, url: root_path, method: "GET" do |f| %>
<%= f.input :flavors, label: "Category", collection: $categories, as: :check_boxes %>
<%= f.input :strengths, label: "Flavor", collection: $flavors, as: :check_boxes %>
<%= f.submit "Search", class: "btn btn-primary" %>
<%= link_to "Reset", root_path %>
<% end %>#app/models/cocktail.rb$flavors = ['sweet', 'citrusy', 'sour', 'strong', 'bitter']
$categories = ["Before Dinner Cocktail", "All Day Cocktail", "Longdrink", "Sparkling Cocktail", "After Dinner Cocktail", "Hot Drink"]
Once my search form is filled out, I want to send the results to the back end and return the results.
5. Add the Filtering Mechanism Based on Tags
Because I am filtering both on the flavors and the strengths, I want to account for the user filling out either both or one of the parts of the form (I suppose they could also fill out none of the form and submit it as empty, so we will want to take care of that scenario too).
The results will come as two arrays, something like [“Sour,” “Fruity”] and [“Strong”], or occasionally, [“ ”]. To combine these two arrays, I will concatenate and flatten them, and then reject any accidental blank values.
@filter = params["search"]["flavors"].concat(params["search"]["strengths"]).flatten.reject(&:blank?)
@cocktails = @filter.empty? ? Cocktail.all : Cocktail.all.tagged_with(@filter, any: true)
@cocktails = Cocktail.all
6. That’s It!
The basic filter is complete, but there are still some limitations. Right now, it returns a match of any of the present parameters, which doesn’t help me narrow down the search as much as I want it to.
I also need to refresh the page with every search.
What if we could create a dynamic filter that refreshes the results automatically and limits them to the precise search combination I am looking for?
For that, we need to introduce AJAX and bolster our filtering mechanism.
GitHub Repository here.
Heroku Demo here.