#111 Advanced Search Form (revised)
- Download:
- source codeProject Files in Zip (94.9 KB)
- mp4Full Size H.264 Video (17 MB)
- m4vSmaller H.264 Video (6.99 MB)
- webmFull Size VP8 Video (10.8 MB)
- ogvFull Size Theora Video (13.5 MB)
Way back in episode 37 we showed how you can add a simple search form to a page. This form allows users to type in a simple query into a text field and get a list of the matching products back.
The search form performs a GET request that calls the ProductsController
’s index
action, passing a search
parameter to it. For most scenarios this is the best way to add searching to an application. Even if you want to have multiple search fields using a GET request for the search is a good approach.
There may be times though when you want to add more advanced searching to an application and present a complicated form to the user. In these cases there may be too many parameters and too much data to send in a GET request. An example of such a form is the advanced search page on the vBulletin forums. This form has a over a dozen different fields for submitting various search parameters, probably more than you’d want to submit via a GET request.
So how should we handle this kind of situation in a Rails application? The key is not to treat search as a simple request but as a separate resource that has a model and which is backed by a database.
Using a Search Model
To add advanced searching to our simple e-commerce application we’ll need to add a Search
model and give it attributes to match the fields we want to have in our new advanced search form. We’re going to allow users to search by keyword, category and minimum and maximum price.
$ rails g model search keywords:string category_id:integer min_price:decimal max_price:decimal
We’ll need to migrate the database with rake db:migrate
to add the new table.
Next we’ll create a SearchesController
to handle the form and show the results.
$ rails g controller searches
Searches should be a RESTful style resource so we’ll add it as such in the routes file.
Store::Application.routes.draw do root to: 'products#index' resources :products resources :searches end
Now that we have a Search
resource we’ll add an advanced search link to the products page underneath the simple search form. This will point to the SearchesController
’s new action.
<h1>Products</h1> <%= form_tag products_path, method: :get do %> <p> <%= text_field_tag :search, params[:search] %> <%= button_tag "Search", name: nil %> </p> <% end %> <p><%= link_to "Advanced Search", new_search_path %></p> <div id="products"> <%= render @products %> </div>
Next we’ll create that action in the controller.
class SearchesController < ApplicationController def new @search = Search.new end end
Then create a view to go along with it.
<h1>Advanced Search</h1> <%= form_for @search do |f| %> <div class="field"> <%= f.label :keywords %><br /> <%= f.text_field :keywords %> </div> <div class="field"> <%= f.label :category_id %><br /> <%= f.collection_select :category_id, Category.order(:name), :id, :name, include_blank: true %> </div> <div class="field"> <%= f.label :min_price, "Price Range" %><br /> <%= f.text_field :min_price, size: 10 %> - <%= f.text_field :max_price, size: 10 %> </div> <div class="actions"><%= f.submit "Search" %></div> <% end %>
This form is fairly simple. It uses form_for
on our @search
object and has a text field for the keywords
, a dropdown for the category
, and two more text fields for the minimum and maximum price fields. We can now click on the advanced search link and view our new form.
We’ll need a create
action to handle the user submitting the form. This will create a new Search
record then redirect to its show page to show the results so we’ll write the show action now as well.
class SearchesController < ApplicationController def new @search = Search.new end def create @search = Search.create!(params[:search]) redirect_to @search end def show @search = Search.find(params[:id]) end end
We’ll need a template for show
on which we can display the results.
<h1>Search Results</h1> <%= render @search.products %>
Our template uses a new products method on the Search
model (we’ll write this shortly) that returns a list of the matching products and render
s it. This renders a partial for each Product
and we’ll write that partial now.
<div class="product"> <h2><%= link_to product.name, product %></h2> <div class="details"> <%= number_to_currency(product.price) %> <% if product.category %> | Category: <%= product.category.name %> <% end %> </div> </div>
Now we just have to add the products
method to the Search
model.
class Search < ActiveRecord::Base def products @products ||= find_products end private def find_products products = Product.order(:name) products = products.where("name like ?", "%#{keywords}%") if keywords.present? products = products.where(category_id: category_id) if category_id.present? products = products.where("price >= ?", min_price) if min_price.present? products = products.where("price <= ?", max_price) if max_price.present? products end end
We’ve cached the results of our search in an instance variable so that if products
is called multiple times the search won’t be performed more than once. The actual work has been delegated to a private method called find_products
. The logic in this method is fairly specific to our application but it should be easy to modify for other searches. In this case it gets the products by name then adds where clauses for the name
, category_id
, min_price
and max_price
, if that parameter has been supplied by the user. Finally it returns the list of matching products.
We could, if we wanted to, make the form even fancier and give the user the option to specify the order in which the results should be presented and the maximum number of results that should be returned.
We can try out our form now. We’ll search for products called “catan” in the “Toys & Games” category with a minimum price of $10.
This brings back the two matching products.
Clearing Old Searches
One thing to be aware of when using this approach is that every advanced search made stores a record in the database. This means that the searches
table can become very large so it’s a good idea to regularly clear out old searches. We can easily create a rake
task to do this.
desc "Remove searches older than a month" task :remove_old_searches => :environment do Search.delete_all ["created_at < ?", 1.month.ago] end
We need to make this task dependent on the Rails environment which we do by passing in the :environment
option. We then delete any searches made over a month ago.
Whenever this rake task is run it will remove old searches but ideally we want it to run automatically via a cron
job. The best way to do this is to use the Whenever gem to set this up as a daily task. This gem was covered back in episode 164.
That is for our episode on creating an advanced search form. One advantage of this approach is that as the search is stored in the database it’s easy to allow users to save searches and give them links to search they’ve made.