いらないキャッシュを消すことでRubyスクリプトが倍速で動作するようになった話です。
先日、るりまサーチをRackspaceのクラウドサーバー(メモリ1GB)からさくらのVPS 512(メモリ512MB)に移行しました。理由はRackspaceのサーバーが遠くにあるのでレスポンスがもっさりするからです。最速検索がウリなのにこれでは遅いのでないかと誤解されてしまいます。さくらのVPSにしたら近くにあるためサクサクとレスポンスが返ってくるようになりました。しかも、リソース(主にメモリ量)はスケールダウンしているのにです。
るりまサーチはバックエンドの全文検索エンジンgroongaが高速に動作するのとそれほどアクセスがない(!)ため、CPUやI/Oがネックになることはありません。それよりも、ほとんどメモリを搭載していないマシンで動かしているため、ボトルネックになるとすればメモリです。実メモリ以上にメモリを使おうとしてスワップを使い始めると遅くなります。
るりまサーチに一番負荷がかかるのは1日1回のるりまデータの更新です。もう少し言うとBitClustでHTMLを生成する処理が一番負荷が高いのです。このとき、BitClustのHTML生成プロセスはCPUを100%使い*1、500MB程度のメモリを使用します*2。Rackspaceのときのように1GBのメモリが載っているサーバーであれば耐えられますが、さくらのVPS 512のように512MBしかメモリが載っていないサーバーでは致命的です。スワップを使いはじめてI/O待ちが多発し、システムの応答性が著しく悪化します。
ということで、BitClustがHTMLを生成するときのメモリ使用量を改善したのですが、そのときに2倍くらい高速に動作するようになりました。その高速化の方法を一言でいうと「いらないキャッシュを消す」です。具体的な変更点でいうとr4873(4行)とr4874(1行)です。
キャッシュはメモリを通常より使って処理時間を短くする方法なので、キャッシュを消すことでメモリ使用量が減るのは納得できます。しかし、処理時間も短くなるのは意外ですよね。
なお、メモリ使用量と実行時間は以下のようになりました。Ruby 1.8.7の場合も1.9.3の場合もメモリ使用量は半分以下、実行時間はほぼ半分になっています。
| RSS | VSZ | 実行時間 | |
|---|---|---|---|
| 変更前(Ruby 1.9.3) | 187MB | 237MB | 55秒 |
| 変更後(Ruby 1.9.3) | 84MB | 134MB | 38秒 |
| 変更前(Ruby 1.8.7) | 525MB | 558MB | 1分52秒 |
| 変更後(Ruby 1.8.7) | 132MB | 165MB | 59秒 |
メモリ使用量が減って速くなった場合はだいたいGCに関係していると相場が決まっています。では、こんなときはどうやって確認したらよいでしょうか。Ruby 1.9.2以降にはGC::Profilerという便利なモジュールが追加されているのでこれを使います。
GCがどんな感じで実行されているかを調べたい処理の前でGC::Profiler.enableをし、処理を実行した後にGC::Profiler.reportで結果を表示します。
1 2 3 |
GC::Profiler.enable # ... GCについて調べたい処理 ... GC::Profiler.report |
GC::Profiler.reportは以下のようにGCの統計結果を表示してくれます。
GC 445 invokes.
Index Invoke Time(sec) Use Size(byte) Total Size(byte) Total Object GC Time(ms)
1 0.092 964640 2257680 56442 0.00000000000000000000
...
まず、いらないキャッシュを消さない場合の結果を見てみましょう。
GC 341 invokes.
Index Invoke Time(sec) Use Size(byte) Total Size(byte) Total Object GC Time(ms)
1 0.080 964600 2257680 56442 0.00000000000000000000
...
27 0.348 1893800 4057280 101432 4.00100000000003230838
...
50 0.764 3283520 7296560 182414 8.00100000000003674927
...
73 1.588 6627960 13120720 328018 12.00099999999992839150
...
106 3.720 11035000 23607480 590187 32.00199999999986744115
...
124 5.532 19510200 42486920 1062173 56.00399999999972067144
...
151 9.741 36667720 44008400 1100210 96.00600000000092393293
...
187 16.541 50259800 62249800 1556245 128.00800000000123191057
...
237 28.470 61845360 72409360 1810234 176.01100000000258205546
...
328 53.351 63548120 80262160 2006554 148.00900000000183354132
オブジェクト数が増える毎にGCにかかる時間が増えています。
次に、いらないキャッシュを消した場合の結果を見てみましょう。
GC 445 invokes.
Index Invoke Time(sec) Use Size(byte) Total Size(byte) Total Object GC Time(ms)
1 0.092 964640 2257680 56442 0.00000000000000000000
...
27 0.376 1893800 4057280 101432 8.00099999999997990585
...
50 0.824 3283520 7296560 182414 12.00100000000004030198
...
73 1.644 6627400 13120720 328018 12.00099999999992839150
...
106 3.928 6736400 13120720 328018 20.00100000000015754154
...
256 14.061 10642920 23607480 590187 32.00199999999853162080
...
302 18.561 18979640 42486920 1062173 52.00299999999913325155
...
432 36.330 25030040 42486920 1062173 48.00300000000135014488
トータルのGC回数は増えていますが、オブジェクト数の最大値が増えていかないため、1回のGCにかかる時間が短くなっています。この差が全体の実行時間に効いてきているんですね。
速くするためにやみくもにキャッシュしていると、生きているオブジェクト数が多くなるためにGC時間が長くなってしまい、逆に遅くなることがあります。キャッシュしたのに思ったより速くならなかった場合は必要なくなったキャッシュを持ち続けていないか確認し、いらないキャッシュを消すことを試してみましょう。ただし、ある程度のオブジェクト数をキャッシュしていない場合はあまり関係がないでしょう。
ApacheとPhusion PassengerでWebアプリケーションをデプロイしている際に、たまにシステムが不安定になることがありました。調査したところ、原因はレスポンスの取得が遅いクライアントであるということが分かりました。この問題を発見し、原因を特定し、Phusion Passengerを修正するまでについて、紹介します。
るりまサーチではApacheとPhusion Passengerを使っているのですが、たまに動作が不安定になることがありました。その不安定の原因を調査することにしました。
調査した結果、遅いクライアントがるりまサーチに接続するのが不安定になるトリガーになっていることが分かりました。
以下に、詳しく説明します。
Phusion Passengerは、遅いクライアントが接続していると、他のリクエストの処理を行いません。その間に、他のクライアントからのリクエストが処理待ちとして大量に溜まります。そして遅いクライアントがレスポンスを受け取り終わると、Phusion Passengerは、一斉に溜まった処理待ちのリクエストを処理し始めるため、システムのリソースを急激に消費します。そして、システムが不安定になります。
この問題の原因を解決するためには、遅いクライアントのリクエストを処理している際にも、他のリクエストの処理ができればいいということになります。
遅いクライアントの時の状況を詳しく説明します。遅いクライアントがレスポンスを受け取るのを待っている間は、サーバー側ではレスポンスの作成の処理は既に完了しているということになります。それにも関わらず他のリクエストの処理を行わないのです。つまり、その間、サーバーは実質的には何も処理を行っていません!
この時間は、無駄な時間と言えます。
なので、この無駄な時間をなくすようにする必要があります。具体的には、Webアプリケーションからの作成されたレスポンスを一旦Phusion Passengerがバッファに保存し、次のリクエストの処理に移ります。遅いクライアントには、そこのバッファから、ゆっくりレスポンスを返せばいいのです。
実は、このような挙動はNginxとPhusion Passengerの構成でデプロイされた場合には実現されています。ですが、ApacheとPhusion Passengerの構成の場合にはそうでは無いのです。
解決の方法が分かったので調べたところ、Phusion Passengerのバグトラッキングシステムにこの問題はすでに登録されていました。
さらにそこには、この問題の解決方法が記載されていました。コメント#3の情報によるとソースコードを書き換えればいいということが分かりました。ソースコードを書き換えることで、Nginxと同じように、Apacheでもレスポンスをバッファに保存する挙動に変更できます。
実際に、るりまサーチで検証したところ、問題は解決されました。遅いクライアントが接続してきた際にも、処理が止まらず、別のリクエストを処理するようになりました。
問題は解決されましたが、この問題を解決するためには、ソースコードを書き換えなければいけません。端的に言って、これは不便です。上のバグレポート内に書いてあるように設定ファイルからレスポンスをバッファに保存するかどうかを切り替えられると便利です。
なので、設定できるようにPhusion Passengerを修正したパッチを作成しました。そしてそのパッチをPhusion Passengerに取り込んでもらうように依頼をしました。
(追記: Fri Nov 25 03:05:28 2011 JSTにPhusion Passengerに無事に取り込んでもらえました。使えるようになるのは、まだリリースされていませんが次のリリースバージョンである、3.0.10からの予定です)
(追記: November 28th, 2011にこの修正が取り込まれたPhusion Passengerの3.0.11がリリースされました(3.0.10はバグがあったためにスキップされました)。
この修正によって、レスポンスをバッファに保存するかどうかを次のように設定することができるようになります。
<VirtualHost *:80>
ServerName www.yourhost.com
DocumentRoot /somewhere/public
PassengerBufferResponse on
# PassengerBufferResponse off # レスポンスのバッファへの保存をしたくない場合。
<Directory /somewhere/public>
AllowOverride all
Options -MultiViews
</Directory>
</VirtualHost>
遅いクライアント問題はレスポンスをバッファに保存することで解決できます。半面、レスポンスをバッファに保存すると、次の副作用が発生するので注意が必要です。
どちらも、Phusion Passengerがレスポンスをメモリ上のバッファに一旦保存してからクライアントに返すようになるためです。
ApacheとPhusion PassengerでデプロイされたWebアプリケーションが不安定になるという問題を調査し、Phusion Passengerを修正しました。
同じような問題に遭遇している場合には、ぜひ試してみてください。
オープンソースカンファレンス2011 DBのOSSDB MySQLセッションでgroongaストレージエンジンについて紹介してきました。
内容はgroongaストレージエンジンが得意なシチュエーションについてベンチマークデータを紹介するというものです。どういうときにgroongaストレージエンジンが高速に動作するかがわかります。
groongaストレージエンジンは以下のような処理が得意です。
groongaストレージエンジンの性能特性を紹介するためにベンチマークデータを紹介しました。ベンチマークはこれらの得意な処理を実行するシチュエーション向けに複数のパターンで行いました。
groongaの全文検索処理の性能を示すためにtwitterから取得したデータを利用しました。測定する処理はフレーズ検索です。約100万件のtweetに対して「"facebook saved"」というような2単語でフレーズ検索します。このようなフレーズ検索を1万回(1万パターン)実行するためにかかった時間が縦軸になっており、グラフが短いほど高速に全文検索が実行されていることを示しています。
groongaストレージエンジンの方がMySQLの開発版5.6.3-labs-innodb-ftsに含まれるInnoDBの全文検索機能よりも10倍程度高速で、MyISAMよりは2倍程度高速でした。
groongaの位置情報検索処理の性能を示すために国土交通相の位置参照情報ダウンロードサービスの2010年版のデータを利用しました。測定する処理は「MBRContains(GeomFromText('LineString(139.850124 38.718204, 140.447158 37.817489)'), location)」というように位置情報でレコードを絞り込み、「ORDER BY name」というように住所でソートする検索です。このような検索を千回(千パターン)実行するためにかかった時間が縦軸になっており、グラフが短いほど高速に位置情報検索が実行されていることを示しています。
groongaストレージエンジンのほうがMyISAMよりも40倍程度高速でした。
groongaはリアルタイム更新が得意です。リアルタイムで更新するために以下の2点を重視しています。
まず、更新性能が高いことを確認し、次に検索負荷が高いときの更新性能を確認します。
高速に更新できることを示すために、98万件のtweetが登録されたデータベースを用意します。このデータベースに対して、2万件のtweetを登録します。このとき1秒あたりに追加したレコード数が縦軸になっており、グラフが長いほど高速に登録されていることを示しています。
groongaストレージエンジンのほうがInnoDBよりも3倍程度高速、MyISAMよりも2倍程度高速、Sphinxよりも3倍程度高速でした。
検索負荷が高いときでも更新性能が落ちないことを示すために、98万件のtweetが登録されたデータベースを用意します。このデータベースに対して、検索負荷(クエリ数/秒)を変えながら2万件のtweetを登録します。このグラフはそれぞれのストレージエンジン毎に見ます。横軸が検索負荷を表していて、左側になるほど検索負荷が小さく、右側になるほど検索負荷が高いことを示しています。グラフが水平になっているほど検索負荷が高くなっても更新性能が落ちていないことを示しています。
groongaストレージエンジンとInnoDBは検索負荷が高くなっても更新性能はそれほど落ちておらず、MyISAMとSphinxは更新性能が落ちていました。
このように、高速に動作するgroongaストレージエンジンですが、以下のように機能制限があります。
そのため、どのようなケースにでも利用できるわけではありません。しかし、groongaストレージエンジンは他のストレージエンジンと組み合わせて使うことができるため、上記の機能制限の一部を解消することができます。
groongaストレージエンジンを他のストレージエンジンと使う場合は全文検索処理・位置情報検索処理のみをgroongaストレージエンジンが行い、それ以外の処理は連携した他のストレージエンジンが行います。そのため、groongaストレージエンジンとInnoDBを一緒に使うと、トランザクションはInnoDBの機能を用いて、全文検索はgroongaストレージエンジンを用いる、ということができます。この仕組みを使うと「トランザクションをサポートしていない」というgroongaストレージエンジンの機能制限を解消することができます。
ただし、更新性能は組み合わせて使うストレージエンジンの性能に依存するため、groongaストレージエンジンの得意な処理である「リアルタイム更新」性能は発揮できません。「高速な全文検索機能」と「高速な位置情報検索機能」のみ利用できます。
InnoDBと組み合わせて利用した場合のベンチマークデータを以下に示します。
まず、全文検索の性能です。
groongaストレージエンジン単体で使った場合とほとんど同じ性能がでています。
次に、位置情報検索の性能です。
こちらもgroongaストレージエンジン単体で使った場合とほとんど同じ性能がでています。
次に、更新性能です。
groongaストレージエンジン単体で使った場合よりも大きく性能が落ちて、組み合わせて使っているInnoDBと同じ程度の性能になっています。
最後に検索負荷が高いときの更新性能です。
InnoDBが検索負荷が高くても更新性能がほとんど落ちないため、groongaストレージエンジンとInnoDBを組み合わせて使った場合でも更新性能がほとんど落ちていません。
OSC2011.DBでgroongaストレージエンジンが得意なシチュエーションのベンチマーク結果を紹介してきました。もちろん、groongaストレージエンジンが得意ではないシチュエーションもあり、そのようなケースでは他のストレージエンジンの方が性能がよくなります。groongaストレージエンジンが苦手なケースについては今月末(2011/11/29)開催の全文検索エンジンgroongaを囲む夕べ 2で紹介する予定です。興味のある方はこちらに参加してみてください。すでに定員を超えていますが、前回の全文検索エンジンgroongaを囲む夕べ #1では最終的に35名のキャンセルになっていましたので、今からでもギリギリ参加できるのではないでしょうか。
groongaストレージエンジンのより詳しい情報についてはgroongaストレージエンジンのサイトも参照してください。
オライリーより、Firefoxの高度な使い方からアドオン開発のノウハウ、新しいWeb技術まで手広く解説・紹介する書籍「Firefox Hacks Rebooted」が、2011年10月26日に発売されました。弊社でMozillaサポート事業に従事している下田も執筆者の一人として名を連ねています。
Firefox Hacks Rebooted ―Mozillaテクノロジ徹底活用テクニック
オライリージャパン
¥ 3,570
内容が多岐に渡るため、「こういう人に読んでもらいたい!」という想定読者が章ごとにそれぞれ異なるのですが、全体を見た時の内容の充実度からは、Firefox用アドオンの開発に関心がある方に特にお薦めと言えるでしょう。そこでこの記事では、開発者視点から本書の見所をいくつか紹介します。
目次を順番に眺めて目を引くのは、2章におけるVimperatorとKeySnailの解説でしょう。VimとEmacsといえば開発者が使う開発環境の二大巨頭で、よくエディタ戦争のネタにもされますが、本書の2章でも、Vimを模したVimperatorとEmacsを模したKeySnailの戦争が繰り広げられています。
……というのは冗談なのですが、Vimperator開発メンバーの一人であるteramakoさんがVimperatorを、KeySnailの作者であるmoozさんがKeySnailをそれぞれ解説し、最後に二人がそれぞれの設計思想の違いを解説するという構成になっていて、こと両アドオンの解説記事としてはこれ以上無い豪華な内容と言えるでしょう。開発者自らによる解説という事で、内容の信頼性の高さも折紙付です。
快適な開発生活を送る上では、効率の良い情報収集や情報発信の方法を知る事も大切です。また、情報は発信する人の所に集まるとも言います。最新の技術へのフォローを欠かす事ができない開発者の人達にとっても、この章の内容は役立つのではないでしょうか。
3章は、FireGesturesなどの作者としても知られるGomitaさんによるAdd-on SDKの解説です。基礎概念の解説に始まり、少しずつ機能を付け足しながら実際に1つのアドオンを完成させるまでの過程をチュートリアル形式で紹介する事により、Add-on SDKを使ったアドオン開発の一通りの流れを理解する事ができます。
Firefox 4以降のバージョンでは高速リリースが採用された事により、従来の形式のアドオンの開発スタイルだとFirefoxの更新に追従するだけでも大変という状況になっています。Add-on SDKはFirefoxのバージョン間の違いを吸収する層としての働きも備えているため、これから新たにアドオンを開発する場合はSDKを利用した方が良いと言えますが、本章はそのファーストステップとして最適でしょう。
また、後の章にはHTML5関連技術などの新しいWeb標準技術の解説もあります。それらを組み合わせる事によって、SDKが提供している機能を超えた様々な事が実現できるようになります。本書は話題が多岐に渡っていますので、全体を通読する中でそういった発展に繋がるヒントを得やすいのではないでしょうか。
弊社所属の下田は、4章と6章の一部として、SDKに依らない・より低層の部分に関する技術情報を寄稿しています。
4章では、再起動のいらないアドオンを、これまでの一般的なアドオン開発の延長線上にある物として捉えた上で、これまで通りにできる事とそうでない事とを整理して、それらに対する対策となる様々なテクニックを紹介しています。
また、プロセス分離型の設計に移行していくにあたって、そもそもFirefoxではどのようにプロセスの分離が実現されているのかを理解し、プロセスが分離された環境ではどのような事に気をつけなくてはならないのかについても解説しています。
SDKの枠を超えた開発を行いたい時や、SDKに含まれている標準ライブラリの中で行われている事を理解したい時などには、各要素技術への理解が必要になってきます。そういった場合も、この章の情報が手がかりとなるかも知れません。
その他にも、非同期処理の記述を支援する軽量ライブラリ「JSDeferred」のFirefox上での活用事例や、CPUの使用率とメモリの使用量を表示するFirefoxアドオン「システムモニター」でも利用している、C言語で開発されたプラットフォームネイティブのライブラリをJavaScriptから透過的に利用する技術「js-ctypes」の解説(6章に収録)など、SDKベースでの開発にも応用可能な情報もあります。
本書には、Web上にあるFirefoxの断片的な情報を取っ掛かりとして、それらをさらにもう1段階・2段階と掘り下げた情報が多数収録されています。自分で情報を探そうとして挫折した方、見つけた情報が中途半端で途方に暮れてしまった方など、表面的な情報よりももっと本質的な情報を求めている方にお薦めと言えるでしょう。
なお、本書の目次や各章のサンプルがWebで公開されています。Firefoxのヘビーユーザーの方やアドオン開発者の方は、役立つトピックが含まれているかもしれませんので、興味を持たれた際には是非一度目を通してみて下さい。
今度の土曜日11/5に開催されるOSC2011.DBの10:35からのセッション「OSSDB MySQL」に少しおじゃましてgroongaストレージエンジンを紹介します。groongaストレージエンジンがイベントなどで紹介されるのは昨年の全文検索エンジンgroongaを囲む夕べ #1以来のはずなので約1年ぶりになります。その間に初のメジャーリリースである1.0.0がリリースされるなど、だいぶ成長しています。そのため、話題はたくさんあるのですが、その中から特に注目すべきところを選り抜いて紹介します。
groongaストレージエンジンについては1ヶ月後の全文検索エンジンgroongaを囲む夕べ 2でも詳しく紹介されますが、一足早く知りたい人はぜひ参加してください。(参加費用は無料ですが参加登録が必須なので注意してください。)
オフィス文書形式が要求されるようなドキュメントではなくて、自分が開発したライブラリのドキュメント(リファレンスマニュアルやチュートリアルなどライブラリのユーザーが読むためのドキュメント)の話です。以下の「ドキュメント」もそのような意味で使っています。
使いやすいライブラリを開発したかったらプログラムだけではなくドキュメントも書くべきです。
ドキュメントを書く習慣があるかどうかは開発者によってあったりなかったりです。使っているプログラミング言語に相関がある気もしますし、リリースするかどうかに相関がある気もします。理由はいろいろあるでしょうが、ドキュメントを書く習慣のない開発者の方が多いでしょう。
書かない理由はこんな感じでしょうか。
一方、書く理由はこんな感じでしょう。
だいたいあっていますか?しかし、使いやすいライブラリを開発したい場合は「ユーザーの視点でAPIを見直すため」にドキュメントを書くという視点を忘れてはいけません。
ドキュメントはライブラリのユーザーが読むものです。そのため、たとえ開発者がドキュメントを書くとしても、開発者の立場ではなく、ユーザーの立場になってドキュメントを書く必要があります。そうしないと「ユーザーが読んでもわからない」・「誰の役にたつのかわからない」ドキュメントになってしまいます。
ドキュメントを書くときはなるべく簡潔な記述になるように意識します。これは「なるべく情報を落とせ」ということではありません。「複雑な説明や長い説明がなくても使えるようにしろ」ということです。
例えば、WebページをPDFで保存するライブラリを作ったとします。もし、以下のようなドキュメントになっていたら要注意です。
まず、WebPrinterオブジェクトを作ります。
web_printer = WebPrinter.new
次にPDFで保存したいURLを設定してWebページをダウンロードします。
web_printer.url = "http://www.clear-code.com/"
web_printer.download
最後に出力PDFファイル名を指定してPDFを出力します。
web_printer.print("clear-code.pdf")
ドキュメントを書いていて「手順が多いな」・「たくさん書いているな」と気付けることが重要です。それに気付いたら「もっと簡潔にドキュメントを書けるようにするにはどんなAPIになっていればよいだろう」と考えてください。これをやるかどうかでライブラリの使い勝手はかなり変わります。
例えば、上述のWebPrinterは以下のように使えるようにすることもできます。
WebPrinter.printにPDFで保存したWebページのURLと出力PDFを指定します。
WebPrinter.print("http://www.clear-code.com/", "clear-code.pdf")
このようなAPIを提供するには以下のような便利メソッドを定義するだけですね。
1 2 3 4 5 6 7 8 9 10 |
class WebPrinter class << self def print(url, pdf_path) printer = new printer.url = url printer.download printer.print(pdf_path) end end end |
このように、ユーザーの立場で考えて、ユーザが使いやすいものになっているかという視点を意識しながらドキュメントを書くことであなたのライブラリはもっと使いやすいものになります。
ドキュメントを書くことがキライな開発者は「ドキュメントを書くよりもコードを書くことに時間を使った方がずっとよいソフトウェアになる」と思っているかもしれません。形だけのドキュメントを書くならたぶんそうなんでしょう。しかし、「ユーザが使いやすいソフトウェアか」という視点でドキュメントを書けば、それはよいソフトウェアの開発につながります。
なお、このような視点でドキュメントを書くとプログラムの改善作業が発生します。そのため、リリース前に慌ててドキュメントを書くとドキュメントもAPIも中途半端な状態のままリリース、あるいは、リリース日の延期になったりします。あらかじめ、ドキュメントを書く時間だけではなく、プログラムを改善する時間も見込んでおきましょう。
参考: Gaucheの開発者のshiroさんも別の視点で見るためにドキュメントを書くというようなことに言及しています。(ただし、「ユーザー視点」というわけではなさそうです。)
groongaにデータを登録して、インデックスを更新すると全文検索をすることができます。ここでは、groongaが内部でどのような処理をして全文検索をしているかを説明します。
まず、以下のように「Yes good」と「Hey good」という文書が登録されているとします。

このとき、「Yes good」で検索したらどうなるかを説明します。
まず、入力の「Yes good」をトークナイズします。このとき使用するトークナイザーは使用する転置インデックスと同じものです。転置インデックスが使用するトークナイザーは語彙表(lexcion)を見ればわかります。今回はTokenDelimitトークナイザーですね。

TokenDlimitは空白区切りでトークナイズするトークナイザーなので「Yes good」は「Yes」と「good」にトークナイズされます。
トークナイズしたら、それぞれの単語について転置インデックスを参照します。転置インデックスの参照は2段階あります。
まず、単語をキーとしてlexiconを検索し、単語ID(= lexiconのレコードID)を取得します。「Yes」の場合は単語IDは「1」です。

次に、単語IDを使って単語に対応する転置インデックスの値を取得します。単語ID「1」に対応する転置インデックスの値は文書ID「1」です。つまり、「Yes」という単語を含む文書は文書IDが「1」の文書だということです。

続いて、単語「good」の転置インデックスを参照します。「Yes」のときと同様にまずはlexiconを検索します。「good」の単語IDは「2」です。

「Yes」の時と同様に、単語IDを使って単語に対応する転置インデックスの値を取得します。単語ID「2」に対応する転置インデックスの値は文書ID「1」と「2」です。つまり、「good」という単語を含む文書は文書IDが「1」の文書と「2」の文書だということです。

元の入力は「Yes good」だったので、「Yes」の転置インデックスにも「good」の転置インデックスにも両方含まれている文書ID「1」だけがヒットした文書になります*1。文書ID「2」は「good」の転置インデックスに含まれていますが、「Yes」の転置インデックスには文書ID「2」が含まれていないので「Yes good」ではヒットしません。

このケースでは「ood」など部分文字列ではヒットしません。どうしてヒットしないかを説明します。
まず、「ood」をトークナイズすると空白がないので「ood」という1単語にトークナイズされます。次に、「ood」という単語でlexiconを検索するとそのような単語は登録されていないので、単語IDを取得できません。そのため、この時点でヒットする文書がないと判断し、転置インデックスは参照しません。
それでは、部分文字列でも検索できるようにするにはどうすればよいかというと、トークナイザーを変更します。どうしてトークナイザーがでてくるかというのは、先に説明した転置インデックスを参照する処理の流れを考えるとわかります。
転置インデックスを参照する処理は以下の2つの処理にわけられます。
「ood」で検索する例で確認した通り、「lexiconから単語IDを取得する」ことができないために部分文字列で検索できていません。よって、部分文字列でも「lexiconから単語IDを取得する」ことができるようにすれば、部分文字列でも検索できるようになります。
ここでトークナイザーの出番です。lexiconに登録される単語は文書をトークナイズして得られた単語です。そのため、トークナイズするときに部分文字列も単語としてトークナイズすればlexiconに単語の部分文字列も登録されます。すると、検索時に部分文字列でlexiconを参照しても単語IDを取得することができます。
groongaにはいくつか組み込みのトークナイザーがあります*2。部分文字列でも検索できるようにするには空白区切りで単語にトークナイズするTokenDelimitではなく、2文字単位で単語にトークナイズするTokenBigramを使います*3。
トークナイザーとしてTokenBigramを使うと「Yes good」は「Ye」・「es」・「s 」・「 g」・「go」・「oo」・「od」・「d」にトークナイズされます。

このとき、「ood」で検索したらどうなるかを説明します。
まず、転置インデックスが使っているのと同じトークナイザーTokenBigramでトークナイズします。「ood」は「oo」・「od」にトークナイズされます。lexiconを検索すると「oo」の単語IDは6で「od」の単語IDは7です。どちらも登録されている単語なのでヒットする文書がありそうです。

次に、単語ID「6」と単語ID「7」の転置インデックスを参照します。「oo」も「od」もどちらも文書ID「1」に含まれていることがわかります。よって、文書ID「1」の文書は「ood」という文字列を含んでいることがわかります*4。

検索時はこのように動作するため、トークナイザーによって検索結果が異なります。
では、トークナイザーはどのような基準で選ぶとよいのでしょうか。一見すると、部分文字列でも検索できるトークナイザーの方がよさそうに見えます。しかし、必ずしもそうとは限りません。部分文字列でもヒットするということは、望んでいない文書もヒットする可能性が増えるということです。
例えば、「cat」で「category」もヒットするようになります。「cat」で検索しているときに「category」に関する文書もヒットすると、それはノイズとなります。ノイズが多いと目的の文書を見つけづらくなってしまうため、使い勝手が悪くなります*5。よって、部分検索もできるようにしたほうがよいかどうかはアプリケーションに依る、ということになります。
なお、groongaは1つの全文検索で複数の転置インデックスを使うことにより、「完全一致した文書はスコアを高めにつけて、部分一致した文書はスコアを低めにつける」ということもできます。これは、転置インデックス毎*6に異なるトークナイザーを利用できるためです。
転置インデックスを用いると全文検索と同じ方法でタグ検索も実現できます。
実は、上記のTokenDelimitを使った全文検索の説明はタグ検索の動作そのものになっています。「Yes」や「good」をタグだと考えてもう一度読みなおしてみてください。
groongaでの(簡略化した)全文検索処理の流れを説明しました。思ったように検索ができない場合は、全文検索の処理の流れを考えながら挙動を確認していくと、どこが問題かをみつけやすくなるはずです。
*1 実際は「両方含まれている」だけではなく「入力と同じ順序で両方含まれている」文書だけを選びます。そのため、転置インデックスには単語が文書中のどこで現れたのかを記録しておく必要があります。groongaでは位置情報も含めるかどうかはカラム定義時のオプションで指定することが可能です。なお、位置情報も含んだ転置インデックスを完全転置インデックスと呼びます。
*2 ここでgroongaのドキュメントにトークナイザー一覧ページを作ってリンクを貼れると嬉しい。
*3 KEY_NORMALIZEを指定すると必ずしも2文字単位ではなくなるので注意すること、ということはここでは省略する。
*4 実際は「両方含まれている」だけではなく「入力と同じ順序で両方含まれている」ことも確認します。つまり、「oo」の次に「od」が出現していることも確認します。
*5 キーワードは「適合率」と「再現率」。
*6 実際は転置インデックス毎ではなくてlexicon毎。
2011/10/13時点でのDebian GNU/Linux sidでの話です。
Debian GNU/Linux上では、KVMやXenなどの仮想マシンを使わなくてもDeiban GNU/LinuxやUbuntu用のパッケージのビルドをできますし、CentOSやFedora用のパッケージのビルドもできます。Windows用のバイナリもDebian GNU/Linux上でビルドできると1台のマシンですべてのパッケージをビルドできるのでgroongaのようにたくさんの環境向けのパッケージを用意する場合に便利です。
以前、Rubyではrake-compilerを使ってWindows用のバイナリ入りgemをビルドできることを紹介しましたが、ここで紹介するのはrake-compilerが中で何をしているかについてです。rake-compilerがやっていることがわかると、rake-compilerなしでも同じことができるため、Rubyの拡張ライブラリ以外でもDebian GNU/Linux上でWindows用バイナリをビルドできるようになります。
Debian GNU/Linux上でWindows用バイナリをビルドするためにはクロスコンパイルする必要があります。そのために、x86(32bit版Windows)にもx64(64bit版Windows)にも対応したMinGW-w64を使います。MinGW-w64はGCCでWindows用バイナリをビルドするために必要なヘッダーファイルやライブラリなど一式を提供します。
mingw-w64パッケージをインストールしてMinGW-w64環境を作ります。
% sudo aptitude -V -D -y install mingw-w64
準備はこれで完了です。
./configure; make; make installでビルドするGNUビルドシステムを使っているソフトウェアはクロスコンパイルしやすいです。これは、configure内にクロスコンパイル用の処理が含まれているからです。
ここでは、GNUビルドシステムを使っているgroongaを使って、どのようにビルドするかを紹介します。ソースコードをダウンロードして展開しておきましょう。
% wget http://packages.groonga.org/source/groonga/groonga-1.2.6.tar.gz % tar xvzf groonga-1.2.6.tar.gz % cd groonga-1.2.6
クロスコンパイルするときに一番大事なのがconfigureのところです。むしろ、configureをうまくやれたら後はmake; make installするだけなので違いはconfigureだけになるべきです。
configureを実行するときのポイントは--hostオプションです。ここにビルド対象のプラットフォームを指定します。x64用にビルドするときは以下のように--host=x86_64-w64-mingw32を指定します。
% ./configure --host=x86_64-w64-mingw32
もし、システムにlibmecab-devパッケージがあるとLinux用のMeCabが使われてしまいます。libglib2.0-devパッケージも同様です。そのような場合は、以下のようにconfigureで自動検出している部分を無効にしてください。
% ./configure --host=x86_64-w64-mingw32 --without-mecab --disable-benchmark
configureが成功したらいつも通りmakeします。
% make
パッケージを作るときはmake installにDESTDIR変数を指定して一時的な場所にインストールしてからアーカイブするとよいでしょう。
% make install DESTDIR=/tmp/groonga % cd /tmp/groonga/ % mv usr/local groonga-1.2.6 % zip -r groonga-1.2.6.zip groonga-1.2.6
簡単ですね。
なお、x86用にビルドする場合は--host=i686-w64-mingw32を指定してください。
システムにインストールされているMinGWを確認する(つまり、--hostに指定できる値を調べる)には以下のようにします。
% (cd /usr; echo *mingw*) i686-w64-mingw32/ x86_64-w64-mingw32/
この場合は--host=i686-w64-mingw32または--host=x86_64-w64-mingw32を指定できます。
groongaを例にしてDebian GNU/Linux上でWindows用のバイナリをビルドする方法を紹介しました。これで、Debian GNU/Linux上でDebianパッケージもRPMもWindows用バイナリもビルドできてリリース作業が楽になりますね。
全文検索エンジンgroongaを囲む昼下がり@札幌はたっぷり3時間もあるので、「groongaがどのように動いているか」、「より効率的に検索するためにはどうしたらよいか」などといった話ができるはずです。
この文書は、札幌でのgroonga勉強会で使うための「groongaがどのように動いているか」を説明に使うための文書です。後でgroongaのドキュメントにマージする予定です。
それでは、groongaがどのように全文検索用のインデックスを作成しているかを説明します。まず、全文検索機能で重要なオブジェクトを説明して、その後にそれらを使ってどのようにインデックスを作成しているかを説明します。
groongaの全文検索機能で大事なオブジェクトは以下の3つです。
それぞれ順に説明します。
groongaでは、ひとまとまりのデータを「レコード」と呼びます。これはRDBと同じです。RDBでも行ごとにまとまったデータをレコードと呼んでいます。

groongaのテーブルは「レコードID」を管理するオブジェクトです。「レコードID」とはレコードを一意に識別する数値です。

なお、1つのテーブルで管理できるレコード数(レコードID)の理論的な上限値は約2億6千万レコードです。
テーブルには以下の3つの種類があります。
配列はID列を持ったテーブルです。レコードを追加すると新しいレコードIDを払い出します。基本的にレコードIDは1, 2, 3, ...というように順に払い出されます。

ハッシュテーブルはID列とIDと1対1に対応するキーを持ったテーブルです。レコードを追加するときは必ずキーも一緒に指定します。レコードが追加されるとキーに対応したレコードIDを払い出します。ハッシュテーブルが払い出すレコードIDも1, 2, 3, ...というように順に払い出されます。もし、レコードを追加するときに指定したキーが既存のレコードと同じキーだった場合は新しくレコードIDを払い出さず、既存のレコードと同じIDを返します。

パトリシアトライもハッシュテーブルと同様にID列とIDと1対1に対応するキーを持ったテーブルです。レコードの追加も同じように動きます。

配列ではレコードIDでのみレコードを特定できますが、ハッシュテーブルとパトリシアトライはレコードIDだけではなくキーでもレコードを特定できます。
パトリシアトライとハッシュテーブルとの違いはキーの検索方法です。
ハッシュテーブルではキーの検索方法は完全一致検索のみです。つまり、キーからレコードIDを求めるには、求めたいレコードのキーと同一のキーを指定するしかないということです。一方、パトリシアトライでは、完全一致検索だけではなく前方一致検索もできます。
例えば、ククログのデータがgroongaのデータベースに入っているとします。1エントリが1レコードに対応し、エントリが書かれた日付をキーにしているとします。すると、ここ2ヶ月では以下のようなキーになります。
この中から2011/9に書かれたエントリのみを表示したいとします。すると、2011/9に書かれたエントリのレコードIDの一覧を取得しなければいけません。この場合、パトリシアトライを使っていると"2011-9-"でキーを前方一致検索することで実現できます。しかし、ハッシュテーブルではこのようなことはできません。

また、パトリシアトライを使うとキーワードリンクなどといったこともできるようになります。
なお、レコードの参照速度の速い順にテーブルの種類を並べると以下のようになります。
配列と他の2つのテーブルの使い分けは、ID以外にレコードを特定するキーが欲しいかどうかで考えます。ハッシュテーブルとパトリシアトライの使い分けは、キーを完全一致検索だけで使うかどうかで考えます。
テーブルはレコードIDを管理するだけで、レコードが持つ値はカラムに保存します。1つのレコードに対して複数のカラムをひもづけることができるため、レコードは複数の値を持つことができます。

カラムには以下の3つの種類があります。
スカラーカラムは1つの値だけを保存できるカラムです。数値を保存するカラムなら「29」や「2929」などを保存できます。文字列を保存するカラムなら「"groonga"」や「"札幌"」などを保存できます。

ベクターカラムは複数の値を保存できるカラムです。同じ種類の値だけを保存できる配列と考えるのがよいでしょう。数値を保存するカラムなら「[2, 29, 292]」などを保存できます。文字列を保存するカラムなら「["groonga", "札幌"]」などを保存できます。

インデックスカラムは転置インデックスを保存するカラムです。転置インデックスは単語IDとその単語IDが含まれている文書IDをひもづけたデータ構造です。

groongaのインデックスカラムでは、単語IDはインデックスカラムのあるテーブルのレコードIDに対応します。文書IDは検索対象のテーブルのレコードIDに対応します。なお、インデックスカラムがあるテーブルのことを「語彙表(lexicon)」と呼びます。

スカラーカラムとベクターカラムはどのテーブルでも一緒に使えますが、インデックスカラムは配列と一緒に使うことはできません。ハッシュテーブルかパトリシアトライと一緒に使う必要があります。これは、groongaでは単語をキーとしたレコードを作成することによりレコードID(= 単語ID)を作成しているためです。
トークナイザーとは文書から単語を切り出すオブジェクトのことです。例えば、"I am a boy"という文書から「I」、「am」、「a」、「boy」という4つの単語を切り出したりします。この切りだすことを「トークナイズ」と呼びます*1。転置インデックスを作成するときは、単語IDと文書IDをひもづけるために文書内の単語を抽出する必要があります。それを行うのがトークナイザーです。
groongaでは、語彙表用のテーブル毎にトークナイザーを指定します*2。

転置インデックスの更新は元の文書を登録・更新・削除したときにgroongaが自動的に行います。そのため、ユーザが明示的にトークナイザーを使うことはありません。単に指定するだけです。
groongaで利用できるトークナイザーの一部は以下の通りです。
データを更新するとgroonga内部で自動的に転置インデックスが更新されます。そのときの動作を説明します。
まず、最初は、検索対象のテーブル(= 文書を保存するテーブル)にも語彙表のテーブルにもなにもレコードがありません。

それでは、検索対象のテーブルにデータを保存しましょう。

"Yes good"という文書を保存しました。最初の文書なのでレコードID(= 文書ID)は1になっています。データが保存されるとgroongaが内部で自動的に転置インデックスを更新します。

"Yes good"は「Yes」と「good」という2つの単語にトークナイズされます。まずは、「Yes」が語彙表に登録されます。これは最初の単語なのでレコードID(= 単語ID)は1になっています。続いて、単語IDが1に対応するインデックスカラムには保存された文書の文書IDとして1を登録します。
次に2つ目の単語である「good」を処理します。

「good」も「Yes」と同様に処理します。まずは、「good」が語彙表に登録されます。「good」の単語IDは2になります。続いて、単語IDが2に対応するインデックスカラムに保存された文書の文書IDである1を登録します。
もうひとつ文書を保存します。

2番目の文書なので文書IDは2になりました。データが保存されるとgroongaが内部で自動的に転置インデックスを更新します。

"Hey good"は「Hey」と「good」という2つの単語にトークナイズされます。まずは、「Hey」が語彙表に登録され、単語IDが3になります。続いて、単語IDが3に対応するインデックスカラムには保存された文書の文書IDとして2を登録します。
次に2つ目の単語である「good」を処理します。

「good」はすでに登録されている単語なので、新しく追加せずに既存の「good」と同じ単語IDを使います。「good」の単語IDは2なので、対応するインデックスカラムに文書IDとして2を登録します。このとき、すでに文書ID 1も登録されているので文書ID 2は追加します。
このようにして転置インデックスが作成されます。
groonga内部でどのように転置インデックスを更新しているかを説明しました。この動作がわかっていると検索が期待通りに動かないときにどこが問題かを見つけやすくなります。例えば、語彙表のキーに期待通りの単語が入っていなかったら、間違ったトークナイザーを指定しているかもしれません。もし、そもそも語彙表に単語が入っていなかったら、転置インデックスの自動更新が動いていないのでしょう。異なるカラム用にインデックスカラムを作成していないかを確認する必要があります。
それでは、全文検索エンジンgroongaを囲む昼下がり@札幌で会いましょう。
*1 トークナイズして切り出されたものは「トークン」と呼びますが、ここでは「単語」で統一します。「トークン」と呼ばれるのは切り出されたものが必ずしも「単語」単位とはならないからです。
*2 今後、インデックスカラム毎に指定できるようになるかもしれませんが、今のところテーブル毎の設定でとても困っているという声がないため、近い将来に実現されることはないでしょう。
今月も全文検索エンジンgroongaと、groongaをMySQLから使うためのモジュールであるgroongaストレージエンジンがリリースされました。
そして、groonga勉強会の開催が決まりました。
昼下がりの方は来週の土曜日に札幌で開催されます。夕べの方は2ヶ月後の29日に東京で開催されます。夕べは昨年同じ日にちに開催したgroonga勉強会の第2回目という位置づけで、前回と同様に開発している側からのgroongaと関連プロダクトの説明が主になります。一方、昼下がりの方は時間的なゆとりがあることもあり、単に説明を聞くだけではなく、質疑応答にも十分な時間をとれそうです。
groongaに興味のある方はぜひご参加ください。