関口宏司のLuceneブログ

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

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

| スポンサードリンク | - | | - | - |
Lock-less commit
Lucene 2.1より、インデックスセグメントのコミット時にロックをしなくなった。

インデックスセグメントのコミットとは、インデックスファイルの構成が記されているsegmentsファイルを読み書きすることである。従来はコミット動作時はcommit.lockというファイル(実際のファイル名はもっと長い)がIndexWriterやIndexReaderらによって作成され、IndexReaderが読み込みオープン中のsegmentsファイルとインデックスセグメントがIndexWriterにより書き換わったり消されることを防いできた(逆にIndexWriterにより書き込み中(マージなど)のときにIndexReaderが読まないようにする役割もある)。つまり、commit.lockでインデックスセグメントの信頼のコミットメント(約束)を取り付けていたのだ。

ローカルファイルシステムではこのしくみによりひとつのWriterと複数のReaderがインデックスを共有することになんら問題がないが、NFSやSambaなどを使って別筐体でインデックスを共有しようとしたときに問題が起こることがしばしば報告されてきた。

IndexWriterは新しいセグメントファイルを作成すると、古いセグメントファイルを削除しようとするが、古いセグメントファイルで検索が実行中の場合(オープン中のとき)、WindowsではAccess Deniedが発生する。この場合はdeletableファイルが作成され、あとで削除できるように古いセグメントファイルが登録される(Lucene本 P99参照)。Unixの場合は削除は成功するが、実際にファイルが削除されるのは、すべてのIndexReaderがファイルをクローズした時点となる。しかしNFSの場合は(Sambaはどうか不明)どうやらキャッシュが効いてしまうらしく、IndexWriterによる古いセグメントファイルの削除は成功し、NFS経由のIndexReaderがオープンしているファイルが実際に消されてしまう。そして本当に読み込もうとしたときにFileNotFoundExceptionが発生する、というのがNFSの環境下で報告されてきたエラーの原因である。これを考えると、いくらコミットロックをがんばっても解決できないのであった。

NFSは手っ取り早くLuceneアプリケーションをスケールアップさせたいときに食指が動くツールである。NFSでインデックスを共有し、ひとつのWriterがインデックスを更新し、複数のSearcherが適当な間隔でopen/closeしながら検索を行えばよさそうだからだ。しかし上記の理由から、すぐにSearcher側からFileNotFoundExceptionがスローされてしまう。

そこでその対策の第一歩として、Lucene 2.1ではコミット時のロックを廃止したのである。ではどうしたのかというと、segmentsファイルのファイル名をsegmentsという固定の名前ではなく、segments_Nという具合に変更し、Nをインクリメントする命名方法を導入した。こうすれば既存の(IndexReaderが読んでいるかもしれない)古いsegments_Nファイルを書き換えるためにコミットロックを作成する必要がなくなる。そして、IndexWriterは古いsegments_Nファイルと古いセグメントファイルを削除する(前述の通りWindowsではこれが失敗することもあるが、その場合はあとで消すようだ)。しかしそうすると、IndexReaderが古いsegments_Nの情報から取得したセグメントファイルをオープンしようとしたときにFileNotFoundExceptionが発生してしまう可能性がある。そこでLucene 2.1ではIndexReaderがその例外に遭遇したときは新しいsegments_Nファイルを探してそこからロードする再試行をするように変更された。
| 関口宏司 | Luceneインデックス | 20:58 | comments(0) | trackbacks(0) |
QueryParserを使用した範囲検索
ここ数ヶ月ノートPC使用中、いわゆる「ブルースクリーン」の出現で悩まされている。
時期的には、もともと購入時の512Mメモリに1G追加したときからの発生なので、まずはメモリを疑い、Memtest86というツールを使ってチェックしてみたのだが、問題ないようだ。

次にハードディスクを疑い、OS(Windowsである)付属のディスクチェックツールを使ったりしているのだが、悪いところは見つからない。メーカーに修理に出すという手段があるのかもしれないが、その間持ち運べるPCがなくなるのは困る。Vistaも出たことだし(というのは自分が納得するための言い訳に過ぎないが)DuoCoreのノートを買ったほうがいいだろうか、などと考えているところだ。そんなことで、最近は価格.comによく訪れてノートPCの価格を調べたりしている。

というわけで、本日のネタデータはある日ある機種のノートPC価格のショップ比較を使用することにする。それは次のようなものであった:

ショップAショップBショップCショップD
198000198000180000208000


上記のようなデータがLuceneのインデックスに索引付けされているとする(価格はpriceというフィールド名とする)。ここでQueryParserを使って次のような検索式で検索を実行する:



price:[000000 TO 199999]



検索式の意味は、価格が20万円以下のものを探す、ということだ。すると、現在のLuceneバージョン2.1を使うと、次のような結果が得られる(プログラムはこの記事の最後に掲載する):



1.0 ショップA 198000
1.0 ショップB 198000
1.0 ショップC 180000



ここでショップ名の前に表示されている実数は各ドキュメントのスコアである。スコアが同じなので、ドキュメントIDの順(=ドキュメントの登録順)に結果が表示されている。使用目的から考えれば、価格の安い順に並んだ方が親切であるが、それは単にソートの問題なのでここではおいておく。

同じプログラムを、一つ前のバージョンであるLucene 2.0で実行してみる。すると、次のように面白い結果となる:



1.0 ショップC 180000
0.5783994 ショップA 198000
0.5783994 ショップB 198000



Lucene 2.0ではショップAとショップBのドキュメントのスコアが1.0より小さくなり、そのためにLucene 2.1の結果と表示順が異なる結果となった。

この原因は何かというと、Lucene 2.1での次の改善が実施されたためである:

http://issues.apache.org/jira/browse/LUCENE-703

これは何かというと、QueryParserで範囲検索の検索式をQueryに展開する際、Lucene 2.0まではRangeQueryに展開していたものを、Lucene 2.1からはConstantScoreRangeQueryに変更した、というものである。なぜそのような変更がなされたかというと、範囲検索のようなものはもともと、スコアを計算してランキングをするようなものじゃないだろう、というような理由があるためだ。しかしながら、Luceneでは1.4.3までは範囲検索としてはRangeQueryがあり、QueryParserでは範囲検索の検索式をRangeQueryに展開していた(Lucene 2.0まで)。

RangeQueryは実際の検索を実行する際は、TermQueryをBooleanQueryで組み合わせたQueryに展開される。これはWildcardQueryなどでも同様である。どのように展開されるかは、Query.rewrite()を実行して得られたQueryのtoString()を見てみるとよい。今回のサンプルデータでは、RangeQueryは次のように展開される:



price:180000 price:198000



これでLucene 2.0ではスコアが異なる理由がわかるであろう。つまり、価格が198000円のものは180000円のものよりありふれているのでIDF値が低くなってしまうのである。

理屈はそうかもしれないが、結果はいかにも奇妙なものに見える。それがLucene 2.1でRangeQueryがConstantScoreRangeQueryで置き換えられた理由だ。ConstantScoreRangeQueryはその名のとおり固定スコアを返すQueryであり、検索の際もBooleanQueryに展開しない。そのため、TooManyClauses例外が発生してしまう心配もなくなるのである。

今回使用したプログラムを以下に示す。赤字の部分のコメントアウトをはずして実行してみると、いろいろな示唆が得られるであろう:



public class TestQueryParser {

private static final String[] SHOPS = { "ショップA", "ショップB", "ショップC", "ショップD" };
private static final String[] PRICES = { "198000", "198000", "180000", "208000" };
private static final String F_SHOP = "shop";
private static final String F_PRICE = "price";
private static Directory dir;
private static Analyzer analyzer = new WhitespaceAnalyzer();

public static void main(String[] args) throws IOException, ParseException {
makeIndex();
searchIndex();
dir.close();
}

private static void makeIndex() throws IOException{
dir = new RAMDirectory();
// Lucene 2.0に切り替えても大丈夫なように古いコンストラクタを使用
IndexWriter writer = new IndexWriter( dir, analyzer, true );
for( int i = 0; i < SHOPS.length; i++ ){
Document doc = new Document();
doc.add( new Field( F_SHOP, SHOPS[i], Store.YES, Index.NO ) );
doc.add( new Field( F_PRICE, PRICES[i], Store.YES, Index.UN_TOKENIZED ) );
writer.addDocument( doc );
}
writer.close();
}

private static void searchIndex() throws IOException, ParseException{
QueryParser qp = new QueryParser( F_SHOP, analyzer );
Query query = qp.parse( "price:[000000 TO 199999]" );

//QueryParserがどのよなQueryに一次展開しているかを見る
//System.out.println( query.toString() );


IndexSearcher searcher = new IndexSearcher( dir );

// Query.rewrite()の結果を見る
//Query primitive = query.rewrite( searcher.getIndexReader() );
//System.out.println( primitive.toString() );


Hits hits = searcher.search( query );
for( int i = 0; i < hits.length(); i++ ){
Document doc = hits.doc( i );
float score = hits.score( i );
System.out.println( score + "¥t" + doc.get( F_SHOP ) + "¥t" + doc.get( F_PRICE ) );

// スコアの根拠を見る
//Explanation exp = searcher.explain( query, hits.id( i ) );
//System.out.println( exp.toString() );

}
searcher.close();
}
}



なお、Lucene 2.1のQueryParserでの範囲検索の検索式はデフォルトでConstantScoreRangeQueryに展開されるようになったが、以前のようにRangeQueryに展開させたいときは、setUseOldRangeQuery(true)を使用すればよい。
| 関口宏司 | Luceneスコアリング | 13:17 | comments(0) | trackbacks(0) |
便利になったRAMDirectory
Luceneではインデックス(転置索引)を表すDirectory抽象クラスがあり、その拡張としてヒープをインデックスに使用するRAMDirectoryクラスがある。RAMDirectoryはオンメモリで動作することから、同じDirectoryの拡張であるFSDirectoryよりもすばやい動作が期待できる。

たとえばPDFファイルなどのコンテンツが多数記録されたCD-ROMがあり、これに目的のPDFファイルを探すためにLuceneで全文検索機能を実装したプログラムを搭載して配布する場面を考える。CD-ROMにはあらかじめLuceneのプログラム以外にもインデックスが作成されて記録されている。CD-ROMをPCに挿入すると、Luceneの全文検索プログラムが自動起動してPDFを検索キーワードで探すことができるようになっている。

このとき、Luceneのプログラムではインデックスが十分に小さい場合は、次のようにしてRAMDirectoryに読み込むことが可能である:



// "index"はインデックスのファイル名
Directory dir = new RAMDirectory( "index" );



RAMDirectoryにはもちろん、新たにDocumentを登録・索引付けすることも可能である。しかしながら、なにぶん、その寿命はプログラムの寿命と運命を一にしているので、用途といえば単体テストプログラムに使用されたり、せいぜい上記のようなリードオンリーの場面に限られていたのだった。

しかし、Lucene 2.1からDirectory抽象クラスにcopy()メソッドが加わったことから、私はRAMDirectoryの活躍の場がぐっと広がる予感がしている。

このメソッドは名前の通り、Directoryのコピーを行うもので、第一引数にコピー元のDirectoryを、第二引数にコピー先のDirectoryを指定する(第三引数も取り、これはboolean値でtrueの場合はコピー後にコピー元のDirectoryをclose()する)。

これを使用すると、RAMDirectoryをFSDirectoryに永続化することができることに気がつくだろう。

Directory.copy()のプログラム例

Lucene本の第1章のプログラムはRAMDirectoryを使用しているので、これにDirectory.copy()を使うようにしてインデックスを永続化してみる:



public class HelloLucene {

private static final String FIELD_CONTENT = "content";
private static final Directory directory = new RAMDirectory();
private static final Analyzer analyzer = new JapaneseAnalyzer();
private static final QueryParser qp = new QueryParser( FIELD_CONTENT, analyzer );

private static final String[] contents = {
"カツオはサザエの弟", "サザエはワカメの姉", "ワカメはカツオの妹",
"カツオは長男", "サザエは長女", "ワカメは次女",
"マスオはサザエの夫", "波平は舟の夫", "タラちゃんのパパはマスオ",
"サザエとマスオは夫婦", "波平はタラちゃんの祖父", "舟はカツオの母",
"マスオはカツオの義兄", "カツオはタラちゃんの叔父", "舟はワカメの母"
};

public static void main( String[] args ) throws IOException, ParseException {
makeIndex();
backupIndex();
BufferedReader br = new BufferedReader( new InputStreamReader( System.in ) );
String q = null;
while( q == null || !q.equals( "q" ) ){
System.out.print( "¥n検索質問(qで終了)> " );
System.out.flush();
q = br.readLine();
if( !q.equals( "q" ) )
searchIndex( q );
}
br.close();
if( directory != null )
directory.close();
}

private static void makeIndex() throws IOException {
IndexWriter writer = new IndexWriter( directory, analyzer, true );
for( int i = 0; i < contents.length; i++ ){
Document doc = new Document();
doc.add( new Field( FIELD_CONTENT, contents[i], Field.Store.YES, Field.Index.TOKENIZED ) );
writer.addDocument( doc );
}
writer.close();
}

private static void searchIndex( final String q ) throws IOException, ParseException {
IndexSearcher searcher = new IndexSearcher( directory );
Query query = qp.parse( q );
Hits hits = searcher.search( query );
int length = hits.length();
System.out.println( Integer.toString( length ) + "件ヒットしました。" );
for( int i = 0; i < length; i++ ){
Document doc = hits.doc( i );
System.out.println( "¥t" + doc.get( FIELD_CONTENT ) );
}
searcher.close();
}

private static void backupIndex() throws IOException{
Directory persistent = FSDirectory.getDirectory( "index" );
Directory.copy( directory, persistent, false );
persistent.close();
}

}



赤字の部分が新たに追加した部分である。このプログラムの終了後は、indexディレクトリにLuceneのインデックスが保存されるようになる。そのため、プログラム終了後にインデックスをLukeでブラウズすることも可能である(下図)。

luke-copy-directory
| 関口宏司 | Lucene自由自在 | 10:37 | comments(0) | trackbacks(0) |
検索をしないLucene
話はあるコンサル会社のソリューションと弊社の検索ソリューションを組み合わせて製造業のビジネスプロセスを効率化することを計画したことから始まる。

本日はそのパートナーの誘いで松下電工インフォメーションシステムズ(株)主催の「日本発のものづくりITセミナー」に出席した。最近の製造業はどうなっているかを学ぶためである。

開催案内をよく読まずに出席した私は前のミーティングが長引き、基調講演にぎりぎり間に合い、席に着いた。そこで基調講演のタイトルを初めて見て驚いた。それは「モノを作らないものづくり」というものであった。それはものづくりとは言わないだろう。「ものづくり」といえば、私にとってそれは楽しみと同義だ。幼き頃はセロテープと新聞紙とダンボールでいろいろなものを作って遊んだものである。そして小学校に上がればそれは図工という授業になり、一番得意な教科となった。

モノを作らない製造業というのが果たしてありうるのか。それはたとえて言えば、すしを握らないすし職人といえるだろうが、そんなのはすし職人とは言わない。元すし職人だ。あるいは最近の話題で言えば、おふくろさんを歌わない森進一、とまあそういったところだろう。製造業界は私の知らない間に大変なことになっているらしい。

基調講演に続く次のコマのタイトルは、「バーチャルものづくりの推進」というものであった。これもものづくりといえるのだろうか。製造業界は大丈夫なのか。なにしろそれはバーチャルだ。彼らは作ったつもりになるのかもしれないがそれでいいのか。

ちょっと心配になってきたころ、プレゼンターの人が「それではデモしまーす」と言って、Wiiリモコンを取り出した。

というのはウソで、PC上で3Dの試作機を動かして問題がないことを視認できるところを見せたり、製造プロセスの手順書を自動生成するデモを演じてみせたのだった。

「バーチャル」や「モノを作らないものづくり」というのはどうやらITをフル活用して、なるべく設計の上流工程で問題を発見してつぶしたり、試作品を作成する回数を減らしたりすることのようである。最終的には彼らはモノを作るようだ。ホッと胸をなでおろした私であった。

しかし話はこれで終わらない。私は考えたのだった。「検索をしないLucene」というのはどうか。どうかといわれても読者は戸惑うだけだろう。そんなのLuceneじゃない、そんな声が聞こえてきそうだ。しかし、これは私の想像上の話ではないのだった。検索をしないLuceneは存在する。それはSolrである。

Solrは全文検索エンジンにLuceneを採用した、オープンソースの全文検索エンジンフレームワークである。

「検索をしない」とはどういうことかというと、Solrでは数種類のキャッシュを内蔵し、そのうちのひとつは過去に検索したQueryの検索結果一覧を保持しているのだ。そしてQueryがキャッシュ上のキーと一致した場合、過去の検索結果一覧を返すことができるようになっている。果たしてそんな力技のキャッシュのヒット率がどのくらいでるのかという心配はいろいろあることだろう。また、キャッシュに載せるためにそれはsynchronizedされるので、マルチユーザで使われるときにどうなるのか。そこはアプリケーションにSolrを採用するのかどうかを決めるひとつのポイントではある。しかし今日のテーマは「検索をしないLucene」である。ここはヒット率がそこそこあることを前提とさせて欲しい。

そして、ヒットするとすればそれは驚異的なパフォーマンスを発揮するのだった。たとえばLuceneでは300万件超の文書に1万件ヒットする検索語で検索したとき、レスポンスタイムは数ミリ秒ほどかかる(もちろん、CPUやHDDを含むマシン性能に左右されることは言うまでもないが、この数値はごく一般的な性能のマシンで出るものだ)。しかしこれがSolrのキャッシュが効いた場合だと0ミリ秒(1ミリ秒を切るため計測不可)となる。

Solrのそのキャッシュのエントリ数のデフォルトは512である(もちろん、設定ファイルによって変更できる)。サイト内検索の検索窓に入力される検索キーワードのバリエーションは意外と少ない場合があり、上位512個の検索キーワードとその検索結果をメモリに保持するという考えは結構ありなのかもしれない。

検索をしない検索エンジンフレームワーク --- Solrはかなり面白い。
| 関口宏司 | Luceneパフォーマンス | 20:13 | comments(0) | trackbacks(0) |
不要なフィールドの遅延ロード
Lucene 2.1からFieldの遅延ロード機能が搭載された。これはIndexReaderのdocument()メソッドを使ってDocumentをインデックスから読み込むときに、FieldSelectorインタフェースでFieldを選択することができるようにしたものである。

Lucene 2.0までは:



public Document document( int docId );



というメソッドでDocumentを取得できていたが、Lucene 2.1では上記に加えて、次のようなメソッドが追加された:



public Document document( int docId, FieldSelector selector );



たとえば、文字列長が比較的短いtitleと長いcontentというFieldからなるDocumentがあるとき、検索結果一覧を表示するときはtitleだけを使い、その後ユーザから指定されたDocumentのcontentを表示すればよいときに、contentを遅延ロードにより必要な時点でロードするようにすれば処理速度の大幅な向上が期待できる。

サンプルプログラム

インデックスは手元にある18万4千件のDocumentからなるものを使用する(したがってこのプログラムにはインデックスを作成する部分はない)。

Field数は30くらいあるものだが、このサンプルプログラムでは(フィールド名を秘すために)titleとcontent(遅延ロードの対象となるtitleより長い文字列が入っている)と置き換えてある。

まず、遅延ロードと従来の全Fieldロードを比較するため、次のような抽象クラスを用意する:



public abstract class AbstractDispAllDocs {

private static final String INDEX = "/path/to/your/index";
private String[] titles;
private String[] contents;

protected abstract Document getDocument( IndexReader reader, int id ) throws IOException;

protected Document[] getAllDocs(IndexReader reader) throws IOException {
Document[] docs = new Document[reader.numDocs()];
int max = reader.maxDoc();
int count = 0;
for( int i = 0; i < max; i++ ){
if( !reader.isDeleted( i ) )
docs[count++] = getDocument( reader, i ); // (4)
}
return docs;
}

protected void execute() throws IOException{

long t1 = System.currentTimeMillis();

IndexReader reader = IndexReader.open( INDEX );
Document[] docs = getAllDocs( reader ); // (1)

titles = new String[docs.length];
int i = 0;
for( Document doc : docs ) // (2)
titles[i++] = doc.get( "title" );

long t2 = System.currentTimeMillis();

contents = new String[docs.length];
i = 0;
for( Document doc : docs ) // (3)
contents[i++] = doc.get( "content" );

long t3 = System.currentTimeMillis();

System.out.println( "========== 結果 ==========" );
System.out.println( "ドキュメント数 : " + docs.length );
System.out.println( "全ドキュメントタイトル取得時間(ms) : " + Long.toString( t2 - t1 ) );
System.out.println( "全ドキュメントコンテンツ取得時間(ms) : " + Long.toString( t3 - t2 ) );

reader.close();
}
}



プログラムの全体の流れは、最初に(1)の部分で全Documentの配列を取得し、(2)の部分で全titleを取得し、(3)の部分で全contentを取得している。最後に(1)の処理時間と(2)の処理時間を表示して終了する。(1)の実行時に、(4)のgetDocument()メソッドにより通常ロードと遅延ロードを切り替えられるようにしている。

遅延ロードのプログラムは次のようになる:



public class LazyLoad extends AbstractDispAllDocs {

private FieldSelector selector;

protected Document getDocument( IndexReader reader, int id ) throws IOException{
return reader.document( id, selector );
}

public static void main(String[] args) throws IOException {
LazyLoad ll = new LazyLoad();
ll.prepareFieldSelector();
ll.execute();
}

private void prepareFieldSelector(){

Set immediate = new HashSet();
immediate.add( "title" );

Set lazy = new HashSet();
lazy.add( "content" );

selector = new SetBasedFieldSelector( immediate, lazy );
}
}



上記のプログラムではFieldSelectorの実装クラスのひとつであるSetBasedFieldSelectorを使用している。このクラスのコンストラクタにSetで即時にロードするFieldと遅延ロードするFieldを設定したものを渡せばよい。

このプログラムを実行すると、通常のロードの場合は次のようになった:



========== 結果 ==========
ドキュメント数 : 184009
全ドキュメントタイトル表示時間(ms) : 8687
全ドキュメントコンテンツ表示時間(ms) : 203



また遅延ロードの場合は次のようになり、期待通りtitleだけが必要な部分で処理速度が向上していることが確認できた:



========== 結果 ==========
ドキュメント数 : 184009
全ドキュメントタイトル表示時間(ms) : 7594
全ドキュメントコンテンツ表示時間(ms) : 1218


| 関口宏司 | Luceneパフォーマンス | 12:48 | comments(0) | trackbacks(0) |
+ Solrによるブログ内検索
+ PROFILE
    123
45678910
11121314151617
18192021222324
25262728293031
<< March 2007 >>
+ 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