Ruby/groongaのサンプルアプリケーションのデモを用意しました。
RailsなどのWebアプリケーションフレームワークを使うほどのものではないので、ActiveGroongaは使わずに、Ruby/groongaとRackの組み合わせになっています。Rackについてはyharaさんの5分でわかるRackなどを読んでみてください。
デモはPassengerで動かしています。PassengerにRackを設置したことがある人なら10分もかからずにサンプルを動かせるのではないかと思います。
デモを見てもらえばわかる通り、小さなサンプルですが以下のように一通りの機能は備えています。
それぞれ、もう少し詳しく見ていきましょう。
通常の検索サイトでは空白で複数のキーワードを区切ることによって検索結果を絞り込むことができます。例えば、「Ruby クリアコード」で検索すると、「Ruby」と「クリアコード」両方にマッチするページがヒットします。いわゆるAND検索です。
まず、1つのキーワードだけを扱う場合のコードを示して、次に複数のキーワードを扱うコードを示します。
1つのキーワードだけを扱う場合はとても単純です。3行です。
1 2 3 4 5 6 7 8 |
# 文書が格納されたテーブルを取得 documents = Groonga::Context.default["documents"] # 文書テーブルから指定されたキーワードにマッチするレコードを検索 records = documents.select do |record| # HTTPの"query"パラメータで指定された単語が # "content"カラムにマッチするかチェック record["content"] =~ request["query"] end |
全文検索を指示するために「=~」演算子を使うなんてとてもRubyらしい書き方ですね。
複数のキーワードで絞り込みを行う場合はrecord["content"] =~ "keyword"という条件をANDでつなげていきます。イメージは以下の通りです。
1 2 3 4 5 |
records = documents.select do |record| (record["content"] =~ keyword1) & (record["content"] =~ keyword2) & ... end |
サンプルではこのようなコードになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
words = request["query"].split records = documents.select do |record| expression = nil words.each do |word| sub_expression = record["content"] =~ word if expression.nil? expression = sub_expression else expression &= sub_expression end end expression end |
ちなみに、injectを使うとこうなります。
1 2 3 4 5 6 7 8 9 10 |
records = documents.select do |record| words.inject(nil) do |expression, word| sub_expression = record["content"] =~ word if expression.nil? sub_expression else expression & sub_expression end end end |
お好みでどうぞ。
このサンプルでは、「同じ文書中に何回キーワードが出現するか」をスコアとして扱っています。スコアは検索結果のレコードが持っているので、それを使って並び替えます。1行です。
1 2 |
# スコアの大きい順に並び替えて、上位20件だけ使う。 records = records.sort([[".:score", "descending"]], :limit => 20) |
groongaでは「:」から始まる特別なアクセス用の名前があります。「:score」もその1つでスコアの値にアクセスするために使います。「:score」の他にはレコードのキーにアクセスする「:key」などがあります。
groongaは、全文検索用の索引を作るときにキーワードを正規化することができます。これにより大文字小文字を区別せず「Ruby」でも「ruby」でも同じように検索することができます。
正規化するためにしなければいけないことは、索引用テーブルを作成する時に:key_normalize => trueオプションを指定するだけです。
Ruby/groongaではテーブルやカラムを定義するためのDSLを用意しています。サンプルのためのテーブル・カラム定義は以下のようになっています。少しActiveRecord風です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# スキーマ定義開始 Groonga::Schema.define do |schema| # 文書格納用テーブル作成 schema.create_table("documents") do |table| table.string("title") # 文書のタイトル table.text("content") # 文書の内容 table.string("path") # 文書の置き場所 table.time("last-modified") # 文書の最終更新時刻 end # 索引用テーブル作成 schema.create_table("terms", :type => :patricia_trie, :key_normalize = true, # キーワードを正規化 :default_tokenizer => "TokenBigram") do |table| table.index("documents.title") # 文書のタイトルの索引を作成 table.index("documents.content") # 文書の内容の索引を作成 end end |
一応コメントを入れましたが、コメントがなくても何をしているのかがわかったのではないでしょうか。
:key_normalize => trueを指定しておくと、あとはgroongaがうまいことやってくれるので、検索時には特に何もする必要はありません。
キーワード周辺の文章を表示することにより、その文書が探している文書かどうかを判断しやすくなります。
たとえば、「Ruby」で検索するとRuby-GNOME2 0.18.0リリース*1がヒットしますが、その場合は「...されました。 Ruby-GNOME2はGTK+を含むGNOME関連ライブラリのRubyバインディング...」という文章も一緒に表示されます。これがあれば、文書を全部読まなくてもおおよその内容を想像しやすくなります。
この機能はKWICやスニペットなどと呼ばれていて、groongaではスニペットと呼んでいます。
スニペットの生成は以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# キーワードを囲むタグ open_tag = "<span class=\"keyword\">" close_tag = "</span>" # スニペットオブジェクトの作成 snippet = Groonga::Snippet.new(:width => 100, :default_open_tag => open_tag, :default_close_tag => close_tag, :html_escape => true, :normalize => true) # キーワードを正規化 # 検索キーワードを登録 request["query"].split.each do |word| snippet.add_keyword(word) end # 本文からスニペットを生成 segments = snippet.execute(record[".content"]) # 整形 separator = "\n<span class='separator'>...</span>\n" snippet_text = segments.join(separator) response.write("<p class=\"snippet\">#{snippet_text}</p>") |
整形用のタグを入れるコードも一緒になっているので多少長くなっていますが、スニペット作成のための処理は以下の3ステップだということがわかります。
簡単ですね。
サンプルアプリケーションを例にして、Ruby/groongaを使うと実用的な機能が揃った検索ページを簡単に作成できることを紹介しました。
サンプルアプリケーションはリリースされたばかりのRuby/groonga 0.0.6の中に入っています。
サンプルアプリケーションを参考にしながらgroongaを使った全文検索ページを作ってみてはいかがでしょうか。
最後に、コマンド列でサンプルアプリケーションのセットアップの方法を示します。
% sudo gem install groonga % cp -r `gem environment gemdir`/gems/groonga-0.0.6/example/ ./ % cd example/search % ../index-html.rb data/database ~/public_html/ # 最後の引数はHTMLのあるディレクトリ % rackup config.ru % firefox http://localhost:9292/
*1 そういえば、先日、Ruby-GNOME2 0.19.1がリリースされました。