JavaScript
Node.js
Bluemix
Watson
ibmcloud

Watson APIを使ったNode.jsアプリの標準実装パターン

はじめに

Watson APIを使ったアプリケーションの作り方で一番簡単な方法はNode-REDを使う方法です。
そのようなNode-RED上の開発方法はマスターして、次の段階としてNode.jsなどを使って本格的に3階層のアプリケーションを開発したいが、どのようにスタートしたらいいかわからないとか、
Watson Developer Cloudからサンプルアプリのソースをダウンロードしてみたが、仕組みが複雑で自分のやりたいことを行うのに、どこをどう直したらいいかわからない、という人はいないでしょうか?
このガイドはそういう人を主な対象として書いたものです。
実は、Watson Developer Cloudのサンプルアプリのほどんどは共通のデザインパターンで実装されていて、これを理解すると、自分でゼロからNode.jsを使ったWatsonサンプルアプリケーションを作ることもできますし、Watson Developer Cloudにあがっているサンプルアプリを改修して自分の目的に合わせたアプリに作り直すこともできるようになります。
このガイドでは、第一部で、私が自分で作ったサンプルアプリを例題としてその実装パターンの解説を行います。
第二部では、Watson Developer Cloud上の実際のサンプルアプリコードと実装パターンの対比を説明し、最近私が行ったLanguage Translatorサンプルの改修を題材にどのような修正を具体的に行ったかの解説を行います。

第一部 自作サンプルアプリのケース

ここで題材にするサンプルアプリは、Discovery NewsというWatson APIを使ったものです。
どのようなことができるかについては、別記事Watsonでニュース記事の評判分析をしよう!に紹介していますので、そちらを参照して下さい。
また、ソースコードについてもgithub ソースURLに公開していますので、ダウンロードしていただき、それを見ながら以下の解説を読んでいただけると理解が早いと思います。

1-1 主要ファイル構成と、実行環境の関係

これから説明する標準パターンで重要な役目を持つのは app.jsとpublic配下のindex.html, discovery-sample.jsの3つのファイルです。
このうち、app.jsはIBM Cloud上のCloud Foundryの中でNode.jsアプリとして稼働します(開発中はPCローカルで動かすことも可)。
index.htmlとdiscovery-sample.jsは、PC端末内のブラウザで稼働します。
この場合、さきほど説明したCloud Foundry上のNode.jsは静的HTML用のWEBサーバーとしての役割を果たします。
この関係を図1で示しました。

fig1.png

1-2 実行時のコンポーネント間の呼出し関係

図1は、ソースコードと実行時の配置の関係を示したものですが、視点を変えて実行時のコンポーネントの呼出し関係で作ると図2のようになります。

fig2.png

この図を見ればわかるとおり、ブラウザのアプリ内からWatson APIを呼び出す場合は、
1. ブラウザ内のJavaScript(例題ではdiscpvery-sample.js)
2. サーバー(Node.js)内のJavaScript(例題ではapp.js)
3. サーバー内Node.jsにロードされるwatson-developer-cloudライブラリ
4. Watson APIの一つであるDiscovery API
という経路で、Watson APIを呼び出している形になります。
これが、この資料で一番の目的している「Watsonアプリの標準的実装パターン」の肝となります。
では、なぜこのようなややこしい実装形態にしているのでしょうか?

理由としては、大きく以下のことがあります。
1つは、下の図のように、Watson APIを呼び出すために使っているwatson-developer-cloudライブラリが依存関係の複雑なもののため、npmコマンドが使えないブラウザで同じ環境を作るのが難しいことです。

npm-ls.png

もう1つは、ブラウザが持っている「CORS (Cross-Origin Resource Sharing)制限」機能対策です。
CORS制限については、説明していると長くなるのでAPIサーバを立てるためのCORS設定決定版などを参照していただきたいのですが、要は「ブラウザの中からajaxなどを使ってhttp/httpsリクエストを出す場合は、セキュリティ対策の一貫として自分のhtmlコードをダウンロードした以外のサイトへのリクエストを認めない」というものです。APIサーバー側のレスポンスヘッダの設定次第でこの制約をスルーする方法もないわけではないのですが、実装が難しくなります。図2に示したように、クライアンド側からのajaxのリクエスト先をNode.jsのサービスとしておけば、CORS制限に触れずに済むことになります。
もう1つ、このデザインの副次的効果として、APIを呼び出すときの認証情報をブラウザ側は一切持つ必要がなく、Node.jsのサーバーサイド側でのみ持っていればいいこともあります。これもセキュリティを強固にする意味を持ちます。

1-3 コードレベルでの実装

では、次にこのデザインがどのように実装に落ちているかをコードレベルで追っていきたいと思います。

まずは、ブラウザ側で起点となるindex.html内の記述です。
ajax(jsファイル名はjquery-xx.js)をはじめとするいくつかのライブラリに追加で、API呼出しを行っているコードdiscover-sample.jsの記述があります。

index.html(一部)
<script src="js/d3.v3.min.js" charset="utf-8"></script>
<script src="js/jquery-3.2.1.min.js"></script>
<script src="js/jquery-ui.min.js"></script>
<script src="js/jquery.ui.datepicker-ja.min.js"></script>
<script src="js/moment.js"></script>

<script src="discovery-sample.js"></script>

ブラウザ上の「検索」ボタンは、下記のquery_sentiment()関数呼び出しと関連づけられています。

<script>
$(function(){
    $('#send').click(query_sentiment) 
});

(以下略)

</script>

この先の実装はdiscovery-sample.jsになります。
先ほど出てきた関数query_sentiment()の実装は以下のとおりです。
Discovery API呼出し時に必要な検索パラメータはここで実際に作られていることがわかります。
それぞれのパラメータの意味を知りたい場合はDiscovey API Referenceを参照して下さい。

discovery-sample.js(一部)
function query_sentiment() {
    var data = {
        query: $('#query_key').val(),
        count: 0,
        filter: get_filter_str(),
        aggregation: "term(enriched_text.sentiment.document.label).timeslice(publication_date,1day)"
    };
    call_discovery_query( data, 
            get_sentiment_callback, 
            function(XMLHttpRequest,textStatus,errorThrown){alert('error');} );
}

この中で呼び出されている関数call_discovery_query()の実装を下に示します。
この関数の中でajax呼出しが行われています。
ここで注意して欲しいのは、ajax呼出し先のurlが"/query"と相対パス指定になっている点です。こうすることで、ajaxのリクエスト先は自分のファイルのダウンロード元であったNode.jsになるわけです。

discovery-sample.js(一部)
function call_discovery_query( data, success, error ) {
// 決め打ちのenvironment_id,collection_id をパラメータに追加)
    Object.assign(data, {environment_id: 'system', collection_id: 'news'});
// API呼出し
    $.ajax({
        type: "GET", 
        url:  "/query", 
        data: data,
        success: success,
        error: error
        });
}

最後が、サーバーサイド(Node.js)側の実装であるapp.jsコードです。
ブラウザから呼び出された/queryのリクエストをそのまま、watson-developer-cloudのライブラリdicoveryに投げていることがわかると思います。
このライブラリの内部で、Watson APIゲートウェイhttps://gateway.watsonplatform.netへのリクエストが出されているわけです。

app.js(一部)
(途中略)

// Discoveryインスタンスの生成
const watson = require('watson-developer-cloud');
const discovery = new watson.DiscoveryV1({
    version_date: '2017-08-01'
});

(途中略)

// "/query"のパスが指定された場合、Discovery API呼出しを行う
// この呼出しはブラウザ側のajax関数から行われる
app.get("/query", function(req, res, next){

// diacovery API呼出し
    discovery.query( req.query,
        function(error, data) {
            if ( data ) {res.send(data);} 
            else {console.log(error); res.send(error);}
        });
});

1-4 (おまけ)IBM CloudでNode.jsを動かす場合のお作法

以上で説明した点に注意すれば、PCローカルのNode.jsであればWatsonアプリケーションを動かすことができます。
ただ、IBM Cloud上のNode.jsで動かすためには、追加で3点ほど注意することがあります。

  • 認証情報の取得方法
  • ポート番号の指定方法
  • 追加で必要なモジュールの指定方法

です。
この3点に関する具体的な実装手段については別記事
IBM Cloud上のWatsonアプリをローカル環境で開発・デバッグする際のTips
に解説を書きましたので、そちらを参照されて下さい。ここまで例題で取り上げたサンプルアプリは、こちらのガイドにも沿った形での実装となっています。

第二部 Watson Developer Cloudを修正して自分用のサンプルアプリを作る

第二部では、Watson Developer Cloudにあるサンプルソースコードを修正して、自分のアプリケーションを作る方法を具体例を元に説明したいと思います。例題として、つい最近私が実際にやった、「Language Translatorサンプルアプリの修正」を取り上げます。
修正したサンプルの紹介は別記事賢い機械翻訳を試そう!Watson Language Translation プレビュー版サンプルアプリに記載しています。

修正内容は主に次の2点です。
1. 呼び出すAPIにプレビュー版を指定するようリクエストヘッダに特別の指定を行う
2. 画面の使い勝手の向上

2-1 サンプルアプリの構造を理解する

次のコマンドでソースコードををダウンロードした後、ファイルツリーの構造を分析します。

git clone https://github.com/watson-developer-cloud/language-translator-nodejs.git

ダウンロード後に、重要なファイルだけ抽出してツリー表示すると以下のようになります。

├── README.md
├── app.js
├── manifest.yml
├── public
│   ├── css
│   │   └── watson-bootstrap-style.css
│   ├── demo.js
└── views
    └── index.ejs

それぞれの意味は次のとおりです。

README.md: MarkDown記法で記載されたREADME
app.js: サーバーサイドNode.jsのプログラム(第一部のapp.jsと同じ)
manifest.yml: IBM Cloudデプロイ時の構成
watson-bootstrap-style.css: 画面デザインに関するパラメータが記載
demo.js: クライアントサイドのJavaScriptプログラム (第一部のdiscovery-sample.jsと同じ)
index.ejs: ブラウザ用のHTML (第一部のindex.htmlと同じ)

2-2 ファイルごとの変更内容

個別のファイルごとにどのような変更を行ったか、簡単に説明します。

README.md

日本語化し、またプレビュー版を呼び出していることの説明を入れたり、全面的に更新しました。

app.js

リクエストヘッダに特別な指定を入れるため、翻訳時のAPI呼出しには、watson-developer-cloudのライブラリを利用するのをやめて直にリクエストを出すようにしました。具体的には次のような形にしています。
ライブラリを使わず直接APIを呼ぶときの方法については、別記事IBM Cloud上のNode.jsでライブラリを使わずにWatson APIを呼び出す方法を書いていますので、そちらも参考にして下さい。

app.post('/api/translate',  function(req, res, next) {

    const bufferFrom = require('buffer-from');
    const request = require('request');

    var username;
    var password;

    // set username and password on local env
    if (process.env['LANGUAGE_TRANSLATOR_USERNAME']) {
        username = process.env['LANGUAGE_TRANSLATOR_USERNAME']
    }
    if (process.env['LANGUAGE_TRANSLATOR_PASSWORD']){
        password = process.env['LANGUAGE_TRANSLATOR_PASSWORD'];
    }

    // set username and password on ibm cloud env
    if (process.env.VCAP_SERVICES)
    {
        var env = JSON.parse(process.env.VCAP_SERVICES);
        var vcap = env.language_translator;
        username = vcap[0].credentials.username;
        password = vcap[0].credentials.password;
     }

    console.log( 'username: ' + username);
    console.log( 'password: ' + password);

    var headers = {
        Authorization: 'Basic ' + bufferFrom(username + ':' + password).toString('base64'),
        'Content-Type':'application/json',
        'X-Watson-Technology-Preview':'2017-07-01'
    }

    var options = {
        url: 'https://gateway.watsonplatform.net/language-translator/api/v2/translate',
        method: 'POST',
        headers: headers,
        json: true,
        body: {
            text: req.body.text,
            model_id: req.body.model_id
        }
    }

    console.log(options);

    console.log('/v2/translate');
    request(options, function (error, response, body) {
        if ( error ) {
            console.log("error.");
            console.log(error);
            return next(error);
        } else {
            console.log("normal end.");
            console.log(body);
            res.json(body);
        }
    })
})

manifest.yml

メモリーサイズを小さくしたのと、アプリケーション名をcf push時に指定できるよう、構成ファイルからはずしました。

8d7
<   name: language-translator-demo
11c10
<   memory: 256M
---
>   memory: 128M
watson-bootstrap-style.css

テキストエリアを拡大しました。

356c356
<   height: 300px;
---
>   height: 700px;
766c766
<     width: 760px;
---
>     width: 900px;
771c771
<     width: 992px;
---
>     width: 1100px;
776c776
<     width: 992px;
---
>     width: 1700px;
demo.js

次の4行を落としました。細かいところですが、オリジナルは入力テキストが空になると、from to の言語設定がリセットされます。同じ言語で変換することの方が多いので、この処理をなくしました。

138,141d137
<       if ((sourceLangSelect.toLowerCase() === 'detect language') || (sourceLangSelect.toLowerCase() === 'choose language')) {
<         $('#dropdownMenuInput').html('Choose Language <span class="caret"></span>');
<         $('#dropdownMenuOutput').html('Choose Language <span class="caret"></span>');
<       }
index.ejs

第一章で説明したindex.htmlと同じ働きをするファイルです。オリジナルはデモ用にいろいろな説明、リンクがあったのですが、全部削ってシンプルなものにしました。