Google App EngineでLuceneを使ってN-gram全文検索を行ってみる

全文検索エンジンLuceneGoogle App Engine/Javaslim3の上で動かしてみました。indexの作成には、N-gram を使っています。

準備

まずは、Luceneの最新版を取得します。今回は3.0.2を使用しました。

  • lib/lucene-core
  • contrib/contrib/analyzers/common/lucene-analyzers-3.0.2

の二つのjarファイルをprojectの war/WEB-INF/lib にコピーし、build pathに追加します。

GAE特有の問題に対処

Luceneを使うだけであればjarをいれておけばよいのですが、GAE特有の問題がいくつかあります。

Indexの取り扱い

Luceneはindexを保持し、このindexを元に文書を検索します。そのため、このindexをどこにどうやって保存するかが問題となります。

Luceneでは、通常indexはファイルとなっています。しかし、GAEではファイルは扱えません。調べてみるとRAMDirectoryというクラスを使うことで、ファイルではなくメモリ上にindexを展開できるようです。そして、RAMDirectoryはserializableであるため、バイト列にして保存できます。

GAEではblobというデータタイプを使うことでバイト列を保存できますので、RAMDirecotryとして作成したindexをblobに保存することにします。

Datastoreの容量制限

Datastoreでは、一つのエンティティの容量が1MBに制限されています。そのため、大量の文書をindexにいれていくと、indexが大きくなり1MB以上になってしまいます。

この問題への解決方法ですが、LuceneではMultiReaderという仕組みがあり、複数のindexを一度に検索させることができます。従って、複数のエンティティに分散させておいたindexを集めてきて、MultiReaderで一度に検索することができます。

ただし、GAEではヒープメモリが最大100MBに制限されているようです。従って、あまり多くのindexを展開することはできないと思われます。

発生した問題

以上のGAE特有の問題を設計でなんとなく解決しましたが、実装してみるといくつか問題が出てきました。

IndexWriter/IndexReaderでNullPointerExceptionが発生

indexWriterを作成する時にRAMDirectoryをlockするのですが、その時に内部で呼ばれる Directory.makeLock() で NullPointerException が発生しました。

ソースを見ると、指定しない場合は SingleInstanceLockFactory を使うようになっているように見えるのですが、だめなようです。これを回避するには、明示的にLockFactoryを指定すれば大丈夫です。

RAMDirectory ramdir = new RAMDirectory();

SingleInstanceLockFactory lockFac = new SingleInstanceLockFactory();
ramIdx.setLockFactory(lockFac);

この問題はIndexReaderでも同様に問題が発生します。

IndexWriter.optimize()でAccessControlExceptionが出る

local環境では問題なかったのですが、実際にdeployしたところ、IndexWriter.optimize()の実行時に、以下のエラーが出ました。

java.security.AccessControlException: access denied (java.lang.RuntimePermission modifyThreadGroup)
	at java.security.AccessControlContext.checkPermission(AccessControlContext.java:355)
	at java.security.AccessController.checkPermission(AccessController.java:567)
	at java.lang.SecurityManager.checkPermission(SecurityManager.java:549)
	at com.google.apphosting.runtime.security.CustomSecurityManager.checkPermission(CustomSecurityManager.java:45)
	at com.google.apphosting.runtime.security.CustomSecurityManager.checkAccess(CustomSecurityManager.java:93)
	at java.lang.ThreadGroup.checkAccess(ThreadGroup.java:304)
	at java.lang.Thread.init(Thread.java:349)
	at java.lang.Thread.<init>(Thread.java:394)
	at org.apache.lucene.index.ConcurrentMergeScheduler$MergeThread.<init>(ConcurrentMergeScheduler.java:249)
	at org.apache.lucene.index.ConcurrentMergeScheduler.getMergeThread(ConcurrentMergeScheduler.java:236)
	at org.apache.lucene.index.ConcurrentMergeScheduler.merge(ConcurrentMergeScheduler.java:212)
	at org.apache.lucene.index.IndexWriter.maybeMerge(IndexWriter.java:2521)
	at org.apache.lucene.index.IndexWriter.maybeMerge(IndexWriter.java:2516)
	at org.apache.lucene.index.IndexWriter.maybeMerge(IndexWriter.java:2512)
	at org.apache.lucene.index.IndexWriter.flush(IndexWriter.java:3556)
	at org.apache.lucene.index.IndexWriter.closeInternal(IndexWriter.java:1711)
	at org.apache.lucene.index.IndexWriter.close(IndexWriter.java:1674)
	at org.apache.lucene.index.IndexWriter.close(IndexWriter.java:1638)

これは、IndexWriterでindexのmerge時に使っている、ConcurrentMergeSchedulerがスレッドを使っているために起きている問題だと思われます。

これを、スレッドを使わない、SerialMergeSchedulerを使うように変更します。

SerialMergeScheduler serialMerge = new SerialMergeScheduler();
iWriter.setMergeScheduler(serialMerge);

でとりあえず問題はないみたいです。

実装例

実際に実装してみたのが以下のURLです。

http://all-write.appspot.com

SimpleNoteと同じように、ノートを追加していくアプリケーションです。

emailとパスワードの両方共testでログインし、試してみてください。ただし、排他制御はしていないので、同時に二人がtestで入ると問題が起きる場合があることをご了承ください。

今後

例えば1000文書ぐらい追加しての検索や、性能評価などをしてみたいな、とは思っていますが、いつもどおりいつになることやら。