CLOVER🍀

That was when it all began.

LuceneのTerm Vectorを使ってみる

もともとは、Highlighterを使った時にサンプルでTerm Vectorが特別扱いされていたのがきっかけで、ちょっとLuceneのTerm Vectorを使ってみることにしました。

そもそもTerm Vectorというのは何か?という話ですが、

Documentの特定のフィールドにおける、出現単語と単語の出現回数を表したもの

らしいです。出現単語がそのDocumentの「方向」、出現回数が「大きさ」(方向)と考えられるので、Term Vectorですと。

なるほど…。

Term Vectorを使うには、あらかじめDocumentに登録するフィールドに、設定が必要です。

例えば、「title」というフィールドからTerm Vectorを取得する場合には、以下のようなFieldTypeのインスタンスを生成して、設定を行う必要があります。

    val titleFieldType = new FieldType
    titleFieldType.setIndexed(true)
    titleFieldType.setStored(true)
    titleFieldType.setTokenized(true)
    titleFieldType.setStoreTermVectors(true)
    titleFieldType.setStoreTermVectorOffsets(true)
    titleFieldType.setStoreTermVectorPositions(true)
    document.add(new Field("title", title, titleFieldType))

インデキシングの設定やStore、TokenizeはまあいいとしてまずはsetStoreTermVectorsでTerm Vectorを保存するように指示します。

そして、オフセットを取得する場合にはsetStoreTermVectorOffsetsを有効にし、位置を取得する場合にはsetTermVectorPositionsを有効にします。

その他、Payloadsとかありますが、今回はちょっと置いておきます。

で、さらに「abstraction」というフィールドにもTerm Vectorを取得可能なように設定した、こんなDocumentを作成することにします。

  private def createBook(isbn13: String, title: String, price: Int, abstraction: String): Document = {
    val document = new Document
    document.add(new StringField("isbn13", isbn13, Field.Store.YES))

    val titleFieldType = new FieldType
    titleFieldType.setIndexed(true)
    titleFieldType.setStored(true)
    titleFieldType.setTokenized(true)
    titleFieldType.setStoreTermVectors(true)
    titleFieldType.setStoreTermVectorOffsets(true)
    titleFieldType.setStoreTermVectorPositions(true)
    document.add(new Field("title", title, titleFieldType))

    document.add(new StringField("price", price.toString, Field.Store.YES))

    val abstractionFieldType = new FieldType
    abstractionFieldType.setIndexed(true)
    abstractionFieldType.setStored(true)
    abstractionFieldType.setTokenized(true)
    abstractionFieldType.setStoreTermVectors(true)
    abstractionFieldType.setStoreTermVectorOffsets(true)
    abstractionFieldType.setStoreTermVectorPositions(true)
    document.add(new Field("abstraction", abstraction, abstractionFieldType))

    document
  }

いつもの通り、書籍です。

で、適当に本を登録します。

  private def registryDocuments(directory: Directory, luceneVersion: Version, analyzer: Analyzer): Unit =
    for (indexWriter <- new IndexWriter(directory,
                                        new IndexWriterConfig(luceneVersion, analyzer))) {
      indexWriter.addDocument(createBook("978-4774127804",
                                         "Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築",
                                         3360,
                                         "Luceneは全文検索システムを構築するためのJavaのライブラリです。Luceneを使えば,一味違う高機能なWebアプリケーションを作ることができます。たとえばWeb通販・ショッピングサイト,企業サイトの情報サービス,学術系サイトでの情報サービスなどが挙げられます。Webに付加価値を与えることができるのです。本書は,全文検索システムの仕組みと機能を初心者にもわかりやすく解説し,豊富なサンプルコードで実装を示してゆきます。やさしく説明する工夫(挿話)やAjaxとLuceneを組み合わせた「インクリメンタルサーチ」など楽しい仕掛けも盛りだくさん!非常に奥深い世界が広がっています。本書は6章構成になっており、第1章から第4章が前編、第5章と第6章が後編となっている。前編を「基本編」、後編を「応用編」と読み替えてもよい。Luceneのアプリケーションを書くためには「基本編」である第1章から第4章を通読しておくことが望ましい。「応用編」である第5章と第6章は余力があれば読むとよいだろう。第1章は、全文検索とLuceneの基本知識の習得を目標とし、これらについて簡潔に述べている。Luceneに関しては、最初の全文検索のサンプルプログラムを作成し、その内容や動作を紹介している。サンプルプログラムの題材(全文検索のためのコンテンツ)には、日本でもっとも有名なファミリーである「サザエさん一家」を使用している。第2章は、Analyzer(アナライザー)について説明している。Analyzerは、全文検索の対象となるドキュメントテキストを分析し、単語を取り出す働きをするものである。本章では特に日本語のテキストの分析に重きを置き、JapaneseAnalyzerの解説にページ数を割いている。その後、CJKAnalyzer、StandardAnalyzerおよびその他のAnalyzerを紹介する。Luceneは全文検索に「転置索引方式」を採用している。そのため、検索の前にあらかじめ「インデックス」を作成しておかなければならない。第3章では、その「インデックス」の作成方法について説明している。その後、第4章で「インデックス」の検索方法について説明している。全文検索のサンプルデータとしては、サンプルプログラムの内容と動作の理解が進むよう、読者にとって親しみやすいと思われる「技術評論社の書籍データ」と郵便局の「大口事業所等個別番号データ」を用いた。後編の第5章では、前編で習得した知識を使って、全文検索機能を持ったWebアプリケーションを作成する。このWebアプリケーショ<e3><83><b3>の検索機能は、「データベース」、「HTMLファイル」、「XMLファイル」、「PDFファイル」および「Mocrosoft Wordファイル」といった「異種ドキュメント」を透過的に検索し、表示する。第6章では、「セキュリティ」、「検索質問語の強調表示」、「Ajaxを使用したインクリメンタルサーチ」等々といった、より応用的・発展的な内容を取り上げている。読者のLuceneアプリケーションにいろいろな機能を追加する際のヒントとなるだろう。なお、Appendix AにはLuceneはじめ、その他の関連ツールおよび本書のサンプルプログラムのインストールと実行方法を掲載している。"))

      indexWriter.addDocument(createBook("978-4774141756",
                                         "Apache Solr入門 ―オープンソース全文検索エンジン",
                                         3780,
                                         "Apache Solrとは,オープンソースの検索エンジンです.Apache LuceneというJavaの全文検索システムをベースに豊富な拡張性をもたせ,多くの開発者が利用できるように作られました.検索というと,Googleのシステムを使っている企業Webページが多いですが,自前の検索エンジンがあると顧客にとって本当に必要な情報を提供できます.SolrはJavaだけでなく,PHP,Ruby,Python,Perl,Javascriptなどでもプログラミング可能です.本書は検索エンジンの基礎から応用までじっくり解説します."))

      indexWriter.addDocument(createBook("978-4797352009",
                                         "集合知イン・アクション",
                                         3990,
                                         "レコメンデーションエンジンをつくるには?ブログやSNSのテキスト分析、ユーザー嗜好の予測モデル、レコメンデーションエンジン……Web 2.0の鍵「集合知」をJavaで実装しよう! 具体的なコードとともに丹念に解説します。はてなタグサーチやYahoo!日本語形態素解析を活用するサンプルも追加収録。"))

      indexWriter.addDocument(createBook("978-4873115665",
                                         "HBase",
                                         4410,
                                       "ビッグデータのランダムアクセス系処理に欠かせないHBaseについて、基礎から応用までを詳細に解説。クライアントAPI(高度な機能・管理機能)、Hadoopとの結合、アーキテクチャといった開発に関わる事項や、クラスタのモニタリング、パフォーマンスチューニング、管理といった運用の方法を、豊富なサンプルとともに解説します。日本語版ではAWS Elastic MapReduceについての付録を追加。ビッグデータに関心あるすべてのエンジニアに必携の一冊です。"))

      indexWriter.addDocument(createBook("978-4873115900",
                                         "MongoDBイン・アクション",
                                         3570,
                                         "本書はMongoDBを学びたいアプリケーション開発者やDBAに向けて、MongoDBの基礎から応用までを包括的に解説する書籍です。MongoDBの機能やユースケースの概要など基本的な事柄から、ドキュメント指向データやクエリと集計など、MongoDB APIの詳細について、さらにパフォーマンスやトラブルシューティングなど高度なトピックまで豊富なサンプルでわかりやすく解説します。付録ではデザインパターンも紹介。コードはJavaScriptとRubyで書かれていますが、PHP、Java、C++での利用についても触れています。MongoDBを使いこなしたいすべてのエンジニア必携の一冊です。"))
    }

では、Term Vectorを取得してみましょう。Term Vectorを取得したいだけなら、Queryを使わなくてもOKです。

今回は、IndexReader#maxDocでDocumentのIDの最大値から全DocumentのTerm Vectorを取得していきます。

    for {
      reader <- DirectoryReader.open(directory)
      id <- (0 until reader.maxDoc)
    } {

やり方、その1。TermsEnumを全面的に使用します。

IndexReader#getTermVectorsにDocumentのIDを指定することで、Fieldsのインスタンスを取得することができます。下記では、変数名termVectorsと書いていますが…。

      // IndexReader#getTermVector(id, fieldName)とする方法もある
      // その場合、Termsが返却される
      val termVectors = reader.getTermVectors(id)
      termVectors.iterator.asScala.foreach { field =>
        val terms = termVectors.terms(field)
        val reuse: TermsEnum = null

        val termsEnum = terms.iterator(reuse)

        /* 後で */
      }

Fieldsからは、Term Vectorを取得可能にしたフィールド名がiteratorメソッドでIteratorとして得られます。また、フィールド名が分かっていればIndexReader#getTermVector(id, fieldName)で直接Termsを取得することもできます。

今回は、Fields#iteratorで得られたフィールド名から、Fields#termsを使用してTermsクラスのインスタンスを取得しました。さらにTerms#iteratorでTermsEnumのインスタンスを取得するのですが…。

で、このTermsEnum#nextでBytesRefのインスタンスが取得できる間、Term Vectorに関する情報が得られるようにイテレーションできるということになります。

そこを実装したのが、このコードです。

        println(s"Doc[$id] Field: $field")
        println("  TermsEnum#totalTermFreq: TermsEnum#doqFreq: IndexReader#docFreq: value")
        Iterator
          .continually(termsEnum.next)
          .takeWhile(_ != null)
          .foreach { bytesRef =>
            // (ドキュメント内の単語出現回数)
            print(s"    ${termsEnum.totalTermFreq}: ")
            // (ドキュメント内に単語があれば、1?)
            print(s"${termsEnum.docFreq}: ")
            // (単語の出現するドキュメント数)
            print(s"${reader.docFreq(new Term(field, bytesRef.utf8ToString))}: ")
            // 単語
            println(s"${bytesRef.utf8ToString}")
          }
        }

コメントにも書いていますが、TermsEnum#docFreqで現在のイテレーションが指している単語のDocument内の出現回数、TermsEnum#docFreqで単語が出現していれば1…?(ここよくわかっていません)、そしてIndexReader#docFreqで単語の出現するDocument数を得ることができます。これは、インデックス内のDocumentで、という意味です。

単語の文字列自体は、BytesRef#utf8ToStringで取得しましょう。

つまり、全Documentのあるフィールドのある単語の出現回数は、TermsEnum#totalTermFreqの結果を合計すれば算出できることになります。

ある単語を含むDocumentの数自体は、IndexReader#docFreqを使用すればよいというわけです。

一部の出力結果ですが、これを動かした時の結果です。

Doc[1] Field: abstraction
  TermsEnum#totalTermFreq: TermsEnum#doqFreq: IndexReader#docFreq: value
    2: 1: 1: apache
    1: 1: 1: google
    2: 1: 4: java
    1: 1: 2: javascript
    1: 1: 2: lucene
    1: 1: 1: perl
    1: 1: 2: php
    1: 1: 1: python
    1: 1: 2: ruby
    2: 1: 1: solr
    1: 1: 3: web

〜略〜

Doc[1] Field: title
  TermsEnum#totalTermFreq: TermsEnum#doqFreq: IndexReader#docFreq: value
    1: 1: 2: apache
    1: 1: 1: solr
    1: 1: 1: エンジン
    1: 1: 2: オープン
    1: 1: 2: ソース
    1: 1: 2: 入門
    1: 1: 2: 全文
    1: 1: 2: 検索

今回のプログラムを書くにあたり、このサイトが非常に参考になりました。

http://mocobeta-backup.tumblr.com/post/49083727353/lucene-in-action-5-term-vector-1-tfidf

続いて、もうひとつの取得方法。こちらは、DocsAndPositionsEnumを使用します。

      // IndexReader#getTermVector(id, fieldName)とする方法もある
      // その場合、Termsが返却される
      val termVectors = reader.getTermVectors(id)
      termVectors.iterator.asScala.foreach { field =>
        val terms = termVectors.terms(field)
        val reuse: TermsEnum = null

        val termsEnum = terms.iterator(reuse)

        println(s"Doc[$id] Field: $field")
        println("  DocsAndPositionsEnum#freq: value")
        Iterator
          .continually(termsEnum.next)
          .takeWhile(_ != null)
          .foreach { bytesRef =>
            var docsAndPositions: DocsAndPositionsEnum = null

            docsAndPositions = termsEnum.docsAndPositions(null, docsAndPositions)
            if (docsAndPositions.nextDoc != 0) {
              throw new IllegalStateException("you need to call nextDoc() to have the enum positioned")
            }

            // ドキュメント内の単語の出現回数
            val freq = docsAndPositions.freq

            println(s"    $freq: ${bytesRef.utf8ToString}")

            println("      position: startOffset-endOffset")
            for (i <- 1 to freq) {
              // Termの位置
              val position = docsAndPositions.nextPosition
              // Termの出現位置のオフセット(文字単位)
              val startOffset = docsAndPositions.startOffset
              val endOffset = docsAndPositions.endOffset
              println(s"        $position: ${startOffset}-${endOffset}")
            }
          }

最初の部分は、完全に同じなので端折ります。

違うのは、TermsEnumからDocsAndPositionsEnumを取得するところから。

            var docsAndPositions: DocsAndPositionsEnum = null

            docsAndPositions = termsEnum.docsAndPositions(null, docsAndPositions)

DocsAndPositionsEnum#freqで、Document内の単語の出現回数、つまりTermsEnum#totalTermFreqと同じ値が取得できます。

            // ドキュメント内の単語の出現回数
            val freq = docsAndPositions.freq

単語そのものは、変わらずBytesRef#utf8ToString。

            println(s"    $freq: ${bytesRef.utf8ToString}")

取得する値が違うのはここからで、取得したfreqまで1から進めながら、DocsAndPositionsEnum#nextPositionで単語の位置、DocsandPositionsEnum#startOffsetで開始位置、endOffsetで終了位置が取得できます。

            println("      position: startOffset-endOffset")
            for (i <- 1 to freq) {
              // Termの位置
              val position = docsAndPositions.nextPosition
              // Termの出現位置のオフセット(文字単位)
              val startOffset = docsAndPositions.startOffset
              val endOffset = docsAndPositions.endOffset
              println(s"        $position: ${startOffset}-${endOffset}")
            }

Positionが単語単位で進めていった数なのに対して、Offsetは文字単位の位置を表すようです。ここで使用しているAPIを見ると、FieldTypeでPositionとかOffsetを指定していたのがわかりますね。

こちらのサンプルを動かした時の出力結果の一部は、こんな感じです。

Doc[1] Field: abstraction
  DocsAndPositionsEnum#freq: value
    2: apache
      position: startOffset-endOffset
        0: 0-6
        10: 31-37
    1: google
      position: startOffset-endOffset
        45: 105-111
    2: java
      position: startOffset-endOffset
        13: 47-51
        78: 177-181
    1: javascript
      position: startOffset-endOffset
        86: 208-218
    1: lucene
      position: startOffset-endOffset
        11: 38-44
    1: perl
      position: startOffset-endOffset
        85: 203-207
    1: php
      position: startOffset-endOffset
        82: 187-190
    1: python
      position: startOffset-endOffset
        84: 196-202

〜省略〜

Doc[1] Field: title
  DocsAndPositionsEnum#freq: value
    1: apache
      position: startOffset-endOffset
        0: 0-6
    1: solr
      position: startOffset-endOffset
        1: 7-11
    1: エンジン
      position: startOffset-endOffset
        7: 26-30
    1: オープン
      position: startOffset-endOffset
        3: 15-19
    1: ソース
      position: startOffset-endOffset
        4: 19-22
    1: 入門
      position: startOffset-endOffset
        2: 11-13
    1: 全文
      position: startOffset-endOffset
        5: 22-24
    1: 検索
      position: startOffset-endOffset
        6: 24-26

このプログラムを書く時に参考にしたのは、こちらのやり取りです。

http://osdir.com/ml/java-user.lucene.apache.org/2013-04/msg00009.html
http://osdir.com/ml/java-user.lucene.apache.org/2013-04/msg00010.html
http://osdir.com/ml/java-user.lucene.apache.org/2013-04/msg00012.html


途中で、TermsEnum#totalTermFreqとDocsAndPositionsEnum#freqは、何が違うの?みたいな質問に対して、こう答えています。

In case of term vectors, the docs enums contain only one document so
iterator.totalTermFreq() and docsAndPositions.freq() are equal. This
would not be true if you consumed AtomicReader.fields() (since the
docs enums would have several documents).

どうも、AtomicReaderを使用した時に違いが出るようですが、あいにく使ったことがありません…。

それから、今回Payloadというものを無視していますが、PayloadはLuceneのテストコードを見て、使い方を理解しました。

lucene/lucene/core/src/test/org/apache/lucene/index/TestPayloadsOnVectors.java

    Document doc = new Document();
    FieldType customType = new FieldType(TextField.TYPE_NOT_STORED);
    customType.setStoreTermVectors(true);
    customType.setStoreTermVectorPositions(true);
    customType.setStoreTermVectorPayloads(true);
    customType.setStoreTermVectorOffsets(random().nextBoolean());
    Field field = new Field("field", "", customType);
    TokenStream ts = new MockTokenizer(new StringReader("here we go"), MockTokenizer.WHITESPACE, true);
    assertFalse(ts.hasAttribute(PayloadAttribute.class));
    field.setTokenStream(ts);
    doc.add(field);
    writer.addDocument(doc);
    
    Token withPayload = new Token("withPayload", 0, 11);
    withPayload.setPayload(new BytesRef("test"));
    ts = new CannedTokenStream(withPayload);
    assertTrue(ts.hasAttribute(PayloadAttribute.class));
    field.setTokenStream(ts);
    writer.addDocument(doc);

Payloadって、自分で設定するんですね!!

なるほど…最初試していて、何度やってもnullしか取れなかったので…そういうことですか。

いろいろ勉強になりました。それにしても、テキストマイニングとかで役に立ちそうな機能ですね。

それでは、今回作成したコードです。

build.sbt

name := "lucene-term-vector"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.10.3"

organization := "littlewings"

libraryDependencies += "org.apache.lucene" % "lucene-analyzers-kuromoji" % "4.4.0"

src/main/scala/LuceneTermVector.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, FieldType, TextField, StringField}
import org.apache.lucene.index.{DirectoryReader, DocsAndPositionsEnum, FieldInfo, IndexWriter, IndexWriterConfig}
import org.apache.lucene.index.{Term, TermsEnum}
import org.apache.lucene.store.{Directory, RAMDirectory}
import org.apache.lucene.util.Version

object LuceneTermVector {
  def main(args: Array[String]): Unit =
    for (directory <- new RAMDirectory) {
      val luceneVersion = Version.LUCENE_44
      val analyzer = new JapaneseAnalyzer(luceneVersion)

      registryDocuments(directory, luceneVersion, analyzer)

      printTermVectors(directory)
    }

  private def printTermVectors(directory: Directory): Unit =
    for {
      reader <- DirectoryReader.open(directory)
      id <- (0 until reader.maxDoc)
    } {
      // IndexReader#getTermVector(id, fieldName)とする方法もある
      // その場合、Termsが返却される
      val termVectors = reader.getTermVectors(id)
      termVectors.iterator.asScala.foreach { field =>
        val terms = termVectors.terms(field)
        val reuse: TermsEnum = null

        val termsEnum = terms.iterator(reuse)

        println(s"Doc[$id] Field: $field")
        println("  TermsEnum#totalTermFreq: TermsEnum#doqFreq: IndexReader#docFreq: value")
        Iterator
          .continually(termsEnum.next)
          .takeWhile(_ != null)
          .foreach { bytesRef =>
            // (ドキュメント内の単語出現回数)
            print(s"    ${termsEnum.totalTermFreq}: ")
            // (ドキュメント内に単語があれば、1?)
            print(s"${termsEnum.docFreq}: ")
            // (単語の出現するドキュメント数)
            print(s"${reader.docFreq(new Term(field, bytesRef.utf8ToString))}: ")
            // 単語
            println(s"${bytesRef.utf8ToString}")
          }

        /* DocsAndPositionsEnumを使用する場合は、こちら
        println("  DocsAndPositionsEnum#freq: value")
        Iterator
          .continually(termsEnum.next)
          .takeWhile(_ != null)
          .foreach { bytesRef =>
            var docsAndPositions: DocsAndPositionsEnum = null

            docsAndPositions = termsEnum.docsAndPositions(null, docsAndPositions)
            if (docsAndPositions.nextDoc != 0) {
              throw new IllegalStateException("you need to call nextDoc() to have the enum positioned")
            }

            // ドキュメント内の単語の出現回数
            val freq = docsAndPositions.freq

            println(s"    $freq: ${bytesRef.utf8ToString}")

            println("      position: startOffset-endOffset")
            for (i <- 1 to freq) {
              // Termの位置
              val position = docsAndPositions.nextPosition
              // Termの出現位置のオフセット(文字単位)
              val startOffset = docsAndPositions.startOffset
              val endOffset = docsAndPositions.endOffset
              println(s"        $position: ${startOffset}-${endOffset}")
            }
          }
        */
      }
    }

  private def registryDocuments(directory: Directory, luceneVersion: Version, analyzer: Analyzer): Unit =
    for (indexWriter <- new IndexWriter(directory,
                                        new IndexWriterConfig(luceneVersion, analyzer))) {
      indexWriter.addDocument(createBook("978-4774127804",
                                         "Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築",
                                         3360,
                                         "Luceneは全文検索システムを構築するためのJavaのライブラリです。Luceneを使えば,一味違う高機能なWebアプリケーションを作ることができます。たとえばWeb通販・ショッピングサイト,企業サイトの情報サービス,学術系サイトでの情報サービスなどが挙げられます。Webに付加価値を与えることができるのです。本書は,全文検索システムの仕組みと機能を初心者にもわかりやすく解説し,豊富なサンプルコードで実装を示してゆきます。やさしく説明する工夫(挿話)やAjaxとLuceneを組み合わせた「インクリメンタルサーチ」など楽しい仕掛けも盛りだくさん!非常に奥深い世界が広がっています。本書は6章構成になっており、第1章から第4章が前編、第5章と第6章が後編となっている。前編を「基本編」、後編を「応用編」と読み替えてもよい。Luceneのアプリケーションを書くためには「基本編」である第1章から第4章を通読しておくことが望ましい。「応用編」である第5章と第6章は余力があれば読むとよいだろう。第1章は、全文検索とLuceneの基本知識の習得を目標とし、これらについて簡潔に述べている。Luceneに関しては、最初の全文検索のサンプルプログラムを作成し、その内容や動作を紹介している。サンプルプログラムの題材(全文検索のためのコンテンツ)には、日本でもっとも有名なファミリーである「サザエさん一家」を使用している。第2章は、Analyzer(アナライザー)について説明している。Analyzerは、全文検索の対象となるドキュメントテキストを分析し、単語を取り出す働きをするものである。本章では特に日本語のテキストの分析に重きを置き、JapaneseAnalyzerの解説にページ数を割いている。その後、CJKAnalyzer、StandardAnalyzerおよびその他のAnalyzerを紹介する。Luceneは全文検索に「転置索引方式」を採用している。そのため、検索の前にあらかじめ「インデックス」を作成しておかなければならない。第3章では、その「インデックス」の作成方法について説明している。その後、第4章で「インデックス」の検索方法について説明している。全文検索のサンプルデータとしては、サンプルプログラムの内容と動作の理解が進むよう、読者にとって親しみやすいと思われる「技術評論社の書籍データ」と郵便局の「大口事業所等個別番号データ」を用いた。後編の第5章では、前編で習得した知識を使って、全文検索機能を持ったWebアプリケーションを作成する。このWebアプリケーショ<e3><83><b3>の検索機能は、「データベース」、「HTMLファイル」、「XMLファイル」、「PDFファイル」および「Mocrosoft Wordファイル」といった「異種ドキュメント」を透過的に検索し、表示する。第6章では、「セキュリティ」、「検索質問語の強調表示」、「Ajaxを使用したインクリメンタルサーチ」等々といった、より応用的・発展的な内容を取り上げている。読者のLuceneアプリケーションにいろいろな機能を追加する際のヒントとなるだろう。なお、Appendix AにはLuceneはじめ、その他の関連ツールおよび本書のサンプルプログラムのインストールと実行方法を掲載している。"))

      indexWriter.addDocument(createBook("978-4774141756",
                                         "Apache Solr入門 ―オープンソース全文検索エンジン",
                                         3780,
                                         "Apache Solrとは,オープンソースの検索エンジンです.Apache LuceneというJavaの全文検索システムをベースに豊富な拡張性をもたせ,多くの開発者が利用できるように作られました.検索というと,Googleのシステムを使っている企業Webページが多いですが,自前の検索エンジンがあると顧客にとって本当に必要な情報を提供できます.SolrはJavaだけでなく,PHP,Ruby,Python,Perl,Javascriptなどでもプログラミング可能です.本書は検索エンジンの基礎から応用までじっくり解説します."))

      indexWriter.addDocument(createBook("978-4797352009",
                                         "集合知イン・アクション",
                                         3990,
                                         "レコメンデーションエンジンをつくるには?ブログやSNSのテキスト分析、ユーザー嗜好の予測モデル、レコメンデーションエンジン……Web 2.0の鍵「集合知」をJavaで実装しよう! 具体的なコードとともに丹念に解説します。はてなタグサーチやYahoo!日本語形態素解析を活用するサンプルも追加収録。"))

      indexWriter.addDocument(createBook("978-4873115665",
                                         "HBase",
                                         4410,
                                       "ビッグデータのランダムアクセス系処理に欠かせないHBaseについて、基礎から応用までを詳細に解説。クライアントAPI(高度な機能・管理機能)、Hadoopとの結合、アーキテクチャといった開発に関わる事項や、クラスタのモニタリング、パフォーマンスチューニング、管理といった運用の方法を、豊富なサンプルとともに解説します。日本語版ではAWS Elastic MapReduceについての付録を追加。ビッグデータに関心あるすべてのエンジニアに必携の一冊です。"))

      indexWriter.addDocument(createBook("978-4873115900",
                                         "MongoDBイン・アクション",
                                         3570,
                                         "本書はMongoDBを学びたいアプリケーション開発者やDBAに向けて、MongoDBの基礎から応用までを包括的に解説する書籍です。MongoDBの機能やユースケースの概要など基本的な事柄から、ドキュメント指向データやクエリと集計など、MongoDB APIの詳細について、さらにパフォーマンスやトラブルシューティングなど高度なトピックまで豊富なサンプルでわかりやすく解説します。付録ではデザインパターンも紹介。コードはJavaScriptとRubyで書かれていますが、PHP、Java、C++での利用についても触れています。MongoDBを使いこなしたいすべてのエンジニア必携の一冊です。"))
    }

  private def createBook(isbn13: String, title: String, price: Int, abstraction: String): Document = {
    val document = new Document
    document.add(new StringField("isbn13", isbn13, Field.Store.YES))

    val titleFieldType = new FieldType
    titleFieldType.setIndexed(true)
    titleFieldType.setStored(true)
    titleFieldType.setTokenized(true)
    titleFieldType.setStoreTermVectors(true)
    titleFieldType.setStoreTermVectorOffsets(true)
    titleFieldType.setStoreTermVectorPositions(true)
    document.add(new Field("title", title, titleFieldType))

    document.add(new StringField("price", price.toString, Field.Store.YES))

    val abstractionFieldType = new FieldType
    abstractionFieldType.setIndexed(true)
    abstractionFieldType.setStored(true)
    abstractionFieldType.setTokenized(true)
    abstractionFieldType.setStoreTermVectors(true)
    abstractionFieldType.setStoreTermVectorOffsets(true)
    abstractionFieldType.setStoreTermVectorPositions(true)
    document.add(new Field("abstraction", abstraction, abstractionFieldType))

    document
  }

  implicit class AutoCloseableWrapper[A <: AutoCloseable](val underlying: A) extends AnyVal {
    def foreach(fun: A => Unit): Unit =
      try {
        fun(underlying)
      } finally {
        underlying.close()
      }
  }
}