Hatena::ブログ(Diary)

そこはかとなく書くよ。

2011-04-20

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作成時間に効いているんじゃないかとも思いますが、深くは追求していません。

速度評価

評価環境は

です。まあそもそも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のコメントに書かれているとおり、実験的なクラスなことに注意してください。なお、ぼくは(まだ)試していません。

kenjikenji 2013/11/15 09:30 GAE対応はイマイチでした。ある一つのオブジェクトに書き込みしまくるので、ndbの書き込み制約にひっかかってインデックス作成がままなりません。一旦タスクキューに落としてゆっくり作成するか、memcacheをつかって作成後にndbに書き込むか、なんらかの工夫をしないと実用にならないです。あとndbのblobのサイズは1MBに制限があるので、このあたりちゃんとチェックしているのかもこれから調べてみます。

rudirudi 2013/11/15 14:59 そうでしたか。それは残念です。
しかし、これを書いたのはもう2年前ですし、GAEもsearch APIが実装されたので、わざわざwhooshを使う必要はないのでではないでしょうか。

kenjikenji 2013/11/16 11:00 Googleならではの検索精度を期待してSearch APIを使ってみたのですけれど、単語の検索に関しては完全一致でないとヒットせず、がっかりです。検索対象は英語のみ。標準のAPIを使っていればそのうち改良されるかもしれませんが、Search APIがexperientalの頃から変わっていないと思います。

"Did you mean? (もしかして?)"機能も欲しかったところ、Whooshは自前の辞書を元に正しい単語を予測してくれるのでピッタリです。それでなんとかWhooshを改良してGAE使いたいと思っています。

Google Custom Searchも検討しましたがクローラーなのでインデックスの更新タイミングが制御できないので駄目でした。僕のサイトは期間限定のコンテンツが多いので...。

kenjikenji 2013/11/16 11:00 Googleならではの検索精度を期待してSearch APIを使ってみたのですけれど、単語の検索に関しては完全一致でないとヒットせず、がっかりです。検索対象は英語のみ。標準のAPIを使っていればそのうち改良されるかもしれませんが、Search APIがexperientalの頃から変わっていないと思います。

"Did you mean? (もしかして?)"機能も欲しかったところ、Whooshは自前の辞書を元に正しい単語を予測してくれるのでピッタリです。それでなんとかWhooshを改良してGAE使いたいと思っています。

Google Custom Searchも検討しましたがクローラーなのでインデックスの更新タイミングが制御できないので駄目でした。僕のサイトは期間限定のコンテンツが多いので...。

rudirudi 2013/11/20 14:53 なるほど、そうでしたか。Javaでしたら luceneappengine https://code.google.com/p/luceneappengine/ なんてのもあるようですが、whooshを改良してpull requestを送ると、作者が喜ぶと思います。ぜひお願いします。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/rudi/20110420/1303307332