Advanced Search with Ransack

Share this article

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.

Frequently Asked Questions (FAQs) about Advanced Search with Ransack

What is Ransack and how does it work in Ruby on Rails?

Ransack is a powerful gem in Ruby on Rails that simplifies the creation of search forms. It works by creating a form that builds a search query to display the desired results. Ransack provides a simple and intuitive way to create complex searches without the need to write lengthy SQL queries. It allows users to search models and display the results in a user-friendly manner.

How do I install and set up Ransack in my Rails application?

To install Ransack, you need to add the line gem 'ransack' to your Gemfile and then run bundle install from your terminal. After installation, you can use Ransack in your controllers and views. In the controller, you can use the ransack method on your model to create a search object. In the view, you can use the search_form_for helper method to create a search form.

How can I use Ransack for advanced search functionality?

Ransack allows you to create advanced search forms with a variety of options. You can search for records that match all or any of your criteria, and you can also search for records that do not match certain criteria. Ransack also supports sorting of results, which can be done by clicking on the column headers in the results table.

Can I use Ransack with pagination gems like Kaminari or Will_paginate?

Yes, Ransack works well with pagination gems like Kaminari or Will_paginate. After creating a search object with Ransack, you can use the result method to get the search results and then paginate them using the pagination gem’s methods.

How can I customize the search form created by Ransack?

Ransack provides a number of helper methods that you can use to customize your search form. For example, you can use the sort_link helper to create sortable links for your columns, and you can use the search_field helper to create search fields for your attributes.

How can I use Ransack to search associated models?

Ransack allows you to search associated models by using the associations method in your search form. You can specify the association and the attribute you want to search in the form, and Ransack will handle the rest.

Can I use Ransack to search for records in a specific date range?

Yes, Ransack supports date range searches. You can use the gteq and lteq predicates in your search form to search for records that fall within a specific date range.

How can I handle complex searches with Ransack?

For complex searches, Ransack provides the ransacker method, which allows you to define your own search methods. You can use this method to create custom searches that are not supported by the default Ransack methods.

Can I use Ransack with non-ActiveRecord models?

Ransack is designed to work with ActiveRecord models, but it can also be used with non-ActiveRecord models with some additional configuration. You will need to include the Ransack::Adapters::Object module in your model and define the searchable attributes manually.

How can I troubleshoot issues with Ransack?

If you encounter issues with Ransack, you can check the Ransack GitHub page for solutions to common problems. You can also post your issue there to get help from the community. Additionally, you can check the Rails console for any error messages that might give you a clue about what’s going wrong.

Islam WazeryIslam Wazery
View Author

Islam is a freelance web developer with experience spanning the full stack of application development. He is a co-founder of Whitespace which is a web development agency. Besides for that he spends his time working on open source projects that he find intriguing or writing tutorials. He was an ex-Google Summer of Code student in 2012 and a mentor for 2 projects in 2013 for KDE. You can find him on Twitter @wazery_ or check his Linkedin profile linkedin.com/in/wazery.

GlennG
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week