CLOVER🍀

That was when it all began.

Hibernate Search × Infinispan × WildFly

WildFlyに、Hibernate Searchが同梱されるようになったと聞き、せっかくなので試してみることにしました。

Hibernate Searchが使用する、Luceneのインデックスの保存先はInfinispanとします。また、最終的にはWildFlyにCache Containerを定義して、クラスタリングするところまでを目標に頑張りたいと思います。

それに、Hibernate SearchってこれまでInfinispan越しに使っていましたが、JPAと合わせたことはありませんし。

JPA(もちろん実装はHibernate)とHibernate Searchを統合することで、JPAでの更新時に一緒にLuceneのインデックスを作成してくれるみたいです。この時のインデックスの保存先は、メモリ、ファイルシステム、Infinispanから選ぶことができますが、前述の通り今回はInfinispanに保存します。

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

準備

まずは、依存関係の定義。
build.sbt

name := "hibernate-search-with-jpa"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.10.3"

organization := "org.lilttlewings"

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

seq(webSettings: _*)

artifactName := { (version: ScalaVersion, module: ModuleID, artifact: Artifact) =>
  //artifact.name + "." + artifact.extension
  "javaee7-web." + artifact.extension
}

{
val jettyVersion = "9.1.2.v20140210"
libraryDependencies ++= Seq(
  "org.eclipse.jetty" % "jetty-webapp" % jettyVersion % "container",
  "org.eclipse.jetty" % "jetty-plus"   % jettyVersion% "container",
  "javax" % "javaee-web-api" % "7.0" % "provided",
  "org.hibernate" % "hibernate-search-orm" % "4.5.0.Final" % "provided",
  "org.apache.lucene" % "lucene-kuromoji" % "3.6.2" excludeAll(
    ExclusionRule(organization = "org.apache.lucene", name = "lucene-core"),
    ExclusionRule(organization = "org.apache.lucene", name = "lucene-analyzers")
  )
)
}

Hibernate SearchはWildFlyのモジュールとして登録されているので、providedでOKです。その関係でWildFlyにはLucene 3.6.2が入っているのですが、形態素解析器Kuromojiは入っていないので、別途追加しています。

xsbt-web-pluginの設定。
project/plugins.sbt

addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "0.7.0")

Hibernate Searchモジュールを使用するため、jboss-deployment-structure.xmlファイルを作成します。
src/main/webapp/WEB-INF/jboss-deployment-structure.xml

<?xml version="1.0" encoding="UTF-8"?>
<jboss-deployment-structure xmlns="urn:jboss:deployment-structure:1.2">
  <deployment>
    <dependencies>
      <module name="org.hibernate.search.orm" services="export" />
    </dependencies>
  </deployment>
</jboss-deployment-structure>

以下で紹介しているサンプルに習って「services="export"」を付けているんですけど、意味はわかってません…。

では、Hibernate SearchをJPAと合わせて使っていきます。コードを書くにあたり、こちらを参考にしています。

TicketMonster Tutorial Adding a full-text search engine
http://www.jboss.org/jdf/examples/ticket-monster/tutorial/HibernateSearch/

JPAの設定

JPAの設定ファイルに、Hibernate Searchの設定を追加します。
src/main/resources/META-INF/persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
                                 http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
             version="2.1">
  <persistence-unit name="javaee7.web.pu" transaction-type="JTA">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <jta-data-source>java:jboss/datasources/mysqlXaDs</jta-data-source>
    <properties>
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
      <property name="hibernate.show_sql" value="true" />
      <property name="hibernate.format_sql" value="true" />

      <!-- Hibernate Search -->
      <property name="hibernate.search.default.directory_provider" value="infinispan" />
      <property name="hibernate.search.lucene_version" value="LUCENE_36" />
    </properties>
  </persistence-unit>
</persistence>

「hibernate.search.lucene_version」はなくても動きますが、何も書かないと延々と警告されるので。「hibernate.search.default.directory_provider」で、インデックスの保存先(というかLuceneのDirectoryの実装)を設定します。

Entity

ここから書くコードは、先のエントリ

JPAのSecond Level Cacheを試してみる
http://d.hatena.ne.jp/Kazuhira/20140215/1392453183

で書いた内容をベースにしています。ただ、L2キャッシュの部分は取り外しました。

Entityのコード。
src/main/scala/org/littlewings/javaee7/entity/Book.scala

package org.littlewings.javaee7.entity

import scala.beans.BeanProperty

import javax.persistence.{Column, Entity, Id, Table, Version}

import org.apache.lucene.analysis.ja.JapaneseAnalyzer
import org.hibernate.search.annotations.{Analyze, Analyzer, Field, Index, Indexed}

@SerialVersionUID(1L)
@Entity
@Table(name = "book")
@Indexed
class Book extends Serializable {
  @Id
  @Column
  @Field(analyze = Analyze.NO)
  @BeanProperty
  var isbn: String = _

  @Column
  @Field
  @Analyzer(impl = classOf[JapaneseAnalyzer])
  @BeanProperty
  var title: String = _

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

  @Column
  @Field
  @Analyzer(impl = classOf[JapaneseAnalyzer])
  @BeanProperty
  var summary: String = _

  @Column(name = "version_no")
  @BeanProperty
  @Version
  var versionNo: Int = _

  override def toString: String =
    s"isbn = $isbn, title = $title, price = $price, summary = $summary, versionNo = $versionNo"
}

Entityのクラス宣言の部分に、@Indexedアノテーションを付与しています。

@Indexed
class Book extends Serializable {

テーマは書籍ですが、タイトルと概要はKuromojiで形態素解析するように設定。

  @Column
  @Field
  @Analyzer(impl = classOf[JapaneseAnalyzer])
  @BeanProperty
  var title: String = _

インデックスの初期化

サンプルに習い、起動時にインデックスを再作成するクラスを作成しました。ここだけ、EJB…。
src/main/scala/org/littlewings/javaee7/bootstrap/ContextInitializer.scala

package org.littlewings.javaee7.bootstrap

import javax.annotation.PostConstruct
import javax.ejb.{Singleton, Startup}
import javax.persistence.{EntityManager, PersistenceContext}

import org.hibernate.search.jpa.Search

@Singleton
@Startup
class ContextInitializer {
  @PersistenceContext
  private var em: EntityManager = _

  @PostConstruct
  def initialize(): Unit = {
    val fullTextEm = Search.getFullTextEntityManager(em)
    fullTextEm.createIndexer().purgeAllOnStart(true).startAndWait()
  }
}

起動時に、EntityManagerからHibernate SearchのFullTextEntityManagerを取得し、インデックスの再作成を行うコードです。

ただ、この部分は後のクラスタ化の際に取り外すことになります。

FullTextEntityManagerを取得する

他のクラスで@InjectしたEntityManagerから毎回FullTextEntityManager取得してもいいのですが、面倒なのでFullTextEntityManagerを取得するこんなクラスを定義。
src/main/scala/org/littlewings/javaee7/cdi/FullTextEntityManagerProducer.scala

package org.littlewings.javaee7.cdi

import javax.enterprise.context.Dependent
import javax.enterprise.inject.Produces
import javax.persistence.{EntityManager, PersistenceContext}

import org.hibernate.search.jpa.{Search, FullTextEntityManager}

@Dependent
class FullTextEntityManagerProducer {
  @PersistenceContext
  private var em: EntityManager = _

  @Produces
  def createFullTextEntityManager: FullTextEntityManager =
    Search.getFullTextEntityManager(em)
}

これで、FullTextEntityManagerが@Injectできるようになります。

検索を行うクラス

実際に、Hibernate SearchのAPIを使用して、検索を行うクラスを作成します。
src/main/scala/org/littlewings/javaee7/service/BookSearchService.scala

package org.littlewings.javaee7.service

import scala.collection.JavaConverters._

import javax.enterprise.context.RequestScoped
import javax.inject.Inject

import org.apache.lucene.search.Query
import org.hibernate.search.jpa.FullTextEntityManager

import org.littlewings.javaee7.entity.Book

@RequestScoped
class BookSearchService {
  @Inject
  private var fullTextEm: FullTextEntityManager = _

  def indexing(): Unit =
    fullTextEm.createIndexer().startAndWait()

  def search(title: String, summary: String): (Query, Iterable[Book]) = {
    val queryBuilder =
      fullTextEm
        .getSearchFactory
        .buildQueryBuilder
        .forEntity(classOf[Book])
        .get

    val queries =
      List(
        Option(title).map { t =>
          queryBuilder
            .keyword
            .onField("title")
            .matching(t)
            .createQuery
        },
        Option(summary).map { s =>
          queryBuilder
            .keyword
            .onField("summary")
            .matching(s)
            .createQuery
        }
      ).flatten

    val booleanJunction = queryBuilder.bool
    val luceneQuery =
      queries match {
        case titleQuery :: summaryQuery :: Nil =>
          booleanJunction.should(titleQuery).should(summaryQuery).createQuery
        case query :: Nil =>
          booleanJunction.should(query).createQuery
        case _ =>
          throw new IllegalArgumentException(s"Unknown Query[${queries.toString}")
      }

    (luceneQuery,
     fullTextEm
       .createFullTextQuery(luceneQuery, classOf[Book])
       .getResultList
       .asScala
       .asInstanceOf[Iterable[Book]])
  }
}

ここは、普通にHibernate SearchのAPIを使っているだけです。まあ、Scalaでいろいろやってますが…。

一応、ここでは検索対象はタイトルと概要ですね。指定された方、または両方をORでクエリに加えます。あと、ここでもインデックス作成の処理を定義してたりします…。

このServiceクラスを使用する、JAX-RSのリソースクラス。
src/main/scala/org/littlewings/javaee7/rest/BookSearchResource.scala

package org.littlewings.javaee7.rest

import scala.collection.JavaConverters._

import javax.inject.Inject
import javax.ws.rs.{DELETE, GET, Path, PathParam, POST, PUT, Produces, QueryParam}
import javax.ws.rs.core.MediaType

import org.littlewings.javaee7.entity.Book
import org.littlewings.javaee7.service.BookSearchService

@Path("book/search")
class BookSearchResource {
  @Inject
  private var bookSearchService: BookSearchService = _

  @GET
  @Produces(Array(MediaType.APPLICATION_JSON))
  def search(@QueryParam("title") title: String, @QueryParam("summary") summary: String): java.util.Map[String, AnyRef] = {
    val result = bookSearchService.search(title, summary)

    Map("query" -> result._1.toString,
        "hits" -> new Integer(result._2.size),
        "books" -> result._2.asJava)
          .asJava
  }

  @POST
  @Path("indexing")
  @Produces(Array(MediaType.TEXT_PLAIN))
  def indexing: String = {
    bookSearchService.indexing()
    "OK" + System.lineSeparator
  }
}

検索リクエストは、どんなクエリを投げたのかもわかるようにしておきます。

その他、通常のデータベースに対するCRUDを行うServiceや、それを利用するJAX-RSのクラスは

JPAのSecond Level Cacheを試してみる
http://d.hatena.ne.jp/Kazuhira/20140215/1392453183

で作成したものをそのまま使用します。

動作確認

ここで作成したWARファイルを、WildFlyにデプロイします。

デプロイすると、起動時にEJBが初期化を行うため、こんな感じでHibernate Searchがデータ取得を行っていることが確認できます。

21:51:59,204 INFO  [stdout] (Hibernate Search: identifierloader-1) Hibernate: 
21:51:59,205 INFO  [stdout] (Hibernate Search: identifierloader-1)     select
21:51:59,206 INFO  [stdout] (Hibernate Search: identifierloader-1)         count(*) as y0_ 
21:51:59,206 INFO  [stdout] (Hibernate Search: identifierloader-1)     from
21:51:59,206 INFO  [stdout] (Hibernate Search: identifierloader-1)         book this_
21:51:59,210 INFO  [org.hibernate.search.impl.SimpleIndexingProgressMonitor] (Hibernate Search: identifierloader-1) HSEARCH000027: Going to reindex 6 entities
21:51:59,211 INFO  [stdout] (Hibernate Search: identifierloader-1) Hibernate: 
21:51:59,212 INFO  [stdout] (Hibernate Search: identifierloader-1)     select
21:51:59,212 INFO  [stdout] (Hibernate Search: identifierloader-1)         this_.isbn as y0_ 
21:51:59,212 INFO  [stdout] (Hibernate Search: identifierloader-1)     from
21:51:59,213 INFO  [stdout] (Hibernate Search: identifierloader-1)         book this_
21:51:59,216 INFO  [stdout] (Hibernate Search: entityloader-1) Hibernate: 
21:51:59,217 INFO  [stdout] (Hibernate Search: entityloader-1)     select
21:51:59,217 INFO  [stdout] (Hibernate Search: entityloader-1)         this_.isbn as isbn1_0_0_,
21:51:59,217 INFO  [stdout] (Hibernate Search: entityloader-1)         this_.price as price2_0_0_,
21:51:59,217 INFO  [stdout] (Hibernate Search: entityloader-1)         this_.summary as summary3_0_0_,
21:51:59,217 INFO  [stdout] (Hibernate Search: entityloader-1)         this_.title as title4_0_0_,
21:51:59,217 INFO  [stdout] (Hibernate Search: entityloader-1)         this_.version_no as version_5_0_0_ 
21:51:59,218 INFO  [stdout] (Hibernate Search: entityloader-1)     from
21:51:59,218 INFO  [stdout] (Hibernate Search: entityloader-1)         book this_ 
21:51:59,218 INFO  [stdout] (Hibernate Search: entityloader-1)     where
21:51:59,218 INFO  [stdout] (Hibernate Search: entityloader-1)         this_.isbn in (
21:51:59,218 INFO  [stdout] (Hibernate Search: entityloader-1)             ?, ?, ?, ?, ?, ?
21:51:59,219 INFO  [stdout] (Hibernate Search: entityloader-1)         )

で、インデキシングが終わりましたよと。

21:51:59,558 INFO  [org.hibernate.search.impl.SimpleIndexingProgressMonitor] (ServerService Thread Pool -- 76) HSEARCH000028: Reindexed 6 entities

なお、入っているデータは前回同様、このようなJSONから作成したデータです。
book.json

[
    {
        "isbn": "978-4798124605",
        "title": "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava",
        "price": 4410,
        "summary": "エンタープライズJava入門書の決定版!Java EE 6は、大規模な情報システム構築に用いられるエンタープライズ環境向けのプログラミング言語です。"
    },
    {
        "isbn": "978-4798120546",
        "title": "マスタリングJavaEE5 第2版",
        "price": 5670,
        "summary": "EJB3.0、JPA、JSF、Webサービスを完全網羅。新たにJBoss AS、Hibernateにも対応!JavaEE5は、J2EEの高い機能性はそのままに、アプリケーションの開発生産性を高めることを主眼とした、サーバサイドJavaにおけるプラットフォーム、開発、デプロイメントに関する標準仕様です。"
    },
    {
        "isbn": "978-4873114675",
        "title": "JavaによるRESTfulシステム構築",
        "price": 3360,
        "summary": "Java EE 6でサポートされたJAX-RSの特徴とRESTfulアーキテクチャ原則を使って、Javaでの分散Webサービスを設計開発する方法を学ぶ書籍。"
    },
    {
        "isbn": "978-4774127804",
        "title": "Apache Lucene 入門 Java・オープンソース・全文検索システムの構築",
        "price": 3360,
        "summary": "Luceneは全文検索システムを構築するためのJavaのライブラリです。Luceneを使えば,一味違う高機能なWebアプリケーションを作ることができます。"
    },
    {
        "isbn": "978-4774161631",
        "title": "[改訂新版] Apache Solr入門 オープンソース全文検索エンジン",
        "price": 3780,
        "summary": "最新版Apaceh Solr Ver.4.5.1に対応するため大幅な書き直しと原稿の追加を行い、現在の開発環境に合わせて完全にアップデートしました。Apache Solrは多様なプログラミング言語に対応した全文検索エンジンです。"
    },
    {
        "isbn": "978-1933988177",
        "title": "Lucene in Action",
        "price": 5548,
        "summary": "When Lucene first hit the scene five years ago, it was nothing short of amazing. By using this open-source, highly scalable, super-fast search engine, developers could integrate search into applications quickly and efficiently."
    }
]

クエリに、titleを「java」、summaryを全文検索で検索。

$ curl "http://localhost:8080/javaee7-web/rest/book/search?title=java&summary=%E5%85%A8%E6%96%87%E6%A4%9C%E7%B4%A2"
{"query":"title:java (summary:全文 summary:検索)","hits":4,"books":[{"isbn":"978-4774127804","title":"Apache Lucene 入門 Java・オープンソース・全文検索システムの構築","price":3360,"summary":"Luceneは全文検索システムを構築するためのJavaのライブラリです。Luceneを使えば,一味違う高機能なWebアプリケーションを作ることができます。","versionNo":0},{"isbn":"978-4873114675","title":"JavaによるRESTfulシステム構築","price":3360,"summary":"Java EE 6でサポートされたJAX-RSの特徴とRESTfulアーキテクチャ原則を使って、Javaでの分散Webサービスを設計開発する方法を学ぶ書籍。","versionNo":0},{"isbn":"978-4774161631","title":"[改訂新版] Apache Solr入門 オープンソース全文検索エンジン","price":3780,"summary":"最新版Apaceh Solr Ver.4.5.1に対応するため大幅な書き直しと原稿の追加を行い、現在の開発環境に合わせて完全にアップデートしました。Apache Solrは多様なプログラミング言語に対応した全文検索エンジンです。","versionNo":0},{"isbn":"978-4798124605","title":"Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava","price":4410,"summary":"エンタープライズJava入門書の決定版!Java EE 6は、大規模な情報システム構築に用いられるエンタープライズ環境向けのプログラミング言語です。","versionNo":0}]}

クエリは、こんな感じになりました。

"query":"title:java (summary:全文 summary:検索)"

summaryの方は、ちゃんと形態素解析されてますね。

titleだけを指定して、「lucene」で。

$ curl "http://localhost:8080/javaee7-web/rest/book/search?title=lucene"
{"query":"title:lucene","hits":2,"books":[{"isbn":"978-1933988177","title":"Lucene in Action","price":5548,"summary":"When Lucene first hit the scene five years ago, it was nothing short of amazing. By using this open-source, highly scalable, super-fast search engine, developers could integrate search into applications quickly and efficiently.","versionNo":0},{"isbn":"978-4774127804","title":"Apache Lucene 入門 Java・オープンソース・全文検索システムの構築","price":3360,"summary":"Luceneは全文検索システムを構築するためのJavaのライブラリです。Luceneを使えば,一味違う高機能なWebアプリケーションを作ることができます。","versionNo":0}]}

2件ヒット。

ちゃんと動いてそうですね。

WildFlyにCache Containerを定義してデプロイする

続いて、せっかくInfinispanにインデックスを保存しているのですから、今度はクラスタにしてみます。

なんですけど、先ほどまでのコードでは、Infinispanの設定はまったく行っていませんでした。Hibernate Searchが自身で持つデフォルトのInfinispanの設定で、デプロイの度に毎回新しくInfinispanを起動していたからです。

それはそれでもいいのですが、デプロイ/アンデプロイを繰り返すとJMX上で増えてみたりしますし、設定もしてみたいので以下のファイルに専用のCache Containerを定義することにしました。

wildfly-8.0.0.Final/standalone/configuration/standalone-ha.xml

今回は、Infinispanサブシステムに以下のような設定を追加します。

            <cache-container name="hibernate-search" default-cache="lucene-index" jndi-name="java:jboss/infinispan/container/hibernateSearch" start="EAGER">
                <transport lock-timeout="60000"/>
                <replicated-cache name="lucene-indexes-locking" mode="SYNC">
                    <transaction mode="NONE"/>
                    <eviction strategy="NONE"/>
                </replicated-cache>
                <replicated-cache name="lucene-indexes-metadata" mode="SYNC">
                    <transaction mode="NON_XA"/>
                    <eviction strategy="NONE"/>
                </replicated-cache>
                <distributed-cache name="lucene-indexes-data" mode="SYNC">
                    <transaction mode="NON_XA"/>
                    <eviction strategy="NONE"/>
                </distributed-cache>
            </cache-container>

JNDI名を与え、「start="EAGER"」とするところが、たぶんポイントかと…。

設定は、ここを見ながら。
https://docs.jboss.org/author/display/WFLY8/Infinispan+Subsystem

そのうち、ここも見るはず…。
https://docs.jboss.org/author/display/WFLY8/JGroups+Subsystem

ところで、transaction modeの書き方が通常のInfinispanの設定からは見慣れないものになっていますが、こういうことらしいです…。

NONE - no transactions are used in any sense
NON_XA - it maps to the following settings in Infinispan: transaction.useSynchronization == true, transaction.recovery.enabled == false
NON_DURABLE_XA - transaction.useSynchronization == false, transaction.recovery.enabled == false, transaction.syncCommitPhase == true, transaction.syncRollbackPhase == true
FULL_XA - transaction.syncCommitPhase == true, transaction.syncRollbackPhase == true, transaction.useSynchronization == false, transaction.recovery.enabled == true

https://community.jboss.org/thread/201248?tstart=0

そして、このCache Containerを使用するために、persistence.xmlを以下の様に変更します。
src/main/resources/META-INF/persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
                                 http://www.oracle.com/webfolder/technetwork/jsc/xml/ns/persistence/persistence_2_1.xsd">
  <persistence-unit name="javaee7.web.pu" transaction-type="JTA">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <jta-data-source>java:jboss/datasources/mysqlXaDs</jta-data-source>
    <properties>
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
      <property name="hibernate.show_sql" value="true" />
      <property name="hibernate.format_sql" value="true" />

      <!-- Hibernate Search -->
      <property name="hibernate.search.default.directory_provider" value="infinispan" />
      <property name="hibernate.search.lucene_version" value="LUCENE_36" />

      <!-- Infinispan Hibernate Search Integration -->
      <property name="hibernate.search.infinispan.cachemanager_jndiname" value="java:jboss/infinispan/container/hibernateSearch" />
      <property name="hibernate.search.default.locking_cachename" value="lucene-indexes-locking" />
      <property name="hibernate.search.default.data_cachename" value="lucene-indexes-data" />
      <property name="hibernate.search.default.metadata_cachename" value="lucene-indexes-metadata" />
      <property name="hibernate.search.default.chunk_size" value="16384" />
    </properties>
  </persistence-unit>
</persistence>

JNDI名の設定と、各キャッシュの名前を明示的に設定しました。

      <!-- Infinispan Hibernate Search Integration -->
      <property name="hibernate.search.infinispan.cachemanager_jndiname" value="java:jboss/infinispan/container/hibernateSearch" />
      <property name="hibernate.search.default.locking_cachename" value="lucene-indexes-locking" />
      <property name="hibernate.search.default.data_cachename" value="lucene-indexes-data" />
      <property name="hibernate.search.default.metadata_cachename" value="lucene-indexes-metadata" />
      <property name="hibernate.search.default.chunk_size" value="16384" />

キャッシュの名前は、指定しなければデフォルトになるのでそれでもいいですが、Cache Containerでデフォルトの名前で設定することになりますが…。

参考)
3.3.1. Infinispan Directory configuration
http://docs.jboss.org/hibernate/search/4.5/reference/en-US/html_single/#infinispan-directories

あとは、デプロイすれば、このCache Containerを使うように動作してくれます。

WildFlyを起動させる時は、standalone-ha.xmlを使用するので、以下のようになります。

$ wildfly-8.0.0.Final/bin/standalone.sh -c standalone-ha.xml

クラスタ化する

最後、Infinispanのクラスタリングへ。

クラスタ化する前に、以下のコードに変更を加えます。

@Singleton
@Startup
class ContextInitializer {
  @PersistenceContext
  private var em: EntityManager = _

  @PostConstruct
  def initialize(): Unit = {
    /* インデックスの保存先をInfinispanにして、
     * かつクラスタにする場合はこのコードは外す
    val fullTextEm = Search.getFullTextEntityManager(em)
    fullTextEm.createIndexer().purgeAllOnStart(true).startAndWait()
     */
  }
}

起動時に、インデックスの初期化をしないようにします。これは、LuceneのIndexWriterを複数持てないためで、後から起動したノードがIndexWriterのロックを取れずに

20:46:28,248 ERROR [org.hibernate.search.exception.impl.LogErrorHandler] (Hibernate Search: Index updates queue processor for index org.littlewings.javaee7.entity.Book-1) HSEARCH000058: Exception occurred org.apache.lucene.store.LockObtainFailedException: Lock obtain timed out: org.infinispan.lucene.locking.BaseLuceneLock@d0fd340
Primary Failure:
	Entity org.littlewings.javaee7.entity.Book  Id null  Work Type  org.hibernate.search.backend.PurgeAllLuceneWork

とか

20:46:28,253 ERROR [org.hibernate.search.backend.impl.lucene.LuceneBackendQueueTask] (Hibernate Search: Index updates queue processor for index org.littlewings.javaee7.entity.Book-1) HSEARCH000072: Couldn't open the IndexWriter because of previous error: operation skipped, index ouf of sync!
20:46:29,255 ERROR [org.hibernate.search.exception.impl.LogErrorHandler] (ServerService Thread Pool -- 56) HSEARCH000058: HSEARCH000117: IOException on the IndexWriter: org.apache.lucene.store.LockObtainFailedException: Lock obtain timed out: org.infinispan.lucene.locking.BaseLuceneLock@d0fd340

みたいなエラーを見ることになります。

で、それを修正したらWARにして、とりあえずWildFlyを停止してWARファイルを消去。

$ rm wildfly-8.0.0.Final/standalone/deployments/javaee7-web.war*

Cache Containerの設定を含み、同じ構成のWildFlyのコピーを作成します。

$ cp -Rp wildfly-8.0.0.Final wildfly-8.0.0.Final-2

それぞれを、以下の様に起動します。

# Node1
$ wildfly-8.0.0.Final/bin/standalone.sh -c standalone-ha.xml -Djboss.node.name=node1

# Node2
$ wildfly-8.0.0.Final-2/bin/standalone.sh -c standalone-ha.xml -Djboss.socket.binding.port-offset=1000 -Djboss.node.name=node2

2つ目のノードは、ポートを1,000ずらしています。

また、ノード名を変えているのは、何もしないとクラスタ上でのノード名が衝突するからです。

起動したら、それぞれにWARファイルをデプロイして、どちらかのノードでインデックスを初期化します。

$ curl -X POST http://localhost:8080/javaee7-web/rest/book/search/indexing
OK

これができると、クラスタになっているので、もう片方のノードからも全文検索の実施が可能になります。

$ curl "http://localhost:9080/javaee7-web/rest/book/search?title=java&summary=%E5%85%A8%E6%96%87%E6%A4%9C%E7%B4%A2"
{"query":"title:java (summary:全文 summary:検索)","hits":4,"books":[{"isbn":"978-4774127804","title":"Apache Lucene 入門 Java・オープンソース・全文検索システムの構築","price":3360,"summary":"Luceneは全文検索システムを構築するためのJavaのライブラリです。Luceneを使えば,一味違う高機能なWebアプリケーションを作ることができます。","versionNo":0},{"isbn":"978-4873114675","title":"JavaによるRESTfulシステム構築","price":3360,"summary":"Java EE 6でサポートされたJAX-RSの特徴とRESTfulアーキテクチャ原則を使って、Javaでの分散Webサービスを設計開発する方法を学ぶ書籍。","versionNo":0},{"isbn":"978-4774161631","title":"[改訂新版] Apache Solr入門 オープンソース全文検索エンジン","price":3780,"summary":"最新版Apaceh Solr Ver.4.5.1に対応するため大幅な書き直しと原稿の追加を行い、現在の開発環境に合わせて完全にアップデートしました。Apache Solrは多様なプログラミング言語に対応した全文検索エンジンです。","versionNo":0},{"isbn":"978-4798124605","title":"Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava","price":4410,"summary":"エンタープライズJava入門書の決定版!Java EE 6は、大規模な情報システム構築に用いられるエンタープライズ環境向けのプログラミング言語です。","versionNo":0}]}

だいぶ長くなりましたが、こんなところで。

今回作成したソースコードは、こちらにアップしています。

https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/hibernate-search-with-jpa