CLOVER🍀

That was when it all began.

LuceneのFilterを使う

ソートに続いて、今度はFilterで遊んでみました。

Filterってなんだ?という話ですが、IndexSearcher#searchメソッドの引数にQueryと一緒に渡して、検索結果を絞り込んだり、結果をキャッシュして高速化させたりするというのが主な使い方みたいです。

Queryと検索結果を絞り込むという点で機能が被っているものもありますが、スコア結果に反映されなかったり、キャッシュされたりと、役割は異なるようです。

で、Filterは抽象クラスであり、いくつか実装があるようなのでそちらを使うことになります。
http://lucene.apache.org/core/4_3_1/core/org/apache/lucene/search/Filter.html

Filterの使い方は、各種実装クラスのJavadocの他にこちらのページを参考にしています。

Filtering a Lucene search
http://www.javaranch.com/journal/2009/02/filtering-a-lucene-search.html

これ、もしかして「Lucene in Action」の一部ですか?ちょっと情報が古くてRangeFilterとかもう存在していないんですが、それでもけっこう参考になりました。こういうの見ると、第2版を洋書とはいえ読んでみたくなりますね。

では、使ってみましょう。

build.sbt

name := "lucene-filter"

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"
)

相変わらず、AnalyzerにはKuromojiを使います。

そして、作ったプログラム。
src/main/scala/LuceneFilter.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, Term}
import org.apache.lucene.queries._
import org.apache.lucene.queryparser.classic.QueryParser
import org.apache.lucene.search._
import org.apache.lucene.store.{Directory, RAMDirectory}
import org.apache.lucene.util.{BytesRef, Version}

object LuceneFilter {
  def main(args: Array[String]): Unit = {
    val luceneVersion = Version.LUCENE_43
    val directory = new RAMDirectory
    try {
      createIndex(directory, luceneVersion)

      val allQuery = new MatchAllDocsQuery

      val limit = 20
      openWithSearch(directory) { searcher =>
        filters.foreach { filter =>
          val name = filter.getClass.getSimpleName match {
            case "" => filter.getClass.getSuperclass.getSimpleName
            case n => n
          }

          println(s"=========== ${name} を適用、 Filter => $filter ===========")
          printSearchResult(searcher,
                            searcher.search(allQuery, filter, limit),
                            limit)
          println()
        }
      }
    } finally {
      directory.close()
    }
  }

  private val filters: List[Filter] =
    List(
    /** ここに使うFilterのインスタンスを作成 **/
    )

  private def createIndex(directory: Directory, luceneVersion: Version): Unit = {
    val config =
      new IndexWriterConfig(luceneVersion, new JapaneseAnalyzer(luceneVersion))

    val writer = new IndexWriter(directory, config)
    try {
      documents.foreach(writer.addDocument)
    } finally {
      writer.close()
    }
  }

  private val documents: List[Document] =
    List(
      bookDoc("978-4894714991", "Effective Java 第2版", 3780, "2008/11/27"),
      bookDoc("978-4844330844", "Scalaスケーラブルプログラミング第2版", 4830, "2011/9/27"),
      bookDoc("978-4774147277", "プログラミングGROOVY", 3360, "2011/07/06"),
      bookDoc("978-4274069130", "プログラミングClojure 第2版", 3570, "2013/04/26")
    )

  private def bookDoc(isbn13: String, title: String, price: Int, publishDate: String): Document = {
    val doc = new Document
    doc.add(new StringField("isbn13", isbn13, Field.Store.YES))
    doc.add(new TextField("title", title, Field.Store.YES))
    doc.add(new StringField("price", price.toString, Field.Store.YES))
    doc.add(new StringField("publishDate", publishDate, Field.Store.YES))
    doc
  }

  private def openWithSearch[A](directory: Directory)(f: IndexSearcher => A): A = {
    val reader = DirectoryReader.open(directory)
    try {
      val searcher = new IndexSearcher(reader)
      f(searcher)
    } finally {
      reader.close()
    }
  }

  private def printSearchResult(searcher: IndexSearcher, docs: TopDocs, limit: Int): Unit =
    docs.scoreDocs.take(limit).foreach { h =>
      val hitDoc = searcher.doc(h.doc)
      hitDoc
        .getFields
        .asScala
        .map(_.stringValue)
        .mkString(s"  Score,DocNo[${h.score},${h.doc}] ", " | ", System.lineSeparator)
        .foreach(print)
    }
}

で、上記プログラムの

  private val filters: List[Filter] =
    List(
    /** ここに使うFilterのインスタンスを作成 **/
    )

の部分を埋めていきます。

QueryとFilterの適用には、今回はIndexSearcher#search(Query, Filter, int)を使用しています。

searcher.search(allQuery, filter, limit)

ちなみに、インデックスに入っているデータはこんな感じです。

  Score,DocNo[1.0,0] 978-4894714991 | Effective Java 第2版 | 3780 | 2008/11/27
  Score,DocNo[1.0,1] 978-4844330844 | Scalaスケーラブルプログラミング第2版 | 4830 | 2011/9/27
  Score,DocNo[1.0,2] 978-4774147277 | プログラミングGROOVY | 3360 | 2011/07/06
  Score,DocNo[1.0,3] 978-4274069130 | プログラミングClojure 第2版 | 3570 | 2013/04/26

今回は、各クラスについてそんなに詳しく調べたわけではないので、さらさらと。

FieldCacheRangeFilter
http://lucene.apache.org/core/4_3_1/core/org/apache/lucene/search/FieldCacheRangeFilter.html

指定した範囲の中に、クエリの結果を絞り込むFilterです。下限、上限を含むかどうかの指定ができます。また、同じフィールドに対してはキャッシュの効果を持ちます。

作成(List.applyの引数になっています)。

      // 旧RangeFilter?
      // 範囲で絞り込み
      FieldCacheRangeFilter.newStringRange("price", "3360", "3780", true, false),

適用結果。

=========== FieldCacheRangeFilter を適用、 Filter => price:[3360 TO 3780} ===========
  Score,DocNo[1.0,2] 978-4774147277 | プログラミングGROOVY | 3360 | 2011/07/06
  Score,DocNo[1.0,3] 978-4274069130 | プログラミングClojure 第2版 | 3570 | 2013/04/26

FieldCacheTermsFilter
http://lucene.apache.org/core/4_3_1/core/org/apache/lucene/search/FieldCacheTermsFilter.html

特定のフィールドに対して、Stringで単語を複数指定して、それをもって結果を絞り込みます。こちらもキャッシュの効果があります。

      // Termを使うFilter?
      // 挙動がよくわかりません…Tokenizeしたものと、相性悪い??
      new FieldCacheTermsFilter("price", "3780", "4830"),

使うとまあ、妙な挙動をしてトークン化するフィールドとはなんか相性悪そうなイメージを受けました。普通にTermQueryでヒットするようなものも、このFilterではスルーされましたし。

=========== FieldCacheTermsFilter を適用、 Filter => org.apache.lucene.search.FieldCacheTermsFilter@95c81d8 ===========
  Score,DocNo[1.0,0] 978-4894714991 | Effective Java 第2版 | 3780 | 2008/11/27
  Score,DocNo[1.0,1] 978-4844330844 | Scalaスケーラブルプログラミング第2版 | 4830 | 2011/9/27

FieldValueFilter
http://lucene.apache.org/core/4_3_1/core/org/apache/lucene/search/FieldValueFilter.html

これはもう単純に、ヒットしたドキュメントのIDをキャッシュするFilterみたいです。

      // クエリでヒットしたドキュメントのIDを、キャッシュするFilter
      new FieldValueFilter("price"),  // nagate を false にすると、結果が反転する

第2引数にnagateというbooleanな引数を取れて、これをfalseにすると結果が反転します。今回のように、何もしないと全件ヒットするようなMatchAllDocsQueryに対してnagateをfalseにすると、ヒット件数が0になります。

というわけで、結果は全件取得です。

=========== FieldValueFilter を適用、 Filter => FieldValueFilter [field=price, negate=false] ===========
  Score,DocNo[1.0,0] 978-4894714991 | Effective Java 第2版 | 3780 | 2008/11/27
  Score,DocNo[1.0,1] 978-4844330844 | Scalaスケーラブルプログラミング第2版 | 4830 | 2011/9/27
  Score,DocNo[1.0,2] 978-4774147277 | プログラミングGROOVY | 3360 | 2011/07/06
  Score,DocNo[1.0,3] 978-4274069130 | プログラミングClojure 第2版 | 3570 | 2013/04/26

PrefixFilter
http://lucene.apache.org/core/4_3_1/core/org/apache/lucene/search/PrefixFilter.html

使い方は、PrefixQuery的な。

      // Prefix検索と同様
      new PrefixFilter(new Term("title", "groovy")),
=========== PrefixFilter を適用、 Filter => PrefixFilter(title:groovy) ===========
  Score,DocNo[1.0,2] 978-4774147277 | プログラミングGROOVY | 3360 | 2011/07/06

QueryWrapperFilter
http://lucene.apache.org/core/4_3_1/core/org/apache/lucene/search/QueryWrapperFilter.html

Queryで絞り込んだ結果に対して、さらにQueryを適用するFilterです。

      // 別のQueryをラップするFilter
      new QueryWrapperFilter({
        val b = new BooleanQuery
        b.add(new TermQuery(new Term("title", new BytesRef("プログラミング"))), BooleanClause.Occur.MUST)
        b.add(new TermQuery(new Term("title", "groovy")), BooleanClause.Occur.MUST)
        b
      }),
=========== QueryWrapperFilter を適用、 Filter => QueryWrapperFilter(+title:プログラミング +title:groovy) ===========
  Score,DocNo[1.0,2] 978-4774147277 | プログラミングGROOVY | 3360 | 2011/07/06

BooleanFilter
http://lucene.apache.org/core/4_3_1/queries/org/apache/lucene/queries/BooleanFilter.html

他のFilterを、BooleanQueryのようにMUST(AND)やSHUOLD(OR)などで連結するFilterです。

      // 他のFilterをBooleanQueryのようにラップするFilter
      {
        val bf = new BooleanFilter
        bf.add(new TermsFilter(new Term("title", "スケーラブルプログラミング")), BooleanClause.Occur.MUST)
        bf.add(new TermsFilter(new Term("title", "scala")), BooleanClause.Occur.MUST)
        bf
      },
=========== BooleanFilter を適用、 Filter => BooleanFilter(+title:スケーラブルプログラミング +title:scala) ===========
  Score,DocNo[1.0,1] 978-4844330844 | Scalaスケーラブルプログラミング第2版 | 4830 | 2011/9/27

CachingWrapperFilter
http://lucene.apache.org/core/4_3_1/core/org/apache/lucene/search/CachingWrapperFilter.html

他のFilterの結果を、キャッシュするFilterです。

      // 引数のFilterの結果をキャッシュするFilter
      new CachingWrapperFilter({
        val bf = new BooleanFilter
        bf.add(new TermsFilter(new Term("title", "スケーラブルプログラミング")), BooleanClause.Occur.MUST)
        bf.add(new TermsFilter(new Term("title", "scala")), BooleanClause.Occur.MUST)
        bf
      })
=========== CachingWrapperFilter を適用、 Filter => CachingWrapperFilter(BooleanFilter(+title:スケーラブルプログラミング +title:scala)) ===========
  Score,DocNo[1.0,1] 978-4844330844 | Scalaスケーラブルプログラミング第2版 | 4830 | 2011/9/27

他にもまだFilterの実装はあるみたいですし、そんなに説明はしていませんが、とりあえずこんなところで。