風鈴、あるいは文字列検索フレームワーク

ダウンロード: fooling-dist-20080220.tar.gz (415KBytes, 要 Python2.4以上) [SVN] [更新履歴]

デモ: http://fooling.tabesugi.net:8080/

ご意見・ご質問はこちらへ: http://groups.google.com/group/fooling-users

Fooling (風鈴) は Python用の文字列検索フレームワークです。


Fooling (風鈴) は Python と ありあわせのライブラリでつくった 文字列検索のためのフレームワークです。数千〜数十万件の文書の中から 日本語の文字列を検索・表示するのに向いています。 Fooling はおもに電子メール、掲示板への投稿や日記など、 時間とともに蓄積し、変更が比較的少ない文書を対象としています。

Fooling は単独で動かすソフトウェアというよりも、 Python プログラムに簡単に組み込んで使えるちょっとした検索機能を 提供することを目標としています。そのため、これは Python 本体と標準ライブラリがあれば動きます。 簡単なインターフェイスや該当文書の表示ルーチンは最初から提供されています。

特色 (ようするに欠点)

検索機能ヘルプ

Fooling では一般に以下のような検索条件が指定できます。 複数の条件はスペースで区切って組み合わせると and の意味になります。

フレーズ   あるいは   "フレーズ"
文書中に含まれるフレーズを検索します。

title:フレーズ
文書のタイトルに含まれるフレーズを検索します。

-フレーズ
指定されたフレーズが含まれない文書を検索します。

|フレーズ|
両端が文字種の境界になっているフレーズを検索します。 たとえば、|日本| は、「ここは日本です」にはマッチしますが、 「ここは日本海です」にはマッチしません。

date:年/月/日   あるいは   date:年/月   あるいは   date:年
指定された年月日に更新された文書を検索します。 ハイフン (-) で範囲を指定することもできます。 例: date:2006/8/31date:2003/1-2005/3/8date:2004-2007

このほかにも、電子メールを検索する際には Subject:To:Message-ID: などのヘッダを個別に検索できます。


使い方

ここでは基本的な使い方のみを説明しています。各コマンドについての詳細は Fooling コマンド一覧 をごらんください。また、コマンドラインツールは Fooling の機能のすべてを サポートしているわけではありません。より高度な使い方をするには API を使ってプログラミングしてください。

1. インストール

特別な操作は必要ありません。 アーカイブを展開し、fooling/ 以下のディレクトリを Python のモジュールとして発見できるようにしておきます。 あるいは PYTHONPATH を使ってもかまいません。

$ cp -Rp fooling/ /usr/local/lib/python2.4/site-packages/

2008年2月のバージョンから追加された読みがな検索を使うためには、 Fooling の Pythonコードが入っているのと同じディレクトリ (通常は fooling-dist-2008xxxx/fooling) に yomidict.tcdb というファイルが必要です。 このファイルを生成するには、fooling のトップディレクトリ (fooling-dist-2008xxxx) で以下の操作を実行してください:

$ make

2. 諸注意

3. インデックスの作成

たとえば元の文書ファイル (HTML) が /doc/Python-2.4-ja/ 以下に置かれているとします。 このディレクトリが文書ファイルの基底ディレクトリ (basedir) となります:

$ ls /doc/Python-2.4-ja/
about.html  api   doc  icons       inst  mac            ref  whatsnew
acks.html   dist  ext  index.html  lib   modindex.html  tut

次にインデックスファイル用のディレクトリ (ここでは /tmp/index/ とします) をつくり、 以下のように入力します:

$ indexer.py -b /doc/Python-2.4-ja/ -t HTMLDocument /tmp/index/ [インデックスするファイル名 ...]

indexer.py コマンドは 与えられた文書から (ひとつ以上の) インデックスファイルを生成します。 オプション -b は文書ファイルの基底ディレクトリ、 -t は文書の種類 (HTML) を指定します。 実際には、コマンドライン引数から大量のファイルを直接指定しようとすると 「引数リストが長すぎる (Argument list too long)」といわれますので、以下のように引数のファイル名を省略し、 find コマンドなどで標準入力からファイル名の一覧を与えてください。

$ ( cd /doc/Python-2.4-ja/; find . -type f -name '*.html' ) | indexer.py -b /doc/Python-2.4-ja/ -t HTMLDocument /tmp/index/
Index: basedir='/doc/Python-2.4-ja/', idxdir='/tmp/index/', max_docs_threshold=2000, max_terms_threshold=50000
...(中略)...................Building index '/tmp/index/idx00000.cdb'(504)...
...(中略).............Building index '/tmp/index/idx00001.cdb'(419)...
...(中略).....................Building index '/tmp/index/idx00002.cdb'(513)...
Done.

こうすると indexer.py は 標準入力から与えられたファイル名を読み、そのファイルをインデックスに追加します。 このときのパス名は、基底ディレクトリからの相対パスで与えてやる必要があります。

以上でインデックスファイルができあがります。 Fooling では、大量の文書をいくつかの集合に分割して複数のインデックスファイルを作ります。 各インデックスファイルは独立しており、それぞれが最大 2000個 (デフォルト) の文書に 関する情報を格納しています。

$ ls -l /tmp/index/
-rw-r--r--    1 yusuke   users     5081895 Sep  3 13:18 idx00000.cdb
-rw-r--r--    1 yusuke   users     4673407 Sep  3 13:19 idx00001.cdb
-rw-r--r--    1 yusuke   users     4319317 Sep  3 13:20 idx00002.cdb

インデックスファイルは "xxxNNNNN.cdb" のような名前になります。 最初の 3文字はプレフィックスで (デフォルトは idx)、あとの 5文字は番号になっています。 ここでは idx00002.cdb がもっとも新しい (最後に作られた) インデックスファイルです。 Fooling はこれらのインデックスファイルを辞書順の後ろから検索します。つまり、 最後にインデックスしたファイルが最初の検索結果として表示される ようになっています。

ためしに検索してみましょう。 コマンドラインからの検索には selection.py コマンドを使います。 検索する際には、文書の基底ディレクトリ (/doc/Python-2.4-ja/) と、 インデックスファイルの置かれているディレクトリ (/tmp/index/) を 両方指定する必要があります:

$ selection.py -b /doc/Python-2.4-ja/ -t HTMLDocument /tmp/index/ "python" "オブジェクト"
1: [Python チュートリアル] ... 7.2.1 ファイルオブジェクトのメソッド 7.2.2 pickle モジュール 8. エラーと例外...
...
10: [11. 標準ライブラリミニツアー - その 2] ... Python チュートリアル ... スレッド間の通信や調整にQueue オブジェクトを使うと、...
1-10 of about 388 results.
インデックスファイルはどれくらい堅牢か?

Fooling で使われるインデックスファイル (.cdb ファイル) はそれぞれ完全に独立しています。 あるインデックスファイルが失われた場合、そこに登録されていた文書は検索できなくなりますが、 それ以外のインデックスファイルに含まれている文書は影響を受けません。 もちろん、失われたインデックスファイルは元の文書から再度作りなおすことが可能です。

4. 文書の追加

インデックスに新しく文書を追加する場合は、 indexer.py コマンドに追加するファイル名を指定します。 構文は同じです:

$ indexer.py -b /doc/Python-2.4-ja/ -t HTMLDocument /tmp/index/ newfile1.html newfile2.html
Index: basedir='/doc/Python-2.4-ja/', idxdir='/tmp/index/', max_docs_threshold=2000, max_terms_threshold=50000
..Building index '/tmp/index/idx00003.cdb'(2)...
Done.
$ ls -l /tmp/index/
-rw-r--r--    1 yusuke   users     5081895 Sep  3 13:18 idx00000.cdb
-rw-r--r--    1 yusuke   users     4673407 Sep  3 13:19 idx00001.cdb
-rw-r--r--    1 yusuke   users     4319317 Sep  3 13:20 idx00002.cdb
-rw-r--r--    1 yusuke   users       40410 Sep  3 13:30 idx00003.cdb

indexer.py コマンドは、すでに /tmp/index/ 内に インデックスファイルが存在している場合、最新のインデックスファイルの「次の名前」 (この場合は idx00003.cdb) でインデックスファイルを作成します。 以後は idx00003.cdb が最新のインデックスファイルとして利用されます。

インデックスファイルの統合

Fooling では、新しく文書を追加するたびごとにインデックスファイルが 最低ひとつずつ増えていきますが、あまり少量の文書からなるインデックスファイルを 沢山つくると検索が遅くなるため、適当な間隔で これらのファイルを統合する必要があります。 インデックスファイルの統合には merger.py コマンドを使います:

$ merger.py /tmp/index/
Remain: '/tmp/index/idx00000.cdb'
Remain: '/tmp/index/idx00001.cdb'
Merging: '/tmp/index/idx00002.cdb.new' (docs=515, est. terms=44382): [</tmp/index/idx00002.cdb (docs=513, terms=44382)>, </tmp/index/idx00003.cdb (docs=2, terms=872)>]
............................................done: docs=515, terms=44893, ents=582242
$ ls -l /tmp/index/
-rw-r--r--    1 yusuke   users     5081895 Sep  3 13:18 idx00000.cdb
-rw-r--r--    1 yusuke   users     4673407 Sep  3 13:19 idx00001.cdb
-rw-r--r--    1 yusuke   users     4344626 Sep  3 13:31 idx00002.cdb
-rw-r--r--    1 yusuke   users     4319317 Sep  3 13:20 idx00002.cdb.bak
-rw-r--r--    1 yusuke   users       40410 Sep  3 13:30 idx00003.cdb.bak

merger.py コマンドは複数の小さいインデックスファイルに含まれる文書を ひとつの大きなインデックスファイルにまとめます。上の例では idx00003.cdb に含まれていた 2つの文書が idx00002.cdb に統合され、 古いファイルはそれぞれ idx00002.cdb.bakidx00003.cdb.bak という名前で 残されています。古いインデックスファイルはすぐに rm してかまいません。 デフォルトでは merger.py は最大 2000件の文書までのインデックスファイルを作成します (これ以上の文書は次のインデックスファイルに分割されます)。

インデックスファイルの統合はなるべくこまめに行ったほうがよいのですが、 この操作は 1ファイル中に含まれる文書数が多くなると時間がかかります (合計10000文書のインデックスを統合するには 10分ぐらいかかる)。 そのため、1日のうちに何十件も新しい文書が追加されるような場合は、 1時間に 1回程度の「細かい (100文書程度の)」統合をおこない、 「大きな (2000文書程度の)」統合は 1日に 1回だけおこなうようにすればよいでしょう。 なお、インデックスファイルの統合中にも検索はできますが、 インデックスファイルの統合と追加を同時に行わないでください

ひとつのインデックスファイル中に含まれる最大文書数は -D オプションで指定します:

(1時間に 1回)
$ merger.py -D 100 /tmp/index/

(1日に 1回)
$ merger.py /tmp/index/
これらは通常 cron タスクとして指定します。統合の回数を少なくするため、 できるだけ多くのファイルを一度にインデックスしたほうが効率的です。

5. 文書の更新

Fooling で使われているインデックスファイルの形式は、一度作成したら 書き換えることはできません。そのため、文書が変更された場合は基本的に 新規の文書としてインデックスに追加するか、あるいはすべての インデックスファイルをゼロから作り直すことになります。いくつかのケースでは、 これはインデックスをいくつかに分割することによって効率的におこなえます。

文書の更新頻度が少ない場合

文書の更新頻度が少ない場合には、更新された文書を 新規文書としてインデックスに「2重に」追加してしまってもかまいません。 検索結果が日付順に表示されているかぎり、必ず新しい文書が先に表示されます。

indexer.py コマンドのデフォルトのふるまいは、 インデックスの最終更新時刻 (= インデックスファイルのうち最後のものが更新された時刻) よりも新しいファイルのみをインデックスに追加します。 そのため、普通にすべてのファイルを indexer.py に与えれば、 古いファイルは自動的に無視され、更新されたファイルだけがインデックスに追加されます。 特別な操作は何も必要ありません。

頻繁に更新されるファイルが限られている場合

ある少数の決まった文書だけが頻繁に更新される場合は、 あらかじめそれ以外の文書をインデックスしておき、 頻繁に更新される文書だけを毎回再インデックスすることができます。 これはインデックスファイルに異なるプレフィックスを使うことによって実現できます。

indexer.py および merger.py コマンドでは、-p オプションによって 作成するインデックスファイルのプレフィックス (アルファベット3文字、通常は idx) を指定できます。 たとえば:

こうしておくと、検索時には bbb で始まるインデックスファイルが 必ず先に参照されるため (辞書順で大きいインデックスファイル名が先に使われる)、 頻繁に更新される文書のほうがつねに静的な文書よりも上位にランキングされることになります。 実際のコマンドラインは以下のようになります:

(最初の 1回だけ実行)
$ cat ほとんど更新されない文書の一覧 | sortbymtime.py | indexer.py -paaa /tmp/index/

(定期的に実行)
$ cat 頻繁に更新される文書の一覧 | sortbymtime.py | indexer.py -R -pbbb /tmp/index/

tools/ ディレクトリに含まれている sortbymtime.py コマンドは、 標準入力から与えられたファイル名をその更新日時の順に並べかえるものです。 (indexer.py は最後にインデックスされた文書を最初に表示するので、 日付の新しい順にランキングするにはこのようにする必要があります。) 2番目の形式にある -R オプションは、 既存のインデックスファイルをすべて無視してゼロからインデックスを作りなおす (「追加」しない) よう指定します。頻繁に更新される文書が 10程度の場合、 すべてを再インデックスしても数秒しかかかりません。

さらに、find コマンドにある -mtime オプションを使うと、 頻繁に更新される文書の一覧をほぼ自動的に発見できます:

(どちらも定期的に実行)
$ find -name '*.html' -type f -mtime +3 | sortbymtime.py | indexer.py -paaa /tmp/index/
$ find -name '*.html' -type f -mtime -3 | sortbymtime.py | indexer.py -R -pbbb /tmp/index/

この例では 3日以上更新されないファイルを「古い文書」としてインデックス aaa に追加し、 3日以内に更新されたファイルはインデックス bbb に登録されることになります (bbb は毎回作り直されます)。 それまで更新されていたファイルが 3日以上更新されなくなると、 これは自動的に aaa のほうに追加されることになります。

多数の文書が頻繁に更新される場合

Fooling は、(活発な Wiki のように) 不特定多数の文書が随時更新されているようなサイトの検索には向きません。 このような場合は、定期的に全インデックスを作り直すしかないでしょう。

6. インデックスファイルの削除

何らかの事情によりインデックスファイルを削除したい場合は、 番号の低い (古い) 方から削除してください。 個々のインデックスファイルは独立しているため、 古いインデックスファイルを削除しても、最近インデックスされた 文書はひきつづき検索できます。

また、インデックスファイルを削除したあとに そのディレクトリに対して merger.pyコマンドを実行すると、 インデックスファイルの名前は 0 からの連番になるよう自動的に変更されます。


性能

(以下の時間はすべて Linux-2.4.31、Pentium 4 (1.8GHz)、メモリ 512 MBytes、7200rpm の IDEディスク 1基で測定したものです)

インデックスにかかる時間

種類 文書数 文書の大きさ インデックス生成時間 インデックスの大きさ
新聞記事 (プレインテキスト形式、日本語): 788,810件 1215 MBytes 630分 (1200文書/分) 622ファイル、4577 MBytes
電子メール (RFC2822形式、日本語および英語): 91,200件 775 MBytes 70分 (1300文書/分) 70ファイル、270 MBytes
Java 1.5 日本語ドキュメント (HTML形式、euc-jp): 9,700件 220 MBytes 38分 (250文書/分) 13ファイル、110 MBytes
Python 2.4 日本語ドキュメント (HTML形式、euc-jp): 1,400件 14 MBytes 3分 (450文書/分) 3ファイル、14 MBytes
Linux-2.4 ソースコード (テキスト形式、英文): 12,700件 120 MBytes 28分 (500文書/分) 23ファイル、116 MBytes

インデックス時間は HTML や MIME ヘッダの解析も含みます。 インデックス時の最大メモリ使用量は 20〜100MBytes です。 なお電子メールは新山の個人宛のもので、 spam を含む約3年分のメッセージを使用しています。 新聞記事は 2006年における日本の新聞サイト約25紙を独自にクロールして取得したものです。

検索にかかる時間

検索内容 最初の 10件を表示 全件を表示 (1回目) 全件を表示 (2回目)
Java ドキュメントから オブジェクト を検索 (4157件) 0.2秒 56.2秒 32.7秒
Java ドキュメントから クラス 継承され を検索 (3360件) 0.5秒 73.7秒 58.4秒
Java ドキュメントから swing ファイル ダイアログ を検索 (47件) 1.1秒 4.1秒 2.1秒
Java ドキュメントから てくるで を検索 (0件) 0.5秒 - -
Python ドキュメントから オブジェクト を検索 (786件) 0.3秒 18.4秒 13.2秒
Python ドキュメントから Guido van Rossum を検索 (24件) 0.2秒 0.7秒 0.3秒
Python ドキュメントから タプル 配列 を検索 (12件) 0.2秒 0.4秒 0.3秒
Python ドキュメントから てくるで を検索 (1件) 0.1秒 - -
Linux ソースコードから Linus Torvalds を検索 (550件) 0.1秒 11.1秒 0.7秒
Linux ソースコードから #ifdef __kernel__ を検索 (124件) 0.6秒 10.0秒 1.0秒

検索時間は snippet (該当部分の文脈) 生成・表示までを含みます。 OS のバッファキャッシュにより、通常 2回目以降の検索は多少速くなります。

扱える最大文書量

Fooling のインデックスファイルには、以下のような制約があります:

ひとつのインデックスファイルには、(文書の大きさにもよりますが) おそらく最大数十万件〜百万件の文書を格納できます。 そのため原理的には数十億の文書を扱えることになりますが、実際にはインデックスファイルは 逐次的に検索されるので、あまりインデックスファイルの数を増やすことはおすすめしません (数百個程度が無難です)。おそらく文書数 1000万件ぐらいが実用になる限界でしょう。 しかし「快適に使える」レベルはもっと少なく、たかだか 100万件程度だと思われます。 まだそこまで試していませんけど。


バグ

きっとまだ山のようにあるはずです (2006年9月7日現在)。 怪しい挙動を発見した際はぜひ作者までお知らせください。

作者への連絡

できるだけこちらにお願いします: http://groups.google.com/group/fooling-users

新山 祐介 (yusuke at cs dot nyu dot edu)


ライセンス

(いわゆる MIT/X ライセンスです、改変・再配布および商品への組み込みはご自由に)

Copyright (c) 2006-2008 Yusuke Shinyama <yusuke at cs dot nyu dot edu>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


Yusuke Shinyama