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

ククログ

最新
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|
タグ:

いらないキャッシュを消すとRubyスクリプトが速くなる

いらないキャッシュを消すことで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の場合もメモリ使用量は半分以下、実行時間はほぼ半分になっています。

RSSVSZ実行時間
変更前(Ruby 1.9.3)187MB237MB55秒
変更後(Ruby 1.9.3)84MB134MB38秒
変更前(Ruby 1.8.7)525MB558MB1分52秒
変更後(Ruby 1.8.7)132MB165MB59秒

メモリ使用量が減って速くなった場合はだいたい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時間が長くなってしまい、逆に遅くなることがあります。キャッシュしたのに思ったより速くならなかった場合は必要なくなったキャッシュを持ち続けていないか確認し、いらないキャッシュを消すことを試してみましょう。ただし、ある程度のオブジェクト数をキャッシュしていない場合はあまり関係がないでしょう。

*1  2コアあるので1コアがフル稼働してしまってもシステム全体としては問題になりません。

*2  Ruby 1.8.7使用時

タグ: Ruby
2011-11-24

ApacheとPhusion PassengerでデプロイしたWebアプリケーションが不調になる問題と解決

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を修正したパッチを作成しました。そしてそのパッチを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を修正しました。

同じような問題に遭遇している場合には、ぜひ試してみてください。

タグ: Ruby
2011-11-17

すべてのMySQLユーザーに高速な全文検索機能を! - OSC2011.DB用資料

オープンソースカンファレンス2011 DBOSSDB MySQLセッションでgroongaストレージエンジンについて紹介してきました。

すべてのMySQLユーザーに高速な全文検索を!

内容はgroongaストレージエンジンが得意なシチュエーションについてベンチマークデータを紹介するというものです。どういうときにgroongaストレージエンジンが高速に動作するかがわかります。

groongaストレージエンジンが得意なシチュエーション

groongaストレージエンジンは以下のような処理が得意です。

  • 全文検索
  • 位置情報検索
  • リアルタイム更新

groongaストレージエンジンの性能特性を紹介するためにベンチマークデータを紹介しました。ベンチマークはこれらの得意な処理を実行するシチュエーション向けに複数のパターンで行いました。

高速な全文検索

groongaの全文検索処理の性能を示すためにtwitterから取得したデータを利用しました。測定する処理はフレーズ検索です。約100万件のtweetに対して「"facebook saved"」というような2単語でフレーズ検索します。このようなフレーズ検索を1万回(1万パターン)実行するためにかかった時間が縦軸になっており、グラフが短いほど高速に全文検索が実行されていることを示しています。

約100万件のtweetに対して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」というように住所でソートする検索です。このような検索を千回(千パターン)実行するためにかかった時間が縦軸になっており、グラフが短いほど高速に位置情報検索が実行されていることを示しています。

約1100万件の番地データに対して千回のMBRContains + ORDER BY検索にかかった時間

groongaストレージエンジンのほうがMyISAMよりも40倍程度高速でした。

リアルタイム更新

groongaはリアルタイム更新が得意です。リアルタイムで更新するために以下の2点を重視しています。

  • 高速に更新できること。
  • 検索負荷が高いときでも更新性能が落ちないこと。

まず、更新性能が高いことを確認し、次に検索負荷が高いときの更新性能を確認します。

高速な更新

高速に更新できることを示すために、98万件のtweetが登録されたデータベースを用意します。このデータベースに対して、2万件のtweetを登録します。このとき1秒あたりに追加したレコード数が縦軸になっており、グラフが長いほど高速に登録されていることを示しています。

98万件のtweetが保存されている状態で2万件のtweetを追加したときの更新スループット

groongaストレージエンジンのほうがInnoDBよりも3倍程度高速、MyISAMよりも2倍程度高速、Sphinxよりも3倍程度高速でした。

参照ロックフリーな更新

検索負荷が高いときでも更新性能が落ちないことを示すために、98万件のtweetが登録されたデータベースを用意します。このデータベースに対して、検索負荷(クエリ数/秒)を変えながら2万件のtweetを登録します。このグラフはそれぞれのストレージエンジン毎に見ます。横軸が検索負荷を表していて、左側になるほど検索負荷が小さく、右側になるほど検索負荷が高いことを示しています。グラフが水平になっているほど検索負荷が高くなっても更新性能が落ちていないことを示しています。

98万件のtweetが保存されている状態で検索負荷を変えながら2万件のtweetを追加したときの更新スループット

groongaストレージエンジンとInnoDBは検索負荷が高くなっても更新性能はそれほど落ちておらず、MyISAMとSphinxは更新性能が落ちていました。

groongaストレージエンジンの機能制限

このように、高速に動作するgroongaストレージエンジンですが、以下のように機能制限があります。

  • トランザクションをサポートしていない。
  • 位置情報はPOINTしかサポートしていない。

そのため、どのようなケースにでも利用できるわけではありません。しかし、groongaストレージエンジンは他のストレージエンジンと組み合わせて使うことができるため、上記の機能制限の一部を解消することができます。

groongaストレージエンジンを他のストレージエンジンと使う場合は全文検索処理・位置情報検索処理のみをgroongaストレージエンジンが行い、それ以外の処理は連携した他のストレージエンジンが行います。そのため、groongaストレージエンジンとInnoDBを一緒に使うと、トランザクションはInnoDBの機能を用いて、全文検索はgroongaストレージエンジンを用いる、ということができます。この仕組みを使うと「トランザクションをサポートしていない」というgroongaストレージエンジンの機能制限を解消することができます。

groongaストレージエンジンと他のストレージエンジンを組み合わせて使う方法

ただし、更新性能は組み合わせて使うストレージエンジンの性能に依存するため、groongaストレージエンジンの得意な処理である「リアルタイム更新」性能は発揮できません。「高速な全文検索機能」と「高速な位置情報検索機能」のみ利用できます。

InnoDBと組み合わせて利用した場合のベンチマークデータを以下に示します。

まず、全文検索の性能です。

約100万件のtweetに対して1万回のフレーズ検索にかかった時間(InnoDBと組み合わせた場合のデータ付き)

groongaストレージエンジン単体で使った場合とほとんど同じ性能がでています。

次に、位置情報検索の性能です。

約1100万件の番地データに対して千回のMBRContains + ORDER BY検索にかかった時間(InnoDBと組み合わせた場合のデータ付き)

こちらもgroongaストレージエンジン単体で使った場合とほとんど同じ性能がでています。

次に、更新性能です。

98万件のtweetが保存されている状態で2万件のtweetを追加したときの更新スループット(InnoDBと組み合わせた場合のデータ付き)

groongaストレージエンジン単体で使った場合よりも大きく性能が落ちて、組み合わせて使っているInnoDBと同じ程度の性能になっています。

最後に検索負荷が高いときの更新性能です。

98万件のtweetが保存されている状態で検索負荷を変えながら2万件のtweetを追加したときの更新スループット(InnoDBと組み合わせた場合のデータ付き)

InnoDBが検索負荷が高くても更新性能がほとんど落ちないため、groongaストレージエンジンとInnoDBを組み合わせて使った場合でも更新性能がほとんど落ちていません。

まとめ

OSC2011.DBでgroongaストレージエンジンが得意なシチュエーションのベンチマーク結果を紹介してきました。もちろん、groongaストレージエンジンが得意ではないシチュエーションもあり、そのようなケースでは他のストレージエンジンの方が性能がよくなります。groongaストレージエンジンが苦手なケースについては今月末(2011/11/29)開催の全文検索エンジンgroongaを囲む夕べ 2で紹介する予定です。興味のある方はこちらに参加してみてください。すでに定員を超えていますが、前回の全文検索エンジンgroongaを囲む夕べ #1では最終的に35名のキャンセルになっていましたので、今からでもギリギリ参加できるのではないでしょうか。

groongaストレージエンジンのより詳しい情報についてはgroongaストレージエンジンのサイトも参照してください。

タグ: groonga
2011-11-07

Firefoxの技術書「Firefox Hacks Rebooted」

オライリーより、Firefoxの高度な使い方からアドオン開発のノウハウ、新しいWeb技術まで手広く解説・紹介する書籍「Firefox Hacks Rebooted」が、2011年10月26日に発売されました。弊社でMozillaサポート事業に従事している下田も執筆者の一人として名を連ねています。

Firefox Hacks Rebooted ―Mozillaテクノロジ徹底活用テクニック
浅井 智也/池田 譲治/小山田 昌史/五味渕 大賀/下田 洋志/寺田 真/松澤 太郎
オライリージャパン
¥ 3,570

内容が多岐に渡るため、「こういう人に読んでもらいたい!」という想定読者が章ごとにそれぞれ異なるのですが、全体を見た時の内容の充実度からは、Firefox用アドオンの開発に関心がある方に特にお薦めと言えるでしょう。そこでこの記事では、開発者視点から本書の見所をいくつか紹介します。

Vimperator vs KeySnail

目次を順番に眺めて目を引くのは、2章におけるVimperatorとKeySnailの解説でしょう。VimとEmacsといえば開発者が使う開発環境の二大巨頭で、よくエディタ戦争のネタにもされますが、本書の2章でも、Vimを模したVimperatorとEmacsを模したKeySnailの戦争が繰り広げられています。

……というのは冗談なのですが、Vimperator開発メンバーの一人であるteramakoさんがVimperatorを、KeySnailの作者であるmoozさんがKeySnailをそれぞれ解説し、最後に二人がそれぞれの設計思想の違いを解説するという構成になっていて、こと両アドオンの解説記事としてはこれ以上無い豪華な内容と言えるでしょう。開発者自らによる解説という事で、内容の信頼性の高さも折紙付です。

快適な開発生活を送る上では、効率の良い情報収集や情報発信の方法を知る事も大切です。また、情報は発信する人の所に集まるとも言います。最新の技術へのフォローを欠かす事ができない開発者の人達にとっても、この章の内容は役立つのではないでしょうか。

Add-on SDKでのアドオン開発

3章は、FireGesturesなどの作者としても知られるGomitaさんによるAdd-on SDKの解説です。基礎概念の解説に始まり、少しずつ機能を付け足しながら実際に1つのアドオンを完成させるまでの過程をチュートリアル形式で紹介する事により、Add-on SDKを使ったアドオン開発の一通りの流れを理解する事ができます。

Firefox 4以降のバージョンでは高速リリースが採用された事により、従来の形式のアドオンの開発スタイルだとFirefoxの更新に追従するだけでも大変という状況になっています。Add-on SDKはFirefoxのバージョン間の違いを吸収する層としての働きも備えているため、これから新たにアドオンを開発する場合はSDKを利用した方が良いと言えますが、本章はそのファーストステップとして最適でしょう。

また、後の章にはHTML5関連技術などの新しいWeb標準技術の解説もあります。それらを組み合わせる事によって、SDKが提供している機能を超えた様々な事が実現できるようになります。本書は話題が多岐に渡っていますので、全体を通読する中でそういった発展に繋がるヒントを得やすいのではないでしょうか。

Add-on SDKの枠を超えたアドオン開発

弊社所属の下田は、4章と6章の一部として、SDKに依らない・より低層の部分に関する技術情報を寄稿しています。

4章では、再起動のいらないアドオンを、これまでの一般的なアドオン開発の延長線上にある物として捉えた上で、これまで通りにできる事とそうでない事とを整理して、それらに対する対策となる様々なテクニックを紹介しています。

また、プロセス分離型の設計に移行していくにあたって、そもそもFirefoxではどのようにプロセスの分離が実現されているのかを理解し、プロセスが分離された環境ではどのような事に気をつけなくてはならないのかについても解説しています。

SDKの枠を超えた開発を行いたい時や、SDKに含まれている標準ライブラリの中で行われている事を理解したい時などには、各要素技術への理解が必要になってきます。そういった場合も、この章の情報が手がかりとなるかも知れません。

その他にも、非同期処理の記述を支援する軽量ライブラリ「JSDeferred」のFirefox上での活用事例や、CPUの使用率とメモリの使用量を表示するFirefoxアドオン「システムモニター」でも利用している、C言語で開発されたプラットフォームネイティブのライブラリをJavaScriptから透過的に利用する技術「js-ctypes」の解説(6章に収録)など、SDKベースでの開発にも応用可能な情報もあります。

Webでは断片的にしか得られない情報のまとめ&発展として

本書には、Web上にあるFirefoxの断片的な情報を取っ掛かりとして、それらをさらにもう1段階・2段階と掘り下げた情報が多数収録されています。自分で情報を探そうとして挫折した方、見つけた情報が中途半端で途方に暮れてしまった方など、表面的な情報よりももっと本質的な情報を求めている方にお薦めと言えるでしょう。

なお、本書の目次各章のサンプルがWebで公開されています。Firefoxのヘビーユーザーの方やアドオン開発者の方は、役立つトピックが含まれているかもしれませんので、興味を持たれた際には是非一度目を通してみて下さい。

つづき: 2011-12-26
タグ: Mozilla
2011-11-02

お知らせ: OSC2011.DBでgroongaストレージエンジンを紹介

今度の土曜日11/5に開催されるOSC2011.DBの10:35からのセッション「OSSDB MySQL」に少しおじゃましてgroongaストレージエンジンを紹介します。groongaストレージエンジンがイベントなどで紹介されるのは昨年の全文検索エンジンgroongaを囲む夕べ #1以来のはずなので約1年ぶりになります。その間に初のメジャーリリースである1.0.0がリリースされるなど、だいぶ成長しています。そのため、話題はたくさんあるのですが、その中から特に注目すべきところを選り抜いて紹介します。

groongaストレージエンジンについては1ヶ月後の全文検索エンジンgroongaを囲む夕べ 2でも詳しく紹介されますが、一足早く知りたい人はぜひ参加してください。(参加費用は無料ですが参加登録が必須なので注意してください。)

2011-11-01

どうして開発者がドキュメントを書くべきか

オフィス文書形式が要求されるようなドキュメントではなくて、自分が開発したライブラリのドキュメント(リファレンスマニュアルやチュートリアルなどライブラリのユーザーが読むためのドキュメント)の話です。以下の「ドキュメント」もそのような意味で使っています。

使いやすいライブラリを開発したかったらプログラムだけではなくドキュメントも書くべきです。

なぜドキュメントを書くか

ドキュメントを書く習慣があるかどうかは開発者によってあったりなかったりです。使っているプログラミング言語に相関がある気もしますし、リリースするかどうかに相関がある気もします。理由はいろいろあるでしょうが、ドキュメントを書く習慣のない開発者の方が多いでしょう。

書かない理由はこんな感じでしょうか。

  • 面倒。
  • 自分しか使わないからいらない。
  • どのように書けばよいかわからない。(どのツールを使えばよいかわからない。)

一方、書く理由はこんな感じでしょう。

  • 他の人が使えるようにしたい。
  • 何度も同じ事を質問されて、個別に説明することの方がドキュメントを用意することより面倒になった。

だいたいあっていますか?しかし、使いやすいライブラリを開発したい場合は「ユーザーの視点でAPIを見直すため」にドキュメントを書くという視点を忘れてはいけません。

ユーザーの視点で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さんも別の視点で見るためにドキュメントを書くというようなことに言及しています。(ただし、「ユーザー視点」というわけではなさそうです。)

つづき: 2011-12-26
2011-10-27

groongaの全文検索処理の流れ

groongaにデータを登録して、インデックスを更新すると全文検索をすることができます。ここでは、groongaが内部でどのような処理をして全文検索をしているかを説明します。

前提

まず、以下のように「Yes good」と「Hey good」という文書が登録されているとします。

登録されている文書と転置インデックス

このとき、「Yes good」で検索したらどうなるかを説明します。

トークナイズ

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

TokenDlimitトークナイザー

TokenDlimitは空白区切りでトークナイズするトークナイザーなので「Yes good」は「Yes」と「good」にトークナイズされます。

転置インデックスの参照

トークナイズしたら、それぞれの単語について転置インデックスを参照します。転置インデックスの参照は2段階あります。

まず、単語をキーとしてlexiconを検索し、単語ID(= lexiconのレコードID)を取得します。「Yes」の場合は単語IDは「1」です。

「Yes」の単語IDを検索

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

単語ID「1」の転置インデックスの値を取得

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

「good」の単語IDを検索

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

単語ID「2」の転置インデックスの値を取得

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

ヒットした文書

部分文字列での全文検索

このケースでは「ood」など部分文字列ではヒットしません。どうしてヒットしないかを説明します。

まず、「ood」をトークナイズすると空白がないので「ood」という1単語にトークナイズされます。次に、「ood」という単語でlexiconを検索するとそのような単語は登録されていないので、単語IDを取得できません。そのため、この時点でヒットする文書がないと判断し、転置インデックスは参照しません。

それでは、部分文字列でも検索できるようにするにはどうすればよいかというと、トークナイザーを変更します。どうしてトークナイザーがでてくるかというのは、先に説明した転置インデックスを参照する処理の流れを考えるとわかります。

転置インデックスを参照する処理は以下の2つの処理にわけられます。

  1. lexiconから単語IDを取得する。
  2. 単語IDを使って転置インデックスの値を取得する。

「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」にトークナイズされます。

TokenBigramを利用

このとき、「ood」で検索したらどうなるかを説明します。

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

「oo」と「od」の単語IDを検索

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

単語ID「1」と「2」の転置インデックスの値を取得

検索時はこのように動作するため、トークナイザーによって検索結果が異なります。

トークナイザーの使い分け

では、トークナイザーはどのような基準で選ぶとよいのでしょうか。一見すると、部分文字列でも検索できるトークナイザーの方がよさそうに見えます。しかし、必ずしもそうとは限りません。部分文字列でもヒットするということは、望んでいない文書もヒットする可能性が増えるということです。

例えば、「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-12-26
タグ: groonga
2011-10-20

Debian GNU/LinuxでWindows用バイナリをビルドする方法

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 installDESTDIR変数を指定して一時的な場所にインストールしてからアーカイブするとよいでしょう。

% 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用バイナリもビルドできてリリース作業が楽になりますね。

2011-10-13

groongaにデータを登録してからインデックスが更新されるまでの流れ

全文検索エンジンgroongaを囲む昼下がり@札幌はたっぷり3時間もあるので、「groongaがどのように動いているか」、「より効率的に検索するためにはどうしたらよいか」などといった話ができるはずです。

この文書は、札幌でのgroonga勉強会で使うための「groongaがどのように動いているか」を説明に使うための文書です。後でgroongaのドキュメントにマージする予定です。

それでは、groongaがどのように全文検索用のインデックスを作成しているかを説明します。まず、全文検索機能で重要なオブジェクトを説明して、その後にそれらを使ってどのようにインデックスを作成しているかを説明します。

主要オブジェクト

groongaの全文検索機能で大事なオブジェクトは以下の3つです。

  1. テーブル
  2. カラム
  3. トークナイザー

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

テーブル

groongaでは、ひとまとまりのデータを「レコード」と呼びます。これはRDBと同じです。RDBでも行ごとにまとまったデータをレコードと呼んでいます。

レコード

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

レコードID

なお、1つのテーブルで管理できるレコード数(レコードID)の理論的な上限値は約2億6千万レコードです。

テーブルには以下の3つの種類があります。

  1. 配列
  2. ハッシュテーブル
  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-8-11"
  • "2011-8-24"
  • "2011-9-5"
  • "2011-9-13"
  • "2011-9-29"

この中から2011/9に書かれたエントリのみを表示したいとします。すると、2011/9に書かれたエントリのレコードIDの一覧を取得しなければいけません。この場合、パトリシアトライを使っていると"2011-9-"でキーを前方一致検索することで実現できます。しかし、ハッシュテーブルではこのようなことはできません。

前方一致検索

また、パトリシアトライを使うとキーワードリンクなどといったこともできるようになります。

なお、レコードの参照速度の速い順にテーブルの種類を並べると以下のようになります。

  1. 配列
  2. ハッシュテーブル
  3. パトリシアトライ

配列と他の2つのテーブルの使い分けは、ID以外にレコードを特定するキーが欲しいかどうかで考えます。ハッシュテーブルとパトリシアトライの使い分けは、キーを完全一致検索だけで使うかどうかで考えます。

カラム

テーブルはレコードIDを管理するだけで、レコードが持つ値はカラムに保存します。1つのレコードに対して複数のカラムをひもづけることができるため、レコードは複数の値を持つことができます。

カラム

カラムには以下の3つの種類があります。

  1. スカラーカラム
  2. ベクターカラム
  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で利用できるトークナイザーの一部は以下の通りです。

  • TokenBigram: バイグラムでトークナイズする。
  • TokenMecab: MeCabを使ってトークナイズする。
  • TokenDelimit: 空白区切りでトークナイズする。

データ更新時の動作

データを更新するとgroonga内部で自動的に転置インデックスが更新されます。そのときの動作を説明します。

まず、最初は、検索対象のテーブル(= 文書を保存するテーブル)にも語彙表のテーブルにもなにもレコードがありません。

データが何もない初期状態

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

1つ目の文書を保存

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

1つ目の文書の1つ目の単語を処理

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

次に2つ目の単語である「good」を処理します。

1つ目の文書の2つ目の単語を処理

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

もうひとつ文書を保存します。

2つ目の文書を保存

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

2つ目の文書の1つ目の単語を処理

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

次に2つ目の単語である「good」を処理します。

2つ目の文書の2つ目の単語を処理

「good」はすでに登録されている単語なので、新しく追加せずに既存の「good」と同じ単語IDを使います。「good」の単語IDは2なので、対応するインデックスカラムに文書IDとして2を登録します。このとき、すでに文書ID 1も登録されているので文書ID 2は追加します。

このようにして転置インデックスが作成されます。

まとめ

groonga内部でどのように転置インデックスを更新しているかを説明しました。この動作がわかっていると検索が期待通りに動かないときにどこが問題かを見つけやすくなります。例えば、語彙表のキーに期待通りの単語が入っていなかったら、間違ったトークナイザーを指定しているかもしれません。もし、そもそも語彙表に単語が入っていなかったら、転置インデックスの自動更新が動いていないのでしょう。異なるカラム用にインデックスカラムを作成していないかを確認する必要があります。

それでは、全文検索エンジンgroongaを囲む昼下がり@札幌で会いましょう。

*1  トークナイズして切り出されたものは「トークン」と呼びますが、ここでは「単語」で統一します。「トークン」と呼ばれるのは切り出されたものが必ずしも「単語」単位とはならないからです。

*2  今後、インデックスカラム毎に指定できるようになるかもしれませんが、今のところテーブル毎の設定でとても困っているという声がないため、近い将来に実現されることはないでしょう。

タグ: groonga
2011-10-05

全文検索エンジンgroongaを囲む昼下がりと夕べのお知らせ

今月も全文検索エンジンgroongaと、groongaをMySQLから使うためのモジュールであるgroongaストレージエンジンがリリースされました。

そして、groonga勉強会の開催が決まりました。

昼下がりの方は来週の土曜日に札幌で開催されます。夕べの方は2ヶ月後の29日に東京で開催されます。夕べは昨年同じ日にちに開催したgroonga勉強会の第2回目という位置づけで、前回と同様に開発している側からのgroongaと関連プロダクトの説明が主になります。一方、昼下がりの方は時間的なゆとりがあることもあり、単に説明を聞くだけではなく、質疑応答にも十分な時間をとれそうです。

groongaに興味のある方はぜひご参加ください。

タグ: groonga
2011-09-29

最新
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|
タグ:
RubyKaigi2008Speaker
RubyKaigi2009Sponsor
RubyKaigi2009Speaker
SapporoRubyKaigi02Sponsor
SapporoRubyKaigi02Speaker
RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer badge_speaker.gif RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer