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に自分自身を登録する以外の事は何もしません。
sample-logger
は、このプラグイン自身の名前です。これはcatalog.json
の中で、プラグインを有効化するために使う事になります。Droonga::Adapter
のサブクラスとして定義する必要があります。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つだけに制限したい場合、すべてのリクエストについてlimit
を1
に指定する必要があります。プラグインを以下のように変更してみましょう:
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
であることに注意して下さい。これは、limit
がcount
には影響を与えないという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"]}>
このように、単純化されたレスポンスを受け取ることができました。
ここで解説したように、アダプターはアプリケーション固有の検索機能を実装するために利用できます。
既存のコマンドと独自のアダプターのみを使って新しいコマンドを追加する方法について学びました。 その過程で、入力メッセージと出力メッセージの両方について、どのように受け取り加工するのかについても学びました。
詳細はリファレンスマニュアルを参照して下さい。