Elasticsearchを理解するためにLuceneを使ってみる

背景

もっとElasticsearchを理解できるように、基本から復習していきます。

(いつまで続くわからないけど)Elasticsearchを基礎から理解するシリーズ第1回はLuceneを使ってみるです。

ElasticsearchはApache Luceneがベースになっているので、Elasticsearchを基本から復習・理解していくには必然的にLuceneのことを理解する必要がありますが、まあまあ難しいのでまずはLuceneを使ってみるところから始めます。

免責

Elasticsearchについて調べたことをまとめています。また、JAVAは初心者です。

おそらく間違えていることがあると思うので、間違いに気づいたらご連絡いただけると嬉しいです。

環境

準備

luceneを追加する

Mavenを使います。

まずdependencyluceneを追加します。追加するバージョンはともに9.4.1です。

追加の仕方、注意点は以下を参照。

ryook.hatenablog.jp

lucene-in-5-minutesをやってみる

ぐぐったらチュートリアルを見つけたので、本のタイトルとisbnコード(両方文字列)を検索するサンプルを試します。

www.lucenetutorial.com

コード

package tutorial;  
  
import org.apache.lucene.analysis.standard.StandardAnalyzer;  
import org.apache.lucene.document.Document;  
import org.apache.lucene.document.Field;  
import org.apache.lucene.document.StringField;  
import org.apache.lucene.document.TextField;  
import org.apache.lucene.index.DirectoryReader;  
import org.apache.lucene.index.IndexReader;  
import org.apache.lucene.index.IndexWriter;  
import org.apache.lucene.index.IndexWriterConfig;  
import org.apache.lucene.queryparser.classic.ParseException;  
import org.apache.lucene.queryparser.classic.QueryParser;  
import org.apache.lucene.search.IndexSearcher;  
import org.apache.lucene.search.Query;  
import org.apache.lucene.search.ScoreDoc;  
import org.apache.lucene.search.TopDocs;  
import org.apache.lucene.store.ByteBuffersDirectory;  
import org.apache.lucene.store.Directory;  
  
  
import java.io.IOException;  
  
public class Main {  
    public static void main(String[] args) throws IOException, ParseException {  
  
        // index の作成
        StandardAnalyzer analyzer = new StandardAnalyzer();  
  
        Directory directory = new ByteBuffersDirectory();  
        IndexWriterConfig config = new IndexWriterConfig((analyzer));  
  
        // ドキュメントの追加
        IndexWriter w = new IndexWriter(directory, config);  
        addDoc(w, "Lucene in Action", "193398817");  
        addDoc(w, "Lucene for Dummies", "55320055Z");  
        addDoc(w, "Managing Gigabytes", "55063554A");  
        addDoc(w, "The Art of Computer Science", "9900333X");  
        w.close();  
  
        // 検索
        // query  の作成
        String queryString = args.length > 0 ? args[0]: "lucene";  
  
        Query q = new QueryParser("title", analyzer).parse(queryString);  
  
        // 検索実行  
        int hitsPerPage = 10;  
        IndexReader reader = DirectoryReader.open(directory);  
        IndexSearcher searcher = new IndexSearcher(reader);  
        TopDocs docs = searcher.search(q, hitsPerPage);  
        ScoreDoc[] hits = docs.scoreDocs;  
  
        // 結果の表示
        System.out.println("Found " + hits.length + " hits.");  
        for (int i=0;i<hits.length;i++){  
            int docId = hits[i].doc;  
            Document d = searcher.doc(docId);  
            System.out.println((i+1) + "." + d.get("isbn") + "\t" + d.get("title"));  
        }  
  
        reader.close();  
    }  
  
    private static void addDoc(IndexWriter w, String title, String isbn) throws IOException {  
        Document doc = new Document();  
        doc.add(new TextField("title", title, Field.Store.YES));  
        doc.add(new StringField("isbn", isbn, Field.Store.YES));  
        w.addDocument(doc);  
    }  
}

index作成

インデックスは、ByteBuffersDirectoryを使って、メモリのヒープ領域に作成します。

ドキュメントの追加

analyzerはStandardAnalyzerを使っています。これは、 StandardTokenizer と LowerCaseFilter と StopFilterが使われます。 雑にいうと、英文の場合は空白で区切られ、すべて小文字として扱われます。

ドキュメントはDocumentに対して、Fieldインスタンスを追加します。各フィールドのフィールドタイプ、フィールド名、値、オプションはFieldクラスで管理しているようです。

まだ中身までちゃんと追えてませんが、文字列処理やindex(luceneでは正確にはsegment)への書き込みはIndexWriterクラスが行います。

queryの作成

当初Mavenlucene-coreだけ追加していたのですが、QueryParserが存在せずに少しハマりました。 QueryParserはlucene-coreではなくて、lucene-queryparser moduleなので忘れずに追加します。

検索実行

検索の流れとしては以下のようになっています。

  1. DirectoryReaderクラスのopenメソッドで、対象とするDirectoryインスタンスを指定
  2. IndexReaderインスタンスを指定して、IndexSearcherクラスのインスタンスを作成
  3. IndexSearcherクラスのsearchメソッドでQueryインスタンスを渡す

実行結果

Found 2 hits.
1.193398817 Lucene in Action
2.55320055Z Lucene for Dummies

検索できた!

indexをファイルに永続化する

String indexDirectoryPath = "./index/";  
Directory directory = FSDirectory.open(Paths.get(indexDirectoryPath));

directoryをByteBuffersDirectoryからFSDirectoryに変更します。

これを実行すると作成したインデックスが永続化されています。pathで指定したindexディレクトリができていて中にファイルが作られています。

中身に関しては以下のブログを参照してください。(自分もまだ理解してない...) https://www.m3tech.blog/entry/2021/08/24/162205

index
├── _0.cfe
├── _0.cfs
├── _0.si
├── segments_1
└── write.lock

(おまけ)インデックスの設定

ここでは細かい説明は省きますが、luceneのインデックスにドキュメントを追加する際にはいくつかのステップをふみます。

ざっくり書くと

  • メモリ上のバッファに追加する
  • バッファがいっぱいになると新しいセグメントをつくる
  • セグメントが一定数になればマージする といったステップがあります。

これらの設定はIndexWriterConfigでやるようです。

IndexWriterConfig config = new IndexWriterConfig(analyzer);

LogMergePolicy policy = new LogDocMergePolicy();  
policy.setMergeFactor(10);  
config.setMergePolicy(policy);  
config.setMaxBufferedDocs(100);  
config.setRAMBufferSizeMB(256.0);

Elasticsearchの設定はここで使われているんだろうなという予想です。

感想

Elasticsearch的にあの処理はこれを使っているのか〜というのがなんとなく見えてきました。 具体的にどういう実装になってるんだろう?というのは引き続き確認していきます。

参考

po3rin.com