Apache Luceneとのダイアログの全文検索:チュートリアル
公開: 2022-03-11Apache Luceneは、ドキュメントの全文検索に使用されるJavaライブラリであり、SolrやElasticsearchなどの検索サーバーの中核です。 また、AndroidアプリやWebバックエンドなどのJavaアプリケーションに埋め込むこともできます。
Luceneの構成オプションは広範囲にわたっていますが、データベース開発者が一般的なテキストのコーパスで使用することを目的としています。 ドキュメントに特定の構造またはコンテンツの種類がある場合は、どちらかを利用して検索品質とクエリ機能を向上させることができます。
この種のカスタマイズの例として、このLuceneチュートリアルでは、何千もの無料の電子書籍を提供するProjectGutenbergのコーパスにインデックスを付けます。 これらの本の多くは小説であることを私たちは知っています。 これらの小説内の対話に特に関心があるとします。 Lucene、Elasticsearch、Solrのいずれも、コンテンツをダイアログとして識別するためのすぐに使用できるツールを提供していません。 実際、彼らはテキスト分析の初期段階で句読点を捨てます。これは、対話であるテキストの部分を識別できることに反します。 したがって、カスタマイズを開始する必要があるのは、これらの初期段階です。
ApacheLucene分析パイプラインの一部
Lucene分析JavaDocは、テキスト分析パイプラインのすべての可動部分の概要を提供します。
大まかに言えば、分析パイプラインは、最初に文字の生のストリームを消費し、最後に単語にほぼ対応する「用語」を生成するものと考えることができます。
標準の分析パイプラインは、次のように視覚化できます。
このパイプラインをカスタマイズして、二重引用符でマークされたテキストの領域を認識する方法を説明します。これをダイアログと呼び、それらの領域で検索するときに発生する一致を増やします。
文字を読む
ドキュメントが最初にインデックスに追加されるとき、文字はJava InputStreamから読み取られるため、ファイル、データベース、Webサービス呼び出しなどから取得できます。ProjectGutenbergのインデックスを作成するには、電子書籍をダウンロードし、これらのファイルを読み取り、インデックスに書き込むための小さなアプリケーションを作成します。 Luceneインデックスの作成とファイルの読み取りはよく行われているため、あまり詳しくは説明しません。 インデックスを作成するための基本的なコードは次のとおりです。
IndexWriter writer = ...; BufferedReader reader = new BufferedReader(new InputStreamReader(... fileInputStream ...)); Document document = new Document(); document.add(new StringField("title", fileName, Store.YES)); document.add(new TextField("body", reader)); writer.addDocument(document);
各電子書籍が単一のLucene Document
に対応していることがわかります。そのため、後で検索結果に一致する書籍のリストが表示されます。 Store.YES
は、ファイル名だけであるタイトルフィールドを保存することを示します。 ただし、電子書籍の本文は検索時に不要であり、ディスク領域を浪費するだけなので、保存したくありません。
ストリームの実際の読み取りは、 addDocument
で始まります。 IndexWriter
は、パイプラインの最後からトークンをプルします。 このプルは、最初のステージであるTokenizer
がInputStream
から読み取るまで、パイプを介して戻ります。
また、Luceneがこれを処理するため、ストリームを閉じないことに注意してください。
文字のトークン化
Lucene StandardTokenizerは句読点を破棄するため、引用符を保持する必要があるため、ここからカスタマイズを開始します。
StandardTokenizer
のドキュメントでは、ソースコードをコピーしてニーズに合わせて調整するように求められていますが、このソリューションは不必要に複雑になります。 代わりに、 CharTokenizer
を拡張します。これにより、「受け入れる」文字を指定できます。「受け入れられる」文字は、トークン間の区切り文字として扱われ、破棄されます。 単語とその周囲の引用符に関心があるため、カスタムTokenizerは次のようになります。
public class QuotationTokenizer extends CharTokenizer { @Override protected boolean isTokenChar(int c) { return Character.isLetter(c) || c == '"'; } }
[He said, "Good day".]
の入力ストリームが与えられると、生成されるトークンは[He]
、 [said]
、 ["Good]
、 [day"]
になります。
引用符がトークン内にどのように散在しているかに注意してください。 見積もりごとに個別のトークンを生成するTokenizer
を作成することは可能ですが、 Tokenizer
は、バッファリングやスキャンなどの面倒でねじ込みやすい詳細にも関係しているため、 Tokenizer
をシンプルに保ち、クリーンアップすることをお勧めします。パイプラインに沿ってさらにトークンストリーム。
フィルタを使用したトークンの分割
トークナイザーの後には、一連のTokenFilter
オブジェクトがあります。 ちなみに、 TokenFilter
はトークンを追加、削除、または変更できるため、このフィルターは少し誤称であることに注意してください。
Luceneが提供するフィルタークラスの多くは単一の単語を想定しているため、単語と引用符が混在するトークンをそれらに流し込むことはできません。 したがって、Luceneチュートリアルの次のカスタマイズは、 QuotationTokenizer
の出力をクリーンアップするフィルターの導入である必要があります。
このクリーンアップには、引用符が単語の先頭に表示される場合は追加の開始引用符トークンの生成、引用符が末尾に表示される場合は終了引用符トークンの生成が含まれます。 簡単にするために、引用符で囲まれた単語の処理は脇に置きます。
TokenFilter
サブクラスの作成には、 incrementToken
という1つのメソッドの実装が含まれます。 このメソッドは、パイプ内の前のフィルターでincrementToken
を呼び出し、その呼び出しの結果を操作して、フィルターが担当するすべての作業を実行する必要があります。 incrementToken
トークンの結果は、トークン処理の現在の状態を記述するAttribute
オブジェクトを介して利用できます。 incrementToken
トークンの実装が戻った後、次のフィルター(またはパイプの最後にある場合はインデックス)のトークンを設定するために属性が操作されていることが期待されます。
パイプラインのこの時点で関心のある属性は次のとおりです。
CharTermAttribute
:現在のトークンの文字を保持するchar[]
バッファが含まれています。 見積もりを削除するか、見積もりトークンを生成するには、これを操作する必要があります。TypeAttribute
:現在のトークンの「タイプ」が含まれます。 トークンストリームに開始引用符と終了引用符を追加しているため、フィルターを使用して2つの新しいタイプを導入します。OffsetAttribute
:Luceneは、オプションで、元のドキュメント内の用語の場所への参照を格納できます。 これらの参照は「オフセット」と呼ばれ、元の文字ストリームへの開始インデックスと終了インデックスにすぎません。 トークンのサブストリングのみを指すようにCharTermAttribute
のバッファーを変更する場合は、それに応じてこれらのオフセットを調整する必要があります。
トークンストリームを操作するためのAPIがなぜそれほど複雑なのか、特に、着信トークンに対してString#split
のようなことを実行できないのはなぜか疑問に思われるかもしれません。 これは、Luceneが高速でオーバーヘッドの少ないインデックス作成用に設計されているためです。これにより、組み込みのトークナイザーとフィルターは、メガバイトのメモリのみを使用しながら、ギガバイトのテキストをすばやく処理できます。 これを実現するために、トークン化とフィルタリング中に割り当てがほとんどまたはまったく行われないため、上記のAttribute
インスタンスは一度割り当てられて再利用されることを目的としています。 トークナイザーとフィルターがこのように記述されていて、それら自体の割り当てを最小限に抑えると、パフォーマンスを損なうことなくLuceneをカスタマイズできます。
これらすべてを念頭に置いて、 ["Hello]
などのトークンを受け取り、 ["]
と[Hello]
の2つのトークンを生成するフィルターを実装する方法を見てみましょう。
public class QuotationTokenFilter extends TokenFilter { private static final char QUOTE = '"'; public static final String QUOTE_START_TYPE = "start_quote"; public static final String QUOTE_END_TYPE = "end_quote"; private final OffsetAttribute offsetAttr = addAttribute(OffsetAttribute.class); private final TypeAttribute typeAttr = addAttribute(TypeAttribute.class); private final CharTermAttribute termBufferAttr = addAttribute(CharTermAttribute.class);
まず、前に見た属性のいくつかへの参照を取得します。 フィールド名には「Attr」という接尾辞を付けて、後で参照するときに明確になるようにします。 一部のTokenizer
実装はこれらの属性を提供しない可能性があるため、 addAttribute
を使用して参照を取得します。 addAttribute
は、属性インスタンスがない場合は属性インスタンスを作成します。それ以外の場合は、そのタイプの属性への共有参照を取得します。 Luceneでは、同じ属性タイプの複数のインスタンスを一度に許可しないことに注意してください。
private boolean emitExtraToken; private int extraTokenStartOffset, extraTokenEndOffset; private String extraTokenType;
フィルタは元のストリームに存在しなかった新しいトークンを導入するため、 incrementToken
の呼び出しの間にそのトークンの状態を保存する場所が必要です。 既存のトークンを2つに分割しているため、新しいトークンのオフセットとタイプだけを知っていれば十分です。 また、 incrementToken
への次の呼び出しでこの追加のトークンが発行されるかどうかを示すフラグもあります。 Luceneは、実際には、 captureState
とrestoreState
のペアのメソッドを提供します。これにより、これが実行されます。 ただし、これらのメソッドにはState
オブジェクトの割り当てが含まれ、実際にはその状態を自分で管理するよりも難しい場合があるため、使用を避けます。
@Override public void reset() throws IOException { emitExtraToken = false; extraTokenStartOffset = -1; extraTokenEndOffset = -1; extraTokenType = null; super.reset(); }
割り当てを積極的に回避する一環として、Luceneはフィルターインスタンスを再利用できます。 この状況では、 reset
を呼び出すと、フィルターが初期状態に戻ることが予想されます。 したがって、ここでは、追加のトークンフィールドをリセットするだけです。
@Override public boolean incrementToken() throws IOException { if (emitExtraToken) { advanceToExtraToken(); emitExtraToken = false; return true; } ...
今、私たちは興味深い部分に到達しています。 パイプラインの初期段階でincrementToken
の実装が呼び出されると、 incrementToken
を呼び出さない機会があります。 そうすることで、 Tokenizer
からトークンをプルしないため、新しいトークンを効果的に導入します。
代わりに、 advanceToExtraToken
を呼び出して追加のトークンの属性を設定し、 emitExtraToken
をfalseに設定して次の呼び出しでこの分岐を回避し、 true
を返します。これは別のトークンが使用可能であることを示します。
@Override public boolean incrementToken() throws IOException { ... (emit extra token) ... boolean hasNext = input.incrementToken(); if (hasNext) { char[] buffer = termBufferAttr.buffer(); if (termBuffer.length() > 1) { if (buffer[0] == QUOTE) { splitTermQuoteFirst(); } else if (buffer[termBuffer.length() - 1] == QUOTE) { splitTermWordFirst(); } } else if (termBuffer.length() == 1) { if (buffer[0] == QUOTE) { typeAttr.setType(QUOTE_END_TYPE); } } } return hasNext; }
残りのincrementToken
は、3つの異なることのいずれかを実行します。 termBufferAttr
は、パイプを通過するトークンの内容を検査するために使用されることを思い出してください。
トークンストリームの最後に到達した場合(つまり、
hasNext
がfalseの場合)、完了して単純に戻ります。複数の文字のトークンがあり、それらの文字の1つが引用符である場合、トークンを分割します。
トークンが単独引用符である場合、それは終了引用符であると見なされます。 理由を理解するために、開始引用符は常に単語の左側に表示されますが(つまり、中間の句読点はありません)、終了引用符は句読点の後に続くことがあります(文のように、
[He told us to "go back the way we came."]
)。 このような場合、終了引用符はすでに別のトークンになっているため、そのタイプを設定するだけで済みます。
splitTermQuoteFirst
とsplitTermWordFirst
は、現在のトークンを単語または引用符にするための属性を設定し、残りの半分を後で消費できるように「追加」フィールドを設定します。 2つの方法は似ているので、 splitTermQuoteFirst
だけを見ていきます。
private void splitTermQuoteFirst() { int origStart = offsetAttr.startOffset(); int origEnd = offsetAttr.endOffset(); offsetAttr.setOffset(origStart, origStart + 1); typeAttr.setType(QUOTE_START_TYPE); termBufferAttr.setLength(1); prepareExtraTerm(origStart + 1, origEnd, TypeAttribute.DEFAULT_TYPE); }
このトークンを最初にストリームに表示される引用符で分割するため、長さを1(つまり、1文字、つまり引用符)に設定してバッファーを切り捨てます。 それに応じてオフセットを調整し(つまり、元のドキュメントの引用符を指す)、タイプを開始引用符に設定します。
prepareExtraTerm
は、 extra*
フィールドを設定し、 emitExtraToken
をtrueに設定します。 これは、「余分な」トークン(つまり、引用符に続く単語)を指すオフセットで呼び出されます。
QuotationTokenFilter
全体はGitHubで入手できます。
余談ですが、このフィルターは1つの追加トークンしか生成しませんが、このアプローチを拡張して、任意の数の追加トークンを導入できます。 extra*
フィールドをコレクションに置き換えるか、生成できる追加のトークンの数に制限がある場合は固定長の配列に置き換えるだけです。 この例については、 SynonymFilter
とそのPendingInput
内部クラスを参照してください。
見積もりトークンの消費とマーキングダイアログ
これらの引用符をトークンストリームに追加するためのすべての努力を行ったので、それらを使用してテキスト内のダイアログのセクションを区切ることができます。
最終目標は、用語が対話の一部であるかどうかに基づいて検索結果を調整することであるため、それらの用語にメタデータを添付する必要があります。 Luceneは、この目的のためにPayloadAttribute
を提供します。 ペイロードは、インデックス内の用語と一緒に格納されるバイト配列であり、後で検索中に読み取ることができます。 これは、フラグが1バイト全体を無駄に占有することを意味するため、スペースを節約するために追加のペイロードをビットフラグとして実装できます。

以下は、分析パイプラインの最後に追加される新しいフィルター、 DialoguePayloadTokenFilter
です。 トークンが対話の一部であるかどうかを示すペイロードを添付します。
public class DialoguePayloadTokenFilter extends TokenFilter { private final TypeAttribute typeAttr = getAttribute(TypeAttribute.class); private final PayloadAttribute payloadAttr = addAttribute(PayloadAttribute.class); private static final BytesRef PAYLOAD_DIALOGUE = new BytesRef(new byte[] { 1 }); private static final BytesRef PAYLOAD_NOT_DIALOGUE = new BytesRef(new byte[] { 0 }); private boolean withinDialogue; protected DialoguePayloadTokenFilter(TokenStream input) { super(input); } @Override public void reset() throws IOException { this.withinDialogue = false; super.reset(); } @Override public boolean incrementToken() throws IOException { boolean hasNext = input.incrementToken(); while(hasNext) { boolean isStartQuote = QuotationTokenFilter .QUOTE_START_TYPE.equals(typeAttr.type()); boolean isEndQuote = QuotationTokenFilter .QUOTE_END_TYPE.equals(typeAttr.type()); if (isStartQuote) { withinDialogue = true; hasNext = input.incrementToken(); } else if (isEndQuote) { withinDialogue = false; hasNext = input.incrementToken(); } else { break; } } if (hasNext) { payloadAttr.setPayload(withinDialogue ? PAYLOAD_DIALOGUE : PAYLOAD_NOT_DIALOGUE); } return hasNext; } }
このフィルターは、 withinDialogue
内で単一の状態を維持するだけでよいため、はるかに簡単です。 開始クォートは、ダイアログのセクション内にいることを示し、終了クォートは、ダイアログのセクションが終了したことを示します。 いずれの場合も、 incrementToken
を2回呼び出すことでクォートトークンが破棄されるため、実際には、クォート開始トークンまたはクォート終了トークンがパイプラインのこのステージを通過することはありません。
たとえば、 DialoguePayloadTokenFilter
はトークンストリームを変換します。
[the], [program], [printed], ["], [hello], [world], ["]`
この新しいストリームに:
[the][0], [program][0], [printed][0], [hello][1], [world][1]
トークナイザーとフィルターを結び付ける
Analyzer
は、通常、 Tokenizer
を一連のTokenFilter
と組み合わせることにより、分析パイプラインを組み立てる役割を果たします。 Analyzer
は、そのパイプラインが分析間でどのように再利用されるかを定義することもできます。 コンポーネントは、使用の合間にreset()
を呼び出す以外は何も必要としないため、これについて心配する必要はありません。これは、Luceneが常に行います。 Analyzer#createComponents(String)
を実装してアセンブリを実行する必要があります。
public class DialogueAnalyzer extends Analyzer { @Override protected TokenStreamComponents createComponents(String fieldName) { QuotationTokenizer tokenizer = new QuotationTokenizer(); TokenFilter filter = new QuotationTokenFilter(tokenizer); filter = new LowerCaseFilter(filter); filter = new StopFilter(filter, StopAnalyzer.ENGLISH_STOP_WORDS_SET); filter = new DialoguePayloadTokenFilter(filter); return new TokenStreamComponents(tokenizer, filter); } }
前に見たように、フィルターにはパイプラインの前のステージへの参照が含まれているため、フィルターをインスタンス化する方法です。 また、 StandardAnalyzer
のいくつかのフィルター( LowerCaseFilter
とStopFilter
)をスライドさせます。 これらの2つは、引用符が分離されていることを確認するためにQuotationTokenFilter
の後に来る必要があります。 QuotationTokenFilter
の後のどこでも実行できるため、 DialoguePayloadTokenFilter
の配置をより柔軟にすることができます。 最終的に削除されるストップワードにダイアログペイロードを挿入する時間を無駄にしないように、 StopFilter
の後に配置します。
これは、動作中の新しいパイプラインの視覚化です(削除した、またはすでに見た標準パイプラインの部分を除く)。
DialogueAnalyzer
は、他のストックAnalyzer
と同じように使用できるようになりました。これで、インデックスを作成して検索に進むことができます。
対話の全文検索
対話のみを検索したい場合は、引用符の外にあるすべてのトークンを単純に破棄することができ、それで完了です。 代わりに、元のトークンをすべてそのままにしておくことで、ダイアログを考慮したクエリを実行したり、テキストの他の部分と同じようにダイアログを処理したりできる柔軟性が得られました。
Luceneインデックスのクエリの基本は、十分に文書化されています。 私たちの目的では、クエリは、 MUST
やSHOULD
などの演算子とそれらの用語に基づく一致ドキュメントで結合されたTerm
オブジェクトで構成されていることを知っていれば十分です。 次に、一致するドキュメントは、構成可能なSimilarity
オブジェクトに基づいてスコアリングされ、それらの結果は、スコア順に並べ替えたり、フィルター処理したり、制限したりできます。 たとえば、Luceneを使用すると、 [hello]
と[world]
の両方の用語を含む必要がある上位10個のドキュメントをクエリできます。
ダイアログに基づいて検索結果をカスタマイズするには、ペイロードに基づいてドキュメントのスコアを調整します。 このための最初の拡張ポイントは、一致する用語の重み付けとスコアリングを担当するSimilarity
になります。
類似性とスコアリング
クエリは、デフォルトでDefaultSimilarity
を使用します。これは、ドキュメント内で発生する頻度に基づいて用語に重みを付けます。 重みを調整するための優れた拡張ポイントであるため、ペイロードに基づいてドキュメントをスコアリングするためにも拡張します。 この目的のために、 DefaultSimilarity#scorePayload
が提供されています。
public final class DialogueAwareSimilarity extends DefaultSimilarity { @Override public float scorePayload(int doc, int start, int end, BytesRef payload) { if (payload.bytes[payload.offset] == 0) { return 0.0f; } return 1.0f; } }
DialogueAwareSimilarity
は、単に非ダイアログペイロードをゼロとしてスコアリングします。 各Term
は複数回一致する可能性があるため、複数のペイロードスコアを持つ可能性があります。 Query
の実装までのこれらのスコアの解釈。
ペイロードを含むBytesRef
に細心の注意を払ってください。バイト配列が以前に保存したペイロードと同じであるとは想定できないため、 offset
でバイトをチェックする必要があります。 インデックスを読み取るときに、LuceneはscorePayload
の呼び出しのためだけに別のバイト配列を割り当てるメモリを浪費しないので、既存のバイト配列への参照を取得します。 Lucene APIに対してコーディングする場合は、開発者の利便性よりもパフォーマンスが優先されることを覚えておく必要があります。
新しいSimilarity
実装ができたので、クエリの実行に使用するIndexSearcher
に設定する必要があります。
IndexSearcher searcher = new IndexSearcher(... reader for index ...); searcher.setSimilarity(new DialogueAwareSimilarity());
クエリと条件
IndexSearcher
がペイロードをスコアリングできるようになったので、ペイロードを認識するクエリも作成する必要があります。 PayloadTermQuery
を使用して、単一のTerm
を照合すると同時に、それらの照合のペイロードを確認することもできます。
PayloadTermQuery helloQuery = new PayloadTermQuery(new Term("body", "hello"), new AveragePayloadFunction());
このクエリは、 bodyフィールド内の用語[hello]
と一致します(これは、ドキュメントのコンテンツを配置する場所であることを思い出してください)。 また、すべての用語の一致から最終的なペイロードスコアを計算する関数を提供する必要があるため、すべてのペイロードスコアを平均するAveragePayloadFunction
をプラグインします。 たとえば、 [hello]
という用語がダイアログ内で2回、ダイアログ外で1回出現する場合、最終的なペイロードスコアは²⁄₃になります。 この最終的なペイロードスコアは、ドキュメント全体に対してDefaultSimilarity
によって提供されるスコアと乗算されます。
対話の外に多くの用語が表示される検索結果を強調せず、対話に用語がまったくないドキュメントのスコアをゼロにするため、平均を使用します。
ダイアログに含まれる複数の用語を検索する場合は、 BooleanQuery
を使用して複数のPayloadTermQuery
オブジェクトを作成することもできます(他のクエリタイプは位置を認識しますが、このクエリでは用語の順序は関係ありません)。
PayloadTermQuery worldQuery = new PayloadTermQuery(new Term("body", "world"), new AveragePayloadFunction()); BooleanQuery query = new BooleanQuery(); query.add(helloQuery, Occur.MUST); query.add(worldQuery, Occur.MUST);
このクエリを実行すると、クエリ構造と類似性の実装がどのように連携するかを確認できます。
クエリの実行と説明
クエリを実行するには、 IndexSearcher
に渡します。
TopScoreDocCollector collector = TopScoreDocCollector.create(10); searcher.search(query, new PositiveScoresOnlyCollector(collector)); TopDocs topDocs = collector.topDocs();
Collector
オブジェクトは、一致するドキュメントのコレクションを準備するために使用されます。
コレクターは、ソート、制限、およびフィルタリングの組み合わせを実現するように構成できます。 たとえば、ダイアログに少なくとも1つの用語が含まれる上位10のスコアリングドキュメントを取得するには、 TopScoreDocCollector
とPositiveScoresOnlyCollector
を組み合わせます。 正のスコアのみを取得すると、ゼロスコアの一致(つまり、対話に用語がないもの)が確実に除外されます。
このクエリの動作を確認するには、クエリを実行してから、 IndexSearcher#explain
を使用して、個々のドキュメントがどのようにスコアリングされたかを確認します。
for (ScoreDoc result : topDocs.scoreDocs) { Document doc = searcher.doc(result.doc, Collections.singleton("title")); System.out.println("--- document " + doc.getField("title").stringValue() + " ---"); System.out.println(this.searcher.explain(query, result.doc)); }
ここでは、検索によって取得されたTopDocs
のドキュメントIDを繰り返し処理します。 また、 IndexSearcher#doc
を使用して、表示するタイトルフィールドを取得します。 "hello"
のクエリの場合、次のようになります。
--- Document whelv10.txt --- 0.072256625 = (MATCH) btq, product of: 0.072256625 = weight(body:hello in 7336) [DialogueAwareSimilarity], result of: 0.072256625 = fieldWeight in 7336, product of: 2.345208 = tf(freq=5.5), with freq of: 5.5 = phraseFreq=5.5 3.1549776 = idf(docFreq=2873, maxDocs=24796) 0.009765625 = fieldNorm(doc=7336) 1.0 = AveragePayloadFunction.docScore() --- Document daved10.txt --- 0.061311778 = (MATCH) btq, product of: 0.061311778 = weight(body:hello in 6873) [DialogueAwareSimilarity], result of: 0.061311778 = fieldWeight in 6873, product of: 3.3166249 = tf(freq=11.0), with freq of: 11.0 = phraseFreq=11.0 3.1549776 = idf(docFreq=2873, maxDocs=24796) 0.005859375 = fieldNorm(doc=6873) 1.0 = AveragePayloadFunction.docScore() ...
出力には専門用語が含まれていますが、スコアリングでカスタムのSimilarity
実装がどのように使用されたか、およびMaxPayloadFunction
がこれらの一致に対して1.0
の乗数を生成した方法を確認できます。 これは、ペイロードがロードされてスコアリングされ、 "Hello"
のすべての一致がダイアログで発生したことを意味します。したがって、これらの結果は、期待どおりの結果になります。
ペイロードを含むProjectGutenbergのインデックスのサイズは約4ギガバイトになりますが、私の控えめな開発マシンでは、クエリが瞬時に発生することも指摘しておく価値があります。 検索の目標を達成するために速度を犠牲にすることはありません。
まとめ
Luceneは、文字の生のストリームを取得し、それらをトークンにバンドルして、インデックス内の用語として保持する、強力な専用の全文検索ライブラリです。 そのインデックスをすばやくクエリしてランク付けされた結果を提供し、効率を維持しながら拡張の十分な機会を提供します。
Luceneをアプリケーションで直接使用するか、サーバーの一部として使用することで、ギガバイトを超えるコンテンツをリアルタイムで全文検索を実行できます。 さらに、カスタム分析とスコアリングにより、ドキュメントのドメイン固有の機能を利用して、結果またはカスタムクエリの関連性を向上させることができます。
このLuceneチュートリアルの完全なコードリストは、GitHubで入手できます。 リポジトリには、インデックスを作成するためのLuceneQueryApp
とクエリを実行するためのLuceneIndexerApp
の2つのアプリケーションが含まれています。
BitTorrentを介してディスクイメージとして取得できるProjectGutenbergのコーパスには、読む価値のある本がたくさん含まれています(Luceneを使用するか、昔ながらの方法で)。
ハッピーインデックス!