CLOVER🍀

That was when it all began.

HazelcastのQueryを試す

HazelcastのQueryについて。3.0から追加されたContinuous Queryについては、今回は対象外にします。

Query
http://www.hazelcast.com/docs/3.1/manual/single_html/#MapQuery

そもそもHazelcastのQueryって何だね?ってところですが、

  • IMap(分散Map)に対して、クエリが投げられる
  • 構文は、SQLのWHERE句ライクなものか、Criteria APIが使用可能
  • (ドキュメントに記載はありませんが、おそらく)IMapに格納するオブジェクトは、JavaBeansが想定されているものと思われます
  • 検索する属性は、Comparableである必要があります(こちらも、ドキュメントには書かれていませんが)
  • インデックスの作成も可能

比較的簡単な属性が検索できる機能、というイメージでよいと思います。コレクションなどは、検索属性の対象外だと。

準備

今回も、Clojure+Leiningenでいきます。

$ lein new app hazelcast-query

project.cljはこんな感じです。

(defproject hazelcast-query "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.5.1"]
                 [com.hazelcast/hazelcast "3.1"]]
  :main ^:skip-aot hazelcast-query.core
  :target-path "target/%s"
  :aot :all
  :profiles {:uberjar {:aot :all}})

検索対象のクラス

ネタは、書籍ということで。
src/hazelcast_query/book.clj

(ns hazelcast-query.book
  (:import (java.io Serializable)
           (java.util Map)))

(gen-class :name hazelcast-query.book.Book
           :implements [java.io.Serializable]
           :state state
           :init init
           :constructors {[java.util.Map] []}
           :methods [[getIsbn13 [] String]
                     [getName [] String]
                     [getPrice [] int]
                     [getPublishDate [] String]
                     [getCategory [] String]
                     [isOutOfPrint [] boolean]])

(defn -init [underlying]
  [[] underlying])

(defn -getIsbn13 [this]
  (:isbn13 (. this state)))

(defn -getName [this]
  (:name (. this state)))

(defn -getPrice [this]
  ;; (println "price called")
  (:price (. this state)))

(defn -getPublishDate [this]
  (:publish-date (. this state)))

(defn -getCategory [this]
  (:category (. this state)))

(defn -isOutOfPrint [this]
  (:out-of-print (. this state)))

(defn -toString [this]
  (str (. this state)))

IMapの値として放り込まれるので、Serializableである必要があります。

属性としては、ISBN、名前、価格、発売日、カテゴリ、絶版を持ちます。
Amazon調べ

HazelcastInstanceの用意とIMapへのデータ登録

ここで使用するクラス定義およびimport文は、こんな感じです。

(ns hazelcast-query.core
  (:gen-class)
  (:import (com.hazelcast.config Config MapConfig MapIndexConfig)
           (com.hazelcast.core Hazelcast HazelcastInstance IMap)
           (com.hazelcast.query EntryObject Predicate PredicateBuilder SqlPredicate)
           (hazelcast-query.book Book)))

それでは、まずHazelcastInstanceは取得済みなものとして

      (let [^HazelcastInstance hazelcast (Hazelcast/newHazelcastInstance config)
            ^IMap book-map (. hazelcast getMap "book-map")]

適当に検索対象のデータを登録します。

        ;; 適当に書籍を登録
        (. book-map put "978-1782167303" (Book. {:isbn13 "978-1782167303"
                                                 :name "Getting Started with Hazelcast"
                                                 :price 4147
                                                 :publish-date "2013-08-27"
                                                 :category "IMDG"
                                                 :out-of-print false}))
        (. book-map put "978-1849518222" (Book. {:isbn13 "978-1849518222"
                                                 :name "Infinispan Data Grid Platform"
                                                 :price 3115
                                                 :publish-date "2012-06-30"
                                                 :category "IMDG"
                                                 :out-of-print false}))
        (. book-map put "978-4274069130" (Book. {:isbn13 "978-4274069130"
                                                 :name "プログラミングClojure 第2版"
                                                 :price 3570
                                                 :publish-date "2013-04-26"
                                                 :category "Clojure"
                                                 :out-of-print false}))
        (. book-map put "978-4774159911" (Book. {:isbn13 "978-4774159911"
                                                 :name "おいしいClojure入門"
                                                 :price 2919
                                                 :publish-date "2013-09-26"
                                                 :category "Clojure"
                                                 :out-of-print false}))
        (. book-map put "978-4774127804" (Book. {:isbn13 "978-4774127804"
                                                 :name "Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築"
                                                 :price 3360
                                                 :publish-date "2006-05-17"
                                                 :category "FullTextSearch"
                                                 :out-of-print true}))
        (. book-map put "978-4774141756" (Book. {:isbn13 "978-4774141756"
                                                 :name "Apache Solr入門 ―オープンソース全文検索エンジン"
                                                 :price 3780
                                                 :publish-date "2010-02-20"
                                                 :category "FullTextSearch"
                                                 :out-of-print false}))

SQLライクな検索

SQLライクな検索を行うためには、SqlPredicateクラスを使用します。

              (let [pred (SqlPredicate. query-string)]
                (doseq [v (. book-map values pred)] ;; keySet、entrySetなどにも使用可能
                  (println (format "  Result: %s" v))))

SqlPredicateクラスは、コンストラクタにWHERE句的な文字列を取ります。このSqlPredicateクラスのインスタンス(正確には、Predicateインターフェースを実装したクラスのインスタンス)をIMapのvalues、keySet、entrySetに与えることで、検索が可能です。もちろん、戻り値は各メソッドにより異なる結果となります。

これに対して、いくつかコンソールからクエリを受け取って実行するプログラムを書きます。

        (doseq [query-string (take-while #(not (= "exit" %))
                                         (repeatedly #(do (print "Query> ")
                                                          (flush)
                                                          (read-line))))]
          (when (and (not (nil? query-string))
                     (not (empty? query-string)))
            (try
              (println (format "Input Query => [%s]" query-string))
              (let [pred (SqlPredicate. query-string)]
                (doseq [v (. book-map values pred)] ;; keySet、entrySetなどにも使用可能
                  (println (format "  Result: %s" v))))
              (catch Exception e
                (println (format "Bad Query[%s], Reason[%s]" query-string e))))))

受け付けたクエリを、SqlPredicateのコンストラクタ引数へと渡して、取得結果(values)を単純に表示します。「exit」で終了です。

では、試してみます。

$ lein run

〜省略〜

Members [1] {
	Member [192.168.129.128]:5701 this
}

10 20, 2013 6:29:17 午後 com.hazelcast.core.LifecycleService
情報: [192.168.129.128]:5701 [dev] Address[192.168.129.128]:5701 is STARTED
10 20, 2013 6:29:17 午後 com.hazelcast.partition.PartitionService
情報: [192.168.129.128]:5701 [dev] Initializing cluster partition table first arrangement...
Query> 

Hazelcastのクエリとしては、

  • AND/OR
  • =、 !=、 <、 <=、 >、 >=
  • BETWEEN
  • LIKE
  • IN

を取ることができます。詳細はドキュメントを見ていただきたいですが、ちょっと変わっているのはbooleanでtrueの属性に対しては検索を行う場合は、特に比較演算子を書く必要がありません。

少し、試してみましょう。

booleanの属性に対して、検索

Query> outOfPrint
Input Query => [outOfPrint]
  Result: {:isbn13 "978-4774127804", :publish-date "2006-05-17", :name "Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築", :out-of-print true, :price 3360, :category "FullTextSearch"}


Query> outOfPrint = false
Input Query => [outOfPrint = false]
  Result: {:isbn13 "978-1782167303", :publish-date "2013-08-27", :name "Getting Started with Hazelcast", :out-of-print false, :price 4147, :category "IMDG"}
  Result: {:isbn13 "978-1849518222", :publish-date "2012-06-30", :name "Infinispan Data Grid Platform", :out-of-print false, :price 3115, :category "IMDG"}
  Result: {:isbn13 "978-4774159911", :publish-date "2013-09-26", :name "おいしいClojure入門", :out-of-print false, :price 2919, :category "Clojure"}
  Result: {:isbn13 "978-4274069130", :publish-date "2013-04-26", :name "プログラミングClojure 第2版", :out-of-print false, :price 3570, :category "Clojure"}
  Result: {:isbn13 "978-4774141756", :publish-date "2010-02-20", :name "Apache Solr入門 ―オープンソース全文検索エンジン", :out-of-print false, :price 3780, :category "FullTextSearch"}

比較演算子とANDを使った検索。

Query> price > 3000 AND category = 'Clojure'
Input Query => [price > 3000 AND category = 'Clojure']
  Result: {:isbn13 "978-4274069130", :publish-date "2013-04-26", :name "プログラミングClojure 第2版", :out-of-print false, :price 3570, :category "Clojure"}

booleanな属性とANDを使った検索。

Query> outOfPrint AND category = 'Clojure'
Input Query => [outOfPrint AND category = 'Clojure']

Clojureで絶版な本はありませんので、結果は0件です。

BETWEEN。

Query> price BETWEEN 2500 AND 3500
Input Query => [price BETWEEN 2500 AND 3500]
  Result: {:isbn13 "978-1849518222", :publish-date "2012-06-30", :name "Infinispan Data Grid Platform", :out-of-print false, :price 3115, :category "IMDG"}
  Result: {:isbn13 "978-4774159911", :publish-date "2013-09-26", :name "おいしいClojure入門", :out-of-print false, :price 2919, :category "Clojure"}
  Result: {:isbn13 "978-4774127804", :publish-date "2006-05-17", :name "Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築", :out-of-print true, :price 3360, :category "FullTextSearch"}

NOT BETWEEN。

Query> price NOT BETWEEN 2500 AND 3500
Input Query => [price NOT BETWEEN 2500 AND 3500]
  Result: {:isbn13 "978-1782167303", :publish-date "2013-08-27", :name "Getting Started with Hazelcast", :out-of-print false, :price 4147, :category "IMDG"}
  Result: {:isbn13 "978-4274069130", :publish-date "2013-04-26", :name "プログラミングClojure 第2版", :out-of-print false, :price 3570, :category "Clojure"}
  Result: {:isbn13 "978-4774141756", :publish-date "2010-02-20", :name "Apache Solr入門 ―オープンソース全文検索エンジン", :out-of-print false, :price 3780, :category "FullTextSearch"}

LIKE。SQLと同様、「%」や「_」が使用できます。

Query> name LIKE 'おいしい%'
Input Query => [name LIKE 'おいしい%']
  Result: {:isbn13 "978-4774159911", :publish-date "2013-09-26", :name "おいしいClojure入門", :out-of-print false, :price 2919, :category "Clojure"}
Query> category LIKE 'Clojur_'
Input Query => [category LIKE 'Clojur_']
  Result: {:isbn13 "978-4774159911", :publish-date "2013-09-26", :name "おいしいClojure入門", :out-of-print false, :price 2919, :category "Clojure"}
  Result: {:isbn13 "978-4274069130", :publish-date "2013-04-26", :name "プログラミングClojure 第2版", :out-of-print false, :price 3570, :category "Clojure"}

ドキュメントに書いていませんが、部分一致も可能です。

Query> name LIKE '%Java%'
Input Query => [name LIKE '%Java%']
  Result: {:isbn13 "978-4774127804", :publish-date "2006-05-17", :name "Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築", :out-of-print true, :price 3360, :category "FullTextSearch"}

まあ、やらない方がいいと思いますが。

NOT LIKE。

Query> name NOT LIKE 'おいしい%'
Input Query => [name NOT LIKE 'おいしい%']
  Result: {:isbn13 "978-1782167303", :publish-date "2013-08-27", :name "Getting Started with Hazelcast", :out-of-print false, :price 4147, :category "IMDG"}
  Result: {:isbn13 "978-1849518222", :publish-date "2012-06-30", :name "Infinispan Data Grid Platform", :out-of-print false, :price 3115, :category "IMDG"}
  Result: {:isbn13 "978-4274069130", :publish-date "2013-04-26", :name "プログラミングClojure 第2版", :out-of-print false, :price 3570, :category "Clojure"}
  Result: {:isbn13 "978-4774127804", :publish-date "2006-05-17", :name "Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築", :out-of-print true, :price 3360, :category "FullTextSearch"}
  Result: {:isbn13 "978-4774141756", :publish-date "2010-02-20", :name "Apache Solr入門 ―オープンソース全文検索エンジン", :out-of-print false, :price 3780, :category "FullTextSearch"}

IN。

Query> category IN ('Clojure', 'IMDG')
Input Query => [category IN ('Clojure', 'IMDG')]
  Result: {:isbn13 "978-1782167303", :publish-date "2013-08-27", :name "Getting Started with Hazelcast", :out-of-print false, :price 4147, :category "IMDG"}
  Result: {:isbn13 "978-1849518222", :publish-date "2012-06-30", :name "Infinispan Data Grid Platform", :out-of-print false, :price 3115, :category "IMDG"}
  Result: {:isbn13 "978-4774159911", :publish-date "2013-09-26", :name "おいしいClojure入門", :out-of-print false, :price 2919, :category "Clojure"}
  Result: {:isbn13 "978-4274069130", :publish-date "2013-04-26", :name "プログラミングClojure 第2版", :out-of-print false, :price 3570, :category "Clojure"}


Query> price IN (4147, 3570)
Input Query => [price IN (4147, 3570)]
  Result: {:isbn13 "978-1782167303", :publish-date "2013-08-27", :name "Getting Started with Hazelcast", :out-of-print false, :price 4147, :category "IMDG"}
  Result: {:isbn13 "978-4274069130", :publish-date "2013-04-26", :name "プログラミングClojure 第2版", :out-of-print false, :price 3570, :category "Clojure"}

NOT IN。

Query> category NOT IN ('Clojure', 'IMDG')
Input Query => [category NOT IN ('Clojure', 'IMDG')]
  Result: {:isbn13 "978-4774127804", :publish-date "2006-05-17", :name "Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築", :out-of-print true, :price 3360, :category "FullTextSearch"}
  Result: {:isbn13 "978-4774141756", :publish-date "2010-02-20", :name "Apache Solr入門 ―オープンソース全文検索エンジン", :out-of-print false, :price 3780, :category "FullTextSearch"}


Query> price NOT IN (4147, 3570)
Input Query => [price NOT IN (4147, 3570)]
  Result: {:isbn13 "978-1849518222", :publish-date "2012-06-30", :name "Infinispan Data Grid Platform", :out-of-print false, :price 3115, :category "IMDG"}
  Result: {:isbn13 "978-4774159911", :publish-date "2013-09-26", :name "おいしいClojure入門", :out-of-print false, :price 2919, :category "Clojure"}
  Result: {:isbn13 "978-4774127804", :publish-date "2006-05-17", :name "Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築", :out-of-print true, :price 3360, :category "FullTextSearch"}
  Result: {:isbn13 "978-4774141756", :publish-date "2010-02-20", :name "Apache Solr入門 ―オープンソース全文検索エンジン", :out-of-print false, :price 3780, :category "FullTextSearch"}

Criteria API

SqlPredicateを使った検索以外にも、Criteria APIを使った検索の方法があります。ここでは、EntryObjectとPredicateBuilderを使用して、Predicateインターフェースのインスタンスを作成します。

Predicateインターフェースのインスタンスなので、IMap#valuesやkeySetなどに適用可能です。

サンプル。

        ;; 下記で、「outOfPrint = false AND price >= 3000 AND category = 'Clojure'」と同じ
        (let [^EntryObject eo (. (PredicateBuilder.) getEntryObject)
              ^Predicate pred (-> eo
                                  (.isNot "outOfPrint")
                                  (.and (-> eo
                                            (.get "price")
                                            (.greaterEqual 3000)))
                                  (.and (-> eo
                                            (.get "category")
                                            (.equal "Clojure"))))]
          (doseq [v (. book-map values pred)]
            (println (format "  Result: %s" v))))

EntryObjectは、PredicateBuilderから取得するようですが、この後EntryObjectの各種メソッドの戻り値がPredicateBuilderになっていて、ここから更に比較演算子などを使用してクエリを組み立てます。例外的に、EntryObject#getだけは、EntryObjectが返ってきますが。

PredicateBuilderはPredicateインターフェースの実装でもあるので、Builderの割にはインスタンス構築のためのメソッド呼び出しなどは不要です…。

実行結果は、端折ります。

インデキシング

このままでもクエリは使用できますが、より高速に検索を行いたい場合はインデックスを貼ることができます。もちろん、トレードオフは更新(エントリの登録オペレーション)が遅くなることです。

インデキシングの設定は2つあって、簡単なのはIMap#addIndexメソッドでIMapに対して直接インデックスを設定します。

        (. book-map addIndex "price" true)  ;; 順序あり
        (. book-map addIndex "publishDate" true)  ;; 順序あり
        (. book-map addIndex "outOfPrint" false)  ;; 順序なし
        (. book-map addIndex "category" false)  ;; 順序なし

第1引数がインデックスを貼る属性名で、第2引数は順序の有無を指定します。順序ありの場合には、trueを指定してください。まあ、Rangeスキャンを行う場合にはつけてくれという感じでしょうが。booleanに順序付きで貼っても意味はないと思います。

もうひとつは、Configに対してMapConfigとMapIndexConfigを使用して、IMap取得前に設定してしまう方法です。

    (let [^Config config (Config.)]
      ;; インデックスの設定は、MapConfigとMapIndexConfigで可能
      (. config addMapConfig (-> (MapConfig. "book-map")
                                 (.addMapIndexConfig (MapIndexConfig."price" true)))) ;; 順序あり

これ、設定ファイルでも代替可能ですが、まだ設定ファイルを書いた例を出していないので割愛。

Explain的なものはなさそうですが、とりあえずインデックスが適用されているかどうかは、getterにログを仕込んだりすると一応確認することができます。

エントリの登録時にgetterが呼ばれ、検索時には呼ばれないことになりますので。

全体の紹介としては、こんな感じです。最後に本体のコードを載せておきます。
src/hazelcast_query/core.clj

(ns hazelcast-query.core
  (:gen-class)
  (:import (com.hazelcast.config Config MapConfig MapIndexConfig)
           (com.hazelcast.core Hazelcast HazelcastInstance IMap)
           (com.hazelcast.query EntryObject Predicate PredicateBuilder SqlPredicate)
           (hazelcast-query.book Book)))

(defn -main
  [& args]
  (try
    (let [^Config config (Config.)]
      ;; インデックスの設定は、MapConfigとMapIndexConfigで可能
      (. config addMapConfig (-> (MapConfig. "book-map")
                                 (.addMapIndexConfig (MapIndexConfig."price" true)))) ;; 順序あり
      (let [^HazelcastInstance hazelcast (Hazelcast/newHazelcastInstance config)
            ^IMap book-map (. hazelcast getMap "book-map")]
        ;; IMapに対しても、設定可能
        ;;(. book-map addIndex "price" true)  ;; 順序あり
        (. book-map addIndex "publishDate" true)  ;; 順序あり
        (. book-map addIndex "outOfPrint" false)  ;; 順序なし
        (. book-map addIndex "category" false)  ;; 順序なし

        ;; 適当に書籍を登録
        (. book-map put "978-1782167303" (Book. {:isbn13 "978-1782167303"
                                                 :name "Getting Started with Hazelcast"
                                                 :price 4147
                                                 :publish-date "2013-08-27"
                                                 :category "IMDG"
                                                 :out-of-print false}))
        (. book-map put "978-1849518222" (Book. {:isbn13 "978-1849518222"
                                                 :name "Infinispan Data Grid Platform"
                                                 :price 3115
                                                 :publish-date "2012-06-30"
                                                 :category "IMDG"
                                                 :out-of-print false}))
        (. book-map put "978-4274069130" (Book. {:isbn13 "978-4274069130"
                                                 :name "プログラミングClojure 第2版"
                                                 :price 3570
                                                 :publish-date "2013-04-26"
                                                 :category "Clojure"
                                                 :out-of-print false}))
        (. book-map put "978-4774159911" (Book. {:isbn13 "978-4774159911"
                                                 :name "おいしいClojure入門"
                                                 :price 2919
                                                 :publish-date "2013-09-26"
                                                 :category "Clojure"
                                                 :out-of-print false}))
        (. book-map put "978-4774127804" (Book. {:isbn13 "978-4774127804"
                                                 :name "Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築"
                                                 :price 3360
                                                 :publish-date "2006-05-17"
                                                 :category "FullTextSearch"
                                                 :out-of-print true}))
        (. book-map put "978-4774141756" (Book. {:isbn13 "978-4774141756"
                                                 :name "Apache Solr入門 ―オープンソース全文検索エンジン"
                                                 :price 3780
                                                 :publish-date "2010-02-20"
                                                 :category "FullTextSearch"
                                                 :out-of-print false}))

        ;; Queryを実行
        (println "====== Interactive Query Start =====")
        (doseq [query-string (take-while #(not (= "exit" %))
                                         (repeatedly #(do (print "Query> ")
                                                          (flush)
                                                          (read-line))))]
          (when (and (not (nil? query-string))
                     (not (empty? query-string)))
            (try
              (println (format "Input Query => [%s]" query-string))
              (let [pred (SqlPredicate. query-string)]
                (doseq [v (. book-map values pred)] ;; keySet、entrySetなどにも使用可能
                  (println (format "  Result: %s" v))))
              (catch Exception e
                (println (format "Bad Query[%s], Reason[%s]" query-string e))))))
        (println "====== Interactive Query End =====")

        ;; Criteria APIを使う
        (println "====== Using Criteria API =====")
        ;; 下記で、「outOfPrint = false AND price >= 3000 AND category = 'Clojure'」と同じ
        (let [^EntryObject eo (. (PredicateBuilder.) getEntryObject)
              ^Predicate pred (-> eo
                                  (.isNot "outOfPrint")
                                  (.and (-> eo
                                            (.get "price")
                                            (.greaterEqual 3000)))
                                  (.and (-> eo
                                            (.get "category")
                                            (.equal "Clojure"))))]
          (doseq [v (. book-map values pred)]
            (println (format "  Result: %s" v))))

        ;; HazelcastInstanceをシャットダウン
        (.. hazelcast getLifecycleService shutdown)))

    ;; 全Hazelcastインスタンスをシャットダウン
    (finally (Hazelcast/shutdownAll))))