Rails: Building Complex Search Filters with ActiveRecord and ez_where

With the release of Rails 3, the plugins and code described below no longer works. Read the updated post on building dynamic complex queries with composed scopes.

The code for this series of articles is also available:

git clone git://github.com/cblunt/blog-complex_search_filters_with_rails.git

This series of posts shows how to use the ez_where plugin to build a complex search filters into a Rails model. Last time we built a User model with a search method that allowed it to perform complex searches on terms and attributes. In this post, you'll see how to tie everything together using a controller and search form.

Populating the Database

Before we get started, it would be handy to populate our database with a lot of data to see the search controller in action. To create fixture data, I use Ryan Bates' Populator and Benjamin Curtis' Faker gems. Once installed, a short rake task is all that's needed to populate the database with some pseudo-random data. Alternatively, you can manually create some user records, e.g. by using script/console.

Lets add a populator rake task. First, make sure you have the faker and populator gems installed:

sudo gem install populator```

Then add a populator task to your project, in a new file `lib/tasks/populate.rake`:

ruby# lib/tasks/populate.rake

namespace :db do

namespace :populate do

desc "Populate the users table with 1000 random users"

task :users => :environment do

  require 'populator'

  require 'faker'

  # Destroy existing data


  # Repopulate

  User.populate(1000) do |user|

    user.first_name = Faker::Name.first_name

    user.last_name = Faker::Name.last_name

    user.email_address = Faker::Internet.email

    user.status = rand(5) + 1

    user.admin = (rand(10) < 5 ? true : false)





With that in place, run the rake task to populate your database with some users:

(For a more thorough tutorial of Populator and Faker, see [Ryan's](http://www.railscasts.com/) [screencast](http://railscasts.com/episodes/126-populating-a-database)).

### Generating the Controller

With plenty of data to work with, it's time to start building our users controller. In this example, I've only included the `index` and `show` actions. For a real-world app, you would want to include the remaining RESTful actions (`edit`, `create`, etc.).

bashruby script/generate controller users index show```

You also need to add a route to your config/routes.rb file so Rails can access the users controller RESTfully:

map.resources, :users```

With some routes set up, it's a good idea to check that everything is connected properly. To do this, we'll just ask the `users` controller `index` action to fetch a list of users. Remember that, without any parameters, our `User.search` method just wraps the `User.find` base method:

ruby# app/controllers/users_controller.rb

def index
@users = User.search(:all) end```

We'll then output these user's on the index page:

# Users

<th>Email Address</th>  


  <% @users.each do |user| %>
<td><%= h [user.first_name, user.last_name].join(' ') %> </td>  
<td><%= h user.email_address %></td>  
<td><%= user.admin? ? 'Yes' : 'No' %></td>  
<td><%= user.status %></td>


  <% end %>


A quick visit to [http://localhost:3000/users](http://localhost:3000/users) will check everything's hooked up (make sure your server is running; `script/server` will start the development server). If all has gone well, you will have a list of several hundred user names.

### The Search Form

Next, we'll add a text box so that we can search for users by terms. These terms will be passed on to the  `User.search` method and used to filter users by first or last name, or email address. See [part 1](http://blog.chrisblunt.com/rails-building-complex-search-filters-with-activerecord-and-ez_where/) for more information on how this works.

In your index view, add the following form just above the results table:

ruby# app/views/users/index.html.erb

<% form_tag users_path, :method => :get do %> <%= text_field_tag "search[terms]", params[:search][:terms] %> <%= submit_tag "Search" %> <% end %>


Notice that we need to make the form submit a GET request; if we used the default POST request, Rails' RESTful routes would assume we wanted to create a new user. If you now reload the page in your browser, entering some search terms should direct you to a URL similar to:

So now all we need to do is pass on those search parameters from our controller to `User.search`. In your controller, change the `index` action to:

ruby# app/controllers/users_controller.rb

def index

params[:search] ||= {}

@users = User.search(:all, :filters => params[:search])


Now reload your page, and enter some search terms in the text field. You'll notice that any terms you enter are matched against the users' names or email addresses.

The Final Touches

With the general search terms working, it would be useful to narrow down our search according to some attribute filters. To do this, we'll add a checkbox and dropdown menu that allows us to filter results by role and admin status. In index.html.erb, update your form with two new tags:

<% form_tag users_path, :method => :get do %>

  <%= text_field_tag "search[terms]", params[:search][:terms] %>

  Only Admins <%= check_box_tag "search[admin]", true, params[:search][:admin] %>

  Status: <%= select_tag "search[status]", options_for_select([["- All -", nil], ["Viewer", "1"], ["Member", "2"], ["Subscriber", "3"], ["Publisher", "4"], ["Editor", "5"]], params[:search][:status]) %>

  <%= submit_tag "Search" %>

<% end %>```

Your controller will now automatically pass on the `:admin` and `:status` values to `User.search`. However, if you try searching for administrators by checking the box, you'll see that no users are returned!

The reason for this is how Rails passes parameters into your controller. The `check_box_tag` returns a string value depending on its status. Our `User.search` method expects an `:admin` filter to be a `Boolean` value, or `nil`. So all we need to do is tell Rails to convert the string "true" to a boolean `true`, or set the `:admin` filter to `nil`.

This is easy to solve by adding a single line in your controller to map the value of the checkbox to `true` or `nil`:

ruby# app/controllers/users_controller.rb

def index

params[:search] ||= {}

# Ensure that params[:search][:admin] is either true or nil.

params[:search][:admin] = (params[:search][:admin] == "true" ? true : nil)

User.search(:all, :filters => params[:search])


Now when you reload the page, you'll be able to search for users and drill down your search results using your new form!

Next Steps

I hope you've found this mini-tutorial series useful and interesting. It's been a great learning experience for me in writing tutorials, and forcing me to refine my ruby coding. I've also found the search functionality useful in my own projects, and have decided to develop it into a generic ActiveRecord plugin. I'll continue to post updates about progress on this blog, and on my github account.

In the near future, I'll post some supplementary information on using the search methods with the will_paginate plugin, and how to AJAXify your seach forms.

In the meantime, if you have any comments or feedback, please let me know in the comments. Thanks!