都会よりも田舎が好きなfujisawaです。Bayesian Setsというアルゴリズムを使って、関連する文書を高速・高精度に検索できるシステムを作成しましたので、そのご紹介をさせていただきます。

Bayesian Setsとは

Bayesian Setsはいくつかアイテムを入力すると、それを補完するようなアイテムを返してくれるアルゴリズムです。原著論文の先頭に”Inspired by Google Sets”と書かれているように、Google Setsを参考にして作成されています。実際にどのような出力が得られるか、Google Setsに以下の表のクエリを検索して試してみますと、

クエリ 出力
apple, banana chocolate, strawberry, vanilla, cherry, …
apple, macintosh software, windows, mac, computer, …

どちらのクエリにも”apple”という言葉が含まれていますが、1つ目のクエリは”banana”と一緒に検索しているため、果物のapple, bananaに関連するアイテムが得られていることが分かります。また2つ目のクエリでは”macintosh”と共に検索しているため、コンピュータ関連のアイテムが結果として得られています。このように入力されたアイテムと同じ概念の集合をオンデマンドに特定し、その集合中の他のアイテムを取得することができます。

Bayesian Setsについてより詳しくは、以下のページをご参照ください。

またBayesian Setsを使った他のシステムには連想検索エンジンReflexaがあり、はてなブックマークの関連エントリなどに使用されているようです。

関連文書検索システムStupa

Bayesian Setsを使用した関連文書検索システムとして、Stupa(ストゥーパ)というシステムを作成しました。昔アジアを旅行したときに見た仏塔(ストゥーパ)から取ったもので、特に検索には関係ありません。

StupaはBayesian Setsにより類似する文書を高精度に検索し、また転置インデックスを構築することで高速に検索を実行できます。登録された文書データや転置インデックスは圧縮して保持しているため、メモリの使用量も少なく実行できます。

stupa

例として日本語版Wikipediaから作成したデータ(約61万エントリ)を使用して、Stupaのコマンドラインツールで関連エントリを検索した結果を以下に示します。結果のリストには、関連するエントリの名前と関連度が1行1エントリで表示されます。

  • 「トヨタ自動車」で検索
  • % bsmgr search /path/wikipedia.tsv
    Read input documents ...
    Query> トヨタ自動車
    トヨタ・プリウス        62.243313
    トヨタ・トヨエース      56.777371
    トヨタ・コロナ  54.229831
    張富士夫        53.586574
    トヨタ・ハイラックス    51.514397
    トヨタ・プロボックス    49.165956
    トヨタ・パブリカ        49.076950
    中村健也        47.865204
    奥田碩  45.499038
    特別仕様車      45.440519
    ...
    (search time: 3.21ms)
    
  • 「かめはめ波」と「ドラゴンボール」で検索
  • Query> かめはめ波       ドラゴンボール
    かめはめ波      263.980312
    トランクス (ドラゴンボール)     72.204788
    ミスター・サタン        69.037348
    ドラゴンボールのアニメオリジナルの登場人物      65.560679
    ドラゴンボールZ 超悟空伝 -突激編-       62.634941
    ベジット        62.190804
    ドラゴンボールZ 燃えつきろ!!熱戦・烈戦・超激戦  54.983379
    ダーブラ        53.873965
    ギニュー特戦隊  51.688147
    ドラゴンボールZ この世で一番強いヤツ    51.597809
    ...
    (search time: 3.95ms)
    

上記は「トヨタ自動車」で検索した場合と、「かめはめ波」「ドラゴンボール」の2つで検索した場合の結果ですが、両方とも関連するエントリが取得できています。検索にかかった時間は3ミリ秒ほどと比較的高速に実行できていることも分かります。

Stupaは入力された文書データや転置インデックスをメモリ上で保持して動作するため、頻繁に文書の追加・削除が行われるようなリアルタイム性の高いアプリケーションでの利用を主に想定して開発しています。例えばTwitter上で、自分の発言に関連するTweetを即座に検索する、といったアプリケーションが作れるかもしれません(Twitterだと1発言あたりの文章が短すぎて精度が悪くなってしまうかもしれませんが)。

またオプションで登録できる文書の最大数を指定すると、最大登録数を超えた場合に古い文書から削除していくようになるため、使用リソースを一定に抑えつつ、常に最新の文書から関連しているものを検索することができます。

Thriftによる検索サーバ

StupaにはC++ライブラリとコマンドラインツールが含まれているのですが、これ以外にThriftを使用した検索サーバも配布しています。Perlのクライアントも同梱されていますので、Webサービス等に実際に導入することも可能だと思います。

stupa-thrift

また現在クライアントはPerl実装しかないのですが、Thrift用のインタフェース定義ファイルがパッケージに含まれていますので、RubyやPythonなど他言語のクライアントを生成することも可能です。より詳しくはThriftのマニュアルをご参照下さい。

まとめ

関連文書検索システムStupaをご紹介させていただきました。StupaはBayesian Setsを使用して、高精度かつ高速に検索を実行することが可能です。今後はmixi上での応用を考えつつ、パフォーマンスの向上等を行っていきたいと思います。もしご興味がある方がいらっしゃいましたら、ぜひご利用下さい!

先日ようやくドラクエ9をクリアしたのですが、切ない話が多くて、たまに泣きそうになってしまったfujisawaです。以前ご紹介したデータクラスタリングツールbayonにいくつか機能追加を行いましたので、その中から以下の2つをご紹介させていただきます。

  • 入力データ中の特徴的なキーを自動的に特定して、クラスタリングの精度を向上させる
  • 事前に行ったクラスタリング結果を使用して、各ドキュメントに関連するクラスタを特定する

入力データから特徴的な要素を特定

bayonでは入力データとして、各ドキュメントに対し、その特徴を表すキーとポイントを指定する必要があります。例えば以下の例では、最近食べたメニューの名前とその回数を、各ユーザの特徴として指定しています。

fujisawa  卵かけご飯 4   みそ汁   6    ソーメン 3
kimura    ステーキ   8   みそ汁   7    寿司     4
...

ここで、実は「みそ汁」は多くの人がよく食べていて、入力データ中のほとんどのドキュメントに特徴として存在しているとします。するとあるドキュメントが特徴「みそ汁」を持っていたとしても、「みそ汁」は元々みんなが食べているものなので、その人を特徴づけるものとはなりません。このようなあまり意味をなさない特徴が入力データ中に多く含まれると、クラスタリング結果の精度が悪くなる原因になりかねません。

以前のバージョンでは、ツールユーザ側で入力データの精査を行って、より特徴的なキーのポイントを上げるようにしていただく必要がありましたが、現行のバージョンでは––idfオプションを指定することでシステム側で自動的に、特徴をよく表すキーのポイントを上げ、逆にあまり特徴的でないキーのポイントを下げられるようにしています。具体的にはあるキーが含まれるドキュメントの数を調べ、その数が小さいほどポイントを上げる、と非常に基礎的な手法を使用しています。より詳しくはwikipediaのtf-idfの説明などをご参照下さい。

ただし、入力データにすでにtf-idfや他の重み付けが施されている場合は、本オプションを使用することで却って精度が下がってしまうこともありえますので、ご注意下さい。

各ドキュメントに関連するクラスタの特定

現在のbayonでは、各ドキュメントは1つのクラスタのみに所属する、ハードクラスタリングの手法を採用しています。ただ実際のデータでは、ただ1つのグループ(クラスタ)のみに属するのではなく、複数のグループに関連することがよくあります。例えば「東京駅」は交通機関というグループにも属しつつ、所在地である東京都という地域のグループにも属すと考えられます。このように関連するグループが複数あるならば、それらすべてを知りたいケースもあると思います。

複数のクラスタへの所属を許す場合をソフトクラスタリングといい、Fuzzy C-meansや、EMアルゴリズムを使用した混合分布モデルによるクラスタリングなど様々なソフトクラスタリング手法が提案されています。

hard and soft clustering

ただしbayonではそれらは用いずに、あらかじめ求めておいたハードクラスタリングの結果を使用して、ドキュメントとクラスタの中心ベクトルとの類似度を測ることで、各ドキュメントに類似するクラスタのリストを取得できるようにしました。これにより擬似的にソフトクラスタリングと同様の結果を得ることができると思います。類似度の計算の際は転置インデックスを構築して高速化を行っており、計算時間も比較的短く実行できます。以下の表は転置インデックスを使用した場合と使用しなかった場合での、類似クラスタの特定にかかった実行時間です。

データ数 クラスタ数 クラスタリング(秒) 転置インデックスOFF(秒) 転置インデックスON(秒)
1000 10 0.17 0.092 0.088
50000 1000 36 48.3 13.4
500000 10000 720 (長時間のため停止) 1385

また上記の手法ではクラスタリング・類似クラスタ特定とシステムを2回実行する手間がかかり、また既存のソフトクラスタリングの手法と比べ精度的に劣る面もあるかもしれませんが、今回は実行速度を優先し上記の手法を取りました。またいくつかのデータで試した限りでは、実用上問題のない結果が得られているのではないかと思います。このやり方では理論的に明らかにおかしい、もっとよい手法がある、などありましたらメール等でぜひご教授いただければ幸いです。

関連クラスタ特定の実行方法

実際に類似クラスタを求める際は、以下の実行例のようにまずクラスタリングを行う際に-c or ––clvectorオプションでクラスタの中心ベクトルを保存しておき、その後-C or ––classifyオプションを指定して再実行してください。また以下の例では、前回の記事と同様に各ユーザの情報を1つのドキュメントと考え、ユーザが聞く音楽のジャンルによってユーザの特徴を表しています。

  1. 入力データの確認
  2. % cat input.tsv
    阿佐田   J-POP       10   J-R&B       6   ロック  4
    小島     ジャズ       8   レゲエ      9
    古川     クラシック   4   ワールド    4
    田村     ジャズ       9   メタル      2   レゲエ  6
    青柳     J-POP        4   ロック      3   HIPHOP  3
    三輪     クラシック   8   ロック      1
    

  3. クラスタリングを実行(クラスタの中心ベクトルを保存しておく)
  4. % bayon -n 3 -c centroid.tsv input.tsv  (クラスタリングを実行)
    1       田村    小島
    2       阿佐田  青柳
    3       三輪    古川
    
    % cat centroid.tsv  (クラスタの中心ベクトルを確認)
    1   ジャズ  0.750476    レゲエ  0.654458    メタル  0.0920378
    2   J-POP   0.806401    ロック  0.451887    HIPHOP  0.277129    J-R&B   0.262137
    3   クラシック  0.921175    ワールド    0.383297    ロック  0.0672347
    

  5. 各ドキュメントとクラスタとの類似度を計算
  6. % bayon -C centroid.tsv input.tsv
    阿佐田  2       0.928261        3       0.0218138
    小島    1       0.987737
    古川    3       0.922401
    田村    1       0.987737
    青柳    2       0.928262        3       0.034592
    三輪    3       0.922401        2       0.0560497
    

その結果上記のように、「阿佐田」ドキュメントには類似度0.93でクラスタ2、類似度0.02でクラスタ3が関連していることが特定できています。

またクラスタとの類似度を求めるときの入力ドキュメントは、クラスタリングを行ったときの入力とは違うドキュメント集合も使用できます。以下の例では、クラスタリングで使用したユーザとは違うユーザの情報を記入した入力ドキュメントに対し、先ほど求めたクラスタとの類似度を測っています。

% cat input_other.tsv  (新規の入力データ)
安藤    J-POP    5    ジャズ     7     ロック  4
灘      ワールド 6    クラシック 3
荒      レゲエ   5    ロック     3
森山    J-R&B    2    レゲエ     4

% bayon -C centroid.tsv input_other.tsv
安藤    2   0.615543     1    0.55375    3   0.0283486
灘      3   0.754793
荒      1   0.561193     2    0.232494   3   0.034592
森山    1   0.585365     2    0.117231

このようにクラスタリングと類似クラスタの特定の際の入力を分けることで、クラスタリングの際は信頼性の高いドキュメント集合だけを使用してより意味のあるクラスタ結果を得ておき、その後残りのドキュメントに対しては類似クラスタ特定のフェーズで前述のクラスタ結果に寄せていく、といった利用が可能となります。

まとめ

bayonに追加した機能から、特徴的なキーの特定・類似クラスタの特定について紹介させていただきました。ご興味のある方はぜひお試し下さい!

また次回の記事では、クラスタリングと上記で紹介したドキュメントの類似クラスタ特定を組み合わせた、ユーザの嗜好分析のお話をご紹介したいと思います。

まだピクミン2をクリアしてないのでケジメ的に新作ゲームを買えないmikioです。今回は、Tokyo Cabinetを使って激烈簡単に特定サイトの専用の検索機能を設置する方法について説明します。クローリングから検索までを10分くらいの作業で可能にします。

wgettsv

特定サイトの検索エンジン

Web全体の検索機能を作るのは、途方もない技術力と設備を持っているGoogleやMicrosoftなどのビッグプレーヤでないと難しいのが現実です。でも、自分が気に入っているいくつかのサイトを対象とした検索エンジンを作るのであれば個人だってできます。また、インターネットから手が届かないイントラネットのコンテンツの検索機能は自分達で手がけないと構築できません。

ということで、企業用の検索システムが数多く売られていますし、LuceneやGroongaやHyper Estraierなどのオープンソース製品も世に多数存在します。それらに比べてTCによる検索システムが性能的あるいは機能的な観点で優れていると主張するつもりはありませんが、構築が簡単だという点はこの記事で伝わればいいかなと思っています。

実際にやってみる

はい、今から10分です。まずはこちらのコマンドをダウンロードして、wgettsv という名前で保存してください。それを実行します。例として、intra.example.com の全てのHTMLコンテンツを最大10000ファイルだけ収集してきてそれをTSVファイルに保存してみます。URLをあなたのオフィスのものに置き換えて動かしてみてください(もちろん自己責任でですが)。

$ ruby wgettsv -allow 'http://intra\.example\.com/' \
  -max 10000 'http://intra.example.com/' > intra.tsv

「-allow」オプションは、再帰的にコンテンツを取得する際に許容する飛び先のURLを指定します。example.comのイントラネットである intra.example.com ドメインのコンテンツのみを取得するようにしています。「-deny」オプションで禁止パターンを指定することもできます。「-max」オプションは最大取得数を指定します。最後の引数のURLはクロールのエントリポイントです。これは複数指定することもできます。

サイトの規模や回線の速度によりますが、小組織なら5分もあればクロールが終わると思います。ブログなどを再帰的に巡回させるとキリがないので、そういう場合は「-deny」で不要なクローリングを除外してください。クロールしてきたデータは文字コードをUTF-8に正規化した上で body や title や mtime などの属性を抽出し、それらを表現したTSVデータとして標準出力に流します。今回はそれを intra.tsv というファイルにリダイレクトして保存しています。このTSVファイルはTCのテーブルDBのインポートコマンドが期待する形式になっていますので、すぐにもインデックス作成を開始できます。

$ tctmgr importtsv tctsearch.tct intra.tsv
$ tctmgr setindex -it qgram tctsearch.tct title
$ tctmgr setindex -it qgram tctsearch.tct body

TSVファイルからインポートして tctsearch.tct というインデックスを作成して、titleコラムとbodyコラムにインデックスを張っています。5分でクロールしたくらいのレコード数であれば、この作業は数秒で終わると思います。

検索UIを設置します。Tokyo Cabinetのソースパッケージに example というディレクトリがあり、その中に検索UIのサンプルが含まれています。makeしてから、tctsearch.cgi と tctsearch.tmpl をCGIが実行できるディレクトリにコピーしてください。また、インデックスも同じディレクトリに移動してください。

$ cd tokyocabinet-1.4.29/example
$ make
$ cp tctsearch.cgi tctsearch.tmpl ~mikio/public_html
$ mv ~/mikio/tctsearch.tct ~mikio/public_html

あとは、設置した tctsearch.cgi にWebブラウザでアクセスするだけです。ここまで10分以内でできますよね? UIをカスタマイズしたい場合は tctsearch.tmpl を適当に修正してください。インデックスを定期的に更新したい場合は、クロールとインデクシングの手順をシェルスクリプトに書いてcrondに実行させればよいでしょう。

Perl/Ruby/Javaで検索UIを作る

上記で設置したデモ用の検索用CGIスクリプトは、以前の記事で述べたTCのテンプレート直列化機能を用いてC言語で実装したものです。100行に満たないCのコードでここまで表現できるのは我ながら素敵だなと思うわけですが、しかしCプログラマの数は各種スクリプト言語のプログラマの数に比べてかなり少ないのが現実です。Cプログラマでも、UI層をCで書くなんて常軌を逸していると感じるかもしれません。私の趣味はさておき、CGIスクリプトと言えばスクリプト言語の出番ですよね。ということで、PerlとRubyとJavaでサンプルを実装してみました。いずれもロジック部分は50行以下で済んでいます。

tctsearch

Perlで書くとロジック部分は以下のようになります(完全なソースコードはこちら)。ロジック部分と表示部分を完全に分けて記述するのがCGIスクリプトを美しく書くコツです。この例では検索結果の主キーのリストをforeachで回して各々の文書情報をわざわざ@docsという配列に入れて、あとは@docsをforeachで回して表示するようにしています。metasearchとkwicというメソッドが謎な感じですが、それらについては後述します。

# CGI処理オブジェクトを作り、パラメータを受け取る
my $cgi = CGI->new();
my $scriptname = $cgi->script_name();
my $expr = $cgi->param("expr");

# 検索語が入力されていれば検索を行う
my @docs;
if(defined($expr) && length($expr) > 0){

  # データベースを開く
  my $tdb = TokyoCabinet::TDB->new();
  $tdb->open(DBPATH);

  # クエリオブジェクトを作る
  my $tqry = TokyoCabinet::TDBQRY->new($tdb);
  $tqry->addcond("title", $tqry->QCFTSEX, $expr);
  $tqry->setorder("title", $tqry->QOSTRASC);
  $tqry->setlimit(30, 0);
  my $bqry = TokyoCabinet::TDBQRY->new($tdb);
  $bqry->addcond("body", $bqry->QCFTSEX, $expr);
  $bqry->setorder("title", $bqry->QOSTRASC);
  $bqry->setlimit(30, 0);

  # 検索する
  my $res = $tqry->metasearch([ $bqry ], $tqry->MSUNION);

  # 該当文書の情報を取り出して表示用の配列に入れる
  foreach my $pkey (@$res){
    last if(@docs >= 30);
    my $cols = $tdb->get($pkey);
    if(defined($cols)){
      my $kwic = $bqry->kwic($cols, undef, 20, $bqry->KWMUCTRL);
      $cols->{kwic} = $kwic;
      push(@docs, $cols);
    }
  }

  # データベースを閉じる
  $tdb->close();

}

Rubyで書くとロジック部分は以下のようになります(完全なソースコードはこちら)。Perlと全く同じ流れです。

# CGI処理オブジェクトを作り、パラメータを受け取る
cgi = CGI::new
scriptname = cgi.script_name
expr = cgi.params["expr"][0]

# 検索語が入力されていれば検索を行う
docs = []
if expr && expr.length > 0

  # データベースを開く
  tdb = TDB::new
  tdb.open(DBPATH)

  # クエリオブジェクトを作る
  tqry = TDBQRY::new(tdb)
  tqry.addcond("title", TDBQRY::QCFTSEX, expr)
  tqry.setorder("title", TDBQRY::QOSTRASC)
  tqry.setlimit(30, 0)
  bqry = TDBQRY::new(tdb)
  bqry.addcond("body", TDBQRY::QCFTSEX, expr)
  bqry.setorder("title", TDBQRY::QOSTRASC)
  bqry.setlimit(30, 0)

  # 検索する
  res = tqry.metasearch([ bqry ], TDBQRY::MSUNION)

  # 該当文書の情報を取り出して表示用の配列に入れる
  res.each do |pkey|
    break if(docs.length >= 30)
    cols = tdb.get(pkey)
    if cols
      kwic = bqry.kwic(cols, nil, 20, TDBQRY::KWMUCTRL)
      cols["kwic"] = kwic
      docs.push(cols)
    end
  end

  # データベースを閉じる
  tdb.close()

end

Java(JSP)で書くとロジック部分は以下のようになります(完全なソースコードはこちら)。PerlやRubyと全く同じ流れです。

// サーブレットリクエストからパラメータを受け取る
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
String expr = request.getParameter("expr");

// 検索語が入力されていれば検索を行う
List docs = new ArrayList();
if(expr != null && expr.length() > 0){

  // データベースを開く
  TDB tdb = new TDB();
  tdb.open(dbpath, TDB.OREADER);

  // クエリオブジェクトを作る
  TDBQRY tqry = new TDBQRY(tdb);
  tqry.addcond("title", TDBQRY.QCFTSEX, expr);
  tqry.setorder("title", TDBQRY.QOSTRASC);
  tqry.setlimit(30, 0);
  TDBQRY bqry = new TDBQRY(tdb);
  bqry.addcond("body", TDBQRY.QCFTSEX, expr);
  bqry.setorder("title", TDBQRY.QOSTRASC);
  bqry.setlimit(30, 0);
  TDBQRY[] oqrys = { bqry };

  // 検索する
  List res = tqry.metasearch(oqrys, TDBQRY.MSUNION);

  // 該当文書の情報を取り出して表示用の配列に入れる
  Iterator resit = res.iterator();
  while(docs.size() < 30 && resit.hasNext()){
    byte[] pkey = (byte[])resit.next();
    Map cols = tdb.get(new String(pkey, "UTF-8"));
    if(cols != null){
      String[] kwic = bqry.kwic(cols, null, 20, TDBQRY.KWMUCTRL);
      cols.put("kwic", kwic);
      docs.add(cols);
    }
  }

  // データベースを閉じる
  tdb.close();

}

3言語ともほとんど同じ構造になるあたり、tokyocabinet.idlの威力を感じていただけたのではないかと思います。各種言語の良さを100%引き出すインターフェイスにはなっていませんが、どれかひとつでの書き方を知っていれば他の言語でもサクっと仕上げて定時に帰れるというのが素敵ポイントです。

メタ検索

Webコンテンツの全文検索においては、「タイトルもしくは本文のいずれかに検索文字列を含む」という条件を指定するのが普通です。言い換えれば、「検索文字列をタイトルに含む」文書群と「検索文字列を本文に含む」文書群の和集合を提示するということです。TCのテーブルデータベースでは、ひとつのクエリオブジェクトに複数の条件をaddcondメソッドで追加していくことで論理積(AND条件:積集合)を表現することができますが、その方法では論理和(OR条件:和集合)を表現することはできません。それだとWeb検索の典型的なユースケースであるタイトルと本文を対象とした検索では困ったことになります。

そこで、複数のクエリオブジェクトを使って積集合、和集合、差集合を表現するためのメタ検索という機能が提供されています。例えば、qry1.metasearch([qry2,qry3], TDBQRY.MSISECT); とすると、qry1とqry2とqry3の検索結果の積集合(つまり全てクエリの結果に含まれる主キーの集合)を問い合わせることができます。また、qry1.metasearch([qry2,qry3], TDBQRY.MSUNION); とすると、qry1とqry2とqry3の検索結果の和集合(どれかのクエリの結果に含まれる主キーの集合)を問い合わせることができます。qry1.metasearch([qry2,qry3], TDBQRY.MSDIFF); とすると、qry1と「qry2とqry3の和集合」の差集合(つまりqry1に含まれるがqry2にもqry3にも含まれない主キーの集合)を問い合わせることができます。

単純な検索エンジンとしての効率を求めるならば、タイトルと本文を連結した文字列を単一のコラムとして格納しておいて、それを対象とした検索をすればいいでしょう。ただ、そうしてしまうと「タイトルのみに含む」という条件や「本文のみに含む」という条件を指定することができなくなってしまうので、痒いところに手が届かなくなってしまいます。効率を求めるか機能性を求めるかはユースケースに応じて決定すべきなわけですが、後者を選択する場合にはメタ検索機能を重宝することになるでしょう。

メタ検索機能の嬉しい副作用として、複数のデータベースにまたがった検索ができるということが挙げられます。メールを格納したデータベースと接続したクエリオブジェクトとチャットログを格納したデータベースと接続したクエリオブジェクトに対して論理和のメタ検索を行えば、メールとチャットログの統合検索が可能となります。メーリングリスト毎に別個のデータベースを作っておいて、それらをチェックボックスで選択して統合検索するようなUIも簡単に作ることができます。

スケーラビリティの観点からも、メタ検索機能は重要です。いわゆるリアルタイム検索を想定した場合、更新の負荷を下げることが重要となりますが、そのためにはデータベースを細かく分割することが必要となります。すなわち、主キーにハッシュ関数などを適用して格納先のデータベースを決定する、いわゆる水平分散(sharding)という手法です。大規模な文書群を対象とした検索機能を構築する場合にも水平分散は重要となるでしょう。分割数を上げるほどに検索の負荷は上がりますが、更新の負荷はその分だけ下げることができます。よって、分割数を調整することで検索負荷と更新負荷のバランスポイントに近づくことができるわけです。単体のホスト内でもデータベースを分けて水平分散を行い(必要であればマルチスレッドで並列処理を行い)、ホスト間でも水平分散してさらなる性能向上を図るという二段階の視点がスケーラビリティ向上には重要です。ホスト間の分散についてはTCの領域ではありませんのでアプリ層で頑張ってくれというスタンスですが。

KWIC

検索に多数の文書がヒットした場合、文書内のどこに検索語が含まれるかと、その検索語がどんな文脈で使われているかを提示することで、実際にどの文書が欲しいものなのかを判断する手助けをするのが普通です。KWIC(KeyWord In Context)とかスニペットとか呼ばれるような、本文から検索語の周辺部分を抜き出した文字列を提示するということです。Googleとかでも検索結果に太字の検索語とともに周辺文が提示されますよね。

このKWICですが、アプリ側で実装しようとすると意外に大変なのです。材料として検索語のリストと本文の文字列があればよいのですが、処理内容は著しく複雑です。正規表現でやろうとすると周辺文を領域を指定するのが厄介ですし、何より普通の正規表現では検索語をきちんと表現できないのです。というのも、全文検索においては正規表現よりも強烈に文字列の正規化をしているからです。例えば、TCでは検索対象の文字列に以下のような正規化を施しています。

  • 全角英数字を半角英数字にする。
  • 半角カタカナを全角カタカナにする。
  • ウムラウトやセディーユなどのアクセント記号を外す。
  • 英字やギリシャ文字やスラブ文字の大文字を小文字にする。
  • タブや改行やその他の空白文字および制御文字を通常の空白文字にする。

これらの正規化をするだけなら各種のライブラリを使えばよく、正規化した後なら一般的な正規表現の実装で検索語に該当する部分を特定することができるでしょう。しかし、ハイライトさせる文字列は正規化する前のものでないといけないため、正規化した後で正規表現をかけるという方法は通用しないのです。例えば「I’m Mikio Hirabayashi」を「I’m <strong>Mikio</strong> Hirabayashi」としたいとしましょう。正規化して「i’m mikio hirabayashi」にしてから正規表現をかける方法だと、「mikio」の「M」が大文字だったという情報が失われてしまうのでダメです。正規表現のi(ignore-case)オプションを使えばいけそうですが、アクセント記号などを含んでいる場合にはそれもダメです。したがって、インデックス内の文字列の正規化方法を知っている動作主体が、その知識を使って部分文字列の検出および周辺文の抽出を一括で行うことが必要となります。

ということで、KWICを出力するのはインデクサのライブラリの仕事にするのが現実的です。データストレージや情報検索を担当するライブラリとしては異色の機能ですが、仕方ないのです。クエリオブジェクトにkwicメソッドが実装されています。第1引数に文書データのハッシュ、第2引数に本文を格納したコラムの名前(undef/nil/nullの場合はクエリに指定した最初の条件のコラムを自動選択)、第3引数に周辺文の文字数、第4引数にオプションをとります。オプションには、KWMUTAB(検索語をタブでマークアップ)、KWMUCTRL(検索語を0×02と0×03でマークアップ)、KWMUBRCT(検索語を [ ] でマークアップ)、KWNOOVER(周辺文のオーバーラップを禁止)、KWLEAD(文書の先頭を強制的に選択)のビット論理和を指定することができます。いわゆる普通のKWICが欲しい場合は、qry.kwic(cols, nil, 20, TDBQRY.KWMUCTRL) で得た文字列内の制御文字を出力用のマークアップに置換するのが定石でしょう。

まとめ

任意のWebコンテンツを簡易クローラで収集してインデクシングする方法と、そうしてできたインデックスを対象とした検索用ユーザインターフェイスを各種言語バインディングを用いて簡単に実装する方法について説明しました。付属のUIを設置するだけなら10分くらいでできそうだと思っていただければ幸いです。

検索UIの実装ってなかなか楽しいですよね。メタ検索やKWIC生成などの面倒な部分はTCが担当してくれるので、適切なロジックを実現するための業務分析と使いやすいインターフェイスを実現するための情報設計に工数を割くことができます。実装言語が選択できるというところも素敵ポイントかなと思っている今日この頃です。ぜひお試しあれ。

逆転検事を先日クリアして、久しぶりに逆転裁判1〜3をやり直そうか迷い中のfujisawaです。シンプルなデータクラスタリングツールを作成しましたので、そのご紹介をさせていただきます。

クラスタリングとは

クラスタリングとは、対象のデータ集合中で似ているもの同士をまとめて、いくつかのグループにデータ集合を分割することです。データマイニングや統計分析などでよく利用され、データ集合の傾向を調べたいときなどに役に立ちます。

例えば下図の例ですと、当初はデータがゴチャゴチャと混ざっていてよく分からなかったのですが、クラスタリングすることで、実際は3つのグループのデータのみから構成されていることが分かります。

クラスタリング

様々なクラスタリング手法がこれまでに提案されていますが、有名なところではK-means法などが挙げられます。ここでは詳細については触れませんが、クラスタリングについてより詳しく知りたい方は以下のページが詳しいため、そちらをご参照下さい。

軽量データクラスタリングツールbayon

シンプルなデータクラスタリングツールとして、bayonというツールを作成しました。名前は、筆者が好きなアンコール・ワット遺跡のバイヨン寺院から拝借したもので、特にクラスタリングには関係ありません。

bayonはシンプルな構成で、かつ大規模なデータでも実用的なスピードで実行できるようにすることを目標に作成しています。手元の環境でいくつかのデータセットを用意して簡単に実行時間を測ってみたところ、以下の結果が得られました。50万件のデータでも12分程度で完了できており、用途にもよるとは思いますが、実用に耐えるだけの速度は得られていると思います。

データ数 クラスタ数 実行時間(秒)
1000 10 0.17
50000 1000 36
500000 10000 720

bayonはクラスタリング手法として、後述するRepeated Bisection法を採用しているのですが、これは他のクラスタリングツールCLUTOで使用されている手法です。CLUTOは高速かつ精度もよいクラスタリングツールで、GUIでの利用や様々なオプションなどを利用することができる非常に便利なツールです。じゃあそもそもCLUTOを使えばいいのでは?となりそうですが、CLUTOは商用で利用する場合は事前許可が必要となり、使用の際はライセンスに注意しなければなりません。その点bayonはライセンスにGPL2を採用しているため、商用のサービスでの利用も問題ありません。

また手元の環境で試した限りでは、クラスタリングの実行速度はCLUTOよりもbayonの方が上回っています。ただCLUTOは精度向上のためにさらに複雑なことをしているので、単純な比較はできないのですが、さくっと傾向を確認したい場合等にはbayonも一つの選択肢として考慮していただければ幸いです。

実行方法

実際にbayonでクラスタリングを行う際は、まず入力として以下のようなタブ区切りのテキストファイルを用意してください。1行に1つのドキュメントの情報を表し、行の先頭にドキュメントのID、それ以降はそのドキュメントの特徴を表すキーとポイントをペアで記入します。

% cat input.tsv
阿佐田  J-POP      10 	J-R&B     6   ロック   4
小島    ジャズ      8 	レゲエ    9
古川    クラシック  4 	ワールド  4
田村    ジャズ      9 	メタル    2   レゲエ   6
青柳    J-POP       4 	ロック    3   HIPHOP   3
三輪    クラシック  8   ロック    1

例えば上記の例では、1ユーザの情報を1つのドキュメントと考え、ユーザが聞く音楽のジャンルによってそのユーザの特徴を表しています。「阿佐田」「小島」「古川」が各ユーザのID(名前)で、「阿佐田」の特徴を表すキーとして「J-POP」「J-R&B」「ロック」が挙げられています。

次にこの入力ファイルを使って、クラスタリングを実行します。以下ではクラスタ数を3に指定して実行しています。

% bayon -n 3 input.tsv > output.tsv

クラスタリング結果は、以下のようなタブ区切りのテキストになります。1行が1つのクラスタを表し、行の先頭がクラスタのID、それ以降はそのクラスタに所属するドキュメントのIDのリストです。

% cat output.tsv
1       小島    田村
2       阿佐田  青柳
3       古川    三輪

それぞれ、ジャズ好き、J-POP・ロック好き、クラシック好きのクラスタに分割できていることが分かります。

より詳しく知りたい方は、チュートリアルを用意してありますので、そちらをご参照下さい。

クラスタリング手法

bayonでは、Repeated Bisectionと呼ばれるクラスタリング手法を採用しています。Repeated BisectionはクラスタリングツールCLUTOで使用されているクラスタリング手法で、データ集合を繰り返し2分割することでクラスタリングを実行します。K-means法などと比較して、高速に実行でき、また精度も良好なようです。

Repeated Bisectionではまずすべてのデータを1つのクラスタに格納し、その後は以下の1-4の処理を実行して、繰り返してクラスタを2分割してクラスタリングを行います。

  1. 分割するクラスタを1つ選択する(一番クラスタ内のまとまりが悪いものを選択)
  2. クラスタ中からランダムに2つ要素を選択し、それぞれが格納したクラスタを2つ作成する
  3. 元のクラスタ中の全ての要素に対し、2で選んだ要素との類似度を求め、類似度が高い方のクラスタに要素を追加する
  4. 2クラスタ間で要素の移動を行い、分割結果の洗練を行う(移動できる要素がなくなるまで続ける)

repeated bisection

ここで、2-4のプロセスだけに注目してみると、これはクラスタの中心を2つとしてK-means法を実行しているのと同じことをしています。つまりRepeated BisectionはK-meansを繰り返し実行しているだけなのです。

プロセス1でまとまりの悪いクラスタを選択するとき、プロセス4で要素を移動してクラスタの洗練を行うときは、クラスタのまとまり具合を評価する必要があります。このクラスタの評価は、クラスタの各要素とクラスタの中心とのcosine類似度の和としています。この和が大きいほどクラスタの中心に凝集しているようになり、クラスタ中のデータが似た者同士の集まりである、と考えるわけです。ただし、クラスタ中の各要素と中心との類似度をまじめに全て比較するのは計算量も高くなってしまいます。実際には、この値はクラスタ中のベクトルをすべて足した複合ベクトルの長さと同値になり、そのため計算量も少なく評価値を求めることができます。評価値についてはCLUTOの論文に詳しく書かれているため、そちらをご参照下さい。

また、既存のクラスタリング手法には、確率・統計な枠組みを使用したより精度が高いと思われる手法が存在します。今回はシンプルさと実用的なスピードを主な目的としたためそれらは採用しませんでしたが、今後は他の手法も組み込み、実行時に自由に切り替えられるようにできればと考えています。

まとめ

軽量クラスタリングツールbayonのご紹介をさせていただきました。 bayonは容易に使用することができ、また実用に耐えられるだけのスピードを備えています。精度・機能面や、汚いソースコードなどまだまだ改善の余地がたくさんありますが、データの特徴をサッと調べたいときなどには皆様のお役に立てるかもしれません。もしご興味がある方がいらっしゃいましたら、ぜひご利用下さい!

ノートPCの冷却ファンがうるさいのを対処しようとしてWebで調べたら、そのファンの設計者が「静音性へのこだわり」を語ったページにたどり着いて複雑な心境のmikioです。今回は、Tokyo Cabinet(TC)の最新バージョンで実装された動的デフラグ機能について長々と説明します。

断片化とデフラグ

任意のサイズのデータを管理する記憶装置においては、利用可能領域の断片化(fragmentation)の問題が常につきまといます。ファイルシステム上で任意のサイズのファイルを管理する際にも、データベースファイル内で任意のサイズのレコードを管理する際にも、C言語のmalloc/free関数群でメモリの管理をする際にも、様々なレイヤで断片化が起きうるのです。なぜなら、データを削除もしくは移動した際の空き領域を再利用するにあたって、その領域と同じサイズのデータが常に入ってくるとは限らないからです。特にデータのサイズが変わるような更新操作を頻繁に行う場合や、サイズがまちまちのデータの追加や削除を頻繁に行う場合には、断片化の問題が顕著になります。断片化が進むと占有領域がどんどん肥大化してしまい、性能が悪化したり、それ以上データが入れられなくなったりといった不都合が起こることになります。

tcfragment

断片化を解消するには、使用している領域を連続した場所に詰めて再配置するとともに、空き領域も連続させて再配置して再利用可能にすることが必要です。そのような操作を「デフラグ(defragmentation)」と呼びます。Windowsのコントロールパネルなどからファイルシステムのデフラグをかけた経験のある方も多いと思います。メモリ管理のレイヤでは「コンパクション」と呼ばれる機能として実装されています。TCでも従来からデフラグ機能を最適化という名目で実装しています。

tcdefrag

なお、TCには従来から「フリーブロックプール」という機構があります。空き領域のリストを管理して、新たにレコードの領域を割り当てる際に、空き領域の中で要求サイズ以上の最小の領域を再利用することで断片化の影響を最小化するものです。割り当てた領域と空き領域の差分(余り領域)が一定サイズ以上であればさらに空き領域としてリストに還元するといった工夫もしています。しかし、そこまでしても、固定長でなく任意のサイズの領域を割り当てようとする時点で、余り領域が各所に発生するのは避けられず、断片化は起きてしまうのです。

静的デフラグと動的デフラグ

一般的に言って、ファイルシステムやデータベースの文脈でいうところの「デフラグ」ないし「最適化」という操作は、その操作をしている間にその記憶領域全体が掌握されて、他の操作ができなくなることが多いです。このような静的かつ大域的なデフラグ操作をここでは「静的デフラグ」と呼ぶことにします。それに対して、記憶領域全体を掌握することなく、他の操作と並行に行われるような局所的なデフラグ操作を「動的デフラグ」と呼ぶことにします。TCのoptimize系メソッドは静的デフラグですし、MySQLなどのRDBMSで使われるoptimize文も静的デフラグです。一方でZFSやBtrfsなどの新しめのファイルシステムでは動的デフラグを実装しているようです。

静的デフラグの実装は比較的単純で、他スレッドの更新操作をブロックしてしまえばレースコンディションにまつわる複雑な問題に悩まされずに済むので、結果的に効率的な処理を実現することが容易になります。しかし、デフラグをかけている数秒から数分の間に更新操作ができないというのは、24時間365日稼働のWebサービスのバックエンドに適用するには辛いものがあります。RDBMSの場合、デュアルマスタのレプリケーション構成でアクティブマスタを切り替えながら最適化をかけるといったワークアラウンドもありますが、できればそういう面倒は避けたいものです。一方で、動的デフラグであれば、アベイラビリティの問題を特に意識することなく、ストレージのサイズとパフォーマンスを一定に保つことができます。

ということで、TCに動的デフラグを実装してTokyo Tyrant(TT)から利用可能にすることで、24*365対応が必要な皆様のハートにアピールしてしまおうという魂胆です。実装が複雑になりがちですが、それだけの価値があるでしょう。

動的デフラグの実装

レコードの使用領域と空き領域が混在している状態が断片化であるわけですから、それを解消するには、使用領域は使用領域だけで集めて連続した位置に詰めて配置し、空き領域は空き領域で集めて連結して一つの領域にしてフリーブロックプールに入れるという操作を行えばよいことになります。

TCでは、ハッシュデータベース内部に動的デフラグ用のイテレータを備えています。データベースを開いた際にはイテレータはデータベースの先頭に配置され、動的デフラグの遂行が指示される度に、イテレータの位置の領域を読み込みます。読み込んだ領域がレコードであればイテレータを次の領域まで進めます。読み込んだ領域がフリーブロックであれば、次のレコードを読むまでフリーブロックを読み飛ばし、そのレコードをイテレータの位置に移動させて、イテレータをその直後まで進めます。レコードが元々あった領域は空き領域になり、かつ既存のフリーブロックと隣接しているので、連続したフリーブロックを連結した領域とみなし、それをフリーブロックプールに登録します。

tcdefragstep

動的デフラグ用イテレータがファイルの末尾まで到達した場合、直前のレコードの末尾をファイルの終端とみなしてtruncateしてファイルサイズを縮めるとともに、イテレータをファイルの先頭まで戻します。末尾まで到達しなかった場合でも連結されたフリーブロックがフリーブロックプールに戻されて再利用されるので、以降の更新操作で肥大化が起きる確率をどんどん下げていくことができます。

なお、B+木データベースやテーブルデータベースはハッシュデータベースの上に実装されているので、ハッシュデータベースの機能追加の恩恵は自動的にB+木データベースやテーブルデータベースにも波及します。固定長データベースはそもそも断片化が起きないのでデフラグという概念もありません。

使い方

まず、TCおよびTTの最新版をインストールしてください。それで、TCの各種データベースオブジェクトを開く部分のコードに、以下のような自動デフラグの設定を追加してください。ハッシュデータベースでもB+木データベースでもテーブルデータベースでも使い方はほぼ同じです。

TCHDB *hdb = tchdbnew();
tchdbsetdfunit(hdb, 8);  // 必ずopenの前に呼ぶ
tchdbopen(hdb, "casket", HDBOWRITER | HDBOCREAT);

上記でtchdbsetdfunit関数に与えている引数「8」は、断片化を8回検出したら、それに相当する領域を回収するくらいの動的デフラグ操作を暗黙的に行うという意味です。動的デフラグといえども、その操作を行っている間は該当する領域をロックせざるを得ないので、その処理の粒度を指定することは比較的重要です。「1」などとして粒度を小さくすれば並行性は上がりますが、一度に処理できる領域が限られるので時間効率と空間効率は下がります。逆に「128」などとして粒度を大きくすれば、一度に処理できる領域が増えて時間効率と空間効率を上げられますが、その間に他の操作がブロックされる確率も上昇します。私の経験では、ハッシュデータベースでは8くらい、B+木データベースでは2くらい、テーブルデータベースでは4くらいにすると丁度良いような気がしています。なお、デフォルトでは自動デフラグ機能は無効化されています。

抽象データベースでは以下のようにopenの際にパラメータを指定します。抽象データベースから使えるということはTTからも起動時に指定するだけで使えるということですね。楽ちんです。

TCADB *adb = tcadbnew();
tcadbopen(adb, "casket.tch#dfunit=8");

断片化を検出する度にデフラグを少しずつ進めるというのは多くのユースケースで妥当な選択なのですが、高負荷なWebサービスのバックエンドに用いる場合は事情が異なります。例えばmixiの場合、午後11時から日付変わって午前1時までのいわゆるピークタイムの負荷は、それ以外の時間帯の負荷に比べると3倍もしくはそれ以上になります。ピークタイムの負荷がバックエンドに対する要求スループットを決定するので、ピークタイムには余計な処理をしたくないというのが本音です。デフラグなんてのは負荷の比較的低い時間帯にちょこちょこやれば十分でしょう。ということで、任意のタイミングで明示的に動的デフラグ操作を進められるようにもしています。

tchdbdefrag(hdb, 8);

暇な時間帯であれば、更新操作を行った後に上記の関数を呼ぶというのもよいし、別スレッドを立てて継続的に上記の関数を呼ぶというのでもよいでしょう。例によって「8」は、8個分の空き領域を回収するという意味です。ここで「0」を指定すると、データベース全体を動的にデフラグします。全体にやったら静的デフラグじゃないかと言われそうですが、微妙に違います。排他制御のロックをかけたり外したりを繰り返しながら休み休みで処理を進めるので、見掛け上、他のスレッドがブロックされないのです。

性能

動的デフラグの性能を見るための恣意的なテストプログラムを実行してみました。アラインメントやフリーブロックプールが効かないようにチューニングした上で、できるだけたくさん断片化が起きるように、まちまちのサイズのレコードを追加したり削除したりを1000万回繰り返すものです。以下のような結果になりました。

DBサイズ 経過時間
デフォルト 270.58 MB 54.853 秒
dfunit=8 74.797 MB 43.242 秒

データベースのサイズを30%以下に抑えつつも、処理時間が79%に短縮されています。空間効率が上がった分だけ、時間効率も下がるという素晴らしい結果になっています。また、上記のデータベースに静的デフラグをかけると65.33MBになりました。

処理内容は複雑化しているのに時間効率が上がるというのはちょっと不思議な感じがしますね。動的デフラグの処理の中では、I/Oをするにしても同一もしくは隣接ブロックの読み書きをするケースがほとんどなので、ファイルシステムのキャッシュが効きやすく、性能劣化の原因になりにくいと考えられます。その上で、データベースが小さくなった分だけファイルI/Oが効率化されたり各種のレイヤでキャッシュが効きやすくなるのが全体の処理速度を高速化した原因となっていると考えられます。

こんなにいいことずくめならデフォルトで動的デフラグを有効化しろという声も聞こえてきそうですが、肥大化が問題になりにくいようなユースケースで動的デフラグ有効にしてもオーバーヘッドになるだけですし、前述の明示的なデフラグ操作を行いたい場合も多いでしょうから、デフォルトでは依然として無効にしています。

永続的だが時限的なキャッシュ、再び

以前に書いたテーブルデータベースの記事で述べた、期限付きのセッション情報を永続記憶装置上で保持するユースケースを再び考えてみます。TCのテーブルデータベースのコラムとして期限切れ時刻を持たせて、1秒毎に起動する削除関数で古いレコードを消していくというものです。実はこれは最も断片化を起こしやすいユースケースのひとつで、今回の動的デフラグ機能は、キャッシュの肥大化に対して静的なoptimizeをせずに対処するために用意したようなものです。

おさらいになりますが、テストしてみましょう。以下のLuaスクリプトを準備して、ttexpire.luaとして保存してください。

function expire()
   local args = {}
   local cdate = string.format("%d", _time())
   table.insert(args, "addcond\0x\0NUMLE\0" .. cdate)
   table.insert(args, "out")
   local res = _misc("search", args)
   if not res then
      _log("expiration was failed")
   end
   print("rnum=" .. _rnum() .. "  size=" .. _size())
end

まずは、動的デフラグを無効にした状態で、どれくらい肥大化が起きるかを確認しておきます。以下のようにしてサーバを立ち上げてください。

ttserver -ext ttexpire.lua -extpc expire 1 "casket.tct#idx=x:dec#bnum=2000000"

別の端末を開いて、以下のテストコマンドを実行します。これは、50%の確率でランダムなセッション情報を格納し、10%の確率でその削除を試み、40%の確率でセッションの読み出しを行うというクエリを1000万回発行するテストを行います。セッションは入れられた時刻から180秒後に期限切れになって自動削除されます。

tcrtest table -exp 180 localhost 10000000

私の環境では、上記の条件だとだいたい130万セッションくらいを保持した状態でデータベースが維持され、598秒でテストが完了しました。つまり130万セッション保持で16722QPSくらいの性能は出そうです。それはよいのですが、問題は、データベースのサイズが213MBにもなってしまったことです。この調子でサイズが肥大化していけば性能もだんだん劣化してくるので最適化をかけるわけですが、この状態で「tcrmgr optimize localhost」したところ、12.6秒かかってサイズは102MBになりました。このことから、このアクセスパターンではDBが約2倍に肥大化するということと、それを解消するための静的デフラグには約13秒間のブロッキング(=サービス停止)が発生することがわかります。うーむ。それくらいなら無理からぬ運用体制とも言えますが、正直イマイチですね。

今度は、データベースを作り直して、動的デフラグを有効にしてサーバを起動します。

rm -rf casket*
ttserver -ext ttexpire.lua -extpc expire 1 "casket.tct#idx=x:dec#bnum=2000000#dfunit=8"

クライアント側は同じコマンドです。

tcrtest table -exp 180 localhost 10000000

今度は599秒かかり、サイズは133MBでした。つまり肥大化率を1.3倍に抑えて定常運用できるということです。このサイズは同じ処理をいくら続けても増えることはないので、最適化をかける必要はありません。これで安心してセッション情報を入れ続けることができます。

ピーク時のスループットを重視して、ピーク時以外に動的デフラグをかけたい場合には、以下のLuaスクリプトを30分間隔くらいで定期実行(-extpc defrag 1800)するとよいでしょう。動的デフラグなので実行中も他の操作をブロックしないというのがポイントです。

function defrag()
   local time = tonumber(os.date("%H%M"))
   if time >= 2230 or time <= 0130 then
      return nil
   end
   if not _misc("defrag") then
      _log("defragmentation was failed", 2)
   end
end

まとめ

Tokyo Cabinetで実装された動的デフラグ機能の設計と実装について説明し、その性能を確認しました。動的デフラグを有効にすると、サービスを止めてoptimizeすることなく、データベースの肥大化を抑制しながら運用を続けることができます。また、暗黙的な動的デフラグと明示的な動的デフラグを使い分けることで、実サービスのアクセスパターンを踏まえてスループットを追求することができます。そして、それらの工夫で「永続的だが時限的なキャッシュ」のアベイラビリティを向上させられることを示しました。地味な機能を長々と語りましたが、ここまで読んでくれたあなたはその価値をわかってくれますよね?

余談ですが、またもt.maesaka氏が主導して開発を進めているTCを使ったMySQLのストレージエンジン「BlitzDB」でも動的デフラグ機構は活用されるようです。optimizeしないで運用できるというだけでもMyISAMに対してちょっぴりアドバンテージがあるかなと思っている次第です。