プラグイン: リクエストとレスポンスを加工し、既存のコマンドに基づいた新しいコマンドを作成する

チュートリアルのゴール

Droongaプラグインを自分で開発するための手順を身につけましょう。

このページでは、Droongaプラグインによる「加工」(adaption)に焦点を当てます。 最後には、小さな練習用のプラグインを開発して、既存のsearchコマンドに基づく新しいコマンドstoreSearchを開発することになります。

前提条件

入力メッセージの加工

まずsample-loggerという簡単なロガープラグインを使って、適合フェーズに作用するプラグインを作りながら、基礎を学びましょう。

外部のシステムからDroonga Engineにやってくるリクエストを加工する必要がある場合があります。このようなときに、プラグインを利用できます。

このセクションでは、どのようにして前適合フェーズのプラグインを作るのかを見てみていきます。

ディレクトリの構造

基本のチュートリアルで作成したシステムに対してプラグインを追加すると仮定します。 先のチュートリアルでは、Droongaエンジンは engine ディレクトリ内に置かれていました。

プラグインは、適切な位置のディレクトリに置かれる必要があります。ディレクトリを作成しましょう:

# cd engine
# mkdir -p lib/droonga/plugins

ディレクトリを作成した後は、ディレクトリ構造は以下のようになります:

engine
├── catalog.json
├── fluentd.conf
└── lib
    └── droonga
        └── plugins

プラグインの作成

プラグイン用のコードは、プラグイン自身の名前と同じ名前のファイルに書く必要があります。 これから作るプラグインの名前はsample-loggerなので、コードはdroonga/pluginsディレクトリ内のsample-logger.rbの中に書いていくことになります。

lib/droonga/plugins/sample-logger.rb:

require "droonga/plugin"

module Droonga
  module Plugins
    module SampleLoggerPlugin
      extend Plugin
      register("sample-logger")

      class Adapter < Droonga::Adapter
        # メッセージを加工するためのコードをここに書きます。
      end
    end
  end
end

このプラグインは、Droonga Engineに自分自身を登録する以外の事は何もしません。

catalog.jsonでプラグインを有効化する

プラグインを有効化するには、catalog.jsonを更新する必要があります。 プラグインの名前"sample-logger"を、データセットの配下の"plugins"のリストに挿入します。例:

catalog.json:

(snip)
      "datasets": {
        "Starbucks": {
          (snip)
          "plugins": ["sample-logger", "groonga", "crud", "search", "dump", "status"],
(snip)

注意:"sample-logger""search"よりも前に置く必要があります。これは、sample-loggerプラグインがsearchに依存しているからです。Droonga Engineは前適合フェーズにおいて、プラグインをcatalog.jsonで定義された順に適用しますので、プラグイン同士の依存関係は(今のところは)自分で解決しなくてはなりません。

実行と動作を確認する

Droongaを起動しましょう。 Rubyがあなたの書いたプラグインのコード群を見つけられるように、RUBYLIB環境変数に./libを加えることに注意して下さい。

# kill $(cat fluentd.pid)
# RUBYLIB=./lib fluentd --config fluentd.conf --log fluentd.log --daemon fluentd.pid

そうしたら、Engineが正しく動作しているかを確かめます。 まず、以下のようなJSON形式のリクエストを作成します。

search-columbus.json:

{
  "dataset" : "Starbucks",
  "type"    : "search",
  "body"    : {
    "queries" : {
      "stores" : {
        "source"    : "Store",
        "condition" : {
          "query"   : "Columbus",
          "matchTo" : "_key"
        },
        "output" : {
          "elements"   : [
            "startTime",
            "elapsedTime",
            "count",
            "attributes",
            "records"
          ],
          "attributes" : ["_key"],
          "limit"      : -1
        }
      }
    }
  }
}

これは基本のチュートリアルにおいて”Columbus”を検索する例に対応しています。 Protocol Adapterへのリクエストは"body"要素の中に置かれていることに注意して下さい。

droonga-requestコマンドを使ってリクエストをDroonga Engineに送信します:

# droonga-request --tag starbucks search-columbus.json
Elapsed time: 0.021544
[
  "droonga.message",
  1392617533,
  {
    "inReplyTo": "1392617533.9644868",
    "statusCode": 200,
    "type": "search.result",
    "body": {
      "stores": {
        "count": 2,
        "records": [
          [
            "Columbus @ 67th - New York NY  (W)"
          ],
          [
            "2 Columbus Ave. - New York NY  (W)"
          ]
        ]
      }
    }
  }
]

これが検索結果です。

プラグインを動作させる: ログをとる

ここまでで作成したプラグインは、何もしない物でした。それでは、このプラグインを何か面白いことをする物にしましょう。

まず最初に、searchのリクエストを捕まえてログ出力してみます。プラグインを以下のように更新して下さい:

lib/droonga/plugins/sample-logger.rb:

(snip)
    module SampleLoggerPlugin
      extend Plugin
      register("sample-logger")

      class Adapter < Droonga::Adapter
        input_message.pattern = ["type", :equal, "search"]

        def adapt_input(input_message)
          logger.info("SampleLoggerPlugin::Adapter", :message => input_message)
        end
      end
    end
(snip)

input_message.patternで始まる行は、設定です。 この例では、プラグインを"type":"search"という情報を持つすべての入力メッセージに対して働くように定義しています。. 詳しくはリファレンスマニュアルの設定のセクションを参照して下さい。

adapt_inputメソッドは、パターンに当てはまるすべての入力メッセージに対して毎回呼ばれます。 引数のinput_messageは、入力メッセージをラップした物です。

fluentdを再起動します:

# kill $(cat fluentd.pid)
# RUBYLIB=./lib fluentd --config fluentd.conf --log fluentd.log --daemon fluentd.pid

前のセクションと同じリクエストを送信します:

# droonga-request --tag starbucks search-columbus.json
Elapsed time: 0.014714
[
  "droonga.message",
  1392618037,
  {
    "inReplyTo": "1392618037.935901",
    "statusCode": 200,
    "type": "search.result",
    "body": {
      "stores": {
        "count": 2,
        "records": [
          [
            "Columbus @ 67th - New York NY  (W)"
          ],
          [
            "2 Columbus Ave. - New York NY  (W)"
          ]
        ]
      }
    }
  }
]

すると、fluentdのログファイルであるfluentd.logに以下のようなログが出力される事を確認できるでしょう。

2014-02-17 15:20:37 +0900 [info]: SampleLoggerPlugin::Adapter message=#<Droonga::InputMessage:0x007f8ae3e1dd98 @raw_message={"dataset"=>"Starbucks", "type"=>"search", "body"=>{"queries"=>{"stores"=>{"source"=>"Store", "condition"=>{"query"=>"Columbus", "matchTo"=>"_key"}, "output"=>{"elements"=>["startTime", "elapsedTime", "count", "attributes", "records"], "attributes"=>["_key"], "limit"=>-1}}}}, "replyTo"=>{"type"=>"search.result", "to"=>"127.0.0.1:64591/droonga"}, "id"=>"1392618037.935901", "date"=>"2014-02-17 15:20:37 +0900", "appliedAdapters"=>[]}>

このログは、メッセージがSampleLoggerPlugin::Adapterによって受信されて、Droongaに渡されたことを示しています。実際のデータ処理の前に、この時点でメッセージを加工することができます。

プラグインでメッセージを加工する

レスポンスで返されるレコードの数を常に1つだけに制限したい場合、すべてのリクエストについてlimit1に指定する必要があります。プラグインを以下のように変更してみましょう:

lib/droonga/plugins/sample-logger.rb:

(snip)
        def adapt_input(input_message)
          logger.info("SampleLoggerPlugin::Adapter", :message => input_message)
          input_message.body["queries"]["stores"]["output"]["limit"] = 1
        end
(snip)

上の例のように、プラグインはadapt_inputメソッドの引数として渡されるinput_messageを通じて入力メッセージの内容を加工することができます。 詳細は当該メッセージの実装であるクラスのリファレンスマニュアルを参照して下さい。

fluentdを再起動します:

# kill $(cat fluentd.pid)
# RUBYLIB=./lib fluentd --config fluentd.conf --log fluentd.log --daemon fluentd.pid

再起動後、レスポンスはrecordsの値としてレコードを常に(最大で)1つだけ含むようになります。

先の場合と同じリクエストを投げてみましょう:

# droonga-request --tag starbucks search-columbus.json
Elapsed time: 0.017343
[
  "droonga.message",
  1392618279,
  {
    "inReplyTo": "1392618279.0578449",
    "statusCode": 200,
    "type": "search.result",
    "body": {
      "stores": {
        "count": 2,
        "records": [
          [
            "Columbus @ 67th - New York NY  (W)"
          ]
        ]
      }
    }
  }
]

countが依然として2であることに注意して下さい。これは、limitcountには影響を与えないというsearchコマンド自体の仕様によるものです。searchコマンドの詳細についてはsearchコマンドのリファレンスマニュアルを参照して下さい。

すると、fluentdのログファイルであるfluentd.logに以下のようなログが出力される事を確認できるでしょう。

2014-02-17 15:24:39 +0900 [info]: SampleLoggerPlugin::Adapter message=#<Droonga::InputMessage:0x007f956685c908 @raw_message={"dataset"=>"Starbucks", "type"=>"search", "body"=>{"queries"=>{"stores"=>{"source"=>"Store", "condition"=>{"query"=>"Columbus", "matchTo"=>"_key"}, "output"=>{"elements"=>["startTime", "elapsedTime", "count", "attributes", "records"], "attributes"=>["_key"], "limit"=>-1}}}}, "replyTo"=>{"type"=>"search.result", "to"=>"127.0.0.1:64616/droonga"}, "id"=>"1392618279.0578449", "date"=>"2014-02-17 15:24:39 +0900", "appliedAdapters"=>[]}>

出力メッセージの加工

Droonga Engineからの出力メッセージ(例えば検索結果など)を加工したい場合は、別のメソッドを定義することでそれを実現できます。 このセクションでは、出力メッセージを加工するメソッドを定義してみましょう。

出力のメッセージを加工するメソッドを追加する

searchコマンドの結果のログを取ってみましょう。 出力メッセージを処理するために、adapt_outputメソッドを定義します。 説明を簡単にするために、ここではadapt_inputメソッドの定義を一旦削除します。

lib/droonga/plugins/sample-logger.rb:

(snip)
    module SampleLoggerPlugin
      extend Plugin
      register("sample-logger")

      class Adapter < Droonga::Adapter
        input_message.pattern = ["type", :equal, "search"]

        def adapt_output(output_message)
          logger.info("SampleLoggerPlugin::Adapter", :message => output_message)
        end
      end
    end
(snip)

adapt_outputメソッドは、そのプラグイン自身によって捕捉された入力メッセージに起因して送出された出力メッセージに対してのみ呼ばれます(マッチングパターンのみが指定されていて、adapt_inputメソッドが定義されていない場合であっても)。 詳細はプラグイン開発APIのリファレンスマニュアルを参照して下さい。

実行する

fluentdを再起動しましょう:

# kill $(cat fluentd.pid)
# RUBYLIB=./lib fluentd --config fluentd.conf --log fluentd.log --daemon fluentd.pid

次に、検索リクエストを送ります(前のセクションと同じJSONをリクエストとして使います):

# droonga-request --tag starbucks search-columbus.json
Elapsed time: 0.015491
[
  "droonga.message",
  1392619269,
  {
    "inReplyTo": "1392619269.184789",
    "statusCode": 200,
    "type": "search.result",
    "body": {
      "stores": {
        "count": 2,
        "records": [
          [
            "Columbus @ 67th - New York NY  (W)"
          ],
          [
            "2 Columbus Ave. - New York NY  (W)"
          ]
        ]
      }
    }
  }
]

fluentdのログは以下のようになっているはずです:

2014-02-17 15:41:09 +0900 [info]: SampleLoggerPlugin::Adapter message=#<Droonga::OutputMessage:0x007fddcad4d5a0 @raw_message={"dataset"=>"Starbucks", "type"=>"dispatcher", "body"=>{"stores"=>{"count"=>2, "records"=>[["Columbus @ 67th - New York NY  (W)"], ["2 Columbus Ave. - New York NY  (W)"]]}}, "replyTo"=>{"type"=>"search.result", "to"=>"127.0.0.1:64724/droonga"}, "id"=>"1392619269.184789", "date"=>"2014-02-17 15:41:09 +0900", "appliedAdapters"=>["Droonga::Plugins::SampleLoggerPlugin::Adapter", "Droonga::Plugins::Error::Adapter"]}>

ここには、searchの結果がadapt_outputメソッドに渡された事(そしてログ出力された事)が示されています。

結果を適合フェーズで加工する

後適合フェーズにおいて、結果を加工してみましょう。 例えば、リクエストに対する処理が完了した時刻を示すcompletedAtというアトリビュートを加えるとします。 プラグインを以下のように更新して下さい:

lib/droonga/plugins/sample-logger.rb:

(snip)
        def adapt_output(output_message)
          logger.info("SampleLoggerPlugin::Adapter", :message => output_message)
          output_message.body["stores"]["completedAt"] = Time.now
        end
(snip)

上の例のように、出力メッセージはadapt_outputメソッドの引数として渡されるoutput_messageを通じて加工することができます。 詳細は当該メッセージの実装のクラスのリファレンスマニュアルを参照して下さい。

fluentdを再起動します:

# kill $(cat fluentd.pid)
# RUBYLIB=./lib fluentd --config fluentd.conf --log fluentd.log --daemon fluentd.pid

同じ検索リクエストを送ってみましょう:

# droonga-request --tag starbucks search-columbus.json
Elapsed time: 0.013983
[
  "droonga.message",
  1392619528,
  {
    "inReplyTo": "1392619528.235121",
    "statusCode": 200,
    "type": "search.result",
    "body": {
      "stores": {
        "count": 2,
        "records": [
          [
            "Columbus @ 67th - New York NY  (W)"
          ],
          [
            "2 Columbus Ave. - New York NY  (W)"
          ]
        ],
        "completedAt": "2014-02-17T06:45:28.247669Z"
      }
    }
  }
]

リクエストの処理が完了した時刻を含むアトリビュートであるcompletedAtが追加された事を確認できました。 fluentd.logには以下のように出力されているはずです:

2014-02-17 15:45:28 +0900 [info]: SampleLoggerPlugin::Adapter message=#<Droonga::OutputMessage:0x007fd384f3ab60 @raw_message={"dataset"=>"Starbucks", "type"=>"dispatcher", "body"=>{"stores"=>{"count"=>2, "records"=>[["Columbus @ 67th - New York NY  (W)"], ["2 Columbus Ave. - New York NY  (W)"]]}}, "replyTo"=>{"type"=>"search.result", "to"=>"127.0.0.1:64849/droonga"}, "id"=>"1392619528.235121", "date"=>"2014-02-17 15:45:28 +0900", "appliedAdapters"=>["Droonga::Plugins::SampleLoggerPlugin::Adapter", "Droonga::Plugins::Error::Adapter"]}>

入出力メッセージの加工

ここまでで、前適合フェーズと後適合フェーズで動作するプラグインの基本を学びました。 それでは、より実践的なプラグインを開発してみることにしましょう。

Droongaのsearchコマンドを見た時、目的に対していささか柔軟すぎるという印象を持ったことと思います そこで、ここではアプリケーション固有の単純なインターフェースを持つコマンドとして、searchコマンドをラップするstoreSearchというコマンドを、store-searchというプラグインで追加していくことにします。

シンプルなリクエストを受け取る

まず最初に、store-searchプラグインを作ります。 思い出して下さい、プラグインを実装するコードは、これから作ろうとしているプラグインと同じ名前のファイルに書かなくてはなりませんでしたよね。 ですので、実装を書くファイルはdroonga/pluginsディレクトリに置かれたstore-search.rbとなります。StoreSearchPluginを以下のように定義しましょう:

lib/droonga/plugins/store-search.rb:

require "droonga/plugin"

module Droonga
  module Plugins
    module StoreSearchPlugin
      extend Plugin
      register("store-search")

      class Adapter < Droonga::Adapter
        input_message.pattern = ["type", :equal, "storeSearch"]

        def adapt_input(input_message)
          logger.info("StoreSearchPlugin::Adapter", :message => input_message)

          query = input_message.body["query"]
          logger.info("storeSearch", :query => query)

          body = {
            "queries" => {
              "stores" => {
                "source"    => "Store",
                "condition" => {
                  "query"   => query,
                  "matchTo" => "_key",
                },
                "output"    => {
                  "elements"   => [
                    "startTime",
                    "elapsedTime",
                    "count",
                    "attributes",
                    "records",
                  ],
                  "attributes" => [
                    "_key",
                  ],
                  "limit"      => -1,
                }
              }
            }
          }

          input_message.type = "search"
          input_message.body = body
        end
      end
    end
  end
end

次に、プラグインを有効化するためにcatalog.jsonを更新します。 先程作成したsample-loggerは削除しておきます。

catalog.json:

(snip)
      "datasets": {
        "Starbucks": {
          (snip)
          "plugins": ["store-search", "groonga", "crud", "search", "dump", "status"],
(snip)

思い出して下さい、"store-search""search"に依存しているので、"search"よりも前に置く必要があります。

fluentdを再起動します:

# kill $(cat fluentd.pid)
# RUBYLIB=./lib fluentd --config fluentd.conf --log fluentd.log --daemon fluentd.pid

これで、以下のようなリクエストで新しいコマンドを使えるようになりました:

store-search-columbus.json:

{
  "dataset" : "Starbucks",
  "type"    : "storeSearch",
  "body"    : {
    "query" : "Columbus"
  }
}

リクエストを発行するために、以下のようにコマンドを実行しましょう:

# droonga-request --tag starbucks store-search-columbus.json
Elapsed time: 0.01494
[
  "droonga.message",
  1392621168,
  {
    "inReplyTo": "1392621168.0119512",
    "statusCode": 200,
    "type": "storeSearch.result",
    "body": {
      "stores": {
        "count": 2,
        "records": [
          [
            "Columbus @ 67th - New York NY  (W)"
          ],
          [
            "2 Columbus Ave. - New York NY  (W)"
          ]
        ]
      }
    }
  }
]

この時、fluentd.logには以下のようなログが出力されているはずです:

2014-02-17 16:12:48 +0900 [info]: StoreSearchPlugin::Adapter message=#<Droonga::InputMessage:0x007fe4791d3958 @raw_message={"dataset"=>"Starbucks", "type"=>"storeSearch", "body"=>{"query"=>"Columbus"}, "replyTo"=>{"type"=>"storeSearch.result", "to"=>"127.0.0.1:49934/droonga"}, "id"=>"1392621168.0119512", "date"=>"2014-02-17 16:12:48 +0900", "appliedAdapters"=>[]}>
2014-02-17 16:12:48 +0900 [info]: storeSearch query="Columbus"

以上の手順で、単純なリクエストによって店舗の検索を行えるようになりました。

注意:レスポンスのメッセージの"type"の値が"search.result"から"storeSearch.result"に変わっていることに注目して下さい。これは、このレスポンスが、type"storeSearch"であるリクエストに起因して発生した物であるために、Droonga Engineによって自動的に"(入力メッセージのtype).result"というtypeが設定されたからです。言い換えると、出力メッセージのtypeは、adapt_inputでのinput_message.type = "search"のような方法でわざわざ自分で設定し直す必要はありません。

シンプルなレスポンスを返す

次に、結果をより単純な形で、単に店舗の名前の配列だけを返すだけという物にしてみましょう。

adapt_outputを以下のように定義して下さい。

lib/droonga/plugins/store-search.rb:

(snip)
    module StoreSearchPlugin
      extend Plugin
      register("store-search")

      class Adapter < Droonga::Adapter
        (snip)

        def adapt_output(output_message)
          logger.info("StoreSearchPlugin::Adapter", :message => output_message)

          records = output_message.body["stores"]["records"]
          simplified_results = records.flatten

          output_message.body = simplified_results
        end
      end
    end
(snip)

adapt_outputメソッドは、そのプラグインによって捕捉された入力メッセージに対応する出力メッセージのみを受け取ります。

fluentdを再起動します:

# kill $(cat fluentd.pid)
# RUBYLIB=./lib fluentd --config fluentd.conf --log fluentd.log --daemon fluentd.pid

リクエストを送ってみましょう:

# droonga-request --tag starbucks store-search-columbus.json
Elapsed time: 0.014859
[
  "droonga.message",
  1392621288,
  {
    "inReplyTo": "1392621288.158763",
    "statusCode": 200,
    "type": "storeSearch.result",
    "body": [
      "Columbus @ 67th - New York NY  (W)",
      "2 Columbus Ave. - New York NY  (W)"
    ]
  }
]

fluentd.logには以下のようなログが出力されているはずです:

2014-02-17 16:14:48 +0900 [info]: StoreSearchPlugin::Adapter message=#<Droonga::InputMessage:0x007ffb8ada9d68 @raw_message={"dataset"=>"Starbucks", "type"=>"storeSearch", "body"=>{"query"=>"Columbus"}, "replyTo"=>{"type"=>"storeSearch.result", "to"=>"127.0.0.1:49960/droonga"}, "id"=>"1392621288.158763", "date"=>"2014-02-17 16:14:48 +0900", "appliedAdapters"=>[]}>
2014-02-17 16:14:48 +0900 [info]: storeSearch query="Columbus"
2014-02-17 16:14:48 +0900 [info]: StoreSearchPlugin::Adapter message=#<Droonga::OutputMessage:0x007ffb8ad78e48 @raw_message={"dataset"=>"Starbucks", "type"=>"dispatcher", "body"=>{"stores"=>{"count"=>2, "records"=>[["Columbus @ 67th - New York NY  (W)"], ["2 Columbus Ave. - New York NY  (W)"]]}}, "replyTo"=>{"type"=>"storeSearch.result", "to"=>"127.0.0.1:49960/droonga"}, "id"=>"1392621288.158763", "date"=>"2014-02-17 16:14:48 +0900", "appliedAdapters"=>["Droonga::Plugins::StoreSearchPlugin::Adapter", "Droonga::Plugins::Error::Adapter"], "originalTypes"=>["storeSearch"]}>

このように、単純化されたレスポンスを受け取ることができました。

ここで解説したように、アダプターはアプリケーション固有の検索機能を実装するために利用できます。

まとめ

既存のコマンドと独自のアダプターのみを使って新しいコマンドを追加する方法について学びました。 その過程で、入力メッセージと出力メッセージの両方について、どのように受け取り加工するのかについても学びました。

詳細はリファレンスマニュアルを参照して下さい。