2017年11月1日追記
[Watson] Retrieve and Rankのサービス終了について
概要
本稿では、Retrieve and Rank Web interfaceという公式ツールを利用してRetrieve and Rank(以下R&R)のトレーニングを行い、簡単な質問応答システムを作るまでの手順をシリーズ化して解説します。
(1) R&Rおよび公式ツール利用開始〜回答データ投入
(2) 質問データ投入〜Rankerトレーニング
(3) Node-REDアプリケーションとR&Rの連携 ※この記事で説明する部分
まだ第1回の記事、第2回の記事をお読みでない方は、そちらを先にお読みください。
前回までの作業でR&Rのトレーニングが完了し、利用可能になりました。
今回はいよいよアプリケーションからR&Rを呼び出してみます。
免責事項
本稿におけるWatsonサービスに関する解説は執筆者の個人的な見解であり、公式資料に基づいていない内容を含みます。
また本稿の情報は2017年4月現在のものです。
必ずWatson Developer Cloudなどの公式資料も併せて確認してください。
完成イメージ
以下のようなシンプルなWebアプリケーションを、Node-REDを利用して作成します。
Node-REDとは
あらかじめ用意された「ノード」と呼ばれる部品をつなげて「フロー」を作成することにより、最小限のコードでIBM Bluemix上にWebアプリを構築できます。
Node-REDの利用開始
Bluemixのカテゴリーにて「ボイラープレート」-「Node-RED Starter」をクリックします。
アプリ名を指定します。アプリ名+ドメインがルートURLになります。
「作成」をクリックすると、環境作成が開始されます。
5-10分待ち、以下のように「実行中」の表示になったらURLをクリックします。
「Go to your Node-RED flow editor」をクリックします。
フローエディターにアクセスするためのユーザーIDとパスワードを指定します。
左側に並んでいるのがノードです。
Node-REDでは、これらのノードをドラッグ&ドロップし、ノード同士をつなげてフローを作成することでアプリケーションを構築します。
Watson用のノードはあらかじめ準備されており、R&Rのノードもあります。
本アプリのフロー定義
環境
Item | Version |
---|---|
Node-RED | 0.16.2 |
2017年4月末時点でBluemix上にNode-RED環境を構築すると上記バージョンとなりました。
フロー定義のインポート
フロー定義はインポート可能です。
本アプリのフロー定義は以下になります。
[{"id":"506aa228.0d18dc","type":"http in","z":"33444cbb.50d004","name":"","url":"/rr","method":"get","swaggerDoc":"","x":996,"y":711,"wires":[["ef47a863.502158"]]},{"id":"86c01a38.dd5dc8","type":"debug","z":"33444cbb.50d004","name":"Debug","active":true,"console":"false","complete":"payload","x":1387.687744140625,"y":947.4136352539062,"wires":[]},{"id":"656875de.65f10c","type":"function","z":"33444cbb.50d004","name":"回答取り出し","func":"if (msg.payload.response.docs.length > 0) {\n msg.payload = msg.payload.response.docs[0].title + \"です。\";\n} else {\n msg.payload = \"回答できません\";\n}\nreturn msg;\n","outputs":1,"noerr":0,"x":1485,"y":1154,"wires":[["c0549441.3da728","990f985c.6896c8"]]},{"id":"74e06930.a54698","type":"http response","z":"33444cbb.50d004","name":"","x":1309.2205810546875,"y":710.2529907226562,"wires":[]},{"id":"3c2845a4.0d8f0a","type":"change","z":"33444cbb.50d004","name":"質問文の取り出し","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.q","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1133,"y":933,"wires":[["e256f1d1.ecef9","eb8da968.b80668"]]},{"id":"e256f1d1.ecef9","type":"debug","z":"33444cbb.50d004","name":"Debug","active":true,"console":"false","complete":"payload","x":1294,"y":879,"wires":[]},{"id":"c0549441.3da728","type":"debug","z":"33444cbb.50d004","name":"Debug","active":true,"console":"false","complete":"payload","x":1658,"y":1152,"wires":[]},{"id":"6b42b6dc.85bd78","type":"switch","z":"33444cbb.50d004","name":"レスポンスチェック","property":"payload.response.docs","propertyType":"msg","rules":[{"t":"null"},{"t":"nnull"}],"checkall":"true","outputs":2,"x":1368,"y":1065,"wires":[["990f985c.6896c8"],["656875de.65f10c"]]},{"id":"ba873c1.293cbc","type":"comment","z":"33444cbb.50d004","name":"エラーの場合","info":"","x":1521,"y":1026,"wires":[]},{"id":"a4f56bee.7efa18","type":"comment","z":"33444cbb.50d004","name":"正常の場合","info":"","x":1403,"y":1109,"wires":[]},{"id":"ee8036b0.1888d8","type":"comment","z":"33444cbb.50d004","name":"初期表示","info":"","x":993,"y":662,"wires":[]},{"id":"22940a42.554616","type":"comment","z":"33444cbb.50d004","name":"質問応答","info":"","x":993,"y":827,"wires":[]},{"id":"eb8da968.b80668","type":"watson-retrieve-rank-search-and-rank","z":"33444cbb.50d004","name":"R&R search","servicecreds":"","clusterid":"","collectionname":"","searchmode":"search-and-rank","rankerid":"","x":1236,"y":997,"wires":[["86c01a38.dd5dc8","6b42b6dc.85bd78"]]},{"id":"d612ed97.00bf9","type":"http in","z":"33444cbb.50d004","name":"","url":"/rr/ask","method":"get","swaggerDoc":"","x":1019,"y":868,"wires":[["3c2845a4.0d8f0a"]]},{"id":"990f985c.6896c8","type":"http response","z":"33444cbb.50d004","name":"","x":1621,"y":1059,"wires":[]},{"id":"ef47a863.502158","type":"template","z":"33444cbb.50d004","name":"html","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<!DOCTYPE html>\n<html lang=\"ja\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n <meta http-equiv=\"x-ua-compatible\" content=\"ie=edge\">\n <title>Watson R&R Test</title>\n <link rel=\"stylesheet\" href=\"//bootswatch.com/cerulean/bootstrap.min.css\">\n\n <script type=\"text/javascript\">\n // Node-REDのWeb API連携\n var askWatson = function() {\n // ボタン無効\n $(\".btn\").addClass(\"disabled\").addClass(\"btn-default\").removeClass(\"btn-primary\");\n // Watson連携\n $.ajax({\n type: \"GET\",\n url: \"./ask\",\n dataType: \"text\",\n timeout: 10000,\n data: {\n //質問文を取得してパラメータとして渡す\n q: $(\"#question\").val()\n }\n })\n .done(function(data, textStatus, jqXHR) {\n // 通信成功\n // フローから返ってきた結果をanswerに入れる\n $(\"#answer\").val(data);\n // ボタン有効\n $(\".btn\").removeClass(\"disabled\").removeClass(\"btn-default\").addClass(\"btn-primary\");\n })\n .fail(function(jqXHR, textStatus, errorThrown) {\n // 通信エラー\n $(\"#answer\").val(\"通信エラー \" + jqXHR.status + \" : \" + jqXHR.statusText);\n })\n .always(function(data) {\n // Debug\n console.log(data);\n });\n }\n \n // 質問文入力済み判定\n var checkLengthQ = function() {\n if ($(\"#question\").val().length > 0) {\n // ボタン有効\n $(\".btn\").removeClass(\"disabled\").removeClass(\"btn-default\").addClass(\"btn-primary\");\n } else {\n // ボタン無効\n $(\".btn\").addClass(\"disabled\").addClass(\"btn-default\").removeClass(\"btn-primary\");\n }\n }\n </script>\n </head>\n \n <body>\n <nav class=\"navbar navbar-default\">\n <div class=\"container-fluid\">\n <div class=\"navbar-header\">\n <a class=\"navbar-brand\" href=\"{{req._parsedUrl.pathname}}\">Watson R&R Test</a>\n </div>\n </div>\n </nav>\n <div class=\"container-fluid\">\n <div class=\"row\">\n <div class=\"col-xs-1 bg-left\"></div>\n <div class=\"col-xs-10 bg-main\">\n <center>\n <h3>北海道の観光地について質問してください</h3>\n <div class=\"form-group\">\n <textarea class=\"form-control\" rows=3 id=\"question\" placeholder=\"ここに質問を入力してください\" oninput=\"checkLengthQ()\"></textarea>\n <br>\n <a class=\"btn btn-default disabled\" id=\"askButton\" href=\"javascript:askWatson()\">質問する</a>\n </div>\n <br>\n <div class=\"form-group\">\n <textarea class=\"form-control\" rows=5 id=\"answer\" placeholder=\"Watsonからの回答\" disabled=\"\"></textarea>\n </div>\n </center>\n </div>\n <div class=\"col-xs-1 bg-right\"></div>\n </div>\n </div>\n <body>\n \n <script src=\"//ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js\" integrity=\"sha384-3ceskX3iaEnIogmQchP8opvBy3Mi7Ce34nWjpBIwVTHfGYWQS9jwHDVRnpKKHJg7\" crossorigin=\"anonymous\"></script>\n <script src=\"//cdnjs.cloudflare.com/ajax/libs/tether/1.3.7/js/tether.min.js\" integrity=\"sha384-XTs3FgkjiBgo8qjEjBk0tGmf3wPrWtA6coPfQDfFEY8AnYJwjalXCiosYRBIBZX8\" crossorigin=\"anonymous\"></script>\n <script src=\"//maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.5/js/bootstrap.min.js\" integrity=\"sha384-BLiI7JTZm+JWlgKa0M0kGRpJbF2J8q+qreVrKBC47e3K6BW78kGLrCkeRX6I9RoK\" crossorigin=\"anonymous\"></script>\n</html>","x":1154,"y":709,"wires":[["74e06930.a54698"]]}]
クリップボードにコピーし、ハンバーガーメニューの「読み込み」-「クリップボード」を選択します。
フロー定義解説
本アプリはシンプルなSingle Page Applicationです。
「初期表示」のフローではHTMLをブラウザに返します。
Submit時に「質問応答」フローにAjaxでアクセスし、R&Rの応答を返します。
各ノードの解説は以下の通りです。
http request
HTTPリクエストを受け付けるノードです。 Method欄でHTTPメソッドを選択します。 ここで指定したURLがアプリのアクセスポイントとなります。 Nameは全ノード共通のプロパティで、フローエディタ上に表示する名前です。(入力は任意)template
HTMLテンプレートを記述します。 テンプレートエンジン[Mustache](https://mustache.github.io/)を使うことができます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Watson R&R Test</title>
<link rel="stylesheet" href="//bootswatch.com/cerulean/bootstrap.min.css">
<script type="text/javascript">
// Node-REDのWeb API連携
var askWatson = function() {
// ボタン無効
$(".btn").addClass("disabled").addClass("btn-default").removeClass("btn-primary");
// Watson連携
$.ajax({
type: "GET",
url: "./ask",
dataType: "text",
timeout: 10000,
data: {
//質問文を取得してパラメータとして渡す
q: $("#question").val()
}
})
.done(function(data, textStatus, jqXHR) {
// 通信成功
// フローから返ってきた結果をanswerに入れる
$("#answer").val(data);
// ボタン有効
$(".btn").removeClass("disabled").removeClass("btn-default").addClass("btn-primary");
})
.fail(function(jqXHR, textStatus, errorThrown) {
// 通信エラー
$("#answer").val("通信エラー " + jqXHR.status + " : " + jqXHR.statusText);
})
.always(function(data) {
// Debug
console.log(data);
});
}
// 質問文入力済み判定
var checkLengthQ = function() {
if ($("#question").val().length > 0) {
// ボタン有効
$(".btn").removeClass("disabled").removeClass("btn-default").addClass("btn-primary");
} else {
// ボタン無効
$(".btn").addClass("disabled").addClass("btn-default").removeClass("btn-primary");
}
}
</script>
</head>
<body>
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="{{req._parsedUrl.pathname}}">Watson R&R Test</a>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<div class="col-xs-1 bg-left"></div>
<div class="col-xs-10 bg-main">
<center>
<h3>北海道の観光地について質問してください</h3>
<div class="form-group">
<textarea class="form-control" rows=3 id="question" placeholder="ここに質問を入力してください" oninput="checkLengthQ()"></textarea>
<br>
<a class="btn btn-default disabled" id="askButton" href="javascript:askWatson()">質問する</a>
</div>
<br>
<div class="form-group">
<textarea class="form-control" rows=5 id="answer" placeholder="Watsonからの回答" disabled=""></textarea>
</div>
</center>
</div>
<div class="col-xs-1 bg-right"></div>
</div>
</div>
<body>
<script src="//ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js" integrity="sha384-3ceskX3iaEnIogmQchP8opvBy3Mi7Ce34nWjpBIwVTHfGYWQS9jwHDVRnpKKHJg7" crossorigin="anonymous"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/tether/1.3.7/js/tether.min.js" integrity="sha384-XTs3FgkjiBgo8qjEjBk0tGmf3wPrWtA6coPfQDfFEY8AnYJwjalXCiosYRBIBZX8" crossorigin="anonymous"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.5/js/bootstrap.min.js" integrity="sha384-BLiI7JTZm+JWlgKa0M0kGRpJbF2J8q+qreVrKBC47e3K6BW78kGLrCkeRX6I9RoK" crossorigin="anonymous"></script>
</html>
http response
HTTPレスポンスをクライアントに返すノードです。 何も入力すべき項目はないのですが、このノードを配置しないとクライアントに何も返らないので注意が必要です。change
changeノードはデータを編集するために利用します。 msgオブジェクトは、ノード間でやりとりされるデータです。payloadはそのボディ部にあたります。 ここでは、ブラウザから渡された引数payload.qをpayload自体に詰め替えて次のノードに渡しています。retrieve and rank search and rank
R&RのSearch and rank APIを呼び出すためのノードです。Service Credentialsの鉛筆アイコンをクリックすると、サービス資格情報の入力が表示されます。
サービス資格情報は、Bluemixダッシュボードの「サービス」一覧にてR&Rを選択し、「サービス資格情報」タブ「資格情報の表示」で確認できます。
Cluster ID, Collection Name, Ranker IDは、公式ツールで確認し入力します。
switch
判断分岐を行うためのノードです。debug
デバッグ出力を行うためのノードです。以下のように、フローエディタ右側のデバッグエリアにデバッグ情報を出力してくれます。
前述のswitchノードでは、R&Rから"docs"というオブジェクトが返って来れば正常として後続処理を行い、そうでなければR&Rからのエラーをクライアントに返しています。
function
JavaScriptを記述するためのノードです。 次ノードにデータを引き継ぐには、msgオブジェクトをreturnします。補足
R&R search and rankノードではR&Rのレスポンスとして回答データ(Solrコレクションデータ)のIDとTitleしか取得できず、Body(本文)を取得できません。
※2017年4月末現在の情報です。
公式のAPI Referenceのサンプルにも載っていないのですが、実はR&R Search and rank APIではBodyも取得できます。
API Referenceのサンプルにある、"fl=id,title"の部分を"fl=id,title,body"に変更することで、Bodyを取得可能です。
ただし、functionノードにJavaScriptを記述してR&Rを呼び出す必要があります。
Bluemix Node-REDを使ってWatson APIを呼び出す
まとめ
第1回の記事に記載した通り、R&Rは、オープンソースのApache Solrと、Watson独自のRankerという仕組みを組み合わせたサービスです。
その特性上、curlコマンドによるトレーニングは少々わかりづらく、また手間のかかる作業でしたが、公式ツールであるWeb interfaceを利用することで、わかりやすく、簡単にトレーニングが可能になりました。
Node-REDについては他にもいろいろなノードがあり、また自分でノードを作成することもできますので、Node-RED日本ユーザ会のサイトなどをご参照ください。