#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)
エピソード37で、ページに簡単に検索フォームを追加する方法を紹介しました。このフォームではテキストフィールドに簡単な文字列を入力し、一致する商品のリストを表示できました。
この検索フォームはGETリクエストでProductsController
の index
アクションを呼び出してsearch
パラメータを渡します。ほとんどのケースでは、アプリケーションに検索機能を追加するにはこの方法がベストでしょう。複数の検索フィールドがある場合でも、検索にはGETリクエストを使用する方法が適しています。
しかし、アプリケーションにより高度の検索をおこなうために複雑なフォームを提供したいときがあるでしょう。このようなケースでは、GETリクエストで送信するにはパラメータ数やデータ量が多すぎるという場合があります。そのようなフォームの例としては、vBulletinフォーラムの高度な検索のページがあります。このフォームには検索パラメータを送信するための10以上のフィールドがあり、GETリクエストで送信するには多すぎます。
そこでRailsアプリケーションでこのような状況に対応するにはどうすればいいでしょうか? ポイントは、検索を単なるリクエストとして扱うのではなく、モデルを持ちデータベースを裏に備えた、独立したリソースとして扱うという点です。
検索モデルを使用する
簡単なEコマースアプリケーションに高度な検索機能を付加するためには、Search
モデルを追加して、新たに作る高度な検索フォームに持ちたいフィールドに対応する属性を設定します。ユーザにキーワード、分類、最低価格、最高価格による検索を許可します。
$ rails g model search keywords:string category_id:integer min_price:decimal max_price:decimal
rake db:migrate
でデータベースのマイグレーションをおこない、新規テーブルを追加します。
次に、フォームを処理し結果を表示するためのSearchesController
を作成します。
$ rails g controller searches
検索はRESTfulな形式のリソースになるので、routesファイルにはそのように定義して追加します。
Store::Application.routes.draw do root to: 'products#index' resources :products resources :searches end
Search
リソースができたので、商品ページのシンプルな検索フォームの下に高度な検索のリンクを追加します。これをSearchesController
のnewアクションにリンクします。
<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>
次にそのアクションをコントローラ内に作成します。
class SearchesController < ApplicationController def new @search = Search.new end end
そしてそこで使用するビューを作成します。
<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 %>
このフォームはとてもシンプルです。@search
オブジェクトに対してform_for
を使用し、keyword
のためのテキストフィールド、category
のためのドロップダウンメニュー、最低価格と最高価格のための2つのテキストフィールドがあります。高度な検索のリンクをクリックすると新しいフォームが表示されます。
フォームを送信するユーザへの対応としてcreate
アクションが必要です。これによって新規のSearch
レコードが作成されて、showページにリダイレクトされ結果が表示されるので、ここでshowアクションも作成します。
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
結果を表示するshow
のためのテンプレートが必要です。
<h1>Search Results</h1> <%= render @search.products %>
テンプレートではSearch
モデル(この後で作成します)のnew productsメソッドを使用し、一致する商品のリストを返して表示(render
)します。これはProduct
ごとに部分テンプレートを表示処理するので、その部分テンプレートを作成します。
<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>
Search
モデルにproducts
メソッドを追加するだけです。
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
products
が複数回呼び出されたときにそのたびに検索が実行されないように、検索結果をインスタンス変数にキャッシュします。実際の作業はfind_products
というプライベートメソッドに委譲されます。このメソッドのロジックはこのアプリケーション特有のものですが、簡単に修正して他の検索にも転用できます。このケースでは商品を名前で取得し、もしユーザがname
, category_id
, min_price
and max_price
のパラメータを条件に指定したら、where句を追加します。最後に一致する商品のリストを返します。
もし希望するならフォームをもっと便利にして、ユーザに結果を表示するときの並びや返される結果の最大数を指定できるようにすることも可能です。
それではフォームの動きを試してみましょう。名前が「catan」で分類が「Toys & Games」で最低価格が$10である商品を検索します。
2つの商品が一致しました。
古い検索をクリアする
一つ注意しなくてはいけない点として、このアプローチをとる場合、高度な検索がおこなわれるたびにデータベースにレコードが作成されます。つまりsearches
テーブルはとても大きくなってしまう可能性があるので、定期的に古い検索をクリアするのがいいでしょう。これをおこなうrake
タスクを簡単に作ることができます。
desc "Remove searches older than a month" task :remove_old_searches => :environment do Search.delete_all ["created_at < ?", 1.month.ago] end
このタスクをRails環境に依存させたいので、:environment
オプションを渡します。そして作成後1ヶ月たった検索を削除します。
このrakeタスクが実行されると古い検索式が削除されますが、できればcron
ジョブを使って自動的に実行したいところです。これを設定する一番いい方法は、Whenever gemを使用して日次処理タスクとして登録する方法で、エピソード164で紹介しました。
今回のエピソードでは高度な検索用フォームの作成方法をとりあげました。このアプローチの利点のひとつは、検索式がデータベースに蓄積されるので、ユーザの検索式を保存して過去の検索へのリンクを表示させることなどを簡単に実現できます。