まちゅダイアリー
2006-10-08 (日)
■ Rails に(再)挑戦 6日目 - ページの検索
いよいよ RandomNote の特徴である検索に取り掛かる。 本家 RandomNote では、データを管理しているテキストを順に開いて、正規表現で検索している。 さて、どうしようか。
- Page.find(:all) して正規表現 → メモリが持たない
- 全文検索エンジン (Hyper Estraier) を使う → セットアップが大変
- データベースの全文検索機能を使う → データベースに依存する & SQLiteじゃ無理?
悩ましい…ので今は深く考えずに、データベースに対する like 検索(!)で済ますことにする。 AND検索はできないし、インデックスを張れないから処理は重いし、欠点ばかりだけどね。 これも後で考えよう。 まずは Rails での実現方法を覚えるのが先だ。
ビュー
どこから手を付けようか迷ったけど、まずは分かりやすいビューから。 検索フォームは全てのページについているから、レイアウト (app/views/layouts/note.rhtml) に手を加える。
<%= start_form_tag :action => 'search' %> <%= text_field 'search', 'word' %> <%= submit_tag 'Search' %> <%= end_form_tag %>
検索フィールド (text_field) には2つのパラメータ ('search', 'word') を渡している。 こうすると、実際に出力される HTML は以下のようになる。
<form action="/note/search" method="post"> <input id="search_word" name="search[word]" size="30" type="text" /> <input name="commit" type="submit" value="Search" /> </form>
Rails で特長的なのは name="search[word]" かな。 データをハッシュとして扱うのがポイント。
このあたりの処理は scaffold で生成されたコードを読むと参考になる。 ページを生成するときの処理 (NoteController#create) は、
@page = Page.new(params[:page])
としているし、ページを更新するときの処理 (NoteController#update) は
@page.update_attributes(params[:page])
となっている。 モデルに対してハッシュをそのまま突っ込めるようにしているのね。 豪快だ…。
さて、少し脱線したよ。 とにかく、検索フィールドに入力したデータは params[:search][:word] で取得できるみたい。 検索フィールドに前回の検索語句を表示するようにしておこう。
<% word = params[:search][:word] if params[:search] %> <%= start_form_tag :action => 'search' %> <%= text_field 'search', 'word', :value => word %> <%= submit_tag 'Search' %> <%= end_form_tag %>
コントローラに検索機能を追加
ビューを作ったので、次はコントローラ。 NoteController に search メソッドを追加する。
def search
@pages = Page.fulltext_search(params[:search][:word],
:order => 'updated_on DESC')
end
ほとんどモデルに丸投げという…。 まぁ、一覧表示 (list) と同じノリだね。
モデルに検索メソッドを追加
ってことで、ようやくメインの処理。 app/models/page.rb を修正し、モデル (Page) にクラスメソッド (Page.fulltext_search) を追加する。 といっても、 find(:all) の検索条件に body like '%検索語句%' を指定するだけ。
def Page.fulltext_search(word, options = {})
options[:conditions] = ["body like ?", "%#{word}%"]
find(:all, options)
end
さらにビュー
忘れてた。 NoteControll#search の後に呼ばれるビュー (search.rhtml) が必要。 といっても、基本的には一覧表示 (list.rhtml) と同じものが使える。 Pagenate の使い方が分からないので、とりあえずは検索結果を全て表示するか。
<%= render(:partial => 'page', :collection => @pages) %>
検索してみる
ちゃんと動いているみたい。まだ AND 検索はできないけど…。
今日もリポジトリに登録。
■ 本文中のリンク (BracketName)
簡易だけど検索機能を付けたので、次に BracketName を作る。 BracketName とは、 [[ と ]] で括った文字列のこと。 Wiki だと別ページへのリンクになるんだけど RandomNote だとその言葉を検索するリンクになるのが面白い。 以下、 RandomNote の説明文より引用。
[[ ]] で囲まれた言葉は、その言葉を検索するリンクになります。
さて、 HikiDoc を使いながらどうやって文法を拡張するかだけど、同じく HikiDoc を使っている tDiary のソース (wiki_style.rb) を参考にさせてもらった。 WikiSection#to_html のソースを抜粋。
html = HikiDoc::new( string, :level => 3, :empty_element_suffix => '>' ).to_html.strip
html.gsub!( %r!<span class="plugin">\{\{(.+?)\}\}</span>!m ) do
"<%=#{CGI.unescapeHTML($1)}%>"
end
html.gsub!( %r!<div class="plugin">\{\{(.+?)\}\}</div>!m ) do
"<p><%=#{CGI.unescapeHTML($1)}%></p>"
end
なるほど。 HikiDoc で HTML に変換した後に、正規表現で置換 (gsub!) しているのか。 同じように、ヘルパーの parse メソッドを修正する。
def parse(body)
- HikiDoc.new(body, :level => 3).to_html
+ html = HikiDoc.new(body, :level => 3).to_html
+ html.gsub!(%r|<a href="(.+?)">(.+?)</a>|m) do
+ href, text = $1, $2
+ href == text ? link_to_search(text) : Regexp.last_match
+ end
+ html
end
+
+ def link_to_search(text)
+ link_to(text, { :action => 'search', 'search[word]' => text },
+ { :post => true })
+ end
検索へのリンクを生成するロジックは、これまた RandomNote の特徴であるサイドバーの検索履歴で今後も使いそう。 なので、link_to_search という別メソッドにしておいた。 といっても、内部では link_to を呼んでいるだけなんだけど。
link_to は便利なメソッド。 アクション (:action) とパラメータ ('search[word]') を指定するだけで、必要な URL を生成してくれる。 この場合、コントローラは同じ NoteContoroller なので、 URL は
http://192.168.92.210:3000/note/search?search%5Bword%5D=test
のようになる。
これで BracketName は完成。 リポジトリに登録しておしまい。 もうリビジョン14か。
追記
…また忘れてた。 検索への呼び出しは GET じゃなくて POST を使うようにしている。 何故かというと、 RandomNote での検索は副作用(検索数のカウント)を伴うから。 クローラによって消したはずのキーワードがいつの間にか復活するという問題が出ているみたいだし。 一般的に、副作用を伴う操作は POST を使った方がいい。
ということで、コントローラ (NoteController) の search アクションを、 POST のみ受け付けるようにした。
verify :method => :post, :only => [ :create, :update, :search ],
:redirect_to => { :action => :list }
これでリビジョン15。
