1. 概要

Apache Lucene は、さまざまなプログラミング言語から使用できる全文検索エンジンです。

この記事では、ライブラリのコアコンセプトを理解し、簡単なアプリケーションを作成することを試みます。

2. Mavenのセットアップ

開始するには、最初に必要な依存関係を追加しましょう。

<dependency>        
    <groupId>org.apache.lucene</groupId>          
    <artifactId>lucene-core</artifactId>
    <version>7.1.0</version>
</dependency>

最新バージョンはここにあります。

また、検索クエリを解析するには、次のものが必要です。

<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-queryparser</artifactId>
    <version>7.1.0</version>
</dependency>

最新バージョンここを確認してください。

3. コアコンセプト

3.1. インデックス作成

簡単に言えば、Luceneはデータの「転置インデックス」を使用します。ページをキーワードにマッピングする代わりに、本の最後にある用語集のようにキーワードをページにマッピングします。

これにより、テキストを直接検索するのではなく、インデックスを検索するため、検索応答が高速になります。

3.2. ドキュメント

ここで、ドキュメントはフィールドのコレクションであり、各フィールドには値が関連付けられています。

インデックスは通常、1つ以上のドキュメントで構成され、検索結果は最も一致するドキュメントのセットです。

必ずしもプレーンテキストドキュメントであるとは限りません。データベーステーブルまたはコレクションである可能性もあります。

3.3. 田畑

ドキュメントにはフィールドデータを含めることができます。フィールドは通常、データ値を保持するキーです。

title: Goodness of Tea
body: Discussing goodness of drinking herbal tea...

ここで、titlebodyはフィールドであり、一緒にまたは個別に検索できることに注意してください。

3.4. 分析

分析とは、検索を容易にするために、指定されたテキストをより小さく正確な単位に変換することです。

テキストは、キーワードの抽出、一般的な単語や句読点の削除、単語の小文字への変更など、さまざまな操作を経ています。

この目的のために、複数の組み込みアナライザーがあります。

  1. StandardAnalyzer –基本的な文法に基づいて分析し、「a」、「an」などのストップワードを削除します。 また、小文字に変換します
  2. SimpleAnalyzer –文字なしの文字に基づいてテキストを分割し、小文字に変換します
  3. WhiteSpaceAnalyzer –空白に基づいてテキストを分割します

使用およびカスタマイズできるアナライザーは他にもあります。

3.5. 検索

インデックスが作成されると、 QueryIndexSearcherを使用してそのインデックスを検索できます。検索結果は通常、取得したデータを含む結果セットです。

IndexWritter はインデックスの作成を担当し、IndexSearcherはインデックスの検索を担当することに注意してください。

3.6. クエリ構文

Luceneは、非常に動的で記述しやすいクエリ構文を提供します。

フリーテキストを検索するには、クエリとしてテキストStringを使用します。

特定のフィールドのテキストを検索するには、次を使用します。

fieldName:text

eg: title:tea

範囲検索:

timestamp:[1509909322,1572981321]

ワイルドカードを使用して検索することもできます。

dri?nk

ワイルドカード「?」の代わりに1文字を検索します

d*k

「d」で始まり「k」で終わる単語を、間に複数の文字を入れて検索します。

uni*

「uni」で始まる単語が見つかります。

これらのクエリを組み合わせて、より複雑なクエリを作成することもできます。 AND、NOT、ORなどの論理演算子を含めます。

title: "Tea in breakfast" AND "coffee"

クエリ構文の詳細ここ

4. シンプルなアプリケーション

簡単なアプリケーションを作成し、いくつかのドキュメントにインデックスを付けましょう。

まず、メモリ内のインデックスを作成し、それにいくつかのドキュメントを追加します。

...
Directory memoryIndex = new RAMDirectory();
StandardAnalyzer analyzer = new StandardAnalyzer();
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
IndexWriter writter = new IndexWriter(memoryIndex, indexWriterConfig);
Document document = new Document();

document.add(new TextField("title", title, Field.Store.YES));
document.add(new TextField("body", body, Field.Store.YES));

writter.addDocument(document);
writter.close();

ここでは、 TextField を使用してドキュメントを作成し、IndexWriterを使用してインデックスに追加します。 TextField コンストラクターの3番目の引数は、フィールドの値が保存するかどうかも指定します。

アナライザーは、データまたはテキストをチャンクに分割し、それらからストップワードをフィルターで除外するために使用されます。 ストップワードは、「a」、「am」、「is」などの単語です。 これらは完全に与えられた言語に依存します。

次に、検索クエリを作成し、追加されたドキュメントのインデックスを検索してみましょう。

public List<Document> searchIndex(String inField, String queryString) {
    Query query = new QueryParser(inField, analyzer)
      .parse(queryString);

    IndexReader indexReader = DirectoryReader.open(memoryIndex);
    IndexSearcher searcher = new IndexSearcher(indexReader);
    TopDocs topDocs = searcher.search(query, 10);
    List<Document> documents = new ArrayList<>();
    for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
        documents.add(searcher.doc(scoreDoc.doc));
    }

    return documents;
}

search()メソッドでは、2番目の整数引数は、返される上位の検索結果の数を示します。

それをテストしてみましょう:

@Test
public void givenSearchQueryWhenFetchedDocumentThenCorrect() {
    InMemoryLuceneIndex inMemoryLuceneIndex 
      = new InMemoryLuceneIndex(new RAMDirectory(), new StandardAnalyzer());
    inMemoryLuceneIndex.indexDocument("Hello world", "Some hello world");
    
    List<Document> documents 
      = inMemoryLuceneIndex.searchIndex("body", "world");
    
    assertEquals(
      "Hello world", 
      documents.get(0).get("title"));
}

ここでは、「title」と「body」の2つのフィールドを持つ単純なドキュメントをインデックスに追加し、検索クエリを使用して同じものを検索しようとします。

6. Luceneクエリ

インデックス作成と検索の基本に慣れてきたので、もう少し深く掘り下げてみましょう。

前のセクションでは、基本的なクエリ構文と、それをに変換する方法を見てきました。 クエリを使用するインスタンス QueryParser。

Luceneは、さまざまな具体的な実装も提供します。

6.1. TermQuery

用語は検索の基本単位であり、フィールド名と検索対象のテキストが含まれています。

TermQuery は、単一の用語で構成されるすべてのクエリの中で最も単純です。

@Test
public void givenTermQueryWhenFetchedDocumentThenCorrect() {
    InMemoryLuceneIndex inMemoryLuceneIndex 
      = new InMemoryLuceneIndex(new RAMDirectory(), new StandardAnalyzer());
    inMemoryLuceneIndex.indexDocument("activity", "running in track");
    inMemoryLuceneIndex.indexDocument("activity", "Cars are running on road");

    Term term = new Term("body", "running");
    Query query = new TermQuery(term);

    List<Document> documents = inMemoryLuceneIndex.searchIndex(query);
    assertEquals(2, documents.size());
}

6.2. prefixQuery

「startswith」という単語を含むドキュメントを検索するには:

@Test
public void givenPrefixQueryWhenFetchedDocumentThenCorrect() {
    InMemoryLuceneIndex inMemoryLuceneIndex 
      = new InMemoryLuceneIndex(new RAMDirectory(), new StandardAnalyzer());
    inMemoryLuceneIndex.indexDocument("article", "Lucene introduction");
    inMemoryLuceneIndex.indexDocument("article", "Introduction to Lucene");

    Term term = new Term("body", "intro");
    Query query = new PrefixQuery(term);

    List<Document> documents = inMemoryLuceneIndex.searchIndex(query);
    assertEquals(2, documents.size());
}

6.3. WildcardQuery

名前が示すように、ワイルドカード「*」または「?」を使用できます。 検索用:

// ...
Term term = new Term("body", "intro*");
Query query = new WildcardQuery(term);
// ...

6.4. PhraseQuery

これは、ドキュメント内の一連のテキストを検索するために使用されます。

// ...
inMemoryLuceneIndex.indexDocument(
  "quotes", 
  "A rose by any other name would smell as sweet.");

Query query = new PhraseQuery(
  1, "body", new BytesRef("smell"), new BytesRef("sweet"));

List<Document> documents = inMemoryLuceneIndex.searchIndex(query);
// ...

PhraseQuery コンストラクターの最初の引数がslop、と呼ばれることに注意してください。これは、一致する用語間の単語数の距離です。

6.5. FuzzyQuery

これは、類似しているが必ずしも同一ではないものを検索するときに使用できます。

// ...
inMemoryLuceneIndex.indexDocument("article", "Halloween Festival");
inMemoryLuceneIndex.indexDocument("decoration", "Decorations for Halloween");

Term term = new Term("body", "hallowen");
Query query = new FuzzyQuery(term);

List<Document> documents = inMemoryLuceneIndex.searchIndex(query);
// ...

「Halloween」というテキストを検索してみましたが、「hallowen」のつづりが間違っていました。

6.6. BooleanQuery

2つ以上の異なるタイプのクエリを組み合わせて、複雑な検索を実行する必要がある場合があります。

// ...
inMemoryLuceneIndex.indexDocument("Destination", "Las Vegas singapore car");
inMemoryLuceneIndex.indexDocument("Commutes in singapore", "Bus Car Bikes");

Term term1 = new Term("body", "singapore");
Term term2 = new Term("body", "car");

TermQuery query1 = new TermQuery(term1);
TermQuery query2 = new TermQuery(term2);

BooleanQuery booleanQuery 
  = new BooleanQuery.Builder()
    .add(query1, BooleanClause.Occur.MUST)
    .add(query2, BooleanClause.Occur.MUST)
    .build();
// ...

7. 検索結果の並べ替え

また、特定のフィールドに基づいて検索結果ドキュメントを並べ替えることもあります。

@Test
public void givenSortFieldWhenSortedThenCorrect() {
    InMemoryLuceneIndex inMemoryLuceneIndex 
      = new InMemoryLuceneIndex(new RAMDirectory(), new StandardAnalyzer());
    inMemoryLuceneIndex.indexDocument("Ganges", "River in India");
    inMemoryLuceneIndex.indexDocument("Mekong", "This river flows in south Asia");
    inMemoryLuceneIndex.indexDocument("Amazon", "Rain forest river");
    inMemoryLuceneIndex.indexDocument("Rhine", "Belongs to Europe");
    inMemoryLuceneIndex.indexDocument("Nile", "Longest River");

    Term term = new Term("body", "river");
    Query query = new WildcardQuery(term);

    SortField sortField 
      = new SortField("title", SortField.Type.STRING_VAL, false);
    Sort sortByTitle = new Sort(sortField);

    List<Document> documents 
      = inMemoryLuceneIndex.searchIndex(query, sortByTitle);
    assertEquals(4, documents.size());
    assertEquals("Amazon", documents.get(0).getField("title").stringValue());
}

取得したドキュメントを、川の名前であるタイトルフィールドで並べ替えようとしました。 SortField コンストラクターのブール引数は、ソート順を逆にするためのものです。

8. インデックスからドキュメントを削除する

特定のTerm:に基づいて、インデックスからいくつかのドキュメントを削除してみましょう。

// ...
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
IndexWriter writer = new IndexWriter(memoryIndex, indexWriterConfig);
writer.deleteDocuments(term);
// ...

これをテストします:

@Test
public void whenDocumentDeletedThenCorrect() {
    InMemoryLuceneIndex inMemoryLuceneIndex 
      = new InMemoryLuceneIndex(new RAMDirectory(), new StandardAnalyzer());
    inMemoryLuceneIndex.indexDocument("Ganges", "River in India");
    inMemoryLuceneIndex.indexDocument("Mekong", "This river flows in south Asia");

    Term term = new Term("title", "ganges");
    inMemoryLuceneIndex.deleteDocument(term);

    Query query = new TermQuery(term);

    List<Document> documents = inMemoryLuceneIndex.searchIndex(query);
    assertEquals(0, documents.size());
}

9. 結論

この記事は、ApacheLuceneの使用を開始するための簡単な紹介でした。 また、さまざまなクエリを実行し、取得したドキュメントを並べ替えました。

いつものように、例のコードはGithubにあります。