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"],
(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"],
(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"]}>
このように、単純化されたレスポンスを受け取ることができました。
ここで解説したように、アダプターはアプリケーション固有の検索機能を実装するために利用できます。
既存のコマンドと独自のアダプターのみを使って新しいコマンドを追加する方法について学びました。 その過程で、入力メッセージと出力メッセージの両方について、どのように受け取り加工するのかについても学びました。
詳細はリファレンスマニュアルを参照して下さい。