株式会社クリアコード > ククログ

ククログ

«Pikzie 0.9.5リリース 最新 2009-08-07»
タグ:

groongaをRackに載せて全文検索

Ruby/groongaのサンプルアプリケーションのデモを用意しました。

RailsなどのWebアプリケーションフレームワークを使うほどのものではないので、ActiveGroongaは使わずに、Ruby/groongaとRackの組み合わせになっています。Rackについてはyharaさんの5分でわかるRackなどを読んでみてください。

デモはPassengerで動かしています。PassengerにRackを設置したことがある人なら10分もかからずにサンプルを動かせるのではないかと思います。

機能

デモを見てもらえばわかる通り、小さなサンプルですが以下のように一通りの機能は備えています。

  • 複数キーワードによる絞り込み
  • スコア順による並べ替え
  • 検索キーワードの正規化(「Ruby」でも「ruby」でも検索可能)
  • キーワード周辺の文章の表示

それぞれ、もう少し詳しく見ていきましょう。

複数キーワードによる絞り込み

通常の検索サイトでは空白で複数のキーワードを区切ることによって検索結果を絞り込むことができます。例えば、「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ステップだということがわかります。

  1. Groonga::Snippet.new
  2. snippet.add_keyword
  3. snippet.execute

簡単ですね。

まとめ

サンプルアプリケーションを例にして、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がリリースされました。

つづき: 2009-12-21
タグ: Ruby
2009-07-31

«Pikzie 0.9.5リリース 最新 2009-08-07»
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|
タグ: