関口宏司のLuceneブログ

OSS検索ライブラリのLuceneおよびそのサブプロジェクト(Solr/Tika/Mahoutなど)について
スポンサーサイト

一定期間更新がないため広告を表示しています

| スポンサードリンク | - | | - | - |
Lucene 4.0で実施されたIndexReaderの主なリファクタリング(Uwe氏のブログより)
ある案件でLucene 4.0におけるIndexReaderの変更点について調べる必要があった。いろいろ調べているうちにLuceneコミッターのUwe Schindler さんのブログに行き着いた。こちらの記事が変更における背景などに触れていてなかなかよかった。どうせなら日本語訳を掲載したいと思いUweさんに聞いたところ、快諾をいただけたので、以下に拙訳を掲載する:
本記事はSearchWorkingsに掲載された記事の再掲です。

Luceneは誕生当初よりIndexReaderとIndexWriterのAPIを通じてインデックスを直接的に読み書きできるようにしていました。しかし、APIは現実を反映していませんでした。過去にはIndexWriterからの視点ではこれは価値がありましたが、インデックスを読むときにこれはいくつかの問題の原因となっていました。Luceneインデックスは論理的には単一ですが実際にはそうではありません。最新のLucene trunkの開発ではタイプセーフと性能のために現実に触れられるようにしようとしています。しかし、Composite、AtomicそしてDirectoryReaderの詳細に入る前に、少しこれまでの経緯をおさらいしてみましょう。

バージョン2.9/3.0以降、LuceneはトップレベルのIndexReaderを直接検索実行することからセグメント毎に検索する方法に舵を切りました。Simon Willnauerさんが彼のブログで説明しているとおり、これによりoptimizeはもはや検索性能を最適化するのには必要ではなくなったことを意味します。実際、optimizeは検索を遅くし、最適化後はファイルシステムとLuceneインデックス内部のキャッシュを無効化してしまいます。

標準のLuceneインデックスはいくつかのセグメントと呼ばれるものから成ります。セグメントはそれ自体小さなインデックスとみなせます。インデックス作成中はLuceneは新しいドキュメントを分割されたセグメントに書き出します。ある閾値にセグメント数が達すると、セグメントはマージされます(マージをビジュアルに表現したMike McCandlessさんのブログはこちら):

Luceneのマージのしくみ

Lucene 2.9より以前は、インデックスが複数のセグメントに分かれていようと、セグメントは1つの大きなインデックスのように扱われていました。しかし2.9からはセグメント毎に働くようにシフトしてきました。そして現在は、Luceneのほとんどすべての構成とコンポーネントはセグメント毎に動作するようになっています。とりわけこれによりLuceneは再オープン時にインデックスのすべてではなく、実際の変更分のみをロードします。それでも利用者からは1つの大きな論理インデックスに見えます(下記IndexSearcherのコード例参照)が、内部ではセグメント毎に動作しています:

public void search(Weight weight, Collector collector) throws IOException {  
  // iterate through all segment readers & execute the search  
  for (int i = 0; i < subReaders.length; i++) {  
    // pass the reader to the collector  
    collector.setNextReader(subReaders[i], docStarts[i]);  
    final Scorer scorer = ...;  
    if (scorer != null) { // score documents on this segment  
      scorer.score(collector);  
    }
  }  
}


しかし、論理インデックスとセグメント間の差異は一貫的にコード階層には反映されていませんでした。Lucene 3.xでは、まだサブReaderをイテレートせずトップレベル(論理)Readerにて検索を実行していたのでした。そのため複数セグメントからなるインデックスでは検索は劇的に遅くなりました。そういうことで昔のLuceneではインデックスをよくoptimizeするよう指導されていました。

もう少し詳しく問題を説明しましょう。DirectoryのIndexReaderは内部的にはSegmentReaderを内包したMultiReaderです。もしMultiReaderにTermEnumやポスティングを問い合わせるとそれはオンザフライでサブReaderのタームやポスティングをマージします。このマージ手続きはプライオリティキューまたは類似のデータ構造を用い、それによりサブReaderの数に応じて深刻な性能劣化を引き起こします。

このような内部的な制限にもかかわらず、MultiReaderと組み合わせてSegmentReaderを利用することはLuceneにおいて上位レベルの構造に影響を与えました。インデックスを非転置にしたFieldCacheは検索時にインデックスされた値によって検索結果をソートしたりドキュメント/値のルックアップに使われます。トップレベルReaderを非転置にするとFieldCacheの二重化を引き起こし、同じ内容のキャッシュを複数生成してしまいます。

Lucene 4.0のタイプセーブIndexReader

Lucene 4.0は当初からタームとポスティングデータをMultiReaderまたはDirectoryReader(ディスク上のインデックスをIndexReader.open(Directory)でオープンしたときに返されるReaderの実装)などのコンポジットなReaderから取得できないように設計されました。Lucene trunkの最初のバージョンは非SegmentReaderからFieldやTermsEnumやDocsEnumを取得しようとすると単純にUnsupportedOperationExceptionを投げていました。このタイプセーフの欠如のせいでプログラマーはIndexReaderがcompositeかatomicかをチェックしなければポスティングを取得できるかどうか判断できませんでした。

Lucene 4.0における主なAPI変更のひとつであるLUCENE-2858では、インデックスとそのセグメント上のLuceneクライアントコード視点を完全に変更しました。抽象クラスIndexReaderは検索結果を表示するのにストアフィールドをアクセスするのに本質的なメソッドだけを持つようにリファクタリングされました。それはもはやインデックスからタームまたはポスティングを取得することはできません。削除ドキュメントに関してももはや不可視です。Luceneはクエリリライトやドキュメント収集といった手続きを自動的にサブReaderに委譲します。

もしインデックスの詳細に立ち入り自分のクエリを書きたいのなら、新しい抽象サブクラスであるAtomicReaderまたはCompositeReaderを詳しく調べてください:

AtomicReaderインスタンスは現時点でTerm、ポスティング、DocValueおよびFieldCacheの唯一のソースです。クエリはセグメント毎のアトミックなReader上で実行されるようになり、FieldCacheはAtomicReaderによってキーづけされます。対するCompositeReaderはそれが保持するメンバーを取得するユーティリティメソッドを持っています。しかし気をつけてください、そのメンバーはアトミックである必要はありません。タイプセーフの次に私たちが行ったのは、抽象クラスIndexReaderからIndexCommitとバージョン番号の概念を取り除いたことでした。これらのIndexWriterと関連する機能はDirectoryReaderに移行されました。Lucene trunkにおけるクエリの実行は以下のようになります:

DirectoryReader reader = DirectoryReader.open(directory);  
IndexSearcher searcher = new IndexSearcher(reader);  
Query query = new QueryParser("fieldname", analyzer).parse(“text”);  
TopDocs hits = searcher.search(query, 10);  
ScoreDoc[] docs = hits.scoreDocs;  
Document doc1 = searcher.doc(docs[0].doc);  
// alternative:  
Document doc2 = reader.document(docs[1].doc);


見慣れたコードですね?実際、API利用者にはこの主なリファクタリングは大きな変更をもたらしません。もしアップグレード中にコンパイルエラーが起こったら、それは性能ボトルネックを示している可能性があります。

Filterにおけるセグメント毎セマンティクスの強制

もしカスタムFilterを使ったコードを持っているなら、Luceneの別の新しいクラス階層に気づいているかもしれません(LUCENE-2831参照):IndexReaderContextの仲間であるAtomic/CompositeReaderContextです。これはだいぶ前に追加されましたが、AtomicやCompositeのReaderたちと関連深いものです。

セグメント毎検索に向かうLucene 2.9ではそれを扱えない多くのカスタムクエリとフィルタが開発されました。たとえば、いくつかのFilter実装は渡されたIndexReaderをIndexSearcherへ渡されたIndexReaderと同等であると期待しました。明らかにこのパラダイムシフトは多くのアプリケーション、とりわけこれらクロスセグメントのデータ構造を利用するアプリケーション(Apache Solrのような)を壊しました。

Lucene 4.0では、私たちはIndexReaderContextというSearcherプライベートなReader階層を導入しました。クエリやフィルタを実行中、Luceneはもはや生のReaderをQuery、FilterもしくはCollectionに渡さなくなりました。その代わり、コンポーネントはAtomicReaderContext(本質的な階層葉)を通じてトップレベルReaderに関連したドキュメントベースのような関連プロパティとして用意されます。これによりクエリとフィルタはセグメント毎であるにもかかわらずドキュメントベースIDのロジックにビルドアップされます。

トップレベルReaderはまだ使えるか?

それでもまだトップレベルReaderを使う正当なユースケースはあります。たとえば、インデックスへのアトミックなビューが欲しい場合です。オートコンプリートやファセットなどで完全なインデックスのすべてのタームが欲しい場合があげられます。このためLuceneはAtomicReaderをエミュレートするSlowCompositeReaderWrapperのようなラッパーユーティリティを用意しています。注意:アトミックなエミュレーターの利用は、タームやポスティングやDocValueやFieldCacheのマージが必要になり深刻なスローダウンの原因となるので注意してください。

Terms terms = SlowCompositeReaderWrapper.wrap(directoryReader).terms(“field”);


残念なことに、Apache Solrはこういったコードを多く使っています。Solrのファセットやフィルタリングはアトミックなセグメント毎に動作するように書き換えられなければなりません!これらのプラグインやコンポーネントはSolrIndexSearcherが提供しているアトミックなビューをSolrIndexSearcher.getAtomicReader()を通じて取得しています。

もしメモリ効率がよく速い検索アプリケーションを書きたいのなら、Solr 4.0を使うのではなくLuceneが提供している新しいファセットモジュールとSearcherManagerを使って独自のアプリケーションを書くことをお奨めします!

| 関口宏司 | Luceneクラス解説 | 19:14 | comments(0) | trackbacks(0) |
+ Solrによるブログ内検索
+ PROFILE
      1
2345678
9101112131415
16171819202122
23242526272829
3031     
<< December 2012 >>
+ LINKS
検索エンジン製品 - 比較のポイント
商用検索エンジンを購入した企業担当者は読まないでください。ショックを受けますから・・・
>>製品比較 10のポイント
+ Lucene&Solrデモ
+ ThinkIT記事
+ RECOMMEND
Apache Solr入門 ―オープンソース全文検索エンジン
Apache Solr入門 ―オープンソース全文検索エンジン (JUGEMレビュー »)
関口 宏司,三部 靖夫,武田 光平,中野 猛,大谷 純
+ RECOMMEND
Lucene in Action
Lucene in Action (JUGEMレビュー »)
Erik Hatcher,Otis Gospodnetic,Mike McCandless
FastVectorHighlighterについて解説記事を寄稿しました。
+ RECOMMEND
+ SELECTED ENTRIES
+ RECENT COMMENTS
+ RECENT TRACKBACK
+ CATEGORIES
+ ARCHIVES
+ MOBILE
qrcode
+ SPONSORED LINKS