インデックスのコミット時の動作について詳述されたメール(原文)を見つけたのでその翻訳を掲載しよう。コミュニティの状況について説明し、さらにはコミュニティに解決方法を問うたものとなっている。破壊した件についてこんにちは。コミッターを代表して投稿します。Lucene/SolrとElasticsearchの両方を先ごろテストしたところHotspotまわりを中心に問題が見つかったことはご存知かと思います。9のプレビュー版ビルド40を利用するよう自分たちのテストインフラを先日アップデートしました。これは主にJigsawまわりの問題を確認することが目的でしたが、いくら頑張ってもこのビルドでは問題が発生しないのです。9の最近のコミットでは問題が発生しており(http://hg.openjdk.java.net/jdk9/jdk9/jdk/rev/e5b66323ae45)、それに相当するのがhttps://bugs.openjdk.java.net/browse/JDK-8066915です。この問題に関連し、自分たちでもhttps://issues.apache.org/jira/browse/LUCENE-6169というスレッドを立てました。アプローチ(すべてのファイルで一度だけ書き込みが行われる)を使っています。Luceneの所定の「コミットポイント」で「コミット」する場合は一時的なファイル名に書き込み、それからこのファイル(およびすべての関連ファイル)でfsyncを実行する形になります。これはファイルチャネルではfc.force()をコールするだけ簡単です。StandardCopyOption.ATOMIC_MOVE)」を使ったアトムのリネームで行われます。OSではリネーム処理が全く見えなくなる可能性があります。LinuxのMANページ(http://linux.die.net/man/2/fsync)にはすべてに以下の記述があります。コールはファイルのあるディレクトリ内のエントリがディスクに書き込まれたことを必ずしも保証するものではない。Luceneでも以下の手順で同じことをしています。APIにこの記述がないため、われわれは「有利な推測」に基づいてこれを行うことになります。Windowsではうまくいかないなど(*)、IOExceptionの例外は必ず発生します。9のFileSystemExceptionでうまくいかないことです。FileSystemExceptionはディレクトリです。社外のLuceneリリースは(前述のように)例外をすべて受け入れるため大丈夫なのですが、テストインフラでは、少なくともLinuxとMacOSXでは動いてしまうのです。最新のコードはhttp://goo.gl/vKhtsWにあります。してもサポートされているOSのディレクトリでfsyncができるようにしておきたいのです。前述のコミットを8u40や7u80のリリースには戻したくないのです。9なら代替案を検討できるのですが。8の既存のセマンティクスを残し、どうしてもFileChannelからREAD/WRITEをしたい場合は単純にエラーを発生させるのでしょうか?基盤OSとlibcはこのようにして処理を行っています。ファイル、ディレクトリ、デバイスなど、ファイル記述子はどこでもオープンできますが、この記述子ではできない処理もあり、例外やエラーが発生する場合もあります。ディレクトリをfsyncするための新しいAPIを追加する(どのファイルタイプでもおそらく可能)。Windowsではディレクトリではこれがうまくいかないのでしょうか?上記のリンクにあるIOUtils.fsync()のような形です。意見や対応方法に関するアドバイスを下さい。Uwe*)ただ、このファイルシステムのセマンティクスはファイルが確実に見えるようにしているため、実際のところは問題ではありません。IOExceptionにマッピングされますが、これは全く問題ありません)。}なぜ近似式を使っているかというと、forループを一度回すだけで計算したいためだ。分散や標準偏差を定義通りに計算しようとすると、最初に平均を求め(これでforループを一回回す必要がある)、その平均を使って各値との差の2乗を足していく必要がある(これでforループがもう一度必要)。StatsComponentではforループを一度しか回したくないので、その一回だけでsum(合計)、sumOfSquares(各値の2乗の合計)、min(最小)、max(最大)およびcount(件数)を計算または取得する。最後にmean(平均)と標準偏差を近似式で計算している。その近似式であるが、こちらの方のブログによると、あまりよろしくない結果を出してしまうようである。これよりも良い近似式で同じく1パスで計算する方法がこちらに紹介されているが、特に私は今のところ急いで正確な標準偏差が必要になるわけでもないので、備忘録を残しておくだけにとどめておこう。4.4(別に4.4と特定することもないのだが)のFSTを使ってFSA(有限状態オートマトン)を実装する機会があったので、簡単に使い方を紹介しよう。ここでは次のような単語をFSAに登録し、あとからFSA辞書に単語があるかどうかを問い合わせられるプログラムを作成する。"いいだ"};プログラムは次の通りで、非常に簡単である。}}以下簡単にプログラムの説明をする。FSA辞書に登録する単語一覧である。(5)のfor文の中でこの順番に登録されるが、あらかじめソートされている必要がある。FSTはもともと登録する単語に関連した値(出力)を持たせることができるが、今回は単なるFSAとして使うので、出力値は不要である。nullは指定できないので、ここで固定にダミーのObjectを生成している。出力の不要なFSAではNoOutputsを取得する。(3)で取得したoutputsを指定してFSTのbuilderを取得する。(1)の単語をbuilderにadd()する。単語を登録し終わったら、builderからFSTを生成する。FSA辞書を使って単語を引いてみる。実際にはfstパッケージ内のUtilクラスのget()メソッドを使って、出力を得ている。単語がない場合(FSAが文字列を受理しない場合)、結果はnullとなる。nullでなければ単語は辞書に登録されている。実行結果は次のようになる。プログラムのコンパイルと実行には、lucene-core-4.4.0.jarがあればよい。LuceneとSolrは、バージョン3.1では64ビット版WindowsおよびSolarisの両システム、バージョン3.3では64ビットLinuxシステムにおいてMMapDirectoryをデフォルトで採用している。システムがここ数バージョンとは異なる動作をするようになったため、今回の変更にLuceneとSolrのユーザが混乱している。LuceneとSolrのメーリングリストでは、消費する物理メモリが突然3倍になった理由に疑問を抱くユーザや、リソース使用量に不満を訴えるシステム管理者の投稿が多数見られる。MMapDirectoryの代わりに、動作の遅いSimpleFSDirectoryやNIOFSDirectory(JVMバグ#6265734に起因してWindowsではさらに遅い)を有効にするようsolrconfig.xmlの設定を推奨する意見がコンサルタントからも出始めている。慎重に検討した結果これらのプラットフォームではMMapDirectoryの使用がベストだと判断したLuceneコミッターからすれば、これはどちらかと言えば不快な反応になる。Lucene/Solrがこれまでより格段に優れたパフォーマンスを発揮することが分かっているからだ。今回の変更の背景に関して間違った情報が出回っているため、この素晴らしい検索エンジンが最適でない形で各所にインストールされてしまう。このブログでは、カーネル内の仮想メモリ処理に関する基本的な事実と、それを主にLuceneのパフォーマンス向上に利用するための方法について説明しようと思う(「仮想メモリ超入門」である)。さまざまな人々が投稿するブログやメーリングリストが間違っていて、MMapDirectoryの用途に矛盾するという理由についても明らかにしていく。後半では、mmapエラーなどや、Javaのこなれないヒープアロケーションに起因した最適でないパフォーマンスの回避に必要な詳しいコンフィギュレーションと必要な設定を紹介する。仮想メモリ[1]まずOSのカーネルを考えてみたい。ソフトウェアでI/O処理を行うのは無知なアプローチだ。これは1970年代から行われており、そのパターンは単純なものだ。ディスク上のデータを処理するときは常にシステムカーネルでsyscallを実行し、ポインタを何らかのバッファ(例:Javaのbyte[]アレイ)に渡し、ディスクとの間でバイトをやりとりし、バッファの中身をパースしてプログラムを実行する。(処理能力を消費しすぎる可能性があることから)あまり多くのsyscallを行いたくない場合はソフトウェアでバッファを大きく取って内部でのディスクとのデータ同期を減らすようにする。(RAMDirectoryを使用するなどして)Luceneインデックス全体をJavaのヒープメモリに読み込むようアドバイスするのにはこのような理由もある。X、あるいはSolarisといったモダンOSはすべて、洗練されたファイルシステムキャッシュとメモリ管理機能を使用することで1970年代式のコードより格段に優れたアプローチを用意している。「仮想メモリ」と呼ばれるこの機能は、Luceneインデックスのように巨大で広大な空間を占めるデータ構造処理の優れた代替手段となっている。仮想メモリはコンピュータアーキテクチャに不可欠な部分となっており、インプリメンテーションにはハードウェアサポートが必須で、MMU(メモリ管理ユニット)としてCPUに組み込まれているのが一般的だ。その仕組みはかなりシンプルで、各プロセスが独自に仮想アドレス空間を取得し、そこにライブラリ、ヒープ、そしてスタックの各空間をマッピングする。だいたいの場合、このアドレス空間はオフセット0から始まり、アドレスポインタの再配置が一切不要であるためプログラムコードの読み込みが簡単になっている。どのプロセスからも断片化していない連続した大きなアドレス空間が作業空間として見える。このアドレス空間は物理メモリと一切関連性がないことから「仮想メモリ」と呼ばれ、プロセスからもそのように見える。この広大なアドレス空間は、ソフトウェアを使って実メモリであるかのようにアクセスできる。ほかのプロセスもメモリを消費し、独自に仮想アドレス空間を持っていることは意識する必要がない。基盤OSはCPU内のMMUと連携し、最初のアクセス時にこれらの仮想アドレスを実メモリにマッピングする。これはページテーブルと呼ばれるもので行われ、それがMMUハードウェア内のTLB(頻繁にアクセスされるページをキャッシュする変換索引バッファ)によってバックアップされている。することで、OSは実行中のすべてのプロセスの必要メモリを利用可能な実メモリに分散し、実行中のプログラムから完全に透過的に見えるようにすることができる。仮想メモリ図(Wikipediaより引用[1]、http://en.wikipedia.org/wiki/File:Virtual_memory.svg。3.0許諾)この仮想化を利用すればOSはもう1つのことができるようになる。物理メモリが十分にないときは、プロセスが使わなくなったページを「スワップアウト」したり、物理メモリを別のプロセス用に解放したり、もっと重要なファイルシステム操作をキャッシングできるようになる。ページアウトされた仮想アドレスにプロセスがアクセスしようとすると、それがメインメモリに再ロードされ、プロセスが利用可能になる。プロセスは何もする必要がなく、完全に透過的になっている。これは利用可能なメモリ容量の把握が一切必要ないためアプリケーションにとっては素晴らしいことだが、メモリの負荷が非常に高いLuceneのようなアプリケーションでは問題にもつながる。Luceneと仮想メモリここで、インデックス全体もしくはその大半を「メモリ」(これが仮想メモリにすぎないことは先刻承知だ)に読み込む例を見ていく。RAMDirectoryをアロケートし、そこにインデックスファイルをすべて読み込むとOSの足を引っ張ることになる。OSはディスクアクセスを最適化しようとするため、すべてのディスクI/Oが物理メモリにキャッシング済みだ。これらのキャッシュの内容をすべて自分の仮想アドレス空間にコピーして膨大な量の物理メモリを消費している(しかも、コピー処理の開始を待たなくてはならない)。物理メモリには制限があるので、OSはもちろん大きなRAMDirectoryをスワップアウトする判断を下す必要があるのだが、それはどこに行くのだろうか?再びディスク上(OSのスワップファイル上)に戻るのだ。実際、われわれはディスクから読み込むものすべてをページアウトさせるOSカーネルと格闘している[2]。したがって、インデックスの読み込み時間最適化にRAMDirectoryは得策ではない。加えて、RAMDirectoryの方がガーベッジコレクションや並行処理関連で問題が多いのだ。スワップ領域にデータがあるため、自分のヒープ管理下にあるメモリの解放はJavaのガーベッジコレクタにとって大変な作業となる。そのためにディスクI/Oが増加し、インデックスアクセス速度が低下し、ガーベッジコレクタの動作が原因で検索コードの待ち時間が分単位に達する。インデックスのバッファリングにRAMDirectoryを使わずNIOFSDirectoryやSimpleFSDirectoryを使うと、ディスクやファイルシステムのキャッシュとJavaヒープ内のバッファとの間でデータブロックをコピーするためにコードがOSカーネルに対して多数のsyscallを行う必要が出るなど、さらに代償を払わなくてはならなくなる。検索リクエストがある度に何度もくり返して行う必要があるのだ。いうカーネル機能を使ってディスクファイルにアクセスするMMapDirectoryだ。先のアプローチでは、ファイルシステムキャッシュとローカルJavaヒープの間でデータをコピーするのにsyscallに頼っていたが、ファイルシステムキャッシュに直接アクセスすればいいのではないか。これこそまさにmmapの仕事なのだ。基本的に、mmapはLuceneインデックスをスワップファイルとして処理するようなことをする。syscallはOSカーネルに指示を出し、インデックスファイル全体を前述の仮想アドレス空間に仮想的にマッピングし、それらをLuceneのプロセスが利用可能なRAMのように見せる。すれば、大きなbyte[]アレイ(JavaではJavaコードが安全に利用できるようByteBufferインターフェースによってカプセル化される)になるかのようにディスク上のインデックスファイルをアクセスできるようになる。Luceneのコードからこの仮想アドレス空間にアクセスすればsyscallsは一切不要で、プロセッサのMMUとTLBがマッピングをすべて代わりに処理してくれる。データがディスク上にしかない場合は、MMUが割り込みを発生させ、OSカーネルがデータをファイルシステムキャッシュに読み込む。キャッシュされている場合は、MMU/TLBがそれをファイルシステムキャッシュ内の物理メモリに直接マッピングする。これでネイティブメモリアクセスに変わることになる。バッファのページイン/ページアウトを気にする必要はなく、これらはすべてOSカーネルによって処理される。並行処理の問題も全くなく、標準のbyte[]アレイに対する唯一のオーバーヘッドはJavaのByteBufferインターフェースによって生じるラッピングだけとなる(それでも本物のbyte[]アレイより速度は落ちるが、Javaからmmapを使う方法はこれしかなく、Luceneに付属するどのディレクトリインプリメンテーションよりもこちらの方がはるかに高速だ)。GC問題がすべて回避される。Javaオプション)のアロケートはできるだけ小さく抑えたい。インデックスアクセスがOSキャッシュへの直接渡しに依存することを覚えておきたい。これは、Javaのガーベッジコレクタともかなり相性が良い。OSカーネルがファイルシステムキャッシュとして利用できるよう可能な限り多くの物理メモリを解放する。Luceneのコードがそこで直接動作するためディスクとメモリ間のページング/スワッピング回数が減少することを覚えておきたい。Luceneアプリケーションにヒープをアロケートしすぎるとパフォーマンスに支障が生じる。MMapDirectoryではLuceneがこれを必要としないのだ。Gバイトである0から232-1までのアドレスにしかアクセスすることができない。Gバイトに制限している。Gバイトに制限され、これより大きいファイルは大きいbyte[]アレイとしてこの「小さい」アドレス空間にマッピングできないことになる。この大型ファイルをマッピングすると、仮想空間(「番地」に相当するようなアドレス)はなくなってしまう。既存のシステムの物理メモリサイズがこの大きさを既に超えていることから、リソースを無駄にせずファイルをマッピングするアドレス空間はない(ここでは物理メモリではなく「アドレス空間」)。64ビットプラットフォームでは異なってくる。264-1は巨大な数字で、1800京バイトを越えるため、実際のところはアドレス空間に制限はない。残念ながら、大半のハードウェア(MMU、CPUのバスシステム)やOSはこのアドレス空間をユーザモードアプリケーション(Windows:43ビット)用に47ビットに制限している[4]。それでもTバイト単位のデータをマッピングするためのスペースは残っている。仮想メモリに関するこれまでの説明を注意深く読んでいれば、以下が正しいことは簡単に検証できる。MMapDirectoryは余計なメモリを消費せず、マッピングされたインデックスファイルのサイズはサーバが搭載する物理メモリの制限を受けない。()ファイルではメモリではなくアドレス空間だけを予約する。64ビットプラットフォームのアドレス空間は無償で利用できることを覚えておきたい。MMapDirectoryはインデックス全体を物理メモリに読み込むことはしない。しなければならないのだろうか?OSには、容易なアクセスを可能にするためファイルをアドレス空間にマッピングするよう要求しているだけで、それ以上の要求は決してしていない。JavaとOSはファイル全体をRAMに読み込むオプション(十分な余裕があれば)を任意で提供するが、Luceneはこのオプションを使わない(今後のバージョンで追加の可能性はある)。MMapDirectoryは「top」から膨大な量のメモリが伝わるとサーバに過負荷を与えない。「top」(Linux)にはメモリ関連の3つのコラムがある「VIRT」、「RES」、および「SHR」だ。最初(VIRT、仮想)のものはアロケートされた仮想アドレス空間を伝える(さらに、これは64ビットプラットフォームでは無償だ)。IndexWriterでマージが行われている場合、この数字はインデックスサイズや物理メモリの数倍にもなる。IndexReaderが1つしかオープンされていない場合は、アロケートされたヒープスペース(-Xmx)にインデックスサイズを加えたものとほぼ同じになり、プロセスが使用する物理メモリにはならない。2番目のコラム(RES、常駐)のメモリはプロセスが処理用にアロケートした(物理)メモリ量を示し、Javaヒープスペース以下に収まる。最後のコラム(SHR、共有)はアロケートされた仮想アドレス空間がほかのプロセスと共有しているサイズを示す。MMapDirectoryを使用する複数のJavaアプリケーションが同じインデックスをアクセスしている場合はこの数字が大きくなっていく。一般的には、共有システムライブラリ、JARファイル、そしてプロセス実行イメージ本体(これもmmapされる)が必要とするスペースが見えるようになる。VMのコンフィギュレーションまず第一に、LinuxディストリビューションとSolaris/Windowsのデフォルト設定は全く問題ない。(理解不足から)何でも自分でコントロールしたがる偏執的なシステム管理者もおり、これによりアプリケーションがアロケート可能な仮想アドレス空間の最大量が制限を受けてしまう。-m」の両方が「unlimited」を表示することを確認したい。failed」を表示する可能性がある。それぞれのセグメントが多い膨大な数のインデックスを持つシステムでもこのエラーが発生する場合は、/etc/sysctl.confのカーネルパラメータをチューニングする必要があるかもしれない。vm.max_map_countのデフォルト値は65530だが、これを引き上げる必要があるかもしれない。WindowsとSolarisの両システムでも同様のセッティングが可能だと思うが、その使用方法は読者ご自身でお調べいただきたい。VMのコンフィギュレーションに関してはメモリ要件を検討し直したい。ヒープスペースは本当に必要な量だけ用意して、OSにはできるだけ多くを残しておきたい。経験則として、Lucene/Solrが動作するJavaに物理メモリの四分の一以上をヒープスペースとして使ってはならず、残りのメモリはOSのキャッシュ用に空けておきたい。サーバ上で動作するアプリケーションが増えたらそれに合わせて調整を行う。従来通り、物理メモリは多ければ多い方が良いが、インデックスサイズと同量までの物理メモリは不要だ。頻繁に使用されるページはカーネルがインデックスからうまくページインしてくれる。iotop」でインストール可能)の両方を見る。Luceneプロセスでシステムのスワップイン/スワップアウトが多い場合は、おそらく使いすぎなのでヒープサイズを減らす。Willnauer語録)を追加購入したい。最終手段はSSDになる。mmapをお楽しみあれ。4.0におけるIndexReaderの変更点について調べる必要があった。さんのブログに行き着いた。こちらの記事が変更における背景などに触れていてなかなかよかった。日本語訳を掲載したいと思いUweさんに聞いたところ、快諾をいただけたので、以下に拙訳を掲載する:本記事はSearchWorkingsに掲載された記事の再掲です。Luceneは誕生当初よりIndexReaderとIndexWriterのAPIを通じてインデックスを直接的に読み書きできるようにしていました。APIは現実を反映していませんでした。過去にはIndexWriterからの視点ではこれは価値がありましたが、インデックスを読むときにこれはいくつかの問題の原因となっていました。Luceneインデックスは論理的には単一ですが実際にはそうではありません。trunkの開発ではタイプセーフと性能のために現実に触れられるようにしようとしています。Composite、AtomicそしてDirectoryReaderの詳細に入る前に、少しこれまでの経緯をおさらいしてみましょう。バージョン2.9/3.0以降、LuceneはトップレベルのIndexReaderを直接検索実行することからセグメント毎に検索する方法に舵を切りました。Willnauerさんが彼のブログで説明しているとおり、これによりoptimizeはもはや検索性能を最適化するのには必要ではなくなったことを意味します。実際、optimizeは検索を遅くし、最適化後はファイルシステムとLuceneインデックス内部のキャッシュを無効化してしまいます。標準のLuceneインデックスはいくつかのセグメントと呼ばれるものから成ります。セグメントはそれ自体小さなインデックスとみなせます。インデックス作成中はLuceneは新しいドキュメントを分割されたセグメントに書き出します。2.9より以前は、インデックスが複数のセグメントに分かれていようと、セグメントは1つの大きなインデックスのように扱われていました。2.9からはセグメント毎に働くようにシフトしてきました。現在は、Luceneのほとんどすべての構成とコンポーネントはセグメント毎に動作するようになっています。これによりLuceneは再オープン時にインデックスのすべてではなく、実際の変更分のみをロードします。}しかし、論理インデックスとセグメント間の差異は一貫的にコード階層には反映されていませんでした。3.xでは、まだサブReaderをイテレートせずトップレベル(論理)Readerにて検索を実行していたのでした。そのため複数セグメントからなるインデックスでは検索は劇的に遅くなりました。いうことで昔のLuceneではインデックスをよくoptimizeするよう指導されていました。詳しく問題を説明しましょう。DirectoryのIndexReaderは内部的にはSegmentReaderを内包したMultiReaderです。MultiReaderにTermEnumやポスティングを問い合わせるとそれはオンザフライでサブReaderのタームやポスティングをマージします。このマージ手続きはプライオリティキューまたは類似のデータ構造を用い、それによりサブReaderの数に応じて深刻な性能劣化を引き起こします。このような内部的な制限にもかかわらず、MultiReaderと組み合わせてSegmentReaderを利用することはLuceneにおいて上位レベルの構造に影響を与えました。インデックスを非転置にしたFieldCacheは検索時にインデックスされた値によって検索結果をソートしたりドキュメント/値のルックアップに使われます。トップレベルReaderを非転置にするとFieldCacheの二重化を引き起こし、同じ内容のキャッシュを複数生成してしまいます。4.0は当初からタームとポスティングデータをMultiReaderまたはDirectoryReader(ディスク上のインデックスをIndexReader.open(Directory)でオープンしたときに返されるReaderの実装)などのコンポジットなReaderから取得できないように設計されました。trunkの最初のバージョンは非SegmentReaderからFieldやTermsEnumやDocsEnumを取得しようとすると単純にUnsupportedOperationExceptionを投げていました。このタイプセーフの欠如のせいでプログラマーはIndexReaderがcompositeかatomicかをチェックしなければポスティングを取得できるかどうか判断できませんでした。4.0における主なAPI変更のひとつであるLUCENE-2858では、インデックスとそのセグメント上のLuceneクライアントコード視点を完全に変更しました。抽象クラスIndexReaderは検索結果を表示するのにストアフィールドをアクセスするのに本質的なメソッドだけを持つようにリファクタリングされました。それはもはやインデックスからタームまたはポスティングを取得することはできません。削除ドキュメントに関してももはや不可視です。Luceneはクエリリライトやドキュメント収集といった手続きを自動的にサブReaderに委譲します。インデックスの詳細に立ち入り自分のクエリを書きたいのなら、新しい抽象サブクラスであるAtomicReaderまたはCompositeReaderを詳しく調べてください:AtomicReaderインスタンスは現時点でTerm、ポスティング、DocValueおよびFieldCacheの唯一のソースです。クエリはセグメント毎のアトミックなReader上で実行されるようになり、FieldCacheはAtomicReaderによってキーづけされます。対するCompositeReaderはそれが保持するメンバーを取得するユーティリティメソッドを持っています。気をつけてください、そのメンバーはアトミックである必要はありません。タイプセーフの次に私たちが行ったのは、抽象クラスIndexReaderからIndexCommitとバージョン番号の概念を取り除いたことでした。これらのIndexWriterと関連する機能はDirectoryReaderに移行されました。reader.document(docs[1].doc);見慣れたコードですね?実際、API利用者にはこの主なリファクタリングは大きな変更をもたらしません。アップグレード中にコンパイルエラーが起こったら、それは性能ボトルネックを示している可能性があります。Filterにおけるセグメント毎セマンティクスの強制もしカスタムFilterを使ったコードを持っているなら、Luceneの別の新しいクラス階層に気づいているかもしれません(LUCENE-2831参照):IndexReaderContextの仲間であるAtomic/CompositeReaderContextです。これはだいぶ前に追加されましたが、AtomicやCompositeのReaderたちと関連深いものです。2.9ではそれを扱えない多くのカスタムクエリとフィルタが開発されました。いくつかのFilter実装は渡されたIndexReaderをIndexSearcherへ渡されたIndexReaderと同等であると期待しました。Solrのような)を壊しました。4.0では、私たちはIndexReaderContextというSearcherプライベートなReader階層を導入しました。クエリやフィルタを実行中、Luceneはもはや生のReaderをQuery、FilterもしくはCollectionに渡さなくなりました。その代わり、コンポーネントはAtomicReaderContext(本質的な階層葉)を通じてトップレベルReaderに関連したドキュメントベースのような関連プロパティとして用意されます。これによりクエリとフィルタはセグメント毎であるにもかかわらずドキュメントベースIDのロジックにビルドアップされます。トップレベルReaderはまだ使えるか?それでもまだトップレベルReaderを使う正当なユースケースはあります。インデックスへのアトミックなビューが欲しい場合です。オートコンプリートやファセットなどで完全なインデックスのすべてのタームが欲しい場合があげられます。このためLuceneはAtomicReaderをエミュレートするSlowCompositeReaderWrapperのようなラッパーユーティリティを用意しています。注意:アトミックなエミュレーターの利用は、タームやポスティングやDocValueやFieldCacheのマージが必要になり深刻なスローダウンの原因となるので注意してください。Solrはこういったコードを多く使っています。Solrのファセットやフィルタリングはアトミックなセグメント毎に動作するように書き換えられなければなりません!これらのプラグインやコンポーネントはSolrIndexSearcherが提供しているアトミックなビューをSolrIndexSearcher.getAtomicReader()を通じて取得しています。ついて近くの人に質問されたのでそれをネタに簡単に解説。KeywordAttributeはトークンが「キーワードである」ということを下流のフィルターチェインに伝達するために使われる。上流のTokenFilterなりTokenizerが任意のトークンのKeywordAttributeのsetKeyword(true)を呼んで「キーワードである」ことの印をつけ、下流のTokenFilterが処理しようとするトークンが「キーワードかどうか」を判断して自分の処理をすべきかどうかをisKeyword()を呼んで決める。lucene-gosenなどのJapaneseKatakanaStemFilterなどのstemmerでisKeyword()が呼ばれ、キーワードでない場合にステミング処理をしよう、という具合に使われる。英語のstemmerはある条件下で末尾の"s"を削除するが、これを今は亡きsolrjs(SolrのJavaScriptクライアントAPI)に適用すると、末尾の"s"がとれてsolrjとなってしまい、これは今も健在のSolrのJavaクライアントAPIというまったく別物に変換されてしまう。これではまずいので、「solrjsはキーワードである」と印をつけておけば(setKeyword(true))、これは英語のstemmerで処理されなくなる。Solrでは、schema.xmlにてKeywordMarkerFilterFactoryを使ってprotectedに「キーワード一覧」を記したファイル名を指定することができる。通常solrjsはキーワードであると印がつけられていたりするわけではないので、(Luceneが使われている)Apacheのバグチケット管理システムJIRAなどでsolrjsを検索すると、意図しないステミング処理がされてsolrjが多数ヒットしてしまう。これを避けるには、末尾に"*"をつけて"solrjs*"とする。するとLuceneのクエリパーサーの仕様により、ワイルドカード記号がつけられた文字列はAnalyzerをスキップするので、うまく"solrjs"を検索できる。No-op(空実装)であったIndexSearcherクラスのclose()メソッドが削除された。3.5の時点で、IndexSearcherは自らIndexReaderインスタンスを作成して検索する方法がdeprecatedとなっており、プログラマが別途作成したIndexReaderをIndexSearcherのコンストラクタに渡す方法が推奨されていた。同時にこのときの修正でtrunk(4.0)のIndexSearcherから、IndexReaderを引数に取らないコンストラクタが完全に削除された。https://issues.apache.org/jira/browse/LUCENE-3571今回はこれをさらに推し進めた形となった。3.6/4.0に追加された:https://issues.apache.org/jira/browse/LUCENE-3593使い方は、FieldValueFilterのコンストラクタに調べたい対象フィールド名を渡してFilterオブジェクトを作成し、IndexSearcherのsearch()メソッドに渡せばよい。negate=trueid=zeroFieldValueFilterのコンストラクタにはboolean型の2つめの引数negateを取るものがあり、上記プログラムではそれを用いている。negateがfalseの場合は値がひとつでもあるものをフィルターを通過させるが、negate=trueの場合は逆の意味となる。(3.5)NGramPhraseQueryという、n-gram用に最適化されたPhraseQueryの拡張クラスがまもなく追加される。Estraierの平林さんの論文から拝借したものである。NGramPhraseQuery(2);query.add("ファ");query.add("ァイ");query.add("イル");検索するときに自動的に冗長単語を削除する最適化がなされ、「ファ/0」「イル/2」を検索する。これにより、最大50%の検索速度向上が可能となる。NGramPhraseQueryのコンストラクタに渡された引数は、n-gramのn(サイズ)である。Fridayマージの完了を知る(問)Luceneでインデックスマージの終了を知るには?(答)IndexWriter.waitForMerge()IndexWriter.waitForMerges()を呼ぶ。比較のポイント商用検索エンジンを購入した企業担当者は読まないでください。McCandlessFastVectorHighlighterについて解説記事を寄稿しました。