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