CLOVER🍀

That was when it all began.

Luceneのファセットを使ってみる

今回は、Luceneのファセット機能を使って遊んでみました。ファセットって、あれですね。イメージ的には、Webサイトによくある、カテゴリ単位なんかで件数が一緒に表示されているやつです。

まずは、API Reference。

Lucene Facet
http://lucene.apache.org/core/4_4_0/facet/index.html

参考にしたサイトは、この辺です。

Apache Lucene Faceted Search User's Guide(オフィシャル)
http://lucene.apache.org/core/4_4_0/facet/org/apache/lucene/facet/doc-files/userguide.html

Faceted Search with Lucene 4
http://chimpler.wordpress.com/2013/01/30/faceted-search-with-lucene/

Lucene 4.1 how to index facets?
http://stackoverflow.com/questions/14774841/lucene-4-1-how-to-index-facets

なんですけどね…実は、どれも正しくありません。オフィシャルサイトでさえ。

変更理由は調べていませんが、とにかくAPIがちょこちょこ変わっているので、どの情報も古くなっています。さらに、現在のLucene 4.4.0のクラスのメソッドから、最新のSubversionのtrunkを見ると、また変わっています…。

安定してないのかなぁ?ちなみに、Luceneのファセット機能は、Solrの機能と直接は関係なさそうな?

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

準備

まずはプロジェクトの準備。
build.sbt

name := "lucene-facet-example"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.10.2"

organization := "littlewings"

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

依存関係に、「lucene-facet」を追加します。

続いて、ソースコードに入ります。importから。

import scala.collection.JavaConverters._

import org.apache.lucene.analysis.Analyzer
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.search.{IndexSearcher, MatchAllDocsQuery, MultiCollector, Query, TermQuery, TopScoreDocCollector}
import org.apache.lucene.store.{Directory, RAMDirectory}
import org.apache.lucene.util.Version

import org.apache.lucene.facet.index.FacetFields
import org.apache.lucene.facet.params.FacetSearchParams
import org.apache.lucene.facet.search.{CountFacetRequest, FacetRequest, FacetResultNode, FacetsAccumulator, FacetsCollector}
import org.apache.lucene.facet.taxonomy.{CategoryPath, TaxonomyWriter}
import org.apache.lucene.facet.taxonomy.directory.{DirectoryTaxonomyReader, DirectoryTaxonomyWriter}

まあ、Scalaで書いているので…。

Documentとファセットを登録する

最初のポイントは、Luceneでファセットを使うには、

  • 通常のインデックス用のDirectory
  • ファセット用のDirectory

の2つが必要になるようです。

というわけで、今回はRAMDirectoryを2つ作成します。

    val indexDirectory = new RAMDirectory
    val taxonomyDirectory = new RAMDirectory

IndexWriterもそれぞれ必要になります。

    val writer = new IndexWriter(indexDirectory,
                                 new IndexWriterConfig(luceneVersion, analyzer))
    val taxonomyWriter = new DirectoryTaxonomyWriter(taxonomyDirectory)
    val facetFields = new FacetFields(taxonomyWriter)

ファセットの方は、DirectoryTaxonomyWriterというクラスを使用します。また、そのインスタンスを使用してFacetFieldsクラスのインスタンスも作成しておきます。

ファセットとは直接関係ないですが、今回Documentに登録するデータはこういうものにしました。ScalaのCase Classで書いてますが…。

case class Book(title: String,
                authors: Array[String],
                year: String,
                month: String,
                price: Int,
                language: String)

そして、Documentを作成するわけですが、こういう形になります。

  private def createDocument(taxonomyWriter: TaxonomyWriter ,book: Book, facetFields: FacetFields): Document = {
    val document = new Document

    // Build Document
    document.add(new TextField("title", book.title, Field.Store.YES))
    document.add(new TextField("authors", book.authors.mkString(", "), Field.Store.YES))
    document.add(new StringField("year", book.year, Field.Store.YES))
    document.add(new StringField("month", book.month, Field.Store.YES))
    document.add(new StringField("price", book.price.toString, Field.Store.YES))
    document.add(new StringField("language", book.language, Field.Store.YES))

    // Build Category
    val categoryPaths = List(
      new CategoryPath("publishTime", book.year, book.month),
      new CategoryPath("price", book.price.toString),
      new CategoryPath("language", book.language)
    ) ::: book.authors.map(n => new CategoryPath("author", n)).toList
 
    facetFields.addFields(document, categoryPaths.asJava)

    document
  }

若干、いらんことしてますが…。

まずは、普通にDocumentを作成します。

    // Build Document
    document.add(new TextField("title", book.title, Field.Store.YES))
    document.add(new TextField("authors", book.authors.mkString(", "), Field.Store.YES))
    document.add(new StringField("year", book.year, Field.Store.YES))
    document.add(new StringField("month", book.month, Field.Store.YES))
    document.add(new StringField("price", book.price.toString, Field.Store.YES))
    document.add(new StringField("language", book.language, Field.Store.YES))

TextFieldやStringFieldの使い分けは、適当に流してください。

続いて、CategoryPathのコレクションを作成して、先に作っておいたDocumentと一緒にFacetFieldsに登録します。

    // Build Category
    val categoryPaths = List(
      new CategoryPath("publishTime", book.year, book.month),
      new CategoryPath("price", book.price.toString),
      new CategoryPath("language", book.language)
    ) ::: book.authors.map(n => new CategoryPath("author", n)).toList
 
    facetFields.addFields(document, categoryPaths.asJava)

ここで作成しているCategoryPathの作り方がポイントでStringの可変長引数か、charのデリミタを指定して単一のStringを渡すかのどちらかで、インスタンスを作成します。

このCategoryPathの作り方が、そのままファセットになります。検索時のファセットの結果は、ここで登録したCategoryPathが、指定した階層に対していくつ存在するかを集計してくるものになります。

例えば、日付なら

new CategoryPath("2013", "08")
new CategoryPath("2013", "01")
new CategoryPath("2012", "07")
new CategoryPath("2011", "01")

みたいな感じで表現します。が、ホントに上記だけだとカテゴリ分け的なものがつらいので、先の例では「publishTime」という分類的なものを最初に加えました。

検索対象を書籍として、適当にデータを登録しておきます。

  private def registryDocuments(indexDirectory: Directory,
                                taxonomyDirectory: Directory,
                                luceneVersion: Version,
                                analyzer: Analyzer): Unit = {
    val writer = new IndexWriter(indexDirectory,
                                 new IndexWriterConfig(luceneVersion, analyzer))
    val taxonomyWriter = new DirectoryTaxonomyWriter(taxonomyDirectory)
    val facetFields = new FacetFields(taxonomyWriter)

    writer.addDocument(createDocument(taxonomyWriter,
                                      Book("Effective Java 第2版",
                                           Array("Joshua Bloch", "柴田芳樹"),
                                           "2008",
                                           "11",
                                           3780,
                                           "java"),
                                      facetFields))
    writer.addDocument(createDocument(taxonomyWriter,
                                      Book("パーフェクトJava",
                                           Array("アリエル・ネットワーク株式会社", "井上 誠一郎", "永井 雅人", "松山 智大"),
                                           "2009",
                                           "09",
                                           3780,
                                           "java"),
                                      facetFields))
    writer.addDocument(createDocument(taxonomyWriter,
                                      Book("やさしいJava 第5版",
                                           Array("高橋 麻奈"),
                                           "2013",
                                           "08",
                                           2730,
                                           "java"),
                                      facetFields))
    writer.addDocument(createDocument(taxonomyWriter,
                                      Book("Java言語プログラミングレッスン 第3版(上)",
                                           Array("結城 浩"),
                                           "2013",
                                           "11",
                                           2520,
                                           "java"),
                                      facetFields))
    writer.addDocument(createDocument(taxonomyWriter,
                                      Book("Java言語プログラミングレッスン 第3版(下)",
                                           Array("結城 浩"),
                                           "2013",
                                           "11",
                                           2520,
                                           "java"),
                                      facetFields))
    writer.addDocument(createDocument(taxonomyWriter,
                                      Book("Scalaスケーラブルプログラミング第2版",
                                           Array("Martin Odersky", "Lex Spoon", "Bill Venners", "羽生田 栄一", "水島 宏太", "長尾 高弘"),
                                           "2011",
                                           "09",
                                           4830,
                                           "scala"),
                                      facetFields))
    writer.addDocument(createDocument(taxonomyWriter,
                                      Book("Scala逆引きレシピ (PROGRAMMER’S RECiPE)",
                                           Array("竹添 直樹", "島本 多可子"),
                                           "2012",
                                           "07",
                                           3360,
                                           "scala"),
                                      facetFields))
    writer.addDocument(createDocument(taxonomyWriter,
                                      Book("Scalaプログラミング入門",
                                           Array("デイビッド・ポラック", "羽生田栄一", "大塚庸史"),
                                           "2010",
                                           "03",
                                           3360,
                                           "scala"),
                                      facetFields))
    writer.addDocument(createDocument(taxonomyWriter,
                                      Book("プログラミングScala",
                                           Array("Dean Wampler", "Alex Payne", "株式会社オージス総研 オブジェクトの広場編集部"),
                                           "2011",
                                           "01",
                                           3990,
                                           "scala"),
                                      facetFields))


    writer.close()
    taxonomyWriter.close()

各IndexWriterのcloseはお忘れなく。

ファセットを取得する

それでは登録したデータを検索して、ファセットを取得してみましょう。

Document自体を登録したDirectoryと、ファセット用のDirectoryそれぞれに対して、DirectoryReaderを作成します。

    val reader = DirectoryReader.open(indexDirectory)
    val taxonomyReader = new DirectoryTaxonomyReader(taxonomyDirectory)

    val searcher = new IndexSearcher(reader)

IndexReaderも作成しますが、こちらに渡すDirectoryReaderはDocument自体を管理している方のDirectoryになります。また、ファセット用のDirectoryReaderはDirectoryTaxonomyReaderクラスをnewすることで作成します。

続いて、Collectorを作成します。…前に、Collectorを使っておいてよかったと思います。

      val topScoreDocCollector = TopScoreDocCollector.create(100, true)
      val facetSearchParams =
        new FacetSearchParams(
          new CountFacetRequest(categoryPath, num))

      val facetsCollector = FacetsCollector.create(new FacetsAccumulator(facetSearchParams,
                                                                         reader,
                                                                         taxonomyReader))

作成するCollectorは2つ。TopScoreDocCollectorクラスのインスタンスは、普通に作成します。ファセット用のCollectorは、FacetsCollectorなのですが、FacetSearchParamsクラスのインスタンスを作成し、FacetsCollectorクラスのファクトリメソッドcreateにFacetSearchParamsとIndexReader、そしてDirectoryTaxonomyReaderのインスタンス渡してFacetsAccumulatorを作成しつつ渡します。

…長いですが、これでFacetsCollectorのインスタンスが得られます。

FacetSearchParamsクラスのインスタンスを生成する時、

        new FacetSearchParams(
          new CountFacetRequest(categoryPath, num))

とCountFacetRequestクラスのインスタンスも生成していますが、この時の引数はCategoryPathと取得するファセットの数を決定します。CategoryPathの作成方法は、インデックス作成時と同じです。

第2引数の方はintなのですが、これは各カテゴリの件数を絞り込むのではなくて、取得するファセット自体の件数を指定することになります。例えば、年月のデータに対して、年でファセットを考えたとき

2013/10
2013/03
2013/01
2012/08
2012/02
2011/06

とあって、ファセットにすると

2013 (3)
2012 (2)
2011 (1)

となるわけですが、この時CountFacetRequestの第2引数を1にすると

2013 (3)

となります。

そして、IndexSearcher#searchメソッドで検索するのですが、この時に先ほど作った2つのCollectorをMultiCollectorでラップします。

      searcher.search(query, MultiCollector.wrap(topScoreDocCollector, facetsCollector))

クエリは、そもそも検索したいインデックスに対して投げるものを指定していると思ってください。

IndexSearcher#searchメソッド実行後は、FacetsCollectorから結果が取得できます。

      for {
        facetResult <- facetsCollector.getFacetResults.asScala
        subResult <- facetResult.getFacetResultNode.subResults.asScala
      } {
        fun(subResult)
      }

FacetResult、FacetResultNodeというクラスがここでは使われます。

ここまでのコードブロックを、こんな感じで定義しました。

    val reader = DirectoryReader.open(indexDirectory)
    val taxonomyReader = new DirectoryTaxonomyReader(taxonomyDirectory)

    val searcher = new IndexSearcher(reader)

    def searchFacet[T](query: Query, categoryPath: CategoryPath, num: Int)
                      (fun: FacetResultNode => Unit): Unit = {
      val topScoreDocCollector = TopScoreDocCollector.create(100, true)
      val facetSearchParams =
        new FacetSearchParams(
          new CountFacetRequest(categoryPath, num))

      val facetsCollector = FacetsCollector.create(new FacetsAccumulator(facetSearchParams,
                                                                         reader,
                                                                         taxonomyReader))

      searcher.search(query, MultiCollector.wrap(topScoreDocCollector, facetsCollector))

      for {
        facetResult <- facetsCollector.getFacetResults.asScala
        subResult <- facetResult.getFacetResultNode.subResults.asScala
      } {
        fun(subResult)
      }
    }

では、ちょっとこんな関数を用意して

    val fun: String => FacetResultNode => Unit =
      caseName => {
        println(s"Case[$caseName]")
        subResult =>
          println(s"   label: ${subResult.label}, value: ${subResult.value}")
      }

Queryを実行してみます。

    searchFacet(new MatchAllDocsQuery,
                new CategoryPath("publishTime"), 10)(fun("Query => allDocs, Facet => publishTime"))
    searchFacet(new MatchAllDocsQuery,
                new CategoryPath("publishTime"), 1)(fun("Query => allDocs, Facet => publishTime, num 1"))
    searchFacet(new TermQuery(new Term("language", "java")),
                new CategoryPath("publishTime", "2008"), 10)(fun("Query => language:java, Facet => publishTime/2008"))
    searchFacet(new TermQuery(new Term("language", "scala")),
                new CategoryPath("publishTime"), 10)(fun("Query => language:scala, Facet => publishTime"))
    searchFacet(new MatchAllDocsQuery,
                new CategoryPath("language"), 10)(fun("Query => allDocs, Facet => language"))
    searchFacet(new TermQuery(new Term("language", "scala")),
                new CategoryPath("publishTime", "2011"), 10)(fun("Query => language:scala, Facet => publishTime/2011"))
    searchFacet(new MatchAllDocsQuery,
                new CategoryPath("author"), 20)(fun("Query => allDocs, Facet => author"))

ポイントは、CategoryPathの組み方です。データの絞り込みは、あくまで指定したQueryが効いていることをお忘れなく。

実行結果はこんな感じです。

Case[Query => allDocs, Facet => publishTime]
   label: publishTime/2013, value: 3.0
   label: publishTime/2011, value: 2.0
   label: publishTime/2010, value: 1.0
   label: publishTime/2012, value: 1.0
   label: publishTime/2009, value: 1.0
   label: publishTime/2008, value: 1.0
Case[Query => allDocs, Facet => publishTime, num 1]
   label: publishTime/2013, value: 3.0
Case[Query => language:java, Facet => publishTime/2008]
   label: publishTime/2008/11, value: 1.0
Case[Query => language:scala, Facet => publishTime]
   label: publishTime/2011, value: 2.0
   label: publishTime/2010, value: 1.0
   label: publishTime/2012, value: 1.0
Case[Query => allDocs, Facet => language]
   label: language/java, value: 5.0
   label: language/scala, value: 4.0
Case[Query => language:scala, Facet => publishTime/2011]
   label: publishTime/2011/01, value: 1.0
   label: publishTime/2011/09, value: 1.0
Case[Query => allDocs, Facet => author]
   label: author/結城 浩, value: 2.0
   label: author/株式会社オージス総研 オブジェクトの広場編集部, value: 1.0
   label: author/Alex Payne, value: 1.0
   label: author/Dean Wampler, value: 1.0
   label: author/大塚庸史, value: 1.0
   label: author/羽生田栄一, value: 1.0
   label: author/デイビッド・ポラック, value: 1.0
   label: author/島本 多可子, value: 1.0
   label: author/竹添 直樹, value: 1.0
   label: author/長尾 高弘, value: 1.0
   label: author/水島 宏太, value: 1.0
   label: author/羽生田 栄一, value: 1.0
   label: author/Bill Venners, value: 1.0
   label: author/Lex Spoon, value: 1.0
   label: author/Martin Odersky, value: 1.0
   label: author/高橋 麻奈, value: 1.0
   label: author/松山 智大, value: 1.0
   label: author/永井 雅人, value: 1.0
   label: author/井上 誠一郎, value: 1.0
   label: author/アリエル・ネットワーク株式会社, value: 1.0

ちゃんと、ファセットになりましたよ。

どういうQueryを投げて、ファセットを要求したかは、「Case」と書かれた部分をご確認ください。

最後に、作成したコードを載せておきます。ここまでの解説で、すでにいろいろ見え隠れしていますが、けっこう趣味に走ったコードになっていますけどね…。

src/main/scala/LuceneFacet.scala

import scala.collection.JavaConverters._

import org.apache.lucene.analysis.Analyzer
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.search.{IndexSearcher, MatchAllDocsQuery, MultiCollector, Query, TermQuery, TopScoreDocCollector}
import org.apache.lucene.store.{Directory, RAMDirectory}
import org.apache.lucene.util.Version

import org.apache.lucene.facet.index.FacetFields
import org.apache.lucene.facet.params.FacetSearchParams
import org.apache.lucene.facet.search.{CountFacetRequest, FacetRequest, FacetResultNode, FacetsAccumulator, FacetsCollector}
import org.apache.lucene.facet.taxonomy.{CategoryPath, TaxonomyWriter}
import org.apache.lucene.facet.taxonomy.directory.{DirectoryTaxonomyReader, DirectoryTaxonomyWriter}

object LuceneFacet {
  def main(args: Array[String]): Unit = {
    val luceneVersion = Version.LUCENE_44
    val analyzer = new JapaneseAnalyzer(luceneVersion)

    val indexDirectory = new RAMDirectory
    val taxonomyDirectory = new RAMDirectory

    registryDocuments(indexDirectory, taxonomyDirectory, luceneVersion, analyzer)

    facetQuery(indexDirectory, taxonomyDirectory, luceneVersion)

    indexDirectory.close()
    taxonomyDirectory.close()
  }

  private def registryDocuments(indexDirectory: Directory,
                                taxonomyDirectory: Directory,
                                luceneVersion: Version,
                                analyzer: Analyzer): Unit = {
    val writer = new IndexWriter(indexDirectory,
                                 new IndexWriterConfig(luceneVersion, analyzer))
    val taxonomyWriter = new DirectoryTaxonomyWriter(taxonomyDirectory)
    val facetFields = new FacetFields(taxonomyWriter)

    writer.addDocument(createDocument(taxonomyWriter,
                                      Book("Effective Java 第2版",
                                           Array("Joshua Bloch", "柴田芳樹"),
                                           "2008",
                                           "11",
                                           3780,
                                           "java"),
                                      facetFields))
    writer.addDocument(createDocument(taxonomyWriter,
                                      Book("パーフェクトJava",
                                           Array("アリエル・ネットワーク株式会社", "井上 誠一郎", "永井 雅人", "松山 智大"),
                                           "2009",
                                           "09",
                                           3780,
                                           "java"),
                                      facetFields))
    writer.addDocument(createDocument(taxonomyWriter,
                                      Book("やさしいJava 第5版",
                                           Array("高橋 麻奈"),
                                           "2013",
                                           "08",
                                           2730,
                                           "java"),
                                      facetFields))
    writer.addDocument(createDocument(taxonomyWriter,
                                      Book("Java言語プログラミングレッスン 第3版(上)",
                                           Array("結城 浩"),
                                           "2013",
                                           "11",
                                           2520,
                                           "java"),
                                      facetFields))
    writer.addDocument(createDocument(taxonomyWriter,
                                      Book("Java言語プログラミングレッスン 第3版(下)",
                                           Array("結城 浩"),
                                           "2013",
                                           "11",
                                           2520,
                                           "java"),
                                      facetFields))
    writer.addDocument(createDocument(taxonomyWriter,
                                      Book("Scalaスケーラブルプログラミング第2版",
                                           Array("Martin Odersky", "Lex Spoon", "Bill Venners", "羽生田 栄一", "水島 宏太", "長尾 高弘"),
                                           "2011",
                                           "09",
                                           4830,
                                           "scala"),
                                      facetFields))
    writer.addDocument(createDocument(taxonomyWriter,
                                      Book("Scala逆引きレシピ (PROGRAMMER’S RECiPE)",
                                           Array("竹添 直樹", "島本 多可子"),
                                           "2012",
                                           "07",
                                           3360,
                                           "scala"),
                                      facetFields))
    writer.addDocument(createDocument(taxonomyWriter,
                                      Book("Scalaプログラミング入門",
                                           Array("デイビッド・ポラック", "羽生田栄一", "大塚庸史"),
                                           "2010",
                                           "03",
                                           3360,
                                           "scala"),
                                      facetFields))
    writer.addDocument(createDocument(taxonomyWriter,
                                      Book("プログラミングScala",
                                           Array("Dean Wampler", "Alex Payne", "株式会社オージス総研 オブジェクトの広場編集部"),
                                           "2011",
                                           "01",
                                           3990,
                                           "scala"),
                                      facetFields))


    writer.close()
    taxonomyWriter.close()
  }

  private def createDocument(taxonomyWriter: TaxonomyWriter ,book: Book, facetFields: FacetFields): Document = {
    val document = new Document

    // Build Document
    document.add(new TextField("title", book.title, Field.Store.YES))
    document.add(new TextField("authors", book.authors.mkString(", "), Field.Store.YES))
    document.add(new StringField("year", book.year, Field.Store.YES))
    document.add(new StringField("month", book.month, Field.Store.YES))
    document.add(new StringField("price", book.price.toString, Field.Store.YES))
    document.add(new StringField("language", book.language, Field.Store.YES))

    // Build Category
    val categoryPaths = List(
      new CategoryPath("publishTime", book.year, book.month),
      new CategoryPath("price", book.price.toString),
      new CategoryPath("language", book.language)
    ) ::: book.authors.map(n => new CategoryPath("author", n)).toList
 
    facetFields.addFields(document, categoryPaths.asJava)

    document
  }

  private def facetQuery(indexDirectory: Directory, taxonomyDirectory: Directory, luceneVersion: Version): Unit = {
    val reader = DirectoryReader.open(indexDirectory)
    val taxonomyReader = new DirectoryTaxonomyReader(taxonomyDirectory)

    val searcher = new IndexSearcher(reader)

    def searchFacet[T](query: Query, categoryPath: CategoryPath, num: Int)
                      (fun: FacetResultNode => Unit): Unit = {
      val topScoreDocCollector = TopScoreDocCollector.create(100, true)
      val facetSearchParams =
        new FacetSearchParams(
          new CountFacetRequest(categoryPath, num))

      val facetsCollector = FacetsCollector.create(new FacetsAccumulator(facetSearchParams,
                                                                         reader,
                                                                         taxonomyReader))

      searcher.search(query, MultiCollector.wrap(topScoreDocCollector, facetsCollector))

      for {
        facetResult <- facetsCollector.getFacetResults.asScala
        subResult <- facetResult.getFacetResultNode.subResults.asScala
      } {
        fun(subResult)
      }
    }


    val fun: String => FacetResultNode => Unit =
      caseName => {
        println(s"Case[$caseName]")
        subResult =>
          println(s"   label: ${subResult.label}, value: ${subResult.value}")
      }

    searchFacet(new MatchAllDocsQuery,
                new CategoryPath("publishTime"), 10)(fun("Query => allDocs, Facet => publishTime"))
    searchFacet(new MatchAllDocsQuery,
                new CategoryPath("publishTime"), 1)(fun("Query => allDocs, Facet => publishTime, num 1"))
    searchFacet(new TermQuery(new Term("language", "java")),
                new CategoryPath("publishTime", "2008"), 10)(fun("Query => language:java, Facet => publishTime/2008"))
    searchFacet(new TermQuery(new Term("language", "scala")),
                new CategoryPath("publishTime"), 10)(fun("Query => language:scala, Facet => publishTime"))
    searchFacet(new MatchAllDocsQuery,
                new CategoryPath("language"), 10)(fun("Query => allDocs, Facet => language"))
    searchFacet(new TermQuery(new Term("language", "scala")),
                new CategoryPath("publishTime", "2011"), 10)(fun("Query => language:scala, Facet => publishTime/2011"))
    searchFacet(new MatchAllDocsQuery,
                new CategoryPath("author"), 20)(fun("Query => allDocs, Facet => author"))


    reader.close()
    taxonomyReader.close()
  }
}

case class Book(title: String,
                authors: Array[String],
                year: String,
                month: String,
                price: Int,
                language: String)