Advanced Search with Ransack

Web developer
Tweet

search flat icon, christmas button

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.

((screenshot))

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:

((screenshot-2))

((screenshot-3))

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 %>

((screenshot-4))

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>

((screenshot-5))

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 %>

((screenshot-6))

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.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

Comments
ylluminate

Have you compared performance metrics of using Ransack vs ElasticSearch? It would seem that the latter would be markedly faster, but I'd really like to see some metrics to give some real understanding of this landscape.