CLOVER🍀

That was when it all began.

Luceneで゜ヌト

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の倀をメモリに持ち続けるようです。ナニヌクな単語が倧量に登堎するようだず、キャッシュがずおも倧きくなるかも ず。

キャッシュのサむズは、どのくらい倚くのフィヌルドが定矩されるかには䟝存したせん。実際に゜ヌトに䜿甚されるフィヌルドに察しおのみ利甚される、ずいうみたいですね。