Apache Lucene

Takami Torao Lucene 9.11 Java 21 #Lucene
  • このエントリーをはてなブックマークに追加

概要

Apache Lucene は高性能な全文検索とテキスト解析のためのオープンソースライブラリ。1999 年に Doug Cutting によって最初に開発され、その後 Apache Software Foundation のプロジェクトとして継続的に開発されている。Lucene は Java で書かれており、強力な検索機能はさまざまな規模のアプリケーションに柔軟に対応できる設計となっている。

Lucene はその自由度の高さから Solr や Elasticsearch といった商用ソリューションの検索エンジンとして広く利用されている。

Table of Contents

  1. 概要
  2. 基本的な検索操作
    1. インデックス作成
    2. 文書の検索
    3. 文書の削除

基本的な検索操作

この例では Lucene 9.11 を使用している (Lucene 9 でトークナイザーの artifactId が lucene-analyzers-xxx から lucene-analysis-xxx へ変更されていることに注意)

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>at.hazm</groupId>
  <artifactId>lucene-ja-sample</artifactId>
  <version>1.0-SNAPSHOT</version>

  <dependencies>
    <dependency>
      <groupId>org.apache.lucene</groupId>
      <artifactId>lucene-core</artifactId>
      <version>9.11.0</version>
    </dependency>
    <dependency>
      <groupId>org.apache.lucene</groupId>
      <artifactId>lucene-queryparser</artifactId>
      <version>9.11.0</version>
    </dependency>
    <dependency>
      <groupId>org.apache.lucene</groupId>
      <artifactId>lucene-analysis-kuromoji</artifactId>
      <version>9.11.0</version>
    </dependency>
    <dependency>
      <groupId>org.apache.lucene</groupId>
      <artifactId>lucene-highlighter</artifactId>
      <version>9.11.0</version>
    </dependency>
  </dependencies>
</project>

日本語向けの全文検索は Kuromoji ベースの標準の形態素解析器 JapaneseAnalyzer の他に bi-gram (n-gram) を使用する lucene-analysis-commonCJKAnalyzer を使用することもできる。

インデックス作成

この例では Lucene のインデックスに 4 つのニュース記事を追加している。インデックスの生成は、インデックスの保存に使用するディレクトリを指定して IndexWriter を生成し、Document オブジェクトを追加またはキー指定で更新することで行う。

TextFieldStringField の使い分けに注意。TextField で指定した内容は analyzer でトークン化されてインデックス化されるが、StringField フィールドは解析が行われず文字列全体が一つのトークンとしてインデックス化される。例えば記事のタイトル、本文、コメントなど全文検索を行う必要があるフィールドには TextField を使用し、ID、URL、メールアドレス、製品コードのような正確な一致が必要なフィールドには StringField を使用する。この例では URL をキーとしてニュース記事をインデックスしている。

package at.hazm.contents.lucene;

import java.io.IOException;
import java.nio.file.Paths;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.ja.JapaneseAnalyzer;
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.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;

public class Indexer {
  public static void main(String[] args) throws IOException {
    try (
        Directory index = FSDirectory.open(Paths.get("my-index"));
        Analyzer analyzer = new JapaneseAnalyzer();
        IndexWriter writer = new IndexWriter(index, new IndexWriterConfig(analyzer))) {
      addToIndex(writer,
          "https://www3.nhk.or.jp/news/html/20240608/k10014474721000.html",
          "デンマーク 首相が襲われる 男を逮捕 けが有無など詳細は不明",
          "2024-06-08T02:50:49Z");
      addToIndex(writer,
          "https://news.livedoor.com/topics/detail/26561636/",
          "偽ウクライナのポロシェンコ前大統領と通話 英キャメロン外相",
          "2024-06-08T01:39:55Z");
      addToIndex(writer,
          "https://www.nikkei.com/article/DGXZQOGN080CT0Y4A600C2000000/",
          "国連、イスラエルを「恥ずべき国」指定 子供の権利侵害",
          "2024-06-08T01:06:51Z");
      addToIndex(writer,
          "https://www3.nhk.or.jp/news/html/20240608/k10014474461000.html",
          "マクロン大統領 各国と協力してウクライナに軍の教官派遣へ",
          "2024-06-08T00:18:21Z");
      writer.commit();
      System.out.println("Lucene index created.");
    }
  }

  private static void addToIndex(IndexWriter writer, String url, String content, String date) throws IOException {
    Document doc = new Document();
    doc.add(new StringField("url", url, Field.Store.YES));
    doc.add(new StringField("date", date, Field.Store.YES));
    doc.add(new TextField("content", content, Field.Store.YES));

    // url フィールドをキーとしてインデックスのドキュメントを更新
    writer.updateDocument(new Term("url", url), doc);
  }
}

文書の検索

次に作成したインデックスから「ウクライナ 大統領」の 2 語でニュース記事を検索する。

検索結果はスコアの降順で返される。スコアは検索条件に一致する文書の重要度を表す値で、0 から 1 の範囲で返される。スコアが 1 に近いほど検索条件に一致する文書との類似度が高いことを示している。

ここでは平文のニュース本文を取得しているだけではなく、その中から検索条件に一致した部分を強調表示するために Highlighter を使用している。この例では SimpleHTMLFormatter を使用して検索条件に一致した部分を抽出し <em> タグで囲む処理を行っている。

package at.hazm.contents.lucene;

import java.io.IOException;
import java.nio.file.Paths;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.ja.JapaneseAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
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.search.highlight.Formatter;
import org.apache.lucene.search.highlight.Highlighter;
import org.apache.lucene.search.highlight.InvalidTokenOffsetsException;
import org.apache.lucene.search.highlight.QueryScorer;
import org.apache.lucene.search.highlight.SimpleFragmenter;
import org.apache.lucene.search.highlight.SimpleHTMLFormatter;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;

public class Retriever {
  public static void main(String[] args) throws IOException, ParseException, InvalidTokenOffsetsException {
    String queryString = "ウクライナ 大統領";
    try (
        Directory index = FSDirectory.open(Paths.get("my-index"));
        Analyzer analyzer = new JapaneseAnalyzer();
        IndexReader reader = DirectoryReader.open(index)) {
      Query query = new QueryParser("content", analyzer).parse(queryString);
      IndexSearcher searcher = new IndexSearcher(reader);

      // トップ 10 件の検索結果を取得
      TopDocs topDocs = searcher.search(query, 10);

      // ハイライトの設定
      Formatter formatter = new SimpleHTMLFormatter("<em>", "</em>");
      Highlighter highlighter = new Highlighter(formatter, new QueryScorer(query));
      highlighter.setTextFragmenter(new SimpleFragmenter(10));

      // 検索結果の表示
      for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
        int docId = scoreDoc.doc;
        Document doc = searcher.storedFields().document(docId);
        String url = doc.get("url");
        String date = doc.get("date");
        String content = doc.get("content");
        System.out.println("docId=" + docId + " score=" + scoreDoc.score);
        System.out.println("  " + url);
        System.out.println("  " + date);

        // 内容をハイライトして表示
        String[] fragments = highlighter.getBestFragments(analyzer, "content", content, 3);
        System.out.println("  " + String.join(" ... ", fragments));
      }
    }
  }
}
docId=3 score=0.64567137
  https://www3.nhk.or.jp/news/html/20240608/k10014474461000.html
  2024-06-08T00:18:21Z
  マクロン<em>大統領</em> 各国と協力して<em>ウクライナ</em>に軍の教官派遣へ
docId=1 score=0.6153264
  https://news.livedoor.com/topics/detail/26561636/
  2024-06-08T01:39:55Z
  偽<em>ウクライナ</em>のポロシェンコ前<em>大統領</em>と通話 英キャメロン外相

文書の削除

最後に、URL をキーとしてニュース記事を削除している。この処理を実行して再度検索すると docId=1 に該当する記事が削除されていることがわかるだろう。

package at.hazm.contents.lucene;

import java.io.IOException;
import java.nio.file.Paths;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.ja.JapaneseAnalyzer;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;

public class Remover {
  public static void main(String[] args) throws IOException {
    try (
        Directory index = FSDirectory.open(Paths.get("my-index"));
        Analyzer analyzer = new JapaneseAnalyzer();
        IndexWriter writer = new IndexWriter(index, new IndexWriterConfig(analyzer))) {
      writer.deleteDocuments(new Term("url", "https://news.livedoor.com/topics/detail/26561636/"));
    }
  }
}