隔週連載groonga

第5回Rubyでサーバ要らずの高速全文検索! - rroongaの紹介

前回のMilkodeでの事例紹介では、Rubyでrroongaを使ってソースコード検索エンジンを実装している事例を紹介しました。Milkodeは全文検索エンジンを組み込むことにより、大量のファイルに対しても高速な検索を実現しています。rroongaを使った代表的なアプリケーションの1つです。プログラマにとってとても便利なアプリケーションなので、ぜひ使ってみてください。

前回はユーザ視点からのrroongaの紹介でしたが、今回は違った角度から紹介します。rroongaの歴史、大事にしていることについて説明します。

自分のアプリケーションで利用するプロダクトを検討するときに、プロダクトがどのような方向で作られているかを考慮していますか? 自分のアプリケーションが大事にしたいことをそのプロダクトでも大事にしているなら、相性がよいかもしれません。さて、rroongaはあなたが大事にしたいことを大事にしているでしょうか。

rroongaとは

rroongaはRubyで高速な全文検索機能を使うためのオープンソースソフトウェアです。groongaは全文検索サーバとしても全文検索ライブラリとしても使えますが、rroongaは全文検索ライブラリとして使っています。C言語で書かれたgroongaライブラリをRubyから使えるようにするモジュールがrroongaです。

他の多くの全文検索機能はサービスとして提供されていますが、rroongaはライブラリとして提供されています。これが大きな違いです。類似の構成はsqlite3 gemです。rroongaとsqlite3 gemを比べると、APIで使えるのでrroongaのほうがよりRubyらしく使えます。

全文検索機能の提供方法
Rubyで使える全文検索機能とその機能の提供方法。大きくわけると同一プロセスで提供するタイプと外部サーバで提供するタイプがある。

ここでは同一プロセスで使うタイプと外部サーバにアクセスして使うタイプにわけています。ざっくりいうと、1台で完結するアプリケーションの場合は同一プロセスの方が便利で、そうでない場合は外部サーバのほうが便利です。⁠1台で完結する」とは、1台で扱えるデータの量であり、かつ、アプリケーションが1台のコンピュータ上で動けば十分ということです。同一プロセスモデルの良し悪しは機会があれば別の回に説明します。

ところで、この連載が「mroongaの事例紹介の回の次はmroongaについての解説⁠⁠、⁠rroongaの事例紹介の回の次はrroongaについての解説」となっていることに気づいていたでしょうか? 同一プロセスモデルの良し悪しに興味のある方はぜひrroongaの事例紹介を執筆してください! 応募方法はこの記事の最後にあります。待っています!

話を戻すと、rroongaが全文検索機能をどのように提供しているかを他のソフトウェアと比べて説明しました。次は、rroongaの歴史、rroongaが大事にしていることを説明します。しかし、ちょっと待ってください。その前に別の大事なことを説明しないといけません。

rroongaの読み方

大事なこととは「rroonga」の読み方です。読み方を知らないと他の人に紹介もできませんからね。⁠rroonga」「るるんが」と読みます。名前の由来は「Ruby」「groonga」です。⁠Ruby」「R」「groonga」「roonga」を組み合わせて「rroonga」です。

なお、⁠○roonga」という名前がついたミドルウェアやライブラリは「ぐるんが族」「ぐるんがファミリー」などと呼ぶことがあります。内部的に使われることがほとんどなので通常は目にすることはないでしょうが、もし目にしたときは「groonga⁠⁠、⁠mroonga⁠⁠、⁠rroonga」などのことを指していると認識してください。せっかくなのでこの記事の中で使ってみました。さて、次に出てきたときに覚えているでしょうか。

rroongaの読み方はわかりましたね。これからは「rroonga」と見たら「るるんが」と読んでください。

rroongaの歴史

rroongaの歴史を説明します。

SennaのRubyバインディング

groongaの前身がSennaだったということは第1回で紹介しました。実は、Sennaの頃にもRubyからSennaを使うためのモジュールがありました。そのモジュールには特に名前はついておらず、⁠SennaのRubyバインディング」などと呼ばれていました。

SennaのRubyバインディングはSWIGというツールを使って自動生成していました。SWIGはC/C++のヘッダーファイルを解析して、C/C++で提供している機能をRubyから使うためのコードを自動生成します。このため、SWIGを使えば少ない手間でRubyバインディングを作ることができました。しかし問題がありました。SWIGでCのヘッダーファイルを解析して作ったRubyバインディングは、RubyらしくないAPIになるのです。

Cのヘッダーファイルに対してSWIGを使うと、Cの関数とRubyのモジュール関数が1対1に対応するコードが出力されます。Rubyのクラスやメソッドは定義されません。これは、Cにはクラスやメソッドのための記法がないためです。SWIGはCのヘッダーファイルにある情報からだけではRubyのクラスやメソッドを定義することができません。そのため、⁠単にRubyからCの関数を呼べる」という低レベルのAPIだけができます。Rubyはオブジェクト指向スクリプト言語なので、この低レベルのAPIはとても使いにくいAPIです。

この問題を解決するために次のような工夫をしていました。

  • Cのヘッダーファイルを直接解析するのではなく、SWIG用の情報も加えて解析する
  • SWIGで作ったモジュール関数をRubyレベルでラップしてRubyらしいAPIを提供する

どちらもSWIGを使いながらRubyらしいAPIを提供するためによくやる工夫です。しかし、これらの工夫をしてもまだぎこちないAPIは残っていました。SennaのCのAPIを1つ1つRubyのメソッドに対応付ける作業はしなくてもよいのですが、代わりに、SWIGが自動で対応付けたRubyのモジュール関数をRubyらしいメソッドにラップしていく作業が必要になります。そのラップしていく作業でカバーしきれていない部分があったということです。

このことから、Rubyらしい使いやすいAPIを提供するなら、SWIGを使っても使わなくても手間はそれほど変わらないということがわかりました。そこで、rroongaではSWIGで自動生成することをやめて手で書くことにしました。groongaが提供している関数は2013年5月17日執筆時点で300ほどありますが、大きく増えるということはあまりありません。自動生成に比べて、手で書くと最初は大変です。しかし、API変更があまりないため、groongaの変更への追従はそれほど大きな負担ではありません。それよりも、よりRubyらしいAPIを提供できることが魅力です。

このような経緯で、rroongaは自動生成を使わずに「groongaのRubyバインディング」を実現しています。では、RubyらしいAPIとはどのようなAPIでしょうか。それは、後半で説明します。楽しみにしていてください。

2番目のぐるんが族

実は、rroongaの歴史はgroongaの歴史と同じくらい古いのです。

groongaは2009年3月13日に最初のバージョン0.0.2がリリースされました。rroongaは翌月の肉の日である4月29日に最初のバージョン0.0.1がリリースされました。第3回で紹介したmroongaよりも前です(mroongaは1年5ヶ月後の2010年8月19日に最初のバージョン0.1がリリースされています⁠⁠。

rroongaは本当に初期の段階からgroongaと共に成長してきているのです。知っていましたか?

rroongaに改名した理由

rroongaは、リリース当時は「Ruby/groonga」という名前で、gem名も「groonga」でした。しかし、⁠groonga」という名前が入っているとgroonga本体なのかgroongaのRubyバインディングなのか紛らわしいという意見があり、ライブラリ名もgem名も「rroonga」になりました。これが、最初のリリースから1年経った2010年4月のことです。

groonga、mroongaと同じようにrroongaも名前を変えていますがいくつか違う点があります。

1つは名前を変えた経緯です。groongaやmroongaは開発側の意向で名前を変えていましたが、rroongaはユーザ側からの意見を参考に名前を変えました。そのため、ユーザ側は混乱しませんでした。

もう1つは移行の敷居が低かったことです。rroongaはgemでインストールすることがほとんどです。gemには「依存関係」という仕組みがあるため、それを使ってユーザが意識しなくてもrroongaへ移行できるようにしました。名前変更前のgem「groonga」の依存関係に名前変更後のgem「rroonga」を加えました。こうすることより、groonga gemをインストールすれば、最新版のrrooongaを使えます。

このような理由から、groonga、mroongaの名前変更に比べてrroongaへの名前変更は問題が少なかったです。むしろ「○roonga」という名前になって統一感がでました。そのため、rroongaにとってはこの改名はよかったです。

これがrroongaの歴史です。どんな歴史をたどってきたか、イメージをつかめたでしょうか。

rroongaが大事にしていること

rroongaが大事にしていることは次の2点です。

  • groongaの速度をできるだけ阻害しないこと
  • Rubyで使いやすいこと

それぞれ順に説明します。

groongaの速度をできるだけ阻害しないこと

groongaの特長の1つは「高速であること」です。Rubyからgroongaを使う場合、groongaを直接Cから使うときに比べてオーバーヘッドが発生することは確実です。実行している処理が増えているのでこれは当然です。しかし、特長と呼べなくなるぐらい速度が落ちてしまっては悪影響のほうが大きすぎます。そのため、rroongaではgroongaの速度をできるだけ阻害しないことを大事にしています。

具体的には次のような工夫をしています。

  • 一時的なコピー領域の再利用
  • Rubyレベルでループを回さない

一時的なコピー領域の再利用

groongaの世界のデータをRubyの世界に持っていくときはデータをコピーする必要があります。逆に、Rubyの世界のデータをgroongaの世界に持っていくときもデータをコピーする必要があります。理由は2つあり、メモリ管理の仕組みと整合性をとるためとデータを変換する必要があるためです。コピーをするための領域を再利用することでオーバーヘッドを少なくしています。

Rubyのオブジェクト(Stringなど)はRubyの世界の中でメモリ管理をしています。Rubyの外の世界のメモリをRubyの世界ではうまく管理できません。いつまで長生きするメモリかわからないからです。そのため、groongaの世界のデータはRubyのメモリ管理の仕組みを使ってコピーし、Rubyのメモリ管理の仕組みの中で管理します。

Rubyの世界のデータとgroongaの世界のデータは形式が違うため、相互に変換をする必要があります。そのとき、元のデータを破壊的に変更して変換すると変換元の世界で不整合が発生します。そのためデータをコピーして変換します。ただし、groongaの世界とRubyの世界の「間」の変換処理は一時的なものです。つまり、ここで使う領域は再利用できます。領域を再利用できると、コピーすることは変わりませんが、動的にメモリを割り当てる回数が減ります。動的にメモリを割り当てる処理は重い処理なので、この回数が減るとオーバーヘッドがかなり小さくなります。

Rubyの世界とgroongaの世界の間のデータのやりとり
Rubyの世界とgroongaの世界の間のデータのやりとり。rroongaがそれぞれの世界のデータを変換するために一時的な領域が必要になる。一時的な領域を再利用することで変換時のオーバーヘッドを小さくしている。

この一時的なコピー領域はテーブル単位・カラム単位で1つ持っています。つまり、同じテーブル・カラムに対して値の出し入れを何度も繰り返すときほど効果が大きくなるということです。

ところで、テーブル単位・カラム単位で1つだけで大丈夫なのかと心配になった人もいるのではないでしょうか? これは大丈夫です。Rubyは同時に複数のスレッドが動かないため、1つで十分なのです。

Rubyレベルでループを回さない

オーバーヘッドを少なくするためには、Rubyレベルで処理する量を少なくします。特にループを少なくします。ループでは同じ処理を何度も実行するため、1回の処理時間が少なくても回数が多くなればオーバーヘッドは大きくなります。

では、groongaでループが必要になるときはどんなときでしょうか。それは、検索するときと検索結果に対する処理を実行するときです。

rroongaで検索するときは次のようにテーブルオブジェクトのselectメソッドを使います。

bookmarks = Groonga["Bookmarks"] # "Bookmarks"テーブルを取得
bookmarks.select do |bookmark|
  # http://gihyo.jp/内のブックマークのうち
  bookmark.url.prefix_search("http://gihyo.jp/") &
    # コメントに「groonga」を含むブックマークを検索
    (bookmark.comment =~ "groonga")
end

selectが次のように動くことを想像したのではないでしょうか?

class Table
  def select
    matched_records = []
    each do |record|
      matched_records << record if yield(record)
    end
    matched_records
  end
end

しかし、実際は違います。次のように動きます。ポイントはRubyレベルでは検索式を作るだけで、実際の検索の詳細はgroongaに丸投げしているところです。なお、このコードはイメージなので、実際のクラス名やメソッド名は少し違うことに注意してください。ユーザが詳細を意識する必要はないのでわかりやすそうな名前を選びました。

class Table
  def select
    # 検索式を生成するオブジェクトを作る
    expression_builder = ExpressionBuilder.new
    # selectに指定したブロック内で検索式を作る
    # 前の例を再掲:
    #   bookmarks.select do |bookmark|
    #     bookmark.url.prefix_search("http://gihyo.jp/") &
    #       (bookmark.comment =~ "groonga")
    #   end
    # このブロックの場合は「bookmark」は実は
    # ExpressionBuilderでレコードそのものではない
    # ExpressionBuilderのメソッドを呼ぶことで
    # Rubyの構文で検索式を指定している
    expression_builder = yield(expression_builder)
    # ブロックで作ったExpressionBuilderをコンパイルして検索式を作る
    expression = expression_builder.compile
    # 検索式をgroongaに渡してgroongaレベルで検索
    select_by_groonga(expression)
  end
end

このように検索式だけをRubyレベルで作り検索処理をgroongaに任せることにより、Rubyレベルでループを回さずに済みます。

groonga内部でもできるだけループが少なくするようにしています。検索するときは、できるだけループを少なくするために、テーブルを全件スキャンするのではなくインデックスを使って検索しています。インデックスを使えるかどうかはrroongaから渡された検索式を解析して判断します。

groonga内部で効率よく処理できるようにがんばっているため、rroongaはできるだけgroongaに処理を任せることが基本です。

検索結果に対する処理を実行するときも、できるだけgroongaで処理をします。例えば、集合の演算(和や積など)をするときはgroongaの集合演算機能を使います。これは、複数の検索結果をまとめるときに使います。グループ化やソートをするときもgroongaの機能を使います。

ただし、最終的な検索結果を1つずつ処理するところはRubyレベルでループを回す必要があります。ここでは、必要な分だけループを回すようにしてください。

おさらい

一時的なコピー領域を再利用したり、Rubyレベルでループを回さない工夫をすることにより、rroongaはgroongaの速度を阻害しないような作りになっています。

Rubyで使いやすいこと

rroongaが大事にしていることの2つめはRubyで使いやすいことです。特に次の2つを大事にしています。

  • インストールしやすいこと
  • RubyらしいAPIであること

インストールしやすいこと

ライブラリを使う場合はまずインストールします。ここでつまずいてしまうと、いくらRubyらしいAPIだろうが関係ありません。使えないんですから。rroongaはRubyスクリプトだけのライブラリではなく、Cで書かれた部分もあるライブラリです。そのため、Rubyスクリプトだけのライブラリに比べるとインストールが難しくなっています。

そこで、できるだけインストールでつまずかない工夫をしています。

Cで書かれた部分があるとインストールの難易度がものすごく高くなるのがWindowsです。そのため、Windows用にはビルド済みのgroongaとrroongaを含んだgemを用意しています。WindowsユーザはRubyさえ用意していればgem install rroongaでインストールできます。事前にgroongaをインストールしておく必要もありません。

ところで、Windows用のRubyパッケージにはいくつか種類がありますが、みなさんは何を使っていますか? 多くの人はRuby Installer for Windowsを使っていることでしょう。他にも64ビットWindows用のRails環境を提供する能楽堂などがあります。能楽堂にはrroongaが同梱されているので、gem install rroongaしなくてもすぐに使えます。

Windows以外の環境でもインストールでつまづかない工夫をしています。LinuxやOS X、*BSD環境ではコンパイル環境があることを前提にしています。コンパイル環境さえあれば事前にgroongaをインストールしなくてもrroongaをインストールできるようにしています。どうしているのかというと、rroongaをビルドするときにgroongaがインストールされていなければ、自動でgroongaのソースをダウンロードしてビルドし、その後にrroongaのインストールを再開します。

どちらの工夫も、ポイントは「通常のインストール方法でインストールできる」ことです。通常のインストール方法とは「gem install gem名」です。⁠事前にgroongaをインストールしてください」というのは通常のインストール方法から外れています。groongaを意識させてはいけません。

rroongaを使いたいRubyプログラマは「全文検索をしたい」ということが目的であり、そのためのライブラリをどうやって用意するかはあまり考えたくないことです。通常のインストール方法であれば特に考えずに済みます。本来であれば「groongaのインストール」という事前のひと手間が必要なrroongaのインストールですが、そのひと手間をなくしてインストールしやすくしています。

このように、rroongaはインストールしやすくすることを大事にしています。

RubyらしいAPIであること

もう1つの大事にしていることはRubyらしいAPIであることです。

RubyらしいAPIとはRubyの既存のオブジェクトと同じような使い方になっているAPIということです。例えば、1つずつ繰り返し実行するイテレータにforeachという名前をつけていればそれはRubyらしくありません。ArrayやHashなど既存のクラスはeachという名前を使っています。同じように動くメソッドなら同じような名前を使っているのがRubyらしいAPIです。

rroongaはライブラリとして全文検索機能を提供しています。つまり、Rubyのコードの中でrroongaのオブジェクトのメソッドが使われるということです。既存のRubyのコードの中に入って全文検索処理をしても、他のRubyのコードと違和感がない状態を目指しています。

簡単な例として、レコードを表すGroonga::Recordの場合を紹介します。

レコードには複数のカラムがあり、検索結果を表示するときはカラムの値を取得します。そのままのAPIにすると次のようになります。

user_name = user.get_column_value("name")

get_XXXというメソッド名はRubyではあまり見ないメソッド名です。そのため、他のRubyコードの中に入ると違和感があります。

レコードを複数のキーとバリューのペアを持つコンテナと考えるとどうでしょうか。RubyにはそのようなクラスとしてHashがあります。HashのAPIを参考にすると次のようになります。

user_name = user["name"]

get_column_valueよりもだいぶRubyらしくなりましたね!

では、これで十分でしょうか? レコードを複数の属性と値を持つオブジェクトと考えるとどうでしょうか。そう考えると次のようなAPIになります。

user_name = user.name

これもRubyらしいですね!

なお、rroongaはuser["name"]user.nameもどちらも使えるようになっています。これは、カラム名には-も含めることができるからです。Rubyのメソッド名で-を使うときは次のように特別な使い方をしなければいけません。

user_created_at = user.send("created-at")

これではRubyらしくありません。メソッド名に使えない文字をカラム名に使っているときは、user["name"]という使い方ができるように両方の使い方を用意しています。通常はuser.nameスタイルを使って、それができないとき、あるいはnameの部分が可変のときはuser["name"]スタイルを使うことを推奨しています。

まとめ

rroongaの歴史と大事にしていることを紹介しました。rroongaのサイトにチュートリアルがありますので、試してみたくなった人はぜひそちらを読んでみてください。

次回は事例紹介です。Tritonnからmroongaへ移行したという事例です。2回分のボリュームでじっくり紹介します! 楽しみですね。

おすすめ記事

記事・ニュース一覧