#278 Search with Sunspot
- Download:
- source codeProject Files in Zip (231 KB)
- mp4Full Size H.264 Video (21.6 MB)
- m4vSmaller H.264 Video (11.8 MB)
- webmFull Size VP8 Video (16.1 MB)
- ogvFull Size Theora Video (28.3 MB)
SunspotはRubyアプリケーションに全文検索機能を追加するためのソリューションです。バックグラウンドでSolrを利用し、多くの優れた機能を提供します。今回のエピソードでは、過去のエピソードで使用したブログアプリケーションを例にして、Sunspotを使ってRailsアプリケーションに全文検索機能を追加します。
このアプリケーションは複数の記事を表示するページを持っており、それらを横断して検索を行う機能を実装していきます。これをSQLを使って行うのは困難になりやすく、多くの場合最善のアプローチとは言えません。Sunspotのような専用の全文検索ツールの方が、この機能を実装するにはずっと適した方法です。
Sunspotのインストール
Sunspotはgem形式で提供されるので、通常のようにGemfile
に追記してbundle
を実行することでインストールされます。
source 'http://rubygems.org' gem 'rails', '3.0.9' gem 'sqlite3' gem 'nifty-generators' gem 'sunspot_rails'
gemとその依存関係がインストールされたら、Sunspotの設定ファイルを生成するために次のコマンドを実行します。
$ rails g sunspot_rails:install
このコマンドによって/config/sunspot.yml
にYMLファイルが作成されます。このファイルのデフォルト設定に修正を加える必要はありません。
Sunspotはgemの内部にSolrを組み込んでいるので、別途インストールする必要はありません。つまりインストールした素の状態で機能するので、開発時にはとても簡単に作業できます。起動させるために次のコマンドを実行します。
$ rake sunspot:solr:start
OS X Lionを利用していてJavaランタイムをまだインストールしていない場合、このコマンドを実行したときにランタイムをインストールするようプロンプトが出るでしょう。同じく非推奨の警告(deprecation warning)が表示されるかも知れませんが、これは無視しても問題ありません。このコマンドにより、詳細設定のための追加の設定ファイルが作成されます。それらについてはここでは触れませんが、ドキュメンテーションにこれらのファイルの修正方法の詳細情報があります。
Sunspotの利用
Sunspotをインストールできたので、Article
モデルから利用します。全文検索機能を追加するためにはsearchable
メソッドを使用します。
class Article < ActiveRecord::Base attr_accessible :name, :content, :published_at has_many :comments searchable do text :name, :content end end
このメソッドはブロックをとり、その中で検索したい属性を定義してSunspotにどのデータに索引を設定すればいいかを知らせます。text
メソッドを使用して、全文検索を実行する対象とする属性を定義します。今回の記事の検索のためには、nameとcontentのフィールドを指定します。
Sunspotは自動的に新しいレコードに対して索引を作成しますが、既存のレコードに対してはそれを行いません。Sunspotに対して既存のレコードに索引を作成し直させる場合は、次のコマンドを実行します。
$ rake sunspot:reindex
これですべての記事がSolrのデータベースに入って検索できる状態になったので、indexページの一番上に検索フィールドを追加します。
<% title "Articles" %> <%= form_tag articles_path, :method => :get do %> <p> <%= text_field_tag :search, params[:search] %> <%= submit_tag "Search", :name => nil %> <% end %> <!-- rest of view omitted -->
このフォームはGETを用いてindexアクションに送信されるので、入力された検索パラメータがすべて検索文字列に追加されます。では次にコントローラを修正して、search
パラメータによって記事を取得するようにします。Sunspotで検索を行うには、モデルでsearch
を呼び出してブロックを渡します。ブロック内では複雑な検索を処理するためにいろいろなメソッドを呼び出すことができます。ここではfulltext
メソッドを使用して、それに対してフォームからの検索パラメータを渡します。最後にこの結果のすべてを@search
に割り当てます。これの結果を呼び出して、一致した記事のリストを取得します。
def index @search = Article.search do fulltext params[:search] end @articles = @search.results end
ここまでの部分をテストするために記事のページを再読み込みしてキーワードで検索を行います。すると一致する記事のリストが返されます。
検索は、それが記事のname
かcontent
かに関わらず、検索語を含んだ記事のリストを返します。
Article
モデルのsearchable
ブロックでは、これ以外にも多くのことが可能です。例えばboost
を使って、タイトルが一致する記事の方が本文が一致する記事よりも重要性が高いというように、結果に重み付けをつけることができます。
class Article < ActiveRecord::Base attr_accessible :name, :content, :published_at has_many :comments searchable do text :name, :boost => 5 text :content end end
これは、結果をrelevance(適合度)で並べたい場合に重要です。今回の場合はタイトルに検索語を含む記事が本文に検索語を含む記事よりも検索結果リストの上位に現れます。
searchable
ブロックで指定する属性はデータベースの実際の列名である必要はなく、モデル内で定義するメソッドを使用することもできます。記事が発行された月と年を含む文字列を返すpublish_month
という列を作成し、データベースの列と同じようにそのメソッドに対して検索を行います。
class Article < ActiveRecord::Base attr_accessible :name, :content, :published_at has_many :comments searchable do text :name, :boost => 5 text :content, :publish_month end def publish_month published_at.strftime("%B %Y") end end
この新しい列で検索できるようにrake sunspot:reindex
を再度実行してレコードに索引を設定し直す必要がありますが、そうすることで月名で記事を検索することができるようになります。
メソッドを作成する代わりに、ブロックを渡してブロックの戻り値に対して検索を行うことができます。記事には多くのコメントがついているので、ブロックを用いてコメントの内容を検索する機能を追加します。
class Article < ActiveRecord::Base attr_accessible :name, :content, :published_at has_many :comments searchable do text :name, :boost => 5 text :content, :publish_month text :comments do comments.map(&:content) end end def publish_month published_at.strftime("%B %Y") end end
ブロック内のコンテキストはArticle
のインスタンスなので、 その中で記事に対するコメントを取得して各コメントの本文にマッピングすることができます。これは配列を返しますが、Sunspotはコメントのすべてに索引を作成し検索可能にします。
属性に対する検索
単純な全文検索以上の検索機能を、ある特定の属性に対して追加したい場合はどうすればいいでしょうか。このためには、検索対象にしたい属性のタイプ(文字列か整数か浮動小数かタイムスタンプか)を渡します。検索フィールドにpublished_at
属性を追加するので、time
メソッドを利用します。
class Article < ActiveRecord::Base attr_accessible :name, :content, :published_at has_many :comments searchable do text :name, :boost => 5 text :content, :publish_month text :comments do comments.map(&:content) end time :published_at end def publish_month published_at.strftime("%B %Y") end end
ArticlesController
でこれを利用して、検索対象をpublished_at
の日付が現在の時刻よりも前のものに制限します。そのためにwith
メソッドを利用します。
def index @search = Article.search do fulltext params[:search] with(:published_at).less_than(Time.zone.now) end @articles = @search.results end
これを設定することで、発行されていない記事は検索されなくなります。渡すことができる属性に関する優れたドキュメンテーションがSunspotのwikiページにあります。
ファセット検索
ファセット検索を使用すると、ある属性、例えば記事が発行された月で検索結果をフィルタリングできます。例えば、発行された記事が存在する月のリンクの一覧リストを追加したいとしましょう。リンクをクリックすると、記事のリストにフィルタがかけられて、その月に発行された記事だけを表示します。
これを行うためにまず、publish_month
メソッドのsearchable
ブロックにstring
属性を追加します。
class Article < ActiveRecord::Base attr_accessible :name, :content, :published_at has_many :comments searchable do text :name, :boost => 5 text :content, :publish_month text :comments do comments.map(&:content) end time :published_at string :publish_month end def publish_month published_at.strftime("%B %Y") end end
ArticlesController
のsearch
ブロックでfacet
を呼び出すことでこれをファセットに変えることができます。
def index @search = Article.search do fulltext params[:search] with(:published_at).less_than(Time.zone.now) facet(:publish_month) end @articles = @search.results end
以下のコードを検索ボックスと記事の一覧の間に追加することで、index
ページにこれらのファセットを表示できます。
<div id="facets"> <h3>Published</h3> <ul> <% for row in @search.facet(:publish_month).rows %> <li> <% if params[:month].blank? %> <%= link_to row.value, :month => row.value %> (<%= row.count %>) <% else %> <strong><%= row.value %></strong> (<%= link_to "remove", :month => nil %>) <% end %> </li> <% end %> </ul> </div>
このコードで、publish_month
の各ファセット項目をループして表示しています。@search
オブジェクトで.facet
を呼び出して、ファセットの並び順として指定する属性を渡します。今回の例で言うと:publish_month
、そしてそれに対して.rows
を呼び出すと、その属性でのファセットオプションを返します。
row.value
を呼び出すと、その属性の値、例えば「January 2011」を返します。またrow.count
を呼び出せば、その値に一致する記事数が返されます。もし検索文字列にmonth
パラメータがあったら、その値を、パラメータを削除するための「remove(削除)」リンクと共に表示します。これによって、与えられたファセットを選択して、それをmonth
パラメータを介して渡すという便利な機能を利用できます。
ここでページを読み込み直すと、レコードの索引を設定し直したので、パネルにファセットのリストが月名とその月に発行された記事数という形で表示されます。月を選択すると、検索文字列にmonth
パラメータとして表示されますが、記事はフィルタリングされません。これを修正するために、コントローラのsearch
にもう一つwith
パラメータを追加して、month
パラメータが存在したら月でフィルタリングするようにします。
def index @search = Article.search do fulltext params[:search] with(:published_at).less_than(Time.zone.now) facet(:publish_month) with(:publish_month, params[:month]) ↵ if params[:month].present? end @articles = @search.results end
月を選択すると、その月に発行された記事で正しくフィルタリングされたリストが表示されます。
「remove」リンクをクリックすると全リストが返されます。検索結果もこれに合わせて動作します。検索語を入力すると、一致する記事がある月がリスト表示されます。
ファセットは検索と合わせて利用できる優れた機能です。
Sunspotに関する今回のエピソードは以上です。SunspotはRailsアプリケーションに全文検索機能を追加する優れた方法で、ここで触れることができなかった多くの追加機能を持っています。さらに情報を得るために、忘れずにwikiを参照してください。