CLOVER🍀

That was when it all began.

Infinispan(Embedded Mode)の検索機能を使ってみる

あけましておめでとうございます。2015年、最初のエントリですね。

Embedded ModeでのInfinispanを使った、検索機能を使ってみます。

14. Querying Infinispan
http://infinispan.org/docs/7.0.x/user_guide/user_guide.html#sid-68355061

バージョンが7.0.0.Final以降のInfinispanのEmbedded Modeでは、APIとしては

の2パターン、設定方法としては3通りあります。

それぞれ、順に見ていこうと思います。

依存関係の定義

まずは、InfinispanのQuery Moduleを使用するための依存関係の定義を行います。

build.sbt

name := "embedded-query"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.11.4"

organization := "org.littlewings"

scalacOptions ++= Seq("-Xlint", "-unchecked", "-deprecation", "-feature")

updateOptions := updateOptions.value.withCachedResolution(true)

fork in Test := true

libraryDependencies ++= Seq(
  "org.infinispan" % "infinispan-query" % "7.0.2.Final",
  "net.jcip" % "jcip-annotations" % "1.0" % "provided",
  "org.apache.lucene" % "lucene-analyzers-kuromoji" % "4.10.2",
  "org.scalatest" %% "scalatest" % "2.2.3" % "test"
)

Query Moduleを引き込むと、Query DSLも同時に引き込まれるので、今回はこちらの設定で進めます。

Lucene Kuromojiが入っているのは、Hibernate Searchを使う時のAnalyzerとして。

Query

6.0でQuery DSLが入るまでは、こちらが(Embeddedな)Infinispanで検索を行うための唯一のAPIでした。

Hibernate Search(とLucene)を使って、検索機能を実現します。

Infinispan自体はLuceneのDirectoryの実装としての機能を持っていて、Luceneの3系、4系の両方に対応していたのですが、Hibernate SearchのLucene依存がずっと3系だったので、検索機能もそちらに引っ張られていました。が、7.0.0.Final以降、Hibernate Searchが5系にアップデートされLucene 4系に対応したので、検索機能でもLucene 4系が使えるようになりました。
Lucene 3系のDirectoryの実装は、なくなったっぽい?

では、まずはCacheに登録するクラスを用意。
src/main/scala/org/littlewings/infinispan/query/FullTextIndexingBook.scala

package org.littlewings.infinispan.query

import scala.beans.BeanProperty

import org.hibernate.search.annotations.{ Analyze, Field, Indexed }

object FullTextIndexingBook {
  def apply(isbn: String, title: String, price: Int, summary: String): FullTextIndexingBook = {
    val book = new FullTextIndexingBook
    book.isbn = isbn
    book.title= title
    book.price = price
    book.summary = summary
    book
  }
}

@Indexed
@SerialVersionUID(1L)
class FullTextIndexingBook extends Serializable {
  @Field(analyze = Analyze.NO)
  @BeanProperty
  var isbn: String = _

  @Field
  @BeanProperty
  var title: String = _

  @Field(analyze = Analyze.NO)
  @BeanProperty
  var price: Int = _

  @Field
  @BeanProperty
  var summary: String = _
}

お題は書籍。

とりあえず、JavaBeansである必要がありますが、Infinispanの検索機能で使う場合はJPAのEntityである必要はありません。

コンパニオンオブジェクトがあるのは、テストコードのためのオマケです。

続いて、設定ファイル。ここからは、テスト用のリソースおよびコードになります。
src/test/resources/infinispan-query.xml

<?xml version="1.0" encoding="UTF-8"?>
<infinispan
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="urn:infinispan:config:7.0 http://www.infinispan.org/schemas/infinispan-config-7.0.xsd"
    xmlns="urn:infinispan:config:7.0">
  <jgroups>
    <stack-file name="udp" path="jgroups.xml" />
  </jgroups>

  <cache-container name="queryCacheManager" shutdown-hook="REGISTER">
    <transport cluster="cluster" stack="udp" />
    <jmx duplicate-domains="true" />

    <distributed-cache name="indexingCache">
      <indexing index="LOCAL">
        <property name="default.directory_provider">infinispan</property>
        <property name="analyzer">org.apache.lucene.analysis.ja.JapaneseAnalyzer</property>
        <property name="default.exclusive_index_use">true</property>
        <property name="default.indexmanager">org.infinispan.query.indexmanager.InfinispanIndexManager</property>
        <property name="default.reader.strategy">shared</property>
        <property name="lucene_version">LUCENE_CURRENT</property>
      </indexing>
    </distributed-cache>

    <distributed-cache name="LuceneIndexesData" />
    <replicated-cache name="LuceneIndexesMetadata" />
    <replicated-cache name="LuceneIndexesLocking" />
  </cache-container>
</infinispan>

JGroupsの設定は端折ります。

ポイントは、indexing要素のindexで「LOCAL」を指定してインデキシングが有効になるように設定しているところですね(デフォルトは「NONE」)。

propertyには、Hibernate Search向けの設定が並んでいます。ここで、形態素解析をするようにAnalyzerの設定をしています。

あとは、Luceneのインデックス向けのCacheを3種類定義しています。

続いて、テストコード。

…本体の前に、Infinispanでクラスタを構成してテストするのを補助するようなトレイトを用意。
src/test/scala/org/littlewings/infinispan/query/InfinispanClusteredSpecSupport.scala

package org.littlewings.infinispan.query

import org.infinispan.Cache
import org.infinispan.manager.DefaultCacheManager

trait InfinispanClusteredSpecSupport {
  protected def withCache[K, V](numInstances: Int, fileName: String, cacheName: String)(fun: Cache[K, V] => Unit): Unit = {
    val managers = (1 to numInstances).map(_ => new DefaultCacheManager(fileName))

    try {
      managers.foreach(_.getCache[K, V](cacheName))

      val manager = managers.head
      val cache = manager.getCache[K, V](cacheName)

      fun(cache)
    } finally {
      managers.foreach(_.stop())
    }
  }
}

起動するInfinispanのインスタンス数、設定ファイルのパス、Cacheの名前を指定してクラスタを構成します。また別途Cacheを引数に取る関数を引数に指定して、EmbeddedCacheManager#stopをする前にCacheを操作する関数を実行できます。まあ、この中でテストコードを書いてねってことです。

では、実際のテストコードを。
src/test/scala/org/littlewings/infinispan/query/QuerySpec.scala

package org.littlewings.infinispan.query

import org.apache.lucene.search.{ Sort, SortField }
import org.infinispan.query.Search

import org.scalatest.FunSpec
import org.scalatest.Matchers._

class QuerySpec extends FunSpec with InfinispanClusteredSpecSupport {
  private def sourceBooks: Seq[FullTextIndexingBook] =
    Array(
      FullTextIndexingBook("978-4798042169",
        "わかりやすいJavaEEウェブシステム入門",
        3456,
        "JavaEE7準拠。ショッピングサイトや業務システムで使われるJavaEE学習書の決定版!"),
      FullTextIndexingBook("978-4798124605",
        "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava",
        4410,
        "エンタープライズJava入門書の決定版!Java EE 6は、大規模な情報システム構築に用いられるエンタープライズ環境向けのプログラミング言語です。"),
      FullTextIndexingBook("978-4774127804",
        "Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築",
        3200,
        "Luceneは全文検索システムを構築するためのJavaのライブラリです。Luceneを使えば,一味違う高機能なWebアプリケーションを作ることができます。"),
      FullTextIndexingBook("978-4774161631",
        "[改訂新版] Apache Solr入門 オープンソース全文検索エンジン",
        3780,
        "最新版Apaceh Solr Ver.4.5.1に対応するため大幅な書き直しと原稿の追加を行い、現在の開発環境に合わせて完全にアップデートしました。Apache Solrは多様なプログラミング言語に対応した全文検索エンジンです。"),
      FullTextIndexingBook("978-4048662024",
        "高速スケーラブル検索エンジン ElasticSearch Server",
        3024,
        "Apache Solrを超える全文検索エンジンとして注目を集めるElasticSearch Serverの日本初の解説書です。多くのサンプルを用いた実践的入門書になっています。"),
      FullTextIndexingBook("978-1933988177",
        "Lucene in Action",
        6301,
        "New edition of top-selling book on the new version of Lucene. the coreopen-source technology behind most full-text search and Intelligent Web applications.")
    )

  describe("Infinispan Query Spec") {
    // ここに、テストコードを書く!
  }
}

最初にズラズラと書いているのは、テストに使うデータです。

このテストデータと、先ほど作成したトレイトを使ったテストコードを書いていきます。

まずは、Queryの確認。

    it("Buid Full Text Query") {
      withCache[String, FullTextIndexingBook](3, "infinispan-query.xml", "indexingCache") { cache =>
        sourceBooks.foreach(b => cache.put(b.isbn, b))

        val searchManager = Search.getSearchManager(cache)
        val queryBuilder = searchManager.buildQueryBuilderForClass(classOf[FullTextIndexingBook]).get

        val fullTextQuery =
          queryBuilder
            .keyword
            .onFields("title", "summary")
            .matching("全文検索 Java 日本語")
            .createQuery

        fullTextQuery.toString should be ("(title:全文 title:検索 title:java title:日本 title:日本語 title:語) (summary:全文 summary:検索 summary:java summary:日本 summary:日本語 summary:語)")
      }
    }

この検索APIでは、LuceneのQueryが表に出てきます。ここでは、ちゃんと形態素解析できている(=Analyzerの設定ができている)ことを確認しています。

ポイントは以下の部分で、SearchクラスからSearchManagerを、そしてQueryBuilderの取得へと続くところでしょうか。QueryBuilderから先は、Hibernate SearchのAPIになります。

        val searchManager = Search.getSearchManager(cache)
        val queryBuilder = searchManager.buildQueryBuilderForClass(classOf[FullTextIndexingBook]).get

実際に検索を行ったテスト。

    it("Search") {
      withCache[String, FullTextIndexingBook](3, "infinispan-query.xml", "indexingCache") { cache =>
        sourceBooks.foreach(b => cache.put(b.isbn, b))

        val searchManager = Search.getSearchManager(cache)
        val queryBuilder = searchManager.buildQueryBuilderForClass(classOf[FullTextIndexingBook]).get

        val fullTextQuery =
          queryBuilder
            .keyword
            .onFields("title", "summary")
            .matching("全文検索 Java 日本語")
            .createQuery

        val cacheQuery = searchManager.getQuery(fullTextQuery, classOf[FullTextIndexingBook])

        val books =
          cacheQuery
            .sort(new Sort(new SortField("price", SortField.Type.INT)))
            .list
            .asInstanceOf[java.util.List[FullTextIndexingBook]]

        books should have size 4

        books.get(0).title should be ("高速スケーラブル検索エンジン ElasticSearch Server")
        books.get(1).title should be ("Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築")
        books.get(2).title should be ("[改訂新版] Apache Solr入門 オープンソース全文検索エンジン")
        books.get(3).title should be ("Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava")
      }
    }

このあたりは、Hibernate Searchの世界です。

Query DSL

続いて、Infinispan 6.0から登場した、Query DSLです。

14.9. Infinispan’s Query DSL
http://infinispan.org/docs/7.0.x/user_guide/user_guide.html#_infinispan_s_query_dsl

Infinispan 7.0の時点ではLuceneのインデックスあり/なし、の2種類の利用方法があります。LuceneおよびHibernate Searchに非依存なQuery APIとして作ることを目的にしているみたいです。

こちらは、設定ファイルは両者で共通としました。以降では、このどちらかのCacheを使っていきます。
src/test/resources/infinispan-query-dsl.xml

<?xml version="1.0" encoding="UTF-8"?>
<infinispan
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="urn:infinispan:config:7.0 http://www.infinispan.org/schemas/infinispan-config-7.0.xsd"
    xmlns="urn:infinispan:config:7.0">
  <jgroups>
    <stack-file name="udp" path="jgroups.xml" />
  </jgroups>

  <cache-container name="queryDslCacheManager" shutdown-hook="REGISTER">
    <transport cluster="cluster" stack="udp" />
    <jmx duplicate-domains="true" />

    <distributed-cache name="indexingCache">
      <indexing index="LOCAL">
        <property name="default.directory_provider">infinispan</property>
        <property name="default.exclusive_index_use">true</property>
        <property name="default.indexmanager">org.infinispan.query.indexmanager.InfinispanIndexManager</property>
        <property name="default.reader.strategy">shared</property>
        <property name="lucene_version">LUCENE_CURRENT</property>
      </indexing>
    </distributed-cache>

    <distributed-cache name="LuceneIndexesData" />
    <replicated-cache name="LuceneIndexesMetadata" />
    <replicated-cache name="LuceneIndexesLocking" />

    <distributed-cache name="indexLessCache" />
  </cache-container>
</infinispan>

「indexingCache」が、インデックスの設定(Hibernate Searchの設定)をしたCacheです。が、Analyzerの設定はありません。「indexLessCache」は、インデックスの設定を何もしていないCacheになります。

インデックスを使用するQuery DSL向けに、Lucene用のCacheは定義しておきます。

インデックスを使用したQuery DSL

まずは、Cacheに登録するクラスから。
src/main/scala/org/littlewings/infinispan/query/NonAnalyzedBook.scala

package org.littlewings.infinispan.query

import scala.beans.BeanProperty

import org.hibernate.search.annotations.{ Analyze, Field, Indexed }

object NonAnalyzedBook {
  def apply(isbn: String, title: String, price: Int, summary: String): NonAnalyzedBook = {
    val book = new NonAnalyzedBook
    book.isbn = isbn
    book.title= title
    book.price = price
    book.summary = summary
    book
  }
}

@Indexed
@SerialVersionUID(1L)
class NonAnalyzedBook extends Serializable {
  @Field(analyze = Analyze.NO)
  @BeanProperty
  var isbn: String = _

  @Field(analyze = Analyze.NO)
  @BeanProperty
  var title: String = _

  @Field(analyze = Analyze.NO)
  @BeanProperty
  var price: Int = _

  @Field(analyze = Analyze.NO)
  @BeanProperty
  var summary: String = _
}

パッと見た感じは先ほどの検索機能で使ったクラスと同じなのですが、違うところがすべてのフィールドで

  @Field(analyze = Analyze.NO)

と定義しているところですね。

Hibernate Searchの@Fieldアノテーションはデフォルトでanalyze = Analyze.YESですが、InfinsipanのQuery DSLでインデックス付きで使う場合は、クエリを投げる時にここがYESになっていると実行時エラーになります…。

テストコード。先ほどのInfinispanClusteredSpecSupportトレイトも使用しています。
また、使っているCacheはインデックスを有効化した「indexingCache」です。
src/test/scala/org/littlewings/infinispan/query/WithIndexingQueryDslSpec.scala

package org.littlewings.infinispan.query

import org.infinispan.query.Search
import org.infinispan.query.dsl.{ Query, SortOrder }

import org.scalatest.FunSpec
import org.scalatest.Matchers._

class WithIndexingQueryDslSpec extends FunSpec with InfinispanClusteredSpecSupport {
  private def sourceBooks: Seq[NonAnalyzedBook] =
    Array(
      NonAnalyzedBook("978-4798042169",
        "わかりやすいJavaEEウェブシステム入門",
        3456,
        "JavaEE7準拠。ショッピングサイトや業務システムで使われるJavaEE学習書の決定版!"),
      NonAnalyzedBook("978-4798124605",
        "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava",
        4410,
        "エンタープライズJava入門書の決定版!Java EE 6は、大規模な情報システム構築に用いられるエンタープライズ環境向けのプログラミング言語です。"),
      NonAnalyzedBook("978-4774127804",
        "Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築",
        3200,
        "Luceneは全文検索システムを構築するためのJavaのライブラリです。Luceneを使えば,一味違う高機能なWebアプリケーションを作ることができます。"),
      NonAnalyzedBook("978-4774161631",
        "[改訂新版] Apache Solr入門 オープンソース全文検索エンジン",
        3780,
        "最新版Apaceh Solr Ver.4.5.1に対応するため大幅な書き直しと原稿の追加を行い、現在の開発環境に合わせて完全にアップデートしました。Apache Solrは多様なプログラミング言語に対応した全文検索エンジンです。"),
      NonAnalyzedBook("978-4048662024",
        "高速スケーラブル検索エンジン ElasticSearch Server",
        3024,
        "Apache Solrを超える全文検索エンジンとして注目を集めるElasticSearch Serverの日本初の解説書です。多くのサンプルを用いた実践的入門書になっています。"),
      NonAnalyzedBook("978-1933988177",
        "Lucene in Action",
        6301,
        "New edition of top-selling book on the new version of Lucene. the coreopen-source technology behind most full-text search and Intelligent Web applications.")
    )

  describe("Infinispan With Indexing Query DSL") {
    // ここに、テストコードを書く!
  }
}

最初は、やっぱりテストコードです。使っているクラスが変わっただけで、内容は全部一緒。

まずはクエリを組んでみます。

    it("Build Query") {
      withCache[String, NonAnalyzedBook](3, "infinispan-query-dsl.xml", "indexingCache") { cache =>
        sourceBooks.foreach(b => cache.put(b.isbn, b))

        val queryFactory = Search.getQueryFactory(cache)

        val query: Query =
          queryFactory
            .from(classOf[NonAnalyzedBook])
            .having("title")
            .like("%Java%")
            .and
            .having("title")
            .like("%全文検索%")
            .toBuilder
            .orderBy("price", SortOrder.ASC)
            .build

        query.toString should include ("query=+title:*Java* +title:*全文検索*")
      }
    }

SearchクラスからQueryFactoryを取得して

        val queryFactory = Search.getQueryFactory(cache)

あとはQuery DSLAPIに従ってクエリを組み立てていきます。

        val query: Query =
          queryFactory
            .from(classOf[NonAnalyzedBook])
            .having("title")
            .like("%Java%")
            .and
            .having("title")
            .like("%全文検索%")
            .toBuilder
            .orderBy("price", SortOrder.ASC)
            .build

インデックスを有効にしている場合は、このクエリはLuceneのクエリになります。

        query.toString should include ("query=+title:*Java* +title:*全文検索*")

なのですが、AnalyzeできないようにしているのはLikeをサポートしたり、Query DSLとして使う際にLuceneを意識せざるをえない使わせ方になっちゃうからでしょうね。

実際に検索すると、このようになります。

    it("Search") {
      withCache[String, NonAnalyzedBook](3, "infinispan-query-dsl.xml", "indexingCache") { cache =>
        sourceBooks.foreach(b => cache.put(b.isbn, b))

        val queryFactory = Search.getQueryFactory(cache)

        val query: Query =
          queryFactory
            .from(classOf[NonAnalyzedBook])
            .having("title")
            .like("%Java%")
            .and
            .having("title")
            .like("%全文検索%")
            .toBuilder
            .orderBy("price", SortOrder.ASC)
            .build

        query.getResultSize should be (1)

        val books = query.list.asInstanceOf[java.util.List[NonAnalyzedBook]]

        books should have size 1
        books.get(0).title should be ("Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築")
      }
    }
  }

件数が欲しいだけなら、Query#getResultSizeが使えます。が、このメソッドはページング向けの設定(firstResult、maxResult)を無視した件数を返すようです。今回は、そこまで試していません…。

インデックスを使用しないQuery DSL

続いて、インデックスを使用しない、LuceneHibernate Searchに非依存なQuery DSLです。

これが使えるようになったのは、Infinispan 7.0.0.Finalからとなります。EntryRetriever(EntryIterable)というものを使った仕掛けになっているようです。

なお、この設定で使う場合はリフレクションを使ったフルスキャンになります。

では、使ってみます。まずはCacheに登録するクラス。
src/main/scala/org/littlewings/infinispan/query/Book.scala

package org.littlewings.infinispan.query

import scala.beans.BeanProperty

object Book {
  def apply(isbn: String, title: String, price: Int, summary: String): Book = {
    val book = new Book
    book.isbn = isbn
    book.title= title
    book.price = price
    book.summary = summary
    book
  }
}

@SerialVersionUID(1L)
class Book extends Serializable {
  @BeanProperty
  var isbn: String = _

  @BeanProperty
  var title: String = _

  @BeanProperty
  var price: Int = _

  @BeanProperty
  var summary: String = _
}

(書いているのはScalaですが)完全にHibernate Searchへの依存がなくなりました。

続いてテストコード。
src/test/scala/org/littlewings/infinispan/query/IndexLessQueryDslSpec.scala

package org.littlewings.infinispan.query

import org.infinispan.query.Search
import org.infinispan.query.dsl.{ Query, SortOrder }

import org.scalatest.FunSpec
import org.scalatest.Matchers._

class IndexLessQueryDslSpec extends FunSpec with InfinispanClusteredSpecSupport {
  private def sourceBooks: Seq[Book] =
    Array(
      Book("978-4798042169",
        "わかりやすいJavaEEウェブシステム入門",
        3456,
        "JavaEE7準拠。ショッピングサイトや業務システムで使われるJavaEE学習書の決定版!"),
      Book("978-4798124605",
        "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava",
        4410,
        "エンタープライズJava入門書の決定版!Java EE 6は、大規模な情報システム構築に用いられるエンタープライズ環境向けのプログラミング言語です。"),
      Book("978-4774127804",
        "Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築",
        3200,
        "Luceneは全文検索システムを構築するためのJavaのライブラリです。Luceneを使えば,一味違う高機能なWebアプリケーションを作ることができます。"),
      Book("978-4774161631",
        "[改訂新版] Apache Solr入門 オープンソース全文検索エンジン",
        3780,
        "最新版Apaceh Solr Ver.4.5.1に対応するため大幅な書き直しと原稿の追加を行い、現在の開発環境に合わせて完全にアップデートしました。Apache Solrは多様なプログラミング言語に対応した全文検索エンジンです。"),
      Book("978-4048662024",
        "高速スケーラブル検索エンジン ElasticSearch Server",
        3024,
        "Apache Solrを超える全文検索エンジンとして注目を集めるElasticSearch Serverの日本初の解説書です。多くのサンプルを用いた実践的入門書になっています。"),
      Book("978-1933988177",
        "Lucene in Action",
        6301,
        "New edition of top-selling book on the new version of Lucene. the coreopen-source technology behind most full-text search and Intelligent Web applications.")
    )

  describe("Infinispan IndexLess Query DSL") {
    it("Search") {
      withCache[String, Book](3, "infinispan-query-dsl.xml", "indexLessCache") { cache =>
        sourceBooks.foreach(b => cache.put(b.isbn, b))

        val queryFactory = Search.getQueryFactory(cache)

        val query: Query =
          queryFactory
            .from(classOf[Book])
            .having("title")
            .like("%Java%")
            .and
            .having("title")
            .like("%全文検索%")
            .toBuilder
            .orderBy("price", SortOrder.ASC)
            .build

        query.getResultSize should be (1)

        val books = query.list.asInstanceOf[java.util.List[Book]]

        books should have size 1
        books.get(0).title should be ("Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築")
      }
    }
  }
}

この使い方では、Luceneのクエリのように文字列表現にした結果を得られなかったので、クエリのテストは書いていません。ですが、検索処理そのものはインデックスありの場合と(Cacheに登録しているクラスが違うのを除いて)まったく同じです。

これはこれで、便利なのかもしれません。

必要に応じて、全文検索とフルスキャンを使い分けられたらいいなと思います。

今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/infinispan-getting-started/tree/master/embedded-query