CLOVER🍀

That was when it all began.

Luceneで、同じ名前のフィールドを複数登録してみる

SolrとかElasticsearchを見ていて、multi-valuedなフィールドを時々見かけていたのですが、そういえばLuceneでは触れたことがありません。

使うにあたり、何か特別な考慮がいるのかなぁ?とか思ったのですが、意外とそんなことはありませんでした。このあたり、Elasticsearchの本にも「デフォルトでLuceneはmulti-valuedなフィールドに対応している」とか書いてありましたしね。

早速試してみましょう。

準備

依存関係の定義。
build.sbt

name := "lucene-multi-field"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.10.4"

organization := "org.littlewings"

scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked")

val luceneVersion = "4.7.1"

libraryDependencies ++= Seq(
  "org.apache.lucene" % "lucene-queryparser" % luceneVersion,
  "org.apache.lucene" % "lucene-analyzers-kuromoji" % luceneVersion
)

では、以下のコードを雛形に進めていきます。
src/main/scala/org/littlewings/lucene/multifield/LuceneMultiField.scala

package org.littlewings.lucene.multifield

import scala.collection.JavaConverters._

import org.apache.lucene.analysis.{Analyzer, AnalyzerWrapper}
import org.apache.lucene.analysis.core.KeywordAnalyzer
import org.apache.lucene.analysis.ja.JapaneseAnalyzer
import org.apache.lucene.document.{Document, Field, StringField, TextField}
import org.apache.lucene.index.{DirectoryReader, IndexWriter, IndexWriterConfig}
import org.apache.lucene.queryparser.classic.QueryParser
import org.apache.lucene.search.{IndexSearcher, Query, Sort, SortField, TopFieldCollector}
import org.apache.lucene.store.{Directory, RAMDirectory}
import org.apache.lucene.util.Version

object LuceneMultiField {
  def main(args: Array[String]): Unit = {
    val version = Version.LUCENE_CURRENT
    val analyzer = createAnalyzer(version)
    val queryAnalyzer = createQueryAnalyzer(version)

    // ここにメインの処理を書く!
  }

  implicit class CloseableWrapper[A <: AutoCloseable](val underlying: A) extends AnyVal {
    def foreach(fun: A => Unit): Unit =
      try {
        fun(underlying)
      } finally {
        underlying.close()
      }
  }
}

Analyzerの作成と、インデックスへの登録

まずは、インデックス登録のAnalyzerを作成します。

  private def createAnalyzer(version: Version): Analyzer =
    new JapaneseAnalyzer(version)

そして、ドキュメントの登録。

  private def registerDocuments(directory: Directory, version: Version, analyzer: Analyzer): Unit =
    for (writer <- new IndexWriter(directory,
                                   new IndexWriterConfig(version, analyzer))) {
      Array(
        createDocument(Map("isbn" -> "978-4774127804",
                           "title" -> "Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築",
                           "tags" -> Seq("Java", "Lucene", "全文検索", "オープンソース"),
                           "authors" -> Seq("関口 宏司"))),
        createDocument(Map("isbn" -> "978-4774161631",
                           "title" -> "[改訂新版] Apache Solr入門 オープンソース全文検索エンジン",
                           "tags" -> Seq("Java", "Lucene", "Solr", "全文検索", "オープンソース"),
                           "authors" -> Seq("大谷 純", "阿部 慎一朗", "大須賀 稔", "北野 太郎", "鈴木 教嗣", "平賀 一昭", "株式会社リクルートテクノロジーズ", "株式会社ロンウイット"))),
        createDocument(Map("isbn" -> "978-4048662024",
                          "title" -> "高速スケーラブル検索エンジン ElasticSearch Server",
                          "tags" -> Seq("Java", "Elasticsearch", "全文検索", "オープンソース"),
                          "authors" -> Seq("Rafal Kuc", "Marek Rogozinski", "株式会社リクルートテクノロジーズ", "大岩 達也", "大谷 純", "兼山 元太", "水戸 祐介", "守谷 純之介")))
      ).foreach(writer.addDocument)

      writer.commit()
    }

相変わらず書籍ですが、「tags」フィールドと「authors」フィールドが複数の値を持つようになっています。

で、ドキュメントを登録する箇所。

  private def createDocument(entry: Map[String, Any]): Document = {
    val document = new Document
    document.add(new StringField("isbn", entry("isbn").toString, Field.Store.YES))
    document.add(new TextField("title", entry("title").toString, Field.Store.YES))

    for {
      Seq(tags @ _*) <- entry.get("tags")
      tag <- tags
    } {
      document.add(new StringField("tags", tag.toString, Field.Store.YES))
    }

    for {
      Seq(authors @ _*) <- entry.get("authors")
      author <- authors
    } {
      document.add(new TextField("authors", author.toString, Field.Store.YES))
    }

    document
  }

見るとお分かりかもしれませんが、同じフィールド名で複数の値をそれぞれ登録していけばOKみたいです。呼び出し元では、以下のようにListを渡していますので

"tags" -> Seq("Java", "Lucene", "全文検索", "オープンソース"),

これを要素ごとにそのまま登録していきます。

インデックスへの登録は、これでお終いです。

mainメソッドの中は、こんな感じになります。

  def main(args: Array[String]): Unit = {
    val version = Version.LUCENE_CURRENT
    val analyzer = createAnalyzer(version)
    val queryAnalyzer = createQueryAnalyzer(version)

    for (directory <- new RAMDirectory) {
      registerDocuments(directory, version, analyzer)

      // 検索処理
    }
  }

検索してみる

それでは、検索してみましょう。今回は、QueryParserを使用することにします。

ということは、QueryParserに渡すAnalyzerはフィールドを意識できる必要があるので、専用のAnalyzerを定義します。ここは、AnalyzerWrapperを使用しました。

  private def createQueryAnalyzer(version: Version): Analyzer =
    new AnalyzerWrapper(Analyzer.PER_FIELD_REUSE_STRATEGY) {
      override def getWrappedAnalyzer(fieldName: String): Analyzer =
        fieldName match {
          case "isbn" => new KeywordAnalyzer
          case "title" => createAnalyzer(version)
          case "tags" => new KeywordAnalyzer
          case "authors" => createAnalyzer(version)
        }
    }

まあ、ドキュメント登録時の定義を反映した感じですね。アナライズ対象のフィールドは、インデキシングに使用したAnalyzerと同じものを使用しています。

というか、このフィールドの扱いの差を忘れていて、普通にインデキシングの時と同じAnalyzerを使って、しばらくハマっていました…。

Queryの作成と

  private def createQuery(queryString: String, version: Version, analyzer: Analyzer): Query =
    new QueryParser(version, "title", analyzer).parse(queryString)

Queryを実行して結果を表示するメソッド。

  private def executeQuery(query: Query, sort: Sort, directory: Directory): Unit =
    for (reader <- DirectoryReader.open(directory)) {
      println(s"========== Start ExecuteQuery[$query] ==========")

      val searcher = new IndexSearcher(reader)
      val limit = 1000
      
      val collector =
        TopFieldCollector
          .create(sort,
                  limit,
                  true,
                  false,
                  false,
                  false)

      searcher.search(query, collector)

      val topDocs = collector.topDocs
      val hits = topDocs.scoreDocs

      hits.foreach { h =>
        val hitDoc = searcher.doc(h.doc)

        println(s"Doc, id[${h.doc}]:" + System.lineSeparator +
                hitDoc
                  .getFields
                  .asScala
                  .map(f => s"${f.name}:${f.stringValue}")
                  .mkString("  ", System.lineSeparator + "  ", ""))
      }

      println(s"========== End ExecuteQuery[$query] ==========")
      println()
    }

mainメソッドで、これらのメソッドを呼び出せば完成です。

実行してみる

まずは、普通のQueryを並べてみましょう。

    for (directory <- new RAMDirectory) {
      registerDocuments(directory, version, analyzer)

      executeQuery(createQuery("*:*", version, queryAnalyzer),
                   Sort.RELEVANCE,
                   directory)
      executeQuery(createQuery("title:Lucene title:オープンソース", version, queryAnalyzer),
                   new Sort(new SortField("title", SortField.Type.STRING, false)),
                   directory)
    }

それぞれの実行結果は、こうなります。ひとつ目。

========== Start ExecuteQuery[*:*] ==========
Doc, id[0]:
  isbn:978-4774127804
  title:Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築
  tags:Java
  tags:Lucene
  tags:全文検索
  tags:オープンソース
  authors:関口 宏司
Doc, id[1]:
  isbn:978-4774161631
  title:[改訂新版] Apache Solr入門 オープンソース全文検索エンジン
  tags:Java
  tags:Lucene
  tags:Solr
  tags:全文検索
  tags:オープンソース
  authors:大谷 純
  authors:阿部 慎一朗
  authors:大須賀 稔
  authors:北野 太郎
  authors:鈴木 教嗣
  authors:平賀 一昭
  authors:株式会社リクルートテクノロジーズ
  authors:株式会社ロンウイット
Doc, id[2]:
  isbn:978-4048662024
  title:高速スケーラブル検索エンジン ElasticSearch Server
  tags:Java
  tags:Elasticsearch
  tags:全文検索
  tags:オープンソース
  authors:Rafal Kuc
  authors:Marek Rogozinski
  authors:株式会社リクルートテクノロジーズ
  authors:大岩 達也
  authors:大谷 純
  authors:兼山 元太
  authors:水戸 祐介
  authors:守谷 純之介
========== End ExecuteQuery[*:*] ==========

2つ目。

========== Start ExecuteQuery[title:lucene (title:オープン title:ソース)] ==========
Doc, id[1]:
  isbn:978-4774161631
  title:[改訂新版] Apache Solr入門 オープンソース全文検索エンジン
  tags:Java
  tags:Lucene
  tags:Solr
  tags:全文検索
  tags:オープンソース
  authors:大谷 純
  authors:阿部 慎一朗
  authors:大須賀 稔
  authors:北野 太郎
  authors:鈴木 教嗣
  authors:平賀 一昭
  authors:株式会社リクルートテクノロジーズ
  authors:株式会社ロンウイット
Doc, id[0]:
  isbn:978-4774127804
  title:Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築
  tags:Java
  tags:Lucene
  tags:全文検索
  tags:オープンソース
  authors:関口 宏司
========== End ExecuteQuery[title:lucene (title:オープン title:ソース)] ==========

まあ、普通です。

では、続けてmulti-valuedなフィールドに対してクエリを投げてみましょう。

      executeQuery(createQuery("tags:Lucene", version, queryAnalyzer),
                   Sort.RELEVANCE,
                   directory)

結果。

========== Start ExecuteQuery[tags:Lucene] ==========
Doc, id[0]:
  isbn:978-4774127804
  title:Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築
  tags:Java
  tags:Lucene
  tags:全文検索
  tags:オープンソース
  authors:関口 宏司
Doc, id[1]:
  isbn:978-4774161631
  title:[改訂新版] Apache Solr入門 オープンソース全文検索エンジン
  tags:Java
  tags:Lucene
  tags:Solr
  tags:全文検索
  tags:オープンソース
  authors:大谷 純
  authors:阿部 慎一朗
  authors:大須賀 稔
  authors:北野 太郎
  authors:鈴木 教嗣
  authors:平賀 一昭
  authors:株式会社リクルートテクノロジーズ
  authors:株式会社ロンウイット
========== End ExecuteQuery[tags:Lucene] ==========

…すっごい普通に動きました。あんまり大した話題じゃないのかなー。

ソートとかかけると失敗するのかな?とも思いましたが

      executeQuery(createQuery("tags:Lucene", version, queryAnalyzer),
                   new Sort(new SortField("tags", SortField.Type.STRING, true)),
                   directory)

そんなこともなく。

========== Start ExecuteQuery[tags:Lucene] ==========
Doc, id[1]:
  isbn:978-4774161631
  title:[改訂新版] Apache Solr入門 オープンソース全文検索エンジン
  tags:Java
  tags:Lucene
  tags:Solr
  tags:全文検索
  tags:オープンソース
  authors:大谷 純
  authors:阿部 慎一朗
  authors:大須賀 稔
  authors:北野 太郎
  authors:鈴木 教嗣
  authors:平賀 一昭
  authors:株式会社リクルートテクノロジーズ
  authors:株式会社ロンウイット
Doc, id[0]:
  isbn:978-4774127804
  title:Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築
  tags:Java
  tags:Lucene
  tags:全文検索
  tags:オープンソース
  authors:関口 宏司
========== End ExecuteQuery[tags:Lucene] ==========

ただ、実際にやることはない気がしますけどね。結果もよくわかりませんし…。

その他、3つほどクエリを実行してみましょう。

tagsに対して。

      executeQuery(createQuery("tags:Lucene +tags:Elasticsearch", version, queryAnalyzer),
                   Sort.RELEVANCE,
                   directory)

「Elasticsearch」を必須にしてますね。結果。

========== Start ExecuteQuery[tags:Lucene +tags:Elasticsearch] ==========
Doc, id[2]:
  isbn:978-4048662024
  title:高速スケーラブル検索エンジン ElasticSearch Server
  tags:Java
  tags:Elasticsearch
  tags:全文検索
  tags:オープンソース
  authors:Rafal Kuc
  authors:Marek Rogozinski
  authors:株式会社リクルートテクノロジーズ
  authors:大岩 達也
  authors:大谷 純
  authors:兼山 元太
  authors:水戸 祐介
  authors:守谷 純之介
========== End ExecuteQuery[tags:Lucene +tags:Elasticsearch] ==========

動きましたー。

authorsフィールドに対して、2つのクエリ。2つ目は、ソート付きです。

      executeQuery(createQuery("authors:株式会社 authors:関口", version, queryAnalyzer),
                   Sort.RELEVANCE,
                   directory)

      executeQuery(createQuery("authors:株式会社 authors:ロンウイット", version, queryAnalyzer),
                   new Sort(new SortField("authors", SortField.Type.STRING, true)),
                   directory)

ひとつ目のクエリの結果。

========== Start ExecuteQuery[((authors:株式 authors:株式会社) authors:会社) authors:関口] ==========
Doc, id[0]:
  isbn:978-4774127804
  title:Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築
  tags:Java
  tags:Lucene
  tags:全文検索
  tags:オープンソース
  authors:関口 宏司
Doc, id[1]:
  isbn:978-4774161631
  title:[改訂新版] Apache Solr入門 オープンソース全文検索エンジン
  tags:Java
  tags:Lucene
  tags:Solr
  tags:全文検索
  tags:オープンソース
  authors:大谷 純
  authors:阿部 慎一朗
  authors:大須賀 稔
  authors:北野 太郎
  authors:鈴木 教嗣
  authors:平賀 一昭
  authors:株式会社リクルートテクノロジーズ
  authors:株式会社ロンウイット
Doc, id[2]:
  isbn:978-4048662024
  title:高速スケーラブル検索エンジン ElasticSearch Server
  tags:Java
  tags:Elasticsearch
  tags:全文検索
  tags:オープンソース
  authors:Rafal Kuc
  authors:Marek Rogozinski
  authors:株式会社リクルートテクノロジーズ
  authors:大岩 達也
  authors:大谷 純
  authors:兼山 元太
  authors:水戸 祐介
  authors:守谷 純之介
========== End ExecuteQuery[((authors:株式 authors:株式会社) authors:会社) authors:関口] ==========

2つ目のクエリの結果。

========== Start ExecuteQuery[((authors:株式 authors:株式会社) authors:会社) authors:ロンウイット] ==========
Doc, id[2]:
  isbn:978-4048662024
  title:高速スケーラブル検索エンジン ElasticSearch Server
  tags:Java
  tags:Elasticsearch
  tags:全文検索
  tags:オープンソース
  authors:Rafal Kuc
  authors:Marek Rogozinski
  authors:株式会社リクルートテクノロジーズ
  authors:大岩 達也
  authors:大谷 純
  authors:兼山 元太
  authors:水戸 祐介
  authors:守谷 純之介
Doc, id[1]:
  isbn:978-4774161631
  title:[改訂新版] Apache Solr入門 オープンソース全文検索エンジン
  tags:Java
  tags:Lucene
  tags:Solr
  tags:全文検索
  tags:オープンソース
  authors:大谷 純
  authors:阿部 慎一朗
  authors:大須賀 稔
  authors:北野 太郎
  authors:鈴木 教嗣
  authors:平賀 一昭
  authors:株式会社リクルートテクノロジーズ
  authors:株式会社ロンウイット
========== End ExecuteQuery[((authors:株式 authors:株式会社) authors:会社) authors:ロンウイット] ==========

…やっぱり、ソートの結果はよくわかりませんが。

とりあえず、普通に使えることはわかりました。そんなに気にすることでもなかったのかも?

今回作成したコードは、こちらにアップしています。

https://github.com/kazuhira-r/lucene-examples/tree/master/lucene-multi-field