CLOVER🍀

That was when it all began.

Infinispanで検索してみる

Distributed FrameworkとかMap Reduce Frameworkとか触っていましたが、よくよく考えるとその前に「検索って機能があるか見てないよなー」と思い、ここで触ってみることに。

Querying Infinispan
https://docs.jboss.org/author/display/ISPN/Querying+Infinispan

…まさかのHibernate Search(+Lucene

そんな重量級(注:イメージです)のものが出てくるなんて。

Hibernate Search
http://www.hibernate.org/subprojects/search.html

Apache Lucene(Core)
http://lucene.apache.org/core/

Hibernateなんて、2系を少し触ったことがあるくらいだよ?Luceneも、Hello Worldレベルのものしかやったことないよ?ま、チュートリアルを見つつ頑張ってみましょう。
Hibernate Searchは、また別物だとは思いますが…

準備

とりあえず、build.sbt

name := "infinispan-query-example"

version := "0.0.1"

scalaVersion := "2.10.1"

organization := "littlewings"

fork in run := true

scalacOptions += "-deprecation"

resolvers += "JBoss Public Maven Repository Group" at "http://repository.jboss.org/nexus/content/groups/public-jboss/"

libraryDependencies ++= Seq(
  "org.infinispan" % "infinispan-core" % "5.2.1.Final",
  "org.infinispan" % "infinispan-query" % "5.2.1.Final"
)

src/main/resources/infinispan.xml

<?xml version="1.0" encoding="UTF-8"?>
<infinispan
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="urn:infinispan:config:5.2 http://www.infinispan.org/schemas/infinispan-config-5.2.xsd"
      xmlns="urn:infinispan:config:5.2">
  <global>
    <globalJmxStatistics
        enabled="true"
        jmxDomain="org.infinispan"
        cacheManagerName="DefaultCacheManager"
        />
  </global>

  <default>
    <indexing enabled="true" indexLocalOnly="true">
      <properties>
        <property name="default.directory_provider" value="ram" />
        <!-- FileSystem
        <property name="default.directory_provider" value="filesystem" />
        -->
        <!-- Infinispan
        <property name="default.directory_provider" value="infinispan" />
        -->

        <property name="lucene_version" value="LUCENE_36" />
        <!-- 以下でもOK
        <property name="hibernate.search.lucene_version" value="LUCENE_36" />
        -->
      </properties>
    </indexing>
  </default>
</infinispan>

設定は、ほぼドキュメントのままです。変更したのは、ここでしょうか。

        <property name="lucene_version" value="LUCENE_36" />
        <!-- 以下でもOK
        <property name="hibernate.search.lucene_version" value="LUCENE_36" />
        -->

このバージョン指定を入れないと、Hibernate Searchが

[error] WARN: HSEARCH000075: Configuration setting hibernate.search.lucene_version was not specified, using LUCENE_CURRENT.

と延々怒ってきます。Infinispan 5.2.1.Finalが使っているApache Luceneは、3.6.2です。Lucene 4.2には、Infinispan 5.3まで待つ必要がありそうです。

インデックスの格納先には、以下の3つから選ぶことができます。
https://docs.jboss.org/author/display/ISPN/Querying+Infinispan#QueryingInfinispan-LuceneDirectory

種類 設定ファイルでの書き方 特徴
Ram(メモリ) ram 単一のNodeのMapに格納します。他のNodeとのインデックス共有は不可
ファイルシステム filesystem そのNodeのローカルファイルシステムに格納します。ネットワークファイルシステムを使った共有をすることもできますが、推奨しないとのことです
Infinispan infinispan InfinispanのCacheに格納します。レプリケーションまたは分散Cacheを使用でき、他のNodeとインデックスを共有できます

まあ、普通はRamかInfinispanから選ぶんでしょうね。

使ってみる

では、サンプルを…
src/main/scala/QueryingExample.scala

import scala.collection.JavaConverters._

import java.util.{Calendar, Date}

import org.apache.lucene.analysis.cjk.CJKAnalyzer
import org.apache.lucene.queryParser.QueryParser
import org.apache.lucene.search.{Query, Sort, SortField}
import org.apache.lucene.util.Version

import org.hibernate.search.annotations.{DateBridge, Field, Indexed, IndexedEmbedded, Resolution}

import org.infinispan.Cache
import org.infinispan.query.{CacheQuery, Search}
import org.infinispan.manager.DefaultCacheManager

object QueryingExample {
  def main(args: Array[String]): Unit = {
    val manager = new DefaultCacheManager("infinispan.xml")
    val cache = manager.getCache[String, Book]()

    val luceneVersion = Version.LUCENE_36

    try {
      registerIndexData(cache)

      val query = // Queryの作成
      val sort = None

      println(s"Query = $query")

      val results = search(cache, query, sort)

      println(s"Hits = ${results.size}")

      for (r <- results)
        println("Found --->" + System.lineSeparator + r)
    } finally {
      cache.stop()
      manager.stop()
    }
  }

  def registerIndexData(cache: Cache[String, Book]): Unit = {
    // cacheにデータの登録
  }

  def search(cache: Cache[_, _], fullTextQuery: Query, sort: Option[Sort]): List[_]= {
    // 検索と結果の取得
  }

  def toDate(year: Int, month: Int, day: Int): Date = {
    val calendar = Calendar.getInstance
    calendar.clear()
    calendar.set(Calendar.YEAR, year)
    calendar.set(Calendar.MONTH, month - 1)
    calendar.set(Calendar.DATE, day)
    calendar.getTime
  }
}

インデックスに登録するデータとなるクラスは、こちらになります。

object Book {
  def apply(title: String,
            description: String,
            price: Int,
            publisherYear: Date,
            authors: Set[Author]): Book = {
    val book = new Book
    book.title = title
    book.description = description
    book.price = price
    book.publisherYear = publisherYear
    book.authors = authors.asJava
    book
  }
}

@Indexed
class Book private {
  @Field
  var title: String = _
  @Field
  var description: String = _
  @Field
  var price: Int = _
  @Field
  @DateBridge(resolution = Resolution.YEAR)
  var publisherYear: Date = _
  @IndexedEmbedded
  var authors: java.util.Set[Author] = _

  override def toString: String =
    s"""Book[title = $title,
       |     price = $price,
       |     publisherYear = $publisherYear,
       |     authors = { ${authors.asScala.mkString(", ")} }]""".stripMargin
}

object Author {
  def apply(name: String, surname: String = null): Author = {
    val author = new Author
    author.name = name
    author.surname = surname
    author
  }
}

class Author private {
  @Field
  var name: String = _
  @Field
  var surname: String = _

  override def toString: String =
    s"Author[name = $name]"
}

なんか、ちょっと冗長な上、Scalaっぽくなくなってしまいました…。Case Classは使っていません。Selializableを隠したくないので。

インデックスの定義とするものは、@Indexedアノテーションを付与します。

@Indexed
class Book private {

インデックスのフィールドとするものは、@Fieldアノテーションを付与します。

  @Field
  var title: String = _

他のクラスを入れ子にする場合は、@IndexedEmbeddedアノテーションを付与します。

  @IndexedEmbedded
  var authors: java.util.Set[Author] = _

@IndexedEmbeddedアノテーションをコレクションに付与する場合は、Javaのコレクションである必要があるようですね…。

検索は、LuceneのQueryクラス、Infinispan QueryのSearch/SearchManager/CacheQueryを使用します。

InfinispanのSeachとCacheを使用して、SearchManagerのインスタンスが取得できるので、これに対してLuceneのQueryを渡すと、InfinispanのCacheQueryが戻ってくる、ということになります。

  def search(cache: Cache[_, _], fullTextQuery: Query, sort: Option[Sort]): List[_]= {
    val searchManager = Search.getSearchManager(cache)
    val cacheQuery = searchManager.getQuery(fullTextQuery)

    sort.foreach(cacheQuery.sort)

    val results = cacheQuery.list
    results.asScala.toList
  }

あとは、CacheQueryに対してlistメソッドを実行すれば、検索結果が取得できます。今回は、間にソートの設定も入れていますが

検索対象のデータは、Lucene/Solr/HibernateScalaの本を登録する、ということにします。

  def registerIndexData(cache: Cache[String, Book]): Unit = {
    cache.put("1", Book(title = "Apache Lucene 入門 〜 Java・オープンソース・全文検索システムの構築",
                        description = """Luceneは全文検索システムを構築するためのJavaのライブラリです。
                        |Luceneを使えば,一味違う高機能なWebアプリケーションを作ることができます。
                        |""".stripMargin.replaceAll("""\r?\n""", ""),
                        price = 3360,
                        publisherYear = toDate(2006, 5, 17),
                        authors = Set(Author(name = "関口 宏司"))))
    cache.put("2", Book(title = "Lucene in Action",
                        description = """HIGHLIGHT New edition of top-selling book on the new version of Lucene
                        |--the core open-source technology behind most full-text search and 
                        |"Intelligent Web" applications. """.stripMargin.replaceAll("""\r?\n""", ""),
                        price = 4977,
                        publisherYear = toDate(2010, 6, 30),
                        authors = Set(
                          Author(name = "Michael McCandless"),
                          Author(name = "Erik Hatcher"),
                          Author(name = "Otis Gospodnetic"))))
    cache.put("3", Book(title = "Apache Solr入門 ー オープンソース全文検索エンジン",
                        description = """Apache Solrとは,オープンソースの検索エンジンです.
                        |Apache LuceneというJavaの全文検索システムをベースに豊富な拡張性をもたせ,
                        |多くの開発者が利用できるように作られました.""".stripMargin.replaceAll("""\r?\n""", ""),
                        price = 3680,
                        publisherYear = toDate(2010, 2, 20),
                        authors = Set(
                          Author(name = "関口 宏司"),
                          Author(name = "三部 靖夫"),
                          Author(name = "武田 光平"),
                          Author(name = "中野 猛"),
                          Author(name = "大谷 純"))))
    cache.put("4", Book(title = "Hibernate辞典 設定・マッピング・クエリ逆引きリファレンス",
                        description = """実践的なテクニックと豊富なサンプルで、利用上の悩みを解決!
                        |最新バージョン3.xに対応!""".stripMargin.replaceAll("""\r?\n""", ""),
                        price = 3200,
                        publisherYear = toDate(2008, 8, 7),
                          authors = Set(
                            Author("船木 健児"),
                            Author("三田 淳一"),
                            Author("佐藤 竜一"))))
    cache.put("5", Book(title = "Hibernate (開発者ノートシリーズ)",
                        description = """Javaプログラムからデータベースを利用する際に便利なのが,
                        |O/R(Object-Relational)マッピング・ツールである。O/Rマッピング・ツールを利用すると,
                        |データベースに格納してある表形式のデータをオブジェクトとして取り扱える。""".stripMargin.replaceAll("""\r?\n""", ""),
                        price = 2520,
                        publisherYear = toDate(2004, 12, 1),
                        authors = Set(
                          Author("James Elliott"),
                          Author("佐藤 直生"))))
    cache.put("6", Book(title = "Scalaスケーラブルプログラミング第2版",
                        description = "言語設計者自ら、その手法と思想を説くScalaプログラミングバイブル!",
                        price = 4830,
                        publisherYear = toDate(2011, 9, 27),
                        authors = Set(
                          Author("Martin Odersky"),
                          Author("Lex Spoon"),
                          Author("Bill Venners"),
                          Author("羽生田 栄一"),
                          Author("水島 宏太"),
                          Author("長尾 高弘"))))
    cache.put("7", Book(title = "Scala逆引きレシピ (PROGRAMMER’S RECiPE)",
                        description = """Scalaでコードを書く際の実践ノウハウが凝縮! 本書は、オブジェクト指向言語に
                        |関数型言語の特長をバランスよく取り込んだ、実用的なプログラミング言語「Scala(スカラ)」の
                        |逆引き解説書です。""".stripMargin.replaceAll("""\r?\n""", ""),
                        price = 3360,
                        publisherYear = toDate(2012, 7, 3),
                        authors = Set(
                          Author("竹添 直樹"),
                          Author("島本 多可子"))))
    cache.put("8", Book(title = "Scalaプログラミング入門",
                        description = """Scalaの生みの親、マーティン・オダースキー推薦!
                        |羽生田栄一解説「いまなぜScalaなのか」を掲載! """.stripMargin.replaceAll("""\r?\n""", ""),
                        price = 3360,
                        publisherYear = toDate(2010, 3, 18),
                        authors = Set(
                          Author("デイビッド・ポラック"),
                          Author("羽生田栄一"),
                          Author("大塚庸史"))))
    cache.put("9", Book(title = "プログラミングScala",
                        description = """プログラミング言語Scalaの解説書。
                        |Scala言語の基本的な機能やScala特有の設計について学ぶことができます。""".stripMargin.replaceAll("""\r?\n""", ""),
                        price = 3390,
                        publisherYear = toDate(2011, 1, 20),
                        authors = Set(
                          Author("Dean Wampler"),
                          Author("Alex Payne"),
                          Author("株式会社オージス総研 オブジェクトの広場編集部"))))
  }

本のデータは、Amazonまで…。

では、あとはこれに対してQueryを作成します。今回は、

  • 本のタイトルに「Lucene」または「Scala」を含む
  • 価格が3,000〜4,000円の範囲
  • 結果は、価格の昇順にソート

という条件で、QueryとSortを作成します。

      val queryParser = new QueryParser(luceneVersion, "title", new CJKAnalyzer(luceneVersion))
      val query = queryParser.parse("(Lucene OR Scala) AND price:[3000 TO 4000]")
      val sort = Some(new Sort(new SortField("price", SortField.INT)))

      println(s"Query = $query")

      val results = search(cache, query, sort)

Analyzerは、とりあえずCJKAnalyzerで。

では、実行してみます。

> run
[info] Running QueryingExample 
[error] 3 20, 2013 7:17:15 午後 org.infinispan.factories.GlobalComponentRegistry start
[error] INFO: ISPN000128: Infinispan version: Infinispan 'Delirium' 5.2.1.Final
[error] 3 20, 2013 7:17:16 午後 org.infinispan.query.impl.LifecycleManager cacheStarting
[error] INFO: ISPN014003: Registering Query interceptor
[error] 3 20, 2013 7:17:16 午後 org.hibernate.search.Version <clinit>
[error] INFO: HSEARCH000034: Hibernate Search 4.2.0.Final
[error] 3 20, 2013 7:17:16 午後 org.hibernate.annotations.common.Version <clinit>
[error] INFO: HCANN000001: Hibernate Commons Annotations {4.0.1.Final}
[error] 3 20, 2013 7:17:16 午後 org.infinispan.jmx.CacheJmxRegistration start
[error] INFO: ISPN000031: MBeans were successfully registered to the platform MBean server.
[info] Query = +(title:lucene title:scala) +price:[3000 TO 4000]
[info] Hits = 4
[info] Found --->
[info] Book[title = Apache Lucene 入門 〜 Java・オープンソース・全文検索システムの構築,
[info]      price = 3360,
[info]      publisherYear = Wed May 17 00:00:00 JST 2006,
[info]      authors = { Author[name = 関口 宏司] }]
[info] Found --->
[info] Book[title = Scala逆引きレシピ (PROGRAMMER’S RECiPE),
[info]      price = 3360,
[info]      publisherYear = Tue Jul 03 00:00:00 JST 2012,
[info]      authors = { Author[name = 竹添 直樹], Author[name = 島本 多可子] }]
[info] Found --->
[info] Book[title = Scalaプログラミング入門,
[info]      price = 3360,
[info]      publisherYear = Thu Mar 18 00:00:00 JST 2010,
[info]      authors = { Author[name = デイビッド・ポラック], Author[name = 羽生田栄一], Author[name = 大塚庸史] }]
[info] Found --->
[info] Book[title = プログラミングScala,
[info]      price = 3390,
[info]      publisherYear = Thu Jan 20 00:00:00 JST 2011,
[info]      authors = { Author[name = Dean Wampler], Author[name = Alex Payne], Author[name = 株式会社オージス総研 オブジェクトの広場編集部] }]
[success] Total time: 3 s, completed 2013/03/20 19:17:17

4件、ヒットしました、と。

少々ScalaHibernate Searchの組み合わせでハマったところもあったのですが、導入するだけなら思ったよりも簡単に使えました。

まあ、事前にインデックスの定義をしておかなくてはならないのは、他のNoSQLものと比べてどうなんだろうとか、思うところはありますが…その分高速だといいなー。

実際に使うなら、Luceneディレクトリの設定とかInfinispanのCacheのモードと設定とか
https://docs.jboss.org/author/display/ISPN/Querying+Infinispan#QueryingInfinispan-Cachemodesandmanagingindexes
インデックスのリビルドとか
https://docs.jboss.org/author/display/ISPN/Querying+Infinispan#QueryingInfinispan-RebuildingtheIndex
いろいろあるんでしょうけど。

ドキュメントを読んでいて、インデックスのリビルドにはMapReduceを使っている、というのにはちょっと驚きました。
*ゆえに、少なくともInfinispan 5.2ではリビルドは分散Cacheでなければならず、レプリケーションやローカルキャッシュでは使用できないということらしいです