Luceneの基礎を、少しずつ。今回は、ソートにチャレンジしてみたいと思います。
ソートを行うには、SortクラスとSortFieldクラスを使用し、これらをIndexSearcher#searchメソッドに叩き込みます。
Sort
http://lucene.apache.org/core/4_3_1/core/org/apache/lucene/search/Sort.html
SoftField
http://lucene.apache.org/core/4_3_1/core/org/apache/lucene/search/Sort.html
使い方は、割と簡単なようですが。
準備
では、いつも通りbuild.sbtとソースコードを。
name := "lucene-sorting" version := "0.0.1-SNAPSHOT" scalaVersion := "2.10.2" organization := "littlewings" libraryDependencies ++= Seq( "org.apache.lucene" % "lucene-core" % "4.3.1", "org.apache.lucene" % "lucene-analyzers-kuromoji" % "4.3.1", "org.apache.lucene" % "lucene-queryparser" % "4.3.1" )
src/main/scala/LuceneSorting.scala
import scala.collection.JavaConverters._ 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, TopDocs} import org.apache.lucene.store.{Directory, RAMDirectory} import org.apache.lucene.util.Version import LuceneSorting.AutoCloseableWrapper object LuceneSorting { def main(args: Array[String]): Unit = { val luceneVersion = Version.LUCENE_43 for (directory <- new RAMDirectory) { println("ドキュメント追加") createIndex(directory, luceneVersion) println("検索開始") for (reader <- DirectoryReader.open(directory)) { val searcher = new IndexSearcher(reader) val query = createQuery("contents:ドキュメント OR contents:1 OR contents: 0", luceneVersion) println(s"Query => $query") printSearchResult(searcher, "Non Sort", searcher.search(query, null, 20)) printSearchResult(searcher, "Sort.INDEXORDER", searcher.search(query, 20, Sort.INDEXORDER)) printSearchResult(searcher, "Sort.RELEVANCE", searcher.search(query, 20, Sort.RELEVANCE)) printSearchResult(searcher, "Sort(num1 as STRING)", searcher.search(query, 20, new Sort(new SortField("num1", SortField.Type.STRING)))) printSearchResult(searcher, "Sort(num1 as INT)", searcher.search(query, 20, new Sort(new SortField("num1", SortField.Type.INT)))) try { printSearchResult(searcher, "Sort(join-nums as INT)", searcher.search(query, 20, new Sort(new SortField("join-nums", SortField.Type.INT)))) } catch { case e: NumberFormatException => println(e) } printSearchResult(searcher, "Sort(num1 as INT, num2 as INT)", searcher.search(query, 20, new Sort(new SortField("num1", SortField.Type.INT), new SortField("num2", SortField.Type.INT)))) printSearchResult(searcher, "Sort(num1 as INT, num2 as INT#reverse)", searcher.search(query, 20, new Sort(new SortField("num1", SortField.Type.INT), new SortField("num2", SortField.Type.INT, true)))) printSearchResult(searcher, "Sort(num2#reverse as INT, num1 as INT)", searcher.search(query, 20, new Sort(new SortField("num2", SortField.Type.INT, true), new SortField("num1", SortField.Type.INT)))) printSearchResult(searcher, "Sort(contents as STRING)", searcher.search(query, 20, new Sort(new SortField("contents", SortField.Type.STRING)))) } } } private def createIndex(directory: Directory, luceneVersion: Version): Unit = { val config = new IndexWriterConfig(luceneVersion, new JapaneseAnalyzer(luceneVersion)) new IndexWriter(directory, config).foreach { writer => SampleDocument.createDocs.foreach(writer.addDocument) } } private def createQuery(queryString: String, luceneVersion: Version): Query = new QueryParser(luceneVersion, "num1", new JapaneseAnalyzer(luceneVersion)) .parse(queryString) private def printSearchResult(searcher: IndexSearcher, name: String, docs: TopDocs): Unit = { println(s"$name => ヒット件数:${docs.totalHits}") val hits = docs.scoreDocs hits.take(20).foreach { h => val hitDoc = searcher.doc(h.doc) hitDoc .getFields .asScala .map(_.stringValue) .mkString(s" Score,DocNo[${h.score},${h.doc}] ", " | ", "") .foreach(print) println() } } implicit class AutoCloseableWrapper[A <: AutoCloseable](val underlying: A) extends AnyVal { def foreach(body: A => Unit): Unit = { try { body(underlying) } finally { if (underlying != null) { underlying.close() } } } } } object SampleDocument { def createDocs: List[Document] = { List( create("30", "30", "30-30", "30-30のドキュメントです"), create("1", "1", "1-1", "1-1のドキュメントです"), create("1", "90", "1-90", "1-90のドキュメントです"), create("2", "2", "2-2", "2-2のドキュメントです"), create("2", "50", "2-50", "2-50のドキュメントです"), create("20", "20", "20-20", "20-20のドキュメントです"), create("10", "10", "10-10", "10-10のドキュメントです") ) } private def create(tokens: String*): Document = { val doc = new Document doc.add(stringField("num1", tokens(0))) doc.add(stringField("num2", tokens(1))) doc.add(stringField("join-nums", tokens(2))) doc.add(textField("contents", tokens(3))) doc } private def stringField(name: String, value: String): Field = new StringField(name, value, Field.Store.YES) private def textField(name: String, value: String): Field = new TextField(name, value, Field.Store.YES) }
クエリもドキュメントも超適当です。
ドキュメントは、
List( create("30", "30", "30-30", "30-30のドキュメントです"), create("1", "1", "1-1", "1-1のドキュメントです"), create("1", "90", "1-90", "1-90のドキュメントです"), create("2", "2", "2-2", "2-2のドキュメントです"), create("2", "50", "2-50", "2-50のドキュメントです"), create("20", "20", "20-20", "20-20のドキュメントです"), create("10", "10", "10-10", "10-10のドキュメントです") )
というように、ちょっとわざとらしい感じ。クエリは
val query = createQuery("contents:ドキュメント OR contents:1 OR contents: 0", luceneVersion)
というように、contentsフィールド内の「ドキュメント」「1」「0」があれば、スコアとして上位になるようにしています。
これに対して、Sortを指定する、しないだったり、Sortの条件を変えて動きを見ていきます。
なお、今回はIndexSearcherのsearchメソッドのうち、
public TopFieldDocs search(Query query, int n, Sort sort) throws IOException
を使用しています。Filter、使ってないし…。
IndexSearcher#search(Query, int, Sort)
http://lucene.apache.org/core/4_3_1/core/org/apache/lucene/search/IndexSearcher.html#search%28org.apache.lucene.search.Query,%20int,%20org.apache.lucene.search.Sort%29
結果出力は、こんなメソッドを用意しておりまして
private def printSearchResult(searcher: IndexSearcher, name: String, docs: TopDocs): Unit = { println(s"$name => ヒット件数:${docs.totalHits}") val hits = docs.scoreDocs hits.take(20).foreach { h => val hitDoc = searcher.doc(h.doc) hitDoc .getFields .asScala .map(_.stringValue) .mkString(s" Score,DocNo[${h.score},${h.doc}] ", " | ", "") .foreach(print) println() } }
呼び出し元で、こんな感じで呼び出します。
printSearchResult(searcher, "Sort.INDEXORDER", searcher.search(query, 20, Sort.INDEXORDER))
はい。
動作確認
では、まずはソートをしない場合。
printSearchResult(searcher, "Non Sort", searcher.search(query, null, 20))
結果。
Non Sort => ヒット件数:7 Score,DocNo[0.5187427,1] 1 | 1 | 1-1 | 1-1のドキュメントです Score,DocNo[0.38726074,2] 1 | 90 | 1-90 | 1-90のドキュメントです Score,DocNo[0.034917552,0] 30 | 30 | 30-30 | 30-30のドキュメントです Score,DocNo[0.034917552,3] 2 | 2 | 2-2 | 2-2のドキュメントです Score,DocNo[0.034917552,4] 2 | 50 | 2-50 | 2-50のドキュメントです Score,DocNo[0.034917552,5] 20 | 20 | 20-20 | 20-20のドキュメントです Score,DocNo[0.034917552,6] 10 | 10 | 10-10 | 10-10のドキュメントです
スコアとドキュメントのNoが出力されています。並び順は、スコア順ですね。
それでは、ここからSortクラスを使用します。
まずは、インデックスNoの順にソート。public static finalなフィールド、Sort.INDEXORDERをsearchメソッドに指定します。
printSearchResult(searcher, "Sort.INDEXORDER", searcher.search(query, 20, Sort.INDEXORDER))
結果。
Sort.INDEXORDER => ヒット件数:7 Score,DocNo[NaN,0] 30 | 30 | 30-30 | 30-30のドキュメントです Score,DocNo[NaN,1] 1 | 1 | 1-1 | 1-1のドキュメントです Score,DocNo[NaN,2] 1 | 90 | 1-90 | 1-90のドキュメントです Score,DocNo[NaN,3] 2 | 2 | 2-2 | 2-2のドキュメントです Score,DocNo[NaN,4] 2 | 50 | 2-50 | 2-50のドキュメントです Score,DocNo[NaN,5] 20 | 20 | 20-20 | 20-20のドキュメントです Score,DocNo[NaN,6] 10 | 10 | 10-10 | 10-10のドキュメントです
スコアの欄が「NaN」になり、スコア計算が行われなくなるみたいです。順番は、ドキュメントの番号順になっていますね。
続いて、スコア計算順?Sort.RELEVANCEをsearchメソッドに指定します。
printSearchResult(searcher, "Sort.RELEVANCE", searcher.search(query, 20, Sort.RELEVANCE))
結果。
Sort.RELEVANCE => ヒット件数:7 Score,DocNo[NaN,1] 1 | 1 | 1-1 | 1-1のドキュメントです Score,DocNo[NaN,2] 1 | 90 | 1-90 | 1-90のドキュメントです Score,DocNo[NaN,0] 30 | 30 | 30-30 | 30-30のドキュメントです Score,DocNo[NaN,3] 2 | 2 | 2-2 | 2-2のドキュメントです Score,DocNo[NaN,4] 2 | 50 | 2-50 | 2-50のドキュメントです Score,DocNo[NaN,5] 20 | 20 | 20-20 | 20-20のドキュメントです Score,DocNo[NaN,6] 10 | 10 | 10-10 | 10-10のドキュメントです
スコアこそ「NaN」ですが、並び順はソートしない場合と同じみたいですね。
では、ここから具体的にソートするフィールドとソート方法を指定していきましょう。
Sortを具体的に指示する場合は、Sortクラスのインタンス作成時に、どのフィールドでソートするかをSortFieldのインスタンスを作成して指示します。
また、ソート対象のフィールドは、
- インデックス化の対称とする必要がある
- トークン化するべきではない
- 検索結果としてデータが不要であれば、Storeする必要はない
つまり、こういうことだそうな。
document.add (new Field ("byNumber", Integer.toString(x), Field.Store.NO, Field.Index.NOT_ANALYZED));
参考)
http://lucene.apache.org/core/4_3_1/core/org/apache/lucene/search/Sort.html
また、データをどのように扱ってソートするかを、Sort.Type列挙型で指定します。Sort.Typeには
- BYTE
- BYTES
- CUSTOM
- DOC
- DOUBLE
- FLOAT
- INT
- LONG
- REWRITEABLE
- SCORE
- SHORT
- STRING
- STRING_VAL
というソート方法があります。先の例で使用したのは、DOCとSCOREですね。
それでは、順次使っていきましょう。
num1フィールドを、「STRING」としてソートします。
printSearchResult(searcher, "Sort(num1 as STRING)", searcher.search(query, 20, new Sort(new SortField("num1", SortField.Type.STRING))))
結果。
Sort(num1 as STRING) => ヒット件数:7 Score,DocNo[NaN,1] 1 | 1 | 1-1 | 1-1のドキュメントです Score,DocNo[NaN,2] 1 | 90 | 1-90 | 1-90のドキュメントです Score,DocNo[NaN,6] 10 | 10 | 10-10 | 10-10のドキュメントです Score,DocNo[NaN,3] 2 | 2 | 2-2 | 2-2のドキュメントです Score,DocNo[NaN,4] 2 | 50 | 2-50 | 2-50のドキュメントです Score,DocNo[NaN,5] 20 | 20 | 20-20 | 20-20のドキュメントです Score,DocNo[NaN,0] 30 | 30 | 30-30 | 30-30のドキュメントです
続いて、num1フィールドを「INT」としてソートします。
printSearchResult(searcher, "Sort(num1 as INT)", searcher.search(query, 20, new Sort(new SortField("num1", SortField.Type.INT))))
結果。
Sort(num1 as INT) => ヒット件数:7 Score,DocNo[NaN,1] 1 | 1 | 1-1 | 1-1のドキュメントです Score,DocNo[NaN,2] 1 | 90 | 1-90 | 1-90のドキュメントです Score,DocNo[NaN,3] 2 | 2 | 2-2 | 2-2のドキュメントです Score,DocNo[NaN,4] 2 | 50 | 2-50 | 2-50のドキュメントです Score,DocNo[NaN,6] 10 | 10 | 10-10 | 10-10のドキュメントです Score,DocNo[NaN,5] 20 | 20 | 20-20 | 20-20のドキュメントです Score,DocNo[NaN,0] 30 | 30 | 30-30 | 30-30のドキュメントです
Sort.Typeの指定で、ソート結果が変わったことがわかります。
また、「INT」や「FLOAT」などの型を指定した場合に、実際にその形に変換できないようなデータを渡すと、例外がスローされます。
try { printSearchResult(searcher, "Sort(join-nums as INT)", searcher.search(query, 20, new Sort(new SortField("join-nums", SortField.Type.INT)))) } catch { case e: NumberFormatException => println(e) }
結果。
java.lang.NumberFormatException: Invalid shift value in prefixCoded bytes (is encoded value really an INT?)
見事に、コケます。
複数フィールドでソートする場合は、SortクラスのコンストラクタにSortFieldを複数指定します。
printSearchResult(searcher, "Sort(num1 as INT, num2 as INT)", searcher.search(query, 20, new Sort(new SortField("num1", SortField.Type.INT), new SortField("num2", SortField.Type.INT))))
結果。
Sort(num1 as INT, num2 as INT) => ヒット件数:7 Score,DocNo[NaN,1] 1 | 1 | 1-1 | 1-1のドキュメントです Score,DocNo[NaN,2] 1 | 90 | 1-90 | 1-90のドキュメントです Score,DocNo[NaN,3] 2 | 2 | 2-2 | 2-2のドキュメントです Score,DocNo[NaN,4] 2 | 50 | 2-50 | 2-50のドキュメントです Score,DocNo[NaN,6] 10 | 10 | 10-10 | 10-10のドキュメントです Score,DocNo[NaN,5] 20 | 20 | 20-20 | 20-20のドキュメントです Score,DocNo[NaN,0] 30 | 30 | 30-30 | 30-30のドキュメントです
逆順でソートするには、SortFieldのコンストラクタの第3引数をtrueにしましょう。
printSearchResult(searcher, "Sort(num1 as INT, num2 as INT#reverse)", searcher.search(query, 20, new Sort(new SortField("num1", SortField.Type.INT), new SortField("num2", SortField.Type.INT, true))))
結果。
Sort(num1 as INT, num2 as INT#reverse) => ヒット件数:7 Score,DocNo[NaN,2] 1 | 90 | 1-90 | 1-90のドキュメントです Score,DocNo[NaN,1] 1 | 1 | 1-1 | 1-1のドキュメントです Score,DocNo[NaN,4] 2 | 50 | 2-50 | 2-50のドキュメントです Score,DocNo[NaN,3] 2 | 2 | 2-2 | 2-2のドキュメントです Score,DocNo[NaN,6] 10 | 10 | 10-10 | 10-10のドキュメントです Score,DocNo[NaN,5] 20 | 20 | 20-20 | 20-20のドキュメントです Score,DocNo[NaN,0] 30 | 30 | 30-30 | 30-30のドキュメントです
別に、フィールドの定義順に並んでいる必要もないみたいです。
printSearchResult(searcher, "Sort(num2#reverse as INT, num1 as INT)", searcher.search(query, 20, new Sort(new SortField("num2", SortField.Type.INT, true), new SortField("num1", SortField.Type.INT))))
結果。
Sort(num2#reverse as INT, num1 as INT) => ヒット件数:7 Score,DocNo[NaN,2] 1 | 90 | 1-90 | 1-90のドキュメントです Score,DocNo[NaN,4] 2 | 50 | 2-50 | 2-50のドキュメントです Score,DocNo[NaN,0] 30 | 30 | 30-30 | 30-30のドキュメントです Score,DocNo[NaN,5] 20 | 20 | 20-20 | 20-20のドキュメントです Score,DocNo[NaN,6] 10 | 10 | 10-10 | 10-10のドキュメントです Score,DocNo[NaN,3] 2 | 2 | 2-2 | 2-2のドキュメントです Score,DocNo[NaN,1] 1 | 1 | 1-1 | 1-1のドキュメントです
ちなみに、トークン化されているフィールドを、ムリにソートしようとしてみましたが
printSearchResult(searcher, "Sort(contents as STRING)", searcher.search(query, 20, new Sort(new SortField("contents", SortField.Type.STRING))))
変化が感じられないというか、インデックスのNo順に並んでいる気がします。
Sort(contents as STRING) => ヒット件数:7 Score,DocNo[NaN,0] 30 | 30 | 30-30 | 30-30のドキュメントです Score,DocNo[NaN,1] 1 | 1 | 1-1 | 1-1のドキュメントです Score,DocNo[NaN,2] 1 | 90 | 1-90 | 1-90のドキュメントです Score,DocNo[NaN,3] 2 | 2 | 2-2 | 2-2のドキュメントです Score,DocNo[NaN,4] 2 | 50 | 2-50 | 2-50のドキュメントです Score,DocNo[NaN,5] 20 | 20 | 20-20 | 20-20のドキュメントです Score,DocNo[NaN,6] 10 | 10 | 10-10 | 10-10のドキュメントです
ちなみに、Sortクラスのインスタンスはソート順が変わらなければ、再利用可、スレッドセーフだそうで。
Object Reuse
One of these objects can be used multiple times and the sort order changed between usages.
This class is thread safe.
また、ソートは単語を内部的にキャッシュするようです。キャッシュの大きさは、IndexReader.maxDoc()で返される長さに依存する、IntegerまたはFloatの配列を作成するようですが…。BYTES指定の場合は、下記の計算式になるみたいです。
4 * IndexReader.maxDoc() * (# of different fields actually used to sort)
4は、フィールドの名前…?
Stringを使用した場合は、上記に加えてフィールド中の各Termの値をメモリに持ち続けるようです。ユニークな単語が大量に登場するようだと、キャッシュがとても大きくなるかも…と。
キャッシュのサイズは、どのくらい多くのフィールドが定義されるかには依存しません。実際にソートに使用されるフィールドに対してのみ利用される、というみたいですね。