RailsでAjax: インクリメンタル検索英和辞書
Ruby on Rails を用いて、インクリメンタル検索のできる英和辞書webアプリ を作ってみました。インクリメンタル検索とは、検索語句の入力途中の文字列 から随時検索を行い、結果を動的に書き換えていく検索のことです。対象読者 は基本的な Rails の知識がある人としています。
データベースは SQLite を使用しますが、テーブル構造は単純なので 他のデータベースに変更するのは容易です。
dictアプリケーション用ディレクトリの作成
まずアプリケーションのディレクトリを作成します。アプリの名前は dict と します。
% rails dict
辞書データ
以下の作業は、すべて dict アプリの db ディレクトリ (dict/db) で行いま す。
辞書データは、 GENE95辞書 を用います。このページよりデータをダウンロードし、展開すると gene.txt というテキストファイルがありますので、これを SQLite データベースに入れ ることにします。
gene.txt は PDICテキスト形式というもので、見出し語とそれに対応する内容 (訳語・用例)などが1行づつ交互に入っています。 これをSQLiteに入れるため、以下のようなスキーマを作成しました。 テーブル名は entries とし、見出し語は keyword フィールド、訳語は content フィールドに入れます。
create table entries ( id integer primary key, keyword varchar(255) not null, content text ); create index keyword on entries(keyword);
dict アプリの db ディレクトリに、上記の内容で schema.sql というファイ ルを作り、dict.db という名前でデータベースファイルを作成します。
% sqlite3 dict.db < schema.sql
次に、gene.txt のデータをデータベースに入れる Ruby スクリプト pdic2db.rb を作ります。このスクリプトでは Rails の O/R マッパである ActiveRecord を単体で使用します。
#! /usr/local/bin/ruby -Ku
require 'nkf'
require 'rubygems'
require_gem 'activerecord'
class Entry < ActiveRecord::Base
end
ActiveRecord::Base.establish_connection :adapter => 'sqlite3',
:dbfile => 'dict.db'
Entry.transaction {
while keyword = gets
keyword = NKF.nkf('-w', keyword.chomp)
content = NKF.nkf('-w', gets.chomp)
if keyword =~ /^[\040-\176]+$/
entry = Entry.new('keyword' => keyword, 'content' => content)
entry.save
end
end
}
rubygems の環境では、ActiveRecord を使うのは簡単です。
require 'rubygems'
で rubygems をロードし、
require_gem 'activerecord'
で ActiveRecord をロードします。
class Entry < ActiveRecord::Base end
で、辞書テーブル entries に対応するモデル Entry を定義し、
ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :dbfile => 'dict.db'
でデータベースに接続します。
その後は PDICテキスト形式のファイルから、見出し語と内容を取りだし、文 字コードを UTF-8 に変換してデータベースに insert していきます。 transaction で囲んでいるのは、SQLite の場合このほうがずっと速いからです。
このスクリプトを pdic2db.rb というファイル名で作って、
% ruby pdic2db.rb gene.txt
として実行すると、同じディレクトリにある dict.db に辞書データが挿入さ れます(ある程度時間がかかります)。
database.ymlの設定
dict アプリ用に、データベース設定ファイル database.yml の設定をします。 database.yml はdictアプリの config ディレクトリにあります。ここでは development 環境で動かすため、database.yml を以下の内容にします。
development: adapter: sqlite3 dbfile: db/dict.db
generate scaffoldによるモデルとコントローラの生成
辞書データが用意できたら、dict アプリのモデルとコントローラを作ります。
dict ディレクトリで、
% ruby script/generate scaffold Entry Dict
これで Entry モデルと Dict コントローラができました。
% ruby script/server
として web サーバを起動して、ブラウザで http://localhost:3000/dict に アクセスします。

この時点で、ひとまず辞書データを CRUD することはできるようになりました。
ふつうの検索
インクリメンタル検索の前に、普通の検索機能を作成します。
まず app/views/dict/list.rhtml に検索語入力フォームをつけます。
<h1>Listing entries</h1>
<%= start_form_tag({:action => 'list'}, {:method => 'get'}) %>
<%= text_field_tag :phrase, @phrase %>
<%= submit_tag 'Search' %>
<%= end_form_tag %>
(中略)
<%= link_to 'Previous page', {:page => @entry_pages.current.previous,
:action => 'list', :phrase => @phrase} if @entry_pages.current.previous %>
<%= link_to 'Next page', {:page => @entry_pages.current.next,
:action => 'list', :phrase => @phrase} if @entry_pages.current.next %>
(以下略)
検索語句のフィールド名は phrase としています。ページの上の方にテキスト フィールドを作り、Pagination のページ送りのリンクにも検索語句が渡るよ うに書き換えます。
コントローラには params[:phrase] を通じて入力語句が渡ります。 app/controllers/dict_controller.rb の list メソッドを以下のように書き 換えます。
def list
@phrase = params[:phrase] || ''
phrase = @phrase + '%'
@entry_pages, @entries = paginate :entries, :per_page => 10,
:conditions => ["keyword like ?", phrase]
end
これで、検索フォームに入力された語句で検索(前方一致)するようになります。

インクリメンタル検索
いよいよ Ajax を用いてインクリメンタル機能を実装します。
Railsで Ajax を使うには、javascript のライブラリである prototype.js を ロードしなければなりません。 generate scaffold でレイアウトファイル app/views/layouts/dict.rhtml が 生成されているので、そこの <head>...</head> タグの中に以下の行を入れます。
<%= javascript_include_tag 'prototype' %>
続いて、app/views/dict/list.rhtml を書き換えます。書き換えるのは以下の 2点。
- observe_field 文を追加。
- 検索結果表示部分を別ファイルに分離。
<h1>Listing entries</h1>
<%= start_form_tag({:action => 'list'}, {:method => 'get'}) %>
<%= text_field_tag :phrase, @phrase %>
<%= observe_field(:phrase, :frequency => 0.5, :update => :results,
:url => {:action => :search}) %>
<%= submit_tag 'Search' %>
<%= end_form_tag %>
<div id="results">
<%= render :partial => 'searchresult' %>
</div>
observe_field は javascript ヘルパーで、これは id="phrase" なエレメン ト(ここでは直前のテキストフィールドタグ)を 0.5 秒間隔で見張り、変化が あったら search アクションを呼び出し、その結果で id="results" なエレメ ントを(ページ遷移なしで)書き換えるという意味になります。
検索結果表示部分は、別のファイル app/views/dict/_searchresult.rhtml に 分離し、render :partial で描画するようにします。_searchresult.rhtml は search アクションでも使用します。render :partial を <div id="results"> で囲っているため、observe_field によってこの部分が動的に 書き換えられることになります。
検索結果の partial ファイル app/views/dict/_searchresult.rhtml の内容 は以下のようにします。以前の list.rhtml の、検索結果表示部分そのままで す。
<table>
<tr>
<% for column in Entry.content_columns %>
<th><%= column.human_name %></th>
<% end %>
</tr>
<% for entry in @entries %>
<tr>
<% for column in Entry.content_columns %>
<td><%=h entry.send(column.name) %></td>
<% end %>
<td><%= link_to 'Show', :action => 'show', :id => entry %></td>
<td><%= link_to 'Edit', :action => 'edit', :id => entry %></td>
<td><%= link_to 'Destroy', { :action => 'destroy', :id => entry},
:confirm => 'Are you sure?' %></td>
</tr>
<% end %>
</table>
<%= link_to 'Previous page', {:page => @entry_pages.current.previous,
:action => 'list', :phrase => @phrase} if @entry_pages.current.previous %>
<%= link_to 'Next page', {:page => @entry_pages.current.next,
:action => 'list', :phrase => @phrase} if @entry_pages.current.next %>
<br />
<%= link_to 'New entry', :action => 'new' %>
では次に app/controllers/dict_controller.rb に search アクションを作り ます。内容は、ふつうの検索で list アクションに追加したコードとほとんど 同じです。
def search
@phrase = (request.raw_post || request.query_string).sub(/&.*/, '')
phrase = @phrase + '%'
@entry_pages, @entries = paginate :entries, :per_page => 10,
:conditions => ["keyword like ?", phrase]
render :partial => 'searchresult'
end
observe_field で、入力途中のフィールドの内容を読み出すには request.raw_post を用います。筆者の環境では '&_=' というようなパラメー タが後ろに付加されることがあるため、それを sub で削っています。それ以 降の検索部分は以前のふつうの検索の時と同じで、結果を render :partial で描画して返しています。
これで、ブラウザで http://localhost:3000/dict にアクセスし、入力フォー ムに文字を入力するとインクリメンタルな検索ができるようになっているはず です。Ajax 非対応なブラウザや、javascriptをoffにしているブラウザでも、 Searchボタンでふつうの検索をすることが可能です。
追記:
http://moriq.tdiary.net/20051222.html#p01 によると、上記の '&_=' が付 加されるという問題は、今では解決されています。Rails 1.0 で動作確認した ところ、たしかに sub で削らなくても問題なく動作するようになりました。
変更前
@phrase = (request.raw_post || request.query_string).sub(/&.*/, '')
変更後
@phrase = request.raw_post || request.query_string

Keyword(s):
References:[FrontPage]