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

Maria Schuessler
Jul 9, 2019 · 5 min read
Image for post
Image for post
Photo by Sergey Zolkin on Unsplash

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?

I’ll start with using my Cocktail app from the Simple Search tutorial I wrote on Medium and add tags to that. If you want to start with my boilerplate, you can find it on GitHub.

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.

Image for post
Image for post
Photo by Thought Catalog on Unsplash

1. Add Tags Into Your Application

I’ll start by installing the gem and runnings its migrations.

#Gemfile
gem 'acts-as-taggable-on', '~> 6.0'
#Terminal
rails acts_as_taggable_on_engine:install:migrations
rails db:migrate

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
acts_as_taggable_on :flavors

Now, instead of adding tags to the tag_list, I will be adding two sets of tags — to the category_list and the flavor_list.

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 'json'
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|
Cocktail.create!(name: cocktail["name"],
glass: cocktail["glass"],
preparation: cocktail["preparation"],
category_list: cocktail["category"],
flavor_list: flavors.sample)
end

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.

Image for post
Image for post
At this point, you may feel like you need a cocktail. Photo by kyryll ushakov on Unsplash

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 Cocktail model 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.

#app/controllers/cocktails_controller.rbdef index
if params["search"]
@filter = params["search"]["flavors"].concat(params["search"]["strengths"]).flatten.reject(&:blank?)
@cocktails = @filter.empty? ? Cocktail.all : Cocktail.all.tagged_with(@filter, any: true)
else
@cocktails = Cocktail.all
end
end

6. That’s It!

Image for post
Image for post

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.

Better Programming

Advice for programmers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store