[Python] Python純正の全文検索ライブラリ、Whooshを使ってみた
本当はPython Mini Hack-a-thonでやろうと思ってたネタだったのですが、その前にちょっと準備しておくかーと思ってたらいつのまにか結構やっちゃってたんでまとめておきます。
Whooshとは
whoosh はPython純正の全文検索エンジンのライブラリです。Javaで書かれた全文検索エンジンであるLuceneの影響をかなり受けています。というか、はっきり言ってLuceneとほぼ同じです。
今回はこのwhooshを使って手元のMLを検索してみる、全文検索ツールを試しに作ってみました。
schemeの作成
Whooshでは検索するためにIndexを作成しますが、それにはまずSchemeを定義します。
Indexにはtitleとかurlとか、ドキュメントそのもの以外の情報も格納できます。Schemeとは、Index中のドキュメントに格納されてるフィールドの定義です。どんなフィールドが使えるかは、Whoose.fields.* で定義されています。
今回は、こんな感じにします。
from whoosh.fields import Schema, ID, STORED, NGRAM
schema = Schema(path=ID(stored=True, unique=True),
body=NGRAM(stored=True))
NGRAMというのは、N-Gramで保持する情報を示します。それ以外であればTEXTなどを使えばいいでしょう。
Indexの作成
Indexの実体はディレクトリです。index.create_inでindexを作成します。
import os, os.path
from whoosh import index
if not os.path.exists("indexdir"):
os.mkdir("indexdir")
index.create_in("indexdir", schema)
ix = index.open_dir(indexdir)
ドキュメントの登録
次にIndexにドキュメントを登録します。それにはまずindexを開きます。
import whoosh.index as index
ix = index.open_dir("indexdir")
ドキュメントを登録するには、writer.add_documentを使います。ドキュメントを追加し終わったら、忘れずにcommitします。必要に応じてindexをcloseします。
writer=ix.writer();
for file_path in filenames:
content = get_content(file_path) # ファイルの中身を読み出すメソッド
if (content):
writer.add_document(path=unicode(file_path), body=content)
try:
writer.commit(optimize=True)
except:
print "add failed"
writer.cancel()
ix.close()
なお、unicode()としているぐらいで、登録する情報はUnicodeでないといけません。この点注意してください。
Indexから削除
ドキュメントを削除するために使えるメソッドが用意されています。ここでdocnumとは、内部で使っている番号で、searcher.document_number()を使って得られます。
- delete_document(docnum) * 削除する
- is_deleted(docnum) * 削除されたかチェック
- delete_by_term(fieldname, termtext) * termにマッチしたドキュメントを削除
- delete_by_query(query) * queryにマッチしたドキュメントを削除
Indexの更新
ドキュメントを置き換えたい場合は削除してからまた追加してください。
writer.delete_by_term('path', indexed_path)
to_index.add(indexed_path)
IndexWriter.update_document を使うと便利です。その場合、Scehemeの少なくとも一つはUniqueでないといけません。
writer.update_document(unique_id=u"1", content=u"Replace me")
writer.update_document(unique_id=u"1", content=u"Replacement")
idが1のドキュメントにupdateを二回かけています。この場合、最終的に”Replace me”というドキュメントが削除され、”Replacement”というドキュメントに置き換えられます。
Incrimental Index
すでにドキュメントが追加されているindexに、さらにドキュメントを追加する場合はincrimental indexを使います。
といっても別に特別なメソッドがあるわけではなく、indexを開き、writerを作り、単に writer.add_document() を呼び出すだけです。
検索
検索にはSearcherオブジェクトを使います。
ix = open_index(indexdir)
searcher = ix.searcher()
parser = QueryParser("body", schema = ix.schema)
querystring = unicode(querystring, 'utf-8') # hard coding
q = parser.parse(querystring)
results = searcher.search(q)
for r in results:
print r["body"]
ix.close()
QueryParserでparserを作り、parserにqueryの文字列をセットした後に、searchします。
評価
7227通、74MB分のMLアーカイブを使って評価してみました。
indexの大きさ
indexの大きさは、なんと558MBになってしまいました。ちょっと大きすぎじゃね?ちなみに、NGRAMはデフォルトではminimum lengthが2,max lengthが4で、今回はそのまま使いました。
indexの大きさがindex作成時間に効いているんじゃないかとも思いますが、深くは追求していません。
速度評価
評価環境は * Intel Core i7 2.67MHz * メモリ 4GB * VMware のUbuntu 10.10 (ホストOS windows 7) * SSD
です。まあそもそもVMWareで試しているぐらいでちゃんとした評価じゃないんで、あくまで参考程度にしてください。
評価にはBenchmarkerを使いました。
評価項目は * 7227件のメールを writer.add_documentする
- ファイルを開き、読み込み、add_documentします
- UTF8に変換済みなので、変換コストはなし
- writer.commitする * Optimize=True 付き
の二件です
## benchmarker: release 3.0.1 (for python)
## python platform: linux2 [GCC 4.4.5]
## python version: 2.6.6
## python executable: /home/shirou/Works/VEnvs/pydev/bin/python
## user sys total real
add_document 743.2500 12.4200 755.6700 763.3811
commit 895.2400 374.5700 1269.8100 3539.8293
## Ranking real
add_document 763.3811 (100.0%) *************************
commit 3539.8293 ( 21.6%) *****
## Ratio Matrix real [01] [02]
[01] add_document 763.3811 100.0% 463.7%
[02] commit 3539.8293 21.6% 100.0%
検索は
## user sys total real
search 0.1500 0.0600 0.2100 0.2624
です。この程度であればはっきり言って一瞬です。
というわけで、体感としては70MB程度の文書でもindexの追加にはかなり時間がかかりますが、検索は早い、という感じでしょうか。
これ以上の大規模になった場合にどうなるかはめんどいので検証していません。しかし、個人で簡単に使いたい、という場合には結構有効なのではないでしょうか。
おまけ: Google App Engine対応
Python純正となれば、Google App Engineで動かしてみたい、という気になる人が100人ぐらいいると思いますが、残念ながらindexがディレクトリ、すなわちファイルシステムを前提としているため、動きません。
しかし、whooshはstore.Storageという形で抽象化しており、その中にfiledb.gaeというクラスがあり、これを使えばblobstore上にindexを置くことで、GAE上でも動作することができるようです。
ただ、gae.pyのコメントに書かれているとおり、実験的なクラスなことに注意してください。なお、ぼくは(まだ)試していません。