Advanced Search with Ransack
In this tutorial we will explore how to add complex search functionality into your Rails application. This task will be made easier by the awesome Ransack Ruby gem. Ransack provides excellent helpers and builders for handling advanced searches on your models. It has some really powerful features available out of the box without writing a lot of code, such as sorting or conditional search.
We will use the same application used in the Braintree series . Please refer to that post to setup and style the basic application. Here is a fork of the repository we will use for our application Ransack – MovieStore.
Goal
Here is a list of features we are going to implement:
- Search movies by title
- Search movies by price ranges
- Sort by title, price, release year, ascending or descending
- Search movies by any of its (model) attributes, with options like equal to, greater than, less than, etc.
- Sort by any model attribute
You can view a working demo of this app deployed on Heroku
Exploring Ransack provided demo
ActiveRecordHackery has their own demo application using the Ransack gem here. It provides two modes for searching: basic and advanced. In basic mode, you can only search by first name, last name, email, or post title. In advanced mode, you have a lot of options to customize the search. Try them out to find the power that Ransack provides in complex searching.
Tools
- Ruby
(2.1.0)
– Programming language - Rails
(4.1.1)
– Back-end framework - Foundation 5 – Front-end CSS framework
- Ransack – Ruby gem
- Devise – User authentication
Step 1: Adding Ransack
The first step is to add the Ransack gem to your Gemfile:
gem 'ransack'
Make sure to run bundle install
.
Step 2: Add a Search Object
In the most common use case, the Ransack-provided search
method is called on the model, passing in the search parameters:
@search = Movie.search(params[:q])
The result can be stored in the @movies
instance variable:
@movies = @search.result
The entire index action looks like:
def index
@search = Movie.search(params[:q])
@movies = @search.result
end
Step 3: Add a Search Form
Let’s create a form to handle the search. The form just needs a text field (for the movie title) and a submit button. The form can leave just under the site header.
Add a yield
statement in the header section in the application layout. The content for this yield
is created in the index view.
Open up layouts/application.html.erb file and add:
<%= yield(:search) %>
in the header section. Then, in the index page, add the simple search form as follows:
<% content_for :search do %>
<div class="large-8 small-9 columns">
<%= search_form_for @search do |f| %>
<%= f.text_field :title_cont, class: "radius-left expand", placeholder: "Movie title" %>
</div>
<div class="large-4 small-3 columns">
<%= f.submit "Search", class: "radius-right button" %>
</div>
<% end %>
<% end %>
As you can see, we used Ransack’s helper method search_form_for
to create our search form and passed the search
object created previously. It contains a text_field
with a symbol :title_cont
, which tells Ransack to search for a title that contains some value. The cont here is called a predicate and it is the way Ransack determines what information to match.
As a simple refactoring I moved this content for to a separate partial (_title_search_box.html.erb) in order for it to be reusable. Add a line to the index page to render this partial:
<%= render "title_search_box" %>
Note: Ransack also has a helper for labels, but we didn’t need it in our form.
Step 4 – 1: Add Price Range
In the previous step, we created simple searching functionality. Let’s try something more advanced, like searching for movie price ranges by providing a minimum and maximum value.
This is what we are going to achieve:
We’ll start with search box styling, borrowing some CSS classes from the MovieStore application. The form-container and glassy-bg classes (found in layout.css.scss and helpers.scss, respectively) add a glassy background to the search box. I created a new div with class column
to contain the advanced search box. The box is hidden, initially.
<div class="column">
<div class="advanced-search hide form-container glassy-bg columns">
<a class="close-advanced-search fi-x"></a>
</div>
</div>
Add a button which, when clicked, opens up the advanced search box, as follows:
<div class="column">
<h5>
<a class="show-advanced-search">Advanced Search <span class="fi-plus"/></a>
</h5>
</div>
Now, in movies.js.coffee, add the click
event for this link, showing our advanced search box:
$ ->
$('.show-advanced-search').click ->
$('.advanced-search').show() // which removes the hide class
$(this).hide()
Handle close advanced search box click
event, as well:
$('.close-advanced-search').click ->
$('.advanced-search').hide()
$('.show-advanced-search').show()
Step 4 – 2: Adding Price Range
Time to add the price range to the search form. We will use the same search_form_for
method to create our form with the same @search
instance variable.
In the below code, the symbols :price_gteq
and price_lteq
represent for price greater than or equal to and price less than or equal to, respectively. View a list of the available predicates here
<h4>Advanced Search</h4>
<%= search_form_for @search do |f| %>
<div class="large-5 small-4 columns">
<%= f.label :price_gteq, class: "movie-label" %>
<%= f.text_field :price_gteq, class: "radius", placeholder: "Minumum Price" %>
</div>
<h6 class="large-2 small-4 columns center"><span>And</span></h6>
<div class="large-5 small-4 columns">
<%= f.label :price_lteq, class: "movie-label" %>
<%= f.text_field :price_lteq, class: "radius", placeholder: "Maximum Price" %>
</div>
<div class="column">
<%= f.submit "Search", class: " radius button" %>
</div>
<% end %>
Step 5: Add Sorting
Ransack has a form builder that provides links to sort columns in ascending or descending order. This builder is sort_link
. It is very simple to use, just pass the search object and the column name (attribute) you want to sort and a placeholder for it.
I’ve overridden three Foundation labels in the foundation_and_overrides.scss file.
<h4 class="column">Featured Movies</h4>
<div class="row right padm">
<div class="column">
<div class="filter-label red">
<%= sort_link @search, :title, "Title" %>
</div>
<div class="filter-label dark-golden-rod">
<%= sort_link @search, :price, "Price" %>
</div>
<div class="filter-label dark-slate-gray">
<%= sort_link @search, :release_year, "Release Year" %>
</div>
</div>
</div>
Step 6: Add Conditional Search
In this step, let’s explore how to add conditional search fields. Make a select box with all of the movie attributes and another with all of the predicates. The final input will be a text field for the value to use in the search. Ransack has some form builders for this scenario, as shown in the below code.
Just for fun, I’ve added some sorting functionality, but in a form of selects. One dropdown is for model attributes, and the other is the order (ascending, or descending):
<h4>Advanced Search</h4>
<%= search_form_for @search do |f| %>
<%= f.condition_fields do |c| %>
<div class="large-4 small-4 columns">
<%= c.attribute_fields do |a| %>
<%= a.attribute_select nil, class: "radius" %>
<% end %>
</div>
<div class="large-4 small-4 columns">
<%= c.predicate_select Hash.new, class: "radius" %>
</div>
<div class="large-4 small-4 columns">
<%= c.value_fields do |v| %>
<%= v.text_field :value, class: "radius" %>
<% end %>
</div>
<% end %>
<h5>Sort</h5>
<div class="column">
<%= f.sort_fields do |s| %>
<%= s.sort_select Hash.new, class: "large-5 small-4 columns mrs radius" %>
<% end %>
</div>
<%= f.submit "Search", class: "radius button" %>
<% end %>
Step 7: Refactoring Searching
It’s a good idea to move our code to a partial called condition_fields:
_condition_fields.html.erb
<div class="field">
<div class="large-4 small-4 columns">
<%= f.attribute_fields do |a| %>
<%= a.attribute_select nil, class: "radius" %>
<% end %>
</div>
<div class="large-4 small-4 columns">
<%= f.predicate_select Hash.new, class: "radius" %>
</div>
<div class="large-4 small-4 columns">
<%= f.value_fields do |v| %>
<%= v.text_field :value, class: "radius" %>
<% end %>
<%= link_to "remove", "#", class: "remove_fields" %>
</div>
</div>
Step 8: Link to Add Criteria
Let’s add a link in the advanced search box to add conditions. In index.html.erb, add a link to add the criteria, like so:
<%= f.condition_fields do |c| %>
<%= render "condition_fields", f: c %>
<% end %>
<p><%= link_to_add_fields "Add Conditions", f, :condition %></p>
First, create the link_to_add_fields
helper method in the application helper:
def link_to_add_fields(name, f, type)
new_object = f.object.send "build_#{type}"
id = "new_#{type}"
fields = f.send("#{type}_fields", new_object, child_index: id) do |builder|
render(type.to_s + "_fields", f: builder)
end
link_to(name, '#', class: "add_fields", data: {id: id, fields: fields.gsub("\n", "")})
end
In movies.js.coffee, handle the click
event for the add_fields
class:
$('form').on 'click', '.add_fields', (event) ->
time = new Date().getTime()
regexp = new RegExp($(this).data('id'), 'g')
$(this).before($(this).data('fields').replace(regexp, time))
event.preventDefault()
Step 9: Link to Remove Criteria
It is sensible to also add a link to remove conditions. Add it in the condition_fields partial, accompanying every condition:
<div class="large-4 small-4 columns">
<%= f.value_fields do |v| %>
<%= v.text_field :value, class: "radius" %>
<% end %>
<%= link_to "", "#", class: "remove_fields fi-x" %>
</div>
Back in movies.js.coffee, the necessary click
event handler:
$('form').on 'click', '.remove_fields', (event) ->
$(this).closest('.field').remove()
event.preventDefault()
Step 10: POST the Request
If we add too many conditions, the number of params may reach the limit allowed by GET requests. This can be avoided by using POST instead.
In the config/routes.rb file add:
resources :movies, only: [:show, :index] do
match :search, to: 'movies#index', via: :post, on: :collection
end
Then, in index.html.erb, change the search form to use the new search path, like so:
<%= search_form_for @search, url: search_movies_path, method: :post do |f| %>
Summary
In this tutorial, we used the same online movie store application used in “Build an Online Store with Rails” to add complex searching functionality. By using the Ransack gem, the movie application now has a nice interface for both simple and complex searching.