Maria Schuessler
Aug 27 · 5 min read

The Basics

Let’s jump right into it. This tutorial will be using the simple_form, pg_search, and acts-as-taggablegems, as well as AJAX to build a lighting fast multi-table filter to query your Rails 5 app.

If you haven’t already, take a look at the first two tutorials in this series to build a basic search and filtering mechanism, without dynamic reloading:

Part 1: Build a Simple Search with the simple_form Gem in Rails 5
Part 2: Build a Filtered Search From Scratch for Your Rails 5 Application

The previous search results

I’ll be basing this application on the cocktail search from the previous application, but for this tutorial, I’ll add the following:

  • Multi-table search using the pg_search gem.
  • Asynchronous JavaScript re-loading using ES6 and Webpacker

👨‍💻👩‍💻 Let’s get started!

Step 1. Install pg_search

In the previous app, I queried my database using ActiveRecord — a simple way to generate SQL queries without writing SQL. ActiveRecord is the simplest way to prototype search and filtering. It allows you to quickly find related records for databases of fewer than 1000 records. But, as your database grows, ActiveRecord queries can get overly complex and make your app laggy.

In the immortal words of Emeril Lagasse, in this app, we’re going to kick it up a notch and tap into PostgreSQL’s full-text search with the pg_search gem. PgSearch creates named scopes, which utilize full-text search to create lightning fast search queries in the database.

Install the gem and bundle it into the applicataion.

#Gemfile 
gem 'pg_search'
#terminal
bundle install

Step 2: Add PgSearch to your model

The PgSearch gem works through creating search scopes inside the model. This means that the first step is to include PgSearch in the model. Once it is initialized, you can use the methods available by the gem to build the query.

#app/models/cocktail.rbinclude PgSearch::Model pg_search_scope :global_search,
against: [:name, :preparation ],
associated_against: {
flavors: [:name],
categories: [:name]
},
using: {
tsearch: {any_word: true}
}

Here is a quick breakdown of the search scope:

  • Initialize the search using the methodpg_search scope
  • Give the search method a name — in this case, I am calling it global_search, but this name is complete up to you.
  • Specify the fields you are searching through inside the model using the against syntax. For this scope, I am searching through the name the preparation attributes.
  • Specify which parent model fields you are searching for, using the associated_against syntax. Because acts-as-taggable creates its own database tables, I am able to search against the association through the tag’s name.
  • Use the tsearch parameter to specify a strict or a general search. In this case, my parameter will return any of the matches that are included in the query.

Step 3: Add the search method to the controller.

Once the method is created, add the search scope as a class method in the controller.

The filter variable concatenates the two radio button fields into one array, since pg_search will return any records that match either of the two categories. If you are only searching through one field, or using a text field for the parameter, you can simplify the @filter using a string instead.

To render the page using JavaScript, I added the format.js, which will call index.js.erb file when the form fires the HTTP request .

#app/controllers/cocktails_controller.rbdef index
if params["search"]
@filter = params["search"]["flavors"].concat(params["search"]["strengths"]).flatten.reject(&:blank?)
@cocktails = Cocktail.all.global_search("#{@filter}")
else
@cocktails = Cocktail.all
end
respond_to do |format|
format.html
format.js
end
end

Step 4: Modify your view to respond to JavaScript

Edit the view to render a partial for every cocktail.

This is essential, as the JavaScript will need to refresh only the cocktails that match the new search criteria, so each cocktail needs to be rendered in its own view.

#app/views/cocktails/index.html.erb#OLD APP
<div class="col-xs-12 col-sm-8">
<div id="cocktails">
<% @cocktails.each do |cocktail| %>
<h3><%= cocktail.name %></h3>
<p><%= cocktail.preparation %></p>
<button class="btn-primary">
<%= cocktail.flavor_list %>
</button>
<button class="btn-danger">
<%= cocktail.category_list %>
</button>
<% end %>
</div>
</div>
#NEW APP
<div class="col-xs-12 col-sm-8">
<div id="cocktails">
<%= render 'cocktail', cocktails: @cocktails %>
</div>
</div>
#app/views/cocktails/_cocktail.html.erb
<% cocktails.each do |cocktail| %>
<h3><%= cocktail.name %></h3>
<p><%= cocktail.preparation %></p>
<button class="btn-primary">
<%= cocktail.flavor_list %>
</button>
<button class="btn-danger">
<%= cocktail.category_list %>
</button>
<% end %>

Make sure to modify the simple_form to respond to JavaScript, by adding the remote: true flag to the first line of the form

<%= simple_form_for :search, url: root_path, method: "GET", remote: true do |f| %>

Step 5. Add the javaScript

There are two places to add JavaScript on the page. The first will refresh the results of the page with the new records. Inside the create.js.erb, I am writing a function to grab the cocktails element inside the partial and re-render it on form submission.

The j in front of the render makes sure that the JavaScript is escaped once the partial is rendered, to prevent scripting attacks through the form.

#app/views/cocktails/index.js.erbfunction replaceCocktails (innerHTML) {
const cocktails = document.getElementById('cocktails');
cocktails.innerHTML = innerHTML;
}
replaceCocktails("<%= j render 'cocktail', cocktails: @cocktails %>");

The second bit of JS is responsible for triggering the form submit on checkbox click. This code needs to be included in the app/javascript folder of the application (which will become the default way to ship JS with Rails 6). For the sake of keeping it simple in the tutorial, I’ll include it directly in the HTML, but it can also be exported through the app/javascript folder.

#app/views/cocktails/index.html.erb<script type="text/javascript">
var checkBoxes = document.querySelectorAll(".form-check-input");
var form = document.querySelector('form');

for (const check of checkBoxes) {
check.addEventListener( 'change', function() {
Rails.fire(form, 'submit');
});
}
</script>

Step 6. Fire up the form and search your application with AJAX. 🍻

Just like that, you can build a dynamic search in minutes.

DEMO HEROKU APPLICATION:
https://rails-advanced-search.herokuapp.com

GITHUB REPOSITORY:

The Startup

Medium's largest active publication, followed by +502K people. Follow to join our community.

Maria Schuessler

Written by

Full-Stack Developer \\ Traveler \\ Hot Sauce Entrepreneur \\ Le Wagon Mentor \\ Shanghai-based \\ mariacodes.io

The Startup

Medium's largest active publication, followed by +502K people. Follow to join our community.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade