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/Hibernate/Scalaの本を登録する、ということにします。
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を作成します。今回は、
という条件で、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件、ヒットしました、と。
少々ScalaとHibernate 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でなければならず、レプリケーションやローカルキャッシュでは使用できないということらしいです