まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:nodejs

運用中のウェブアプリケーションに対して、セキュリティ面を考慮して以下のようなリクエスト制限をかける必要が生じたとします:

・(例えば)10分間で 100 回のリクエストを許可する
・許可数を超えた場合はリクエストを処理しない


クラウドやホスティングサーバーで運用する場合は、クラウド/ホスティング側にそのような機能が提供されていることもあると思います。が、もしそのような機能が提供されていない条件下でこのような要件が生じた場合、アプリケーションの実装としてリクエスト制限を用意する必要が出てくるかもしれません。 今回のブログエントリで紹介するのは、Node.js アプリケーションにリクエスト制限をかける実装方法です。


といっても Node.js (バージョン 14 以上)で Express ライブラリを使っている場合であれば express-rate-limit という Express 向けミドルウェアを使うことで簡単に実装できます:
20220228


以下でサンプルを紹介しますが、サンプルコードはこちらに公開しています:
https://github.com/dotnsf/express-rate-limit-sample


例えば現行のコード(app_old.js)が以下のようになっていたとします:
//. app_old.js
var express = require( 'express' ),
    app = express();

app.get( '/', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: true }, null, 2 ) );
  res.end();
});

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

"GET /" リクエストに対して "{ status: true }" を返すだけの内容ですが、処理内容自体はもっと複雑でも構いません。

このアプリケーションに「10分間で 100 回」というリクエスト制限をかける場合は以下のようなコード(app_new.js)に変更します:
//. app_new.js
var express = require( 'express' ),
    app = express();

//. rate limit : 100 times per 10 minutes
var rate = require( 'express-rate-limit' );
var limit = rate({
  windowMs: 10*60*1000,
  max: 100,
  standardHeaders: true,
  legacyHeaders: false
});
app.use( limit );

app.get( '/', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: true }, null, 2 ) );
  res.end();
});

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

express-rate-limit ライブラリをインスタンス化して、属性を指定して app.use() でリクエスト処理前に実行されるようにしています。なお各属性値は以下のような意味です:
・windowMs: 制限をかける時間(ミリ秒)
・max: windowMs で定義した時間でのリクエスト処理上限数
・standardHeaders: "RateLimit-*" でリクエスト制限情報を HTTP ヘッダに含める※
・legacyHeaders: "X-RateLimit-*" でリクエスト制限情報を HTTP ヘッダに含める※

※上述のサンプルでは standardHeaders: true, legacyHeaders: false に指定している

これだけでアプリケーションレベルでリクエスト制限を実装できます。 ただし、この制限は「1インスタンスごとの制限」である点に注意が必要です。クラウド的な言い方だと「1コンテナ」あたりでこの制限が有効になりますが、複数インスタンスで運用した場合、例えば可用性を高める目的で3つのコンテナを起動して運用した場合は、事実上設定値の3倍のリクエスト処理を受け付けることになる、という点に注意が必要です。



備忘録的なブログエントリです。

高速なメモリストアである RedisNode.js から使う場合に多く利用されるライブラリの代表が node-redis です:
2022013000


自分もこれまでに開発した多くのアプリの中で Redis を利用し、Node.js で実装していた場合はほぼ全てで node-redis を使っていました。いわゆる Key-Value 型のデータベースで、高速なデータストア用途だけでなく、ログインセッションの共有保管場所としても使っていました。

これまで使ってきた中でこのような用途がなかったからではあるのですが、今回「登録されている全てのデータのキー」を取得したい、ということがあり、すぐにわからなかったので調べてみました。要は全データを取り出したいのですが、そのためには全キーが必要で、その取得方法を知りたかったのでした。本ブログエントリはその調査結果でもあります。

結論としては RedisClient の keys() というメソッドを使うことで全キーを取得できます。以下サンプルです:
var redis = require( 'redis' );
var redisClient = redis.createClient( "redis://localhost:6379", {} );
redisClient.keys( '*', function( err, results ){
  if( err ){
  }else{
    :
  (results に全キーが配列で格納されている)
    :
  }
});


keys() メソッドを '*'(全て)というパラメータを付けて実行することで全てのキーを取得することができます。

redis CLI でも、
> keys *

のように実行してキー一覧を取得できることは知っていたのですが、node-redis ライブラリで取得する場合の方法を調べるのに手間取ったこともあって、自分でまとめておきました。

コンテナ環境が広まっていくと、アプリケーションやサービスが安定運用できるようになる、という話を耳にします。それはそれで事実だと思うのですが、本当にサービスを安定運用するにはアプリケーションが使うバックエンドも安定運用が必要で、そのあたりの話が飛ばされていることが多いと感じることもあります。 今回のブログエントリはそういう話の例として、Node.js からリレーショナルデータベース(今回は PostgreSQL)を使うアプリケーションでどういった点を考慮すべきか、という例を紹介します。


Node.js から PostgreSQL に接続する、というコード自体は node-postgres という npm パッケージを使うことで簡単に実現できます。具体的な方法もググって容易に見つけることができます。

一方、特にクラウドやコンテナ環境においてはマイクロサービス化を踏まえた設計になっていることも珍しくないと思っています。そのようなケースでは「PostgreSQL がメンテナンス状態になる(接続が切れる)」ことを想定する必要があります。アプリケーションとしては「データベース接続が切断される可能性があり、切断されたら再接続する(それも失敗したら再接続を続ける)」という挙動になるような、ちと面倒な実装が求められます:
20211027



そのような具体的に動くサンプルコードを探していたのですが、ピンポイントで見つけることができず、色々試しながら自分で作ってみました。docker で PostgreSQL イメージを起動し、コンテナを止めたり再スタートすることでアプリも再接続することを確認することができるものです:
https://github.com/dotnsf/postgresql_reconnect


なお、以下の内容は PostgreSQL で(一般的なアプリではこれが普通だと思っていますが)コネクションプーリングを使う前提での接続や SQL 実行を想定したコードを紹介します。


【動作確認】
以下 Node.js が導入された PC と、ローカルの docker を使ってアプリケーションの動作確認する場合の手順を紹介します。

まずはソースコード一式を入手してください。上述の Github リポジトリから git clone するか、ダウンロード&展開して postgresql_reconnect/ プロジェクトを手元に用意します。

まずは docker で PostgreSQL を動かします。起動時に DB を作成しますが、特にテーブルやデータを作ることもなく、単に起動するだけ、です:
$ docker run -d --name postgres -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=P@ssw0rd -e POSTGRES_DB=mydb -p 5432:5432 postgres

↑このコマンドはローカルホストに docker エンジンが導入されている前提で、
 ・ユーザー名: admin
 ・パスワード: P@ssw0rd
 ・データベース名: mydb
 ・公開ポート番号: 5432
でオフィシャル PostgreSQL イメージをコンテナとして起動するように指示するコマンドです。各指定オプションを変更して動かすことも可能ですが、後述するサンプルソースコードはこの内容で PostgreSQL インスタンスが生成される前提で記述されているため、ここから変更する場合はこの後に紹介するサンプルコードの内容も変更内容に合わせて適宜編集してから実行してください:
2021102606



まずは切断時の再接続を考慮しないコード: oldapp.js を実行してみます。ちなみに oldapp.js の内容は以下のようになっています(青字部分が PostgreSQL の接続情報赤字部分が接続部分、そしてピンクが SQL 実行部分です):
//. oldapp.js
var express = require( 'express' ),
    app = express();

var PG = require( 'pg' );

//. PostgreSQL
var pg_hostname = 'localhost';
var pg_port = 5432;
var pg_database = 'mydb';
var pg_username = 'admin';
var pg_password = 'P@ssw0rd';

var pg_clinet = null;
var connectionString = "postgres://" + pg_username + ":" + pg_password + "@" + pg_hostname + ":" + pg_port + "/" + pg_database;//+ "?sslmode=verify-full";
var pg = new PG.Pool({
  connectionString: connectionString
});
pg.connect( function( err, client ){
  if( err ){
    //. 初回起動時に DB が動いていない
    console.log( 'no db on startup', err.code );
  }else{
    console.log( 'connected.' );
    pg_client = client;
  }
});

//. top
app.get( '/', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: true }, null, 2 ) );
  res.end();
});

//. ping
app.get( '/ping', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  var sql = 'select 1';
  var query = { text: sql, values: [] };
  pg_client.query( query, function( err, result ){
    if( err ){
      console.log( { err } );
      res.status( 400 );
      res.write( JSON.stringify( { status: false, error: err }, null, 2 ) );
      res.end();
    }else{
      //console.log( { result } );
      res.write( JSON.stringify( { status: true, result: result }, null, 2 ) );
      res.end();
    }
  });
});


var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

実際に Node.js で動かす場合、まず初回のみ依存ライブラリをインストールするため(Node.js 導入済みの環境で)以下のコマンドを実行します:
$ npm install

そして以下のコマンドで oldapp.js を起動します:
$ node oldapp

起動すると "server starting on 8080 ..." と表示され、サーバーが 8080 番ポートでリクエスト待ち状態になります。また、この oldapp.js の場合は起動直後に PostgreSQL に接続を試みるので "connected." と表示されます:
$ node oldapp
server starting on 8080 ...
connected.

上述の oldapp.js の内容からもわかるのですが、このアプリケーションは "GET /" と "GET /ping" という2つの REST API を処理します。前者は単に { status: true } という JSON を返すだけのものです。また後者は接続した PostgreSQL に対して "SELECT 1" という SQL を実行し、その実行結果を返すものです(PostgreSQL に接続できていればなんらかの結果が返るものです)。

試しに前者を実行してみます。ウェブブラウザで "http://localhost:8080/" にアクセスし、{ status: true } が表示されることを確認します:
2021102601


また後者も実行してみます。同様にウェブブラウザで "http://localhost:8080/ping" にアクセスし、{ status: true, result: { .... } } という文字列が表示されることを確認します:
2021102602



ここまでは普通に成功するはずです。
ここからが本番です。この状態でアプリケーションを動かしたまま PostgreSQL サーバーを止めてみます。docker コマンドで止める場合は
$ docker stop postgres

を実行します(docker デスクトップを使っている場合は稼働中のコンテナの STOP ボタンを押すと止まります):
2021102603



この状態で改めてウェブブラウザで各ページにアクセスするとどうなるか? 期待している挙動という意味では以下のようになると思っています:
・GET / へのリクエストについては(DB を使わないので){ status: true } を返す
・GET /ping へのリクエストについては(DB が止まっているので)「DB エラー」を返す
・(更に)DB が再稼働したら自動的に再接続して、GET /ping に対して SQL 実行結果を返す


ところが、実はこの時点でサーバーはクラッシュしています。$ node oldapp を実行したターミナルには Exception が表示された上にアプリケーションは終了し、プロンプトが表示されてしまっています:
2021102604


つまりサーバーがクラッシュしています。したがって GET /ping どころか、GET / へのリクエストもエラーとなってしまうし、稼働していないので自動再接続もできません:
2021102605


これでは困ってしまいます。簡単なデモ程度が目的の実装であれば oldapp.js の内容でも(とりあえず動くので)いいと思いますが、ある程度安定した連続稼働が求められる実運用を想定するとちょっと心細い状況と言えます。



では oldapp.js と同じ内容で、DB を止めてもアプリケーションが死ぬこともなく、DB が復活したら自動再接続して再び SQL が実行できるようになるような実装はどのようにすればよいでしょうか? その例が newapp.js です:
//. newapp.js
var express = require( 'express' ),
    app = express();

var PG = require( 'pg' );

//. PostgreSQL
var pg_hostname = 'localhost';
var pg_port = 5432;
var pg_database = 'mydb';
var pg_username = 'admin';
var pg_password = 'P@ssw0rd';

var retry_ms = 5000;  //. retry every 5 sec

var connectionString = "postgres://" + pg_username + ":" + pg_password + "@" + pg_hostname + ":" + pg_port + "/" + pg_database;//+ "?sslmode=verify-full";
console.log( 'connecting...' );
var pg = new PG.Pool({
  connectionString: connectionString
});
pg.on( 'error', function( err ){
  console.log( 'db error on starting', err );
  if( err.code && err.code.startsWith( '5' ) ){
    //. terminated by admin?
    try_reconnect( retry_ms );
  }
});

function try_reconnect( ts ){
  setTimeout( function(){
    console.log( 'reconnecting...' );
    pg = new PG.Pool({
      connectionString: connectionString
    });
    pg.on( 'error', function( err ){
      console.log( 'db error on working', err );
      if( err.code && err.code.startsWith( '5' ) ){
        //. terminated by admin?
        try_reconnect( ts );
      }
    });
  }, ts );
}

//. top
app.get( '/', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: true }, null, 2 ) );
  res.end();
});

//. ping
app.get( '/ping', async function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  var conn = null;
  try{
    conn = await pg.connect();
    var sql = 'select 1';
    var query = { text: sql, values: [] };
    conn.query( query, function( err, result ){
      if( err ){
        console.log( { err } );
        res.status( 400 );
        res.write( JSON.stringify( { status: false, error: err }, null, 2 ) );
        res.end();
      }else{
        //console.log( { result } );
        res.write( JSON.stringify( { status: true, result: result }, null, 2 ) );
        res.end();
      }
    });
  }catch( e ){
    res.status( 400 );
    res.write( JSON.stringify( { status: false, error: e }, null, 2 ) );
    res.end();
  }finally{
    if( conn ){
      conn.release();
    }
  }
});


var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

青字部分が PostgreSQL の接続情報赤字部分が接続部分、そしてピンクが SQL 実行部分です(青字部分は全く同じです)。違いを紹介する前にまずは挙動を確認してみましょう。PostgreSQL を再び稼働状態に戻します:
2021102606


この状態で "$ node newapp" を実行して newapp.js を起動します:
$ node newapp
server starting on 8080 ...
connecting...

同じようなメッセージが表示されて、リクエスト待ち状態になります。まずは先ほど同様に GET / や GET /ping を実行します(実行結果自体は先ほどと同じです):
2021102601

2021102602


ではここでも同様に PostgreSQL を強制停止してみましょう。先ほどはアプリケーションがクラッシュしてリクエスト待ち状態ではなくなってしまいましたが、今回はプロンプトには戻らず、引き続き待ち受け状態が続くはずです:
2021102601


この状態で改めて GET / や GET /ping にアクセスしてみます。GET / は変わらず { status: true } を返し、GET /ping は(DB にアクセスできないので) { status: false, error: ... } という内容になりますが、ちゃんとレスポンスを返すことができています:
2021102601

2021102602


そして止まっていた PostgreSQL を再度スタートします:
2021102606



するとアプリケーションが自動的に再接続を行い、少ししてから GET /ping を実行すると、再び SQL 実行が成功した時の画面が表示されます:
2021102602


これでデータベースにメンテナンスが入っても自動再接続して稼働する、という実用的な挙動が実現できました。


【コード説明】
改めて2つのソースコードを比較します。といっても青字部分は共通なので比較の対象からははずし、赤字の接続部分と、ピンク字の SQL 実行部分を比較します。

まずは前者の自動再接続しない方。接続部分と SQL 実行部分は以下のようでした:
var pg_clinet = null;
var connectionString = "postgres://" + pg_username + ":" + pg_password + "@" + pg_hostname + ":" + pg_port + "/" + pg_database;//+ "?sslmode=verify-full";
var pg = new PG.Pool({
  connectionString: connectionString
});
pg.connect( function( err, client ){
  if( err ){
    //. 初回起動時に DB が動いていない
    console.log( 'no db on startup', err.code );
  }else{
    console.log( 'connected.' );
    pg_client = client;
  }
});

  :
  :

//. ping
app.get( '/ping', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  var sql = 'select 1';
  var query = { text: sql, values: [] };
  pg_client.query( query, function( err, result ){
    if( err ){
      console.log( { err } );
      res.status( 400 );
      res.write( JSON.stringify( { status: false, error: err }, null, 2 ) );
      res.end();
    }else{
      //console.log( { result } );
      res.write( JSON.stringify( { status: true, result: result }, null, 2 ) );
      res.end();
    }
  });
});

接続処理では単純に接続文字列を生成してコネクションプーリングを生成し、直後に connect() を実行してクライアントを1つ取り出しています。そしてこのクライアントをこの後の SQL 実行時に使いまわしています。

また SQL 実行処理でも SQL 文字列を定義して↑で取り出したクライアントを使って実行しています。処理そのものはわかりやすいのですが、一方でこれといった例外発生を考慮した内容でもありません(そのため DB が止まってしまうケースが想定できておらず、アプリケーションのクラッシュを引き起こしてしまう内容でした)。


一方の後者、例外発生を考慮して、切断後に自動再接続できるようにした方の接続部分と SQL 実行部分は以下のようでした:
var retry_ms = 5000;  //. retry every 5 sec

var connectionString = "postgres://" + pg_username + ":" + pg_password + "@" + pg_hostname + ":" + pg_port + "/" + pg_database;//+ "?sslmode=verify-full";
console.log( 'connecting...' );
var pg = new PG.Pool({
  connectionString: connectionString
});
pg.on( 'error', function( err ){
  console.log( 'db error on starting', err );
  if( err.code && err.code.startsWith( '5' ) ){
    //. terminated by admin?
    try_reconnect( retry_ms );
  }
});

function try_reconnect( ts ){
  setTimeout( function(){
    console.log( 'reconnecting...' );
    pg = new PG.Pool({
      connectionString: connectionString
    });
    pg.on( 'error', function( err ){
      console.log( 'db error on working', err );
      if( err.code && err.code.startsWith( '5' ) ){
        //. terminated by admin?
        try_reconnect( ts );
      }
    });
  }, ts );
}

  :
  :

//. ping
app.get( '/ping', async function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  var conn = null;
  try{
    conn = await pg.connect();
    var sql = 'select 1';
    var query = { text: sql, values: [] };
    conn.query( query, function( err, result ){
      if( err ){
        console.log( { err } );
        res.status( 400 );
        res.write( JSON.stringify( { status: false, error: err }, null, 2 ) );
        res.end();
      }else{
        //console.log( { result } );
        res.write( JSON.stringify( { status: true, result: result }, null, 2 ) );
        res.end();
      }
    });
  }catch( e ){
    res.status( 400 );
    res.write( JSON.stringify( { status: false, error: e }, null, 2 ) );
    res.end();
  }finally{
    if( conn ){
      conn.release();
    }
  }
});

まず接続処理ではコネクションプーリングを生成するまでは同じですが、ここではそのまま終了します。connect() を実行してクライアントを取り出す、のは実際に SQL を実行する直前の処理に変更しています。またこのコネクションプーリングを管理する変数 pg を使ってエラーハンドリングを行い、DB 切断時に正しくハンドリングできるようにしています(具体的には数秒待ってから再びコネクションプーリングを生成し、新たに生成したコネクションプーリングに対してもエラーハンドリングを行う、という内容です)。

また SQL 実行時には以下のような処理を加えています:
(1)処理全体を try{ .. }catch{ .. }finally{ .. } で括り、どこで切断しても例外処理できるようにする
(2)try{ .. } 内の実際に SQL を実行する直前に pg.connect でクライアントを取り出す
(3)finally{ .. } 内でクライアントをリリースしてコネクションプーリングに戻す

この3つの処理を加えておくことで DB が突然死んでも正しくハンドリングして再接続(再びコネクションプーリングを作る)を試みるようにしています。また結果的に再接続に時間がかかってしまう場合であってもアプリケーションそのものはクラッシュせずに生き続けるので、(DB にアクセスはできないけど)利用者からのリクエストに返答できるようにしています。



・・・というわけで、公開&提供しているのはあくまでサンプルですが、この考え方で PostgreSQL サーバーの切断時でも自動接続して連続稼働ができるようになると思っています。また PostgreSQL 以外のコネクションを使う RDB を利用する際にも応用できる内容です。 利用するインフラプラットフォームによってはこういったメンテナンス時の再接続方法について特殊な機能が用意されていることもあるので、必ずしもこういう方法をとらないといけない、というわけではないのですが、一つのベストプラクティス的な内容だと思っています。

自分が痛い目にあったことに関わる内容でもあるので、困っている方のお役に立てば何より。


本ブログエントリは IBM Cloud アドベントカレンダー 2021 に参加しています。12/4 ぶんのネタです。


IBM Cloud から提供されているマネージド NoSQL データベースである IBM Cloudant をよく使っています。最大の魅力は無料アカウントでも容量 1GB まで使えるストレージで、かつデータが分散管理されているという安全性です。仕様が曖昧な状態からアジャイルに作り始める際は(テーブル定義などを意識する必要がないこともあって)むしろ NoSQL データベースの方が便利だったりもします。

この IBM Cloudant を Node.js プログラムから使う場合のライブラリとして長く @cloudant/cloudant を使ってきましたが、2021/12/31 を以って End of Support を迎えることになり、現在は deprecated なライブラリ、という扱いになっています。2021年8月の今は「まだ使えるけど、もうすぐ使えなくなるよ」って所でしょうか。可能であれば、これから新たに作り始める時には利用を避けたほうが安全だと思います。

で、その後継ライブラリとして公開されているのが @ibm-cloud/cloudant (IBM Cloudant Node.js SDK)です。IBM Cloudant だけでなく、そのベースとなっている Apache CouchDB に対してももちろん使うことができるものです。現在 @cloudant/cloudant ライブラリを使っているアプリケーションはなるべく今年中にこの新しい @ibm-cloud/cloudant に移植することをおすすめします。

・・・と言うのは簡単ですが、実際この @ibm-cloud/cloudant は @cloudant/cloudant と比べてどのくらい似ていて/同じで、移植はどの程度簡単/難しいのでしょう? というわけで、まずは @ibm-cloud/cloudant を使ってみることにしました。


【サンプルソースコード】
以下で紹介するサンプルアプリケーションのソースコードを github で公開しています:
https://github.com/dotnsf/cloudant-node-sdk_sample


【接続方法の決定】
@ibm-cloud/cloudant を使って Cloudant(CouchDB) にアクセスする場合の接続方法には3通りあります:
(1)IAM
(2)COUCHDB_SESSION
(3)BASIC


(1)の IAM は IBM Cloudant を IAM 接続サポート形式で作成した場合に利用できる方法です。公式ドキュメントでも「この方法が使える場合はこの方法で」と紹介されています。

(2)の COUCHDB_SESSION を使った方法でも接続が可能です。

(3)の BASIC はいわゆる「ベーシック認証」です。この3つの中では「この方法しか使えない場合の選択肢」と紹介されています。CouchDB を自分でインストールして使う場合など、この方法で接続する前提のセットアップをしているとこの方法でしか接続できないことになります。もちろん Cloudant でもベーシック認証をサポートした形式で作成している場合は利用可能です。

(1)、(2)、(3)のいずれの方法でアプリケーションとデータベースを接続するかを決めておく必要があります。正確には @ibm-cloud/cloudant は「環境変数を設定して接続する」のですが、どの方法で接続するかによって、設定が必要になる環境変数が変わる点に注意が必要です。


【Node.js から接続する際に必要な環境変数】
@ibm-cloud/cloudant を使って Cloudant/CouchDB に接続する際に設定が必要な環境変数は以下です(横のカッコ付き数字は、上述の(1)、(2)、(3)のどの方法を使った時に必要な環境変数か、を表しています)。また変数名の頭の "CLOUDANT" 部分は別の値でも構いませんが、同じ文字列に統一して設定する必要があります:

変数意味
CLOUDANT_AUTH_TYPE上述の接続方法。"IAM"(1), "COUCHDB_SESSION"(2), "BASIC"(3) のいずれか。デフォルト値は "IAM"
CLOUDANT_URLデータベースの URL((1)、(2)、(3))
CLOUDANT_APIKEYAPI キーの値((1))
CLOUDANT_USERNAMEユーザー名((2)、(3))
CLOUDANT_PASSWORDパスワード((2)、(3))


環境に合わせてこれらの値を用意して実際に接続してみます。


【Node.js から Cloudant に接続】
(1)IAM 接続を行う場合は、上述の表より以下の環境変数を設定します:
process.env['CLOUDANT_AUTH_TYPE'] = 'IAM'; //(デフォルト値なので設定しなくてもよい)
process.env['CLOUDANT_APIKEY'] = 'xxxxx'; //(API Key の値)
process.env['CLOUDANT_URL'] = 'https://xxxxxxx-bluemix.cloudantnosqldb.appdomain.cloud'; //(Cloudant のURL)

そして @ibm-cloud/cloudant ライブラリを読み込んで、以下のコードを実行します:
var { CloudantV1 } = require( '@ibm-cloud/cloudant' );

var client = CloudantV1.newInstance( { serviceName: 'CLOUDANT' } ); 

このコード実行が成功すると、Cloudant に接続したインスタンスが client という変数に格納され、実際のデータの CRUD 処理が可能になります。

なお上記コードの serviceName 値として 'CLOUDANT' を指定していますが、この部分は変更可能です。ただ変更する場合は環境変数として指定した変数名の頭の CLOUDANT 部分をここで指定する値と同じものに変更してください。

(2)COUCHDB_SESSION 接続を行う場合は、上述の表より以下の環境変数を設定します:
process.env['CLOUDANT_AUTH_TYPE'] = 'COUCHDB_SESSION';
process.env['CLOUDANT_USERNAME'] = 'username'; //(ユーザー名の値)
process.env['CLOUDANT_PASSWORD'] = 'password'; //(パスワードの値)
process.env['CLOUDANT_URL'] = 'https://xxxxxxx-bluemix.cloudantnosqldb.appdomain.cloud'; //(Cloudant/CouchDB のURL)

そして @ibm-cloud/cloudant ライブラリを読み込んで、以下のコードを実行します:
var { CloudantV1 } = require( '@ibm-cloud/cloudant' );

var client = CloudantV1.newInstance( { serviceName: 'CLOUDANT' } ); 

(3)BASIC 接続を行う場合は、上述の表より以下の環境変数を設定します:
process.env['CLOUDANT_AUTH_TYPE'] = 'BASIC';
process.env['CLOUDANT_USERNAME'] = 'username'; //(ユーザー名の値)
process.env['CLOUDANT_PASSWORD'] = 'password'; //(パスワードの値)
process.env['CLOUDANT_URL'] = 'http://xxx.xxx.xxx.xxx:5984'; //(Cloudant/CouchDB のURL)

そして @ibm-cloud/cloudant ライブラリを読み込んで、以下のコードを実行します:
var { CloudantV1 } = require( '@ibm-cloud/cloudant' );

var client = CloudantV1.newInstance( { serviceName: 'CLOUDANT', disableSslVerification: true } ); 

なお(CouchDB の場合に多いと想像していますが) SSL 接続が不要の場合は上述のように接続時のパラメータに disableSslVerification: true を追加してください。

参考までに、@cloudant/cloudant の場合は、
var Cloudantlib = require( '@cloudant/cloudant' );
var cloudant = Cloudantlib( { account: "username", password: "password", url: 'https://xxxxxxx-bluemix.cloudantnosqldb.appdomain.cloud' } );
var db = cloudant.db.use( "dbname" );

といった感じでデータベースまでを取得した上で、
db.get( "id", function( err, result ){
  if( err ){
    console.log( err );
  }else{
    console.log( result );
  }
});

のようにして特定データを取得したりしていましたが、この @ibm-cloud/cloudant ではまだデータベースが特定されていない点にご注意ください。


【接続後の CRUD 処理の例】
では接続後の Cloudant クライアントを使って、IBM Cloudant のデータを読み書きしてみましょう。まず対象データベースと id がわかっている特定データを取得してみます。@ibm-cloud/cloudant では以下のような getDocument() メソッドを使うコードとなります:
client.getDocument( { db: "dbname", docId: "id" } ).then( function( result ){
  console.log( result );
}).catch( function( err ){
  console.log( err );
});

client.getDocument() を実行し、そのパラメータ内でデータベースと id を指定する、という方式になります。この点からして @cloudant/cloudant とは異なってますね。

また特定データベースの全データをまとめて取得(@cloudant/cloudant だと db.list() )する場合は以下のような postAllDocs() メソッドを使うコードです:
client.postAllDocs( { db: "dbname", includeDocs: true } ).then( function( result ){
  console.log( result );
}).catch( function( err ){
  console.log( err );
});

データを一件追加する場合は以下のような postDocument() メソッドを使うコードになります:
client.postDocument( { db: "dbname", document: { name: "Kimura" } } ).then( function( result ){
  console.log( result );
}).catch( function( err ){
  console.log( err );
});

以上、あくまでいくつかのメソッドを紹介しただけですが、全般的に行うオペレーションとメソッド名の関係がわかりやすく整頓されているように感じて、比較的慣れやすいのではないかと感じています。



IBM Watson から提供されている AI 系 API の中で、自然言語テキストを分類する機能を持った NLC(Natural Language Classifier) の提供が終了することがアナウンスされました。2021年9月9日までは新規インスタンスの作成が可能ですが、それ以降の新規作成はできません。また既存インスタンスは2022年8月8日まで利用できますが、それまでに移行先を決め、その移行作業を済ませておく必要があります:
https://cloud.ibm.com/docs/natural-language-classifier?topic=natural-language-classifier-migrating


NLC は個人的にも IBM Watson を使い始めるきっかけになった API で、思い入れの深いサービスだったりします。NLC を使って作った多くのデモアプリは WordCamp Tokyo や特定のお客様向けに作ったものを含めて多くの場で紹介させていただきました。感慨深いものがあります。


上記アナウンス内で、公式な移行先として同じ IBM Watson の NLU(Natural Language Understandings) が紹介されています。が、具体的な移行手順や方法に関する情報が少ないこともあり、実際に自分が NLC API を使って開発したアプリケーションをどのように NLU に移行すればよいかがわかりにくいように感じました。 自分自身の場合は普段 Node.js を使ってアプリケーションを開発しているのですが、Node.js の場合は具体的にどのようにすれば NLC から NLU へ移行できるか? という調査をしてみました。実際に Node.js でプログラムを書き、具体的にはまず NLC API を使って、
 ・認証
 ・日本語データ学習
 ・学習状況確認
 ・問い合わせ(分類)
 ・学習の削除
といった5種類のオペレーションを行えるようなアプリケーションを作りました。 そしてそのアプリケーションを NLC から NLU へ実際に移植する、といった作業を行ってみました。結論として上記5種類のオペレーションは NLU でも NLC 同様に行うことができました。ただ API には互換性はなく、ソースコードレベルではそれなりに変更が必要になるため、どの程度の変更が必要になるのか、という調査の意味も含めて作業した様子を以下に紹介します。

なお、NLC は IBM Cloud の(無料版の)ライトアカウント向けには提供されていない API である点に注意してください。ライトアカウントの状態で NLC の新規インスタンスを作成することは(9月9日以前であっても)できません。ベーシック以上の(有償の)アカウントであれば無料枠含めて提供されている API です。 一方の NLU はライトアカウントでも利用することが可能ですが、ライトアカウントの場合はライトプラン(無料)の NLU を1インスタンスのみ作成でき、1インスタンスにつき1モデルだけ学習で作成できます(要は複数の学習モデルを作成するにはベーシック以上のアカウントで、有料プランを選択する必要があります)。
2021082200



【サンプルコードのダウンロード】
以下で紹介する一連の手順を行うための Node.js 用サンプルコードを作って公開しました:
https://github.com/dotnsf/nlc2nlu

git clone するかコードをダウンロード&展開してください。以下の3つのフォルダが含まれています:
|- csvtool
|- nlc
|- nlu

csvtool は実際に NLC や NLU で使うサンプルの学習データを作成するツールです。NLC や NLU を既に使っていて、どのような学習データを用意すればよいかわかっている場合は、ご自身で学習データ(CSV ファイル)を用意いただいてもかまいませんが、そうでない人向けに Yahoo! ニュースの RSS(https://news.yahoo.co.jp/rss) を使って、その場で学習データを作成できるようにしたものです。

nlc と nlu は名前の通りで、学習データを学習させて、学習状況を確認しながら学習が完了したら、実際に適当な日本語テキストを送信して、学習データに基づくテキスト分類を実施します。また実施後に学習データを削除する、といった操作も可能です。これらの内容を NLC および NLU それぞれで行えるようにしたツールが含まれています。

ではそれぞれのフォルダの使い方を説明します。


【csvtool : 学習データの用意】
csvtool/csvgenerator.jsYahoo! ニュース RSS から学習データとしての CSV ファイルを作成します。NLC / NLU とも学習データは以下のフォーマットの CSV ファイルを用意します※:
テキスト,このテキストが属するカテゴリー
テキスト,このテキストが属するカテゴリー
テキスト,このテキストが属するカテゴリー
  :

※ NLU は JSON フォーマットでも学習データを準備して読み込むことが可能ですが、NLC からの移植を考えると NLC と同じ条件で学習データを用意するべきと考え、同じ CSV ファイルを学習データとして使うことにします。

このような(NLC 向けの)学習データを既にお持ちであればそれを使って後述の作業を続けていただいてもかまいませんが、多くの人はそのような学習データを持っていないと思うので、新たに用意することにします。その場合は csvtool/csvgenerator.js を使います。

実行方法は単純で、csvtool フォルダに移動し、まず普通に $ npm install を実行して依存ライブラリを導入します:
$ cd csvtool

$ npm install

その後、node コマンドでこのファイルを実行します。実行コマンドの最後に出力先 CSV ファイル名を指定します(以下の例だと同一フォルダ内の csvfile.csv):
$ node csvgenerator csvfile.csv

実行したタイミングで Yahoo! ニュースの RSS を参照して、その時点のニュースを取得して CSV ファイルに書き出します。なお、このツールでは「経済」、「IT」、「エンタメ」、「科学」、「スポーツ」の5つのカテゴリーのニュースを収集します。 ツールの実行が完了すると、指定した CSV ファイルが以下のような内容で生成されます:
RSS から取り出したニュース内容,このニュースのカテゴリー
RSS から取り出したニュース内容,このニュースのカテゴリー
RSS から取り出したニュース内容,このニュースのカテゴリー
   :

ここまでできれば学習データの準備は完了です。ではこのデータを NLC と NLU それぞれで学習させて問い合わせる、という作業をこれ以降で行っていきましょう。


【nlc : NLC で操作】
まずは NLC API を使って開発したアプリでこのデータを学習させ、NLC API を使って問い合わせを行ってみましょう。先ほどダウンロードしたソースコードの nlc フォルダ内にある nlc/nlc.js ファイルを使って操作します(このコードの内容については後述します)。まずは csvgenerator.js の時と同様に  $ npm install を実行して依存ライブラリを導入します:
$ cd nlc

$ npm install

その後、node コマンドでこのファイルを実行します。この nlc.js はコマンドラインアプリケーションで、その実行時パラメータによって「データ学習」、「学習状況確認」、「問い合わせ(分類)」、「学習データ削除」の4つを行うことができます:
$ node nlc create [csvfilename]  ・・・データ学習

$ node nlc status  ・・・学習状況確認

$ node nlc classify [日本語テキスト] [classifier_id]  ・・・問い合わせ(分類)

$ node nlc delete [classifier_id]  ・・・学習データ削除

では実際に nlc ツールを使いながら操作を確認してみましょう。まずは IBM Cloud にウェブブラウザでログインして、NLC インスタンスを作成し、「サービス資格情報」(を必要であれば作成して)の内容を確認します:
2021082201


この中に apikey 属性値と url 属性値が含まれているので、それらの値を nlc/settings.js ファイル内の exports.nlc_apiKey 値および exports.nlc_url 値にそれぞれコピーして保存します:
exports.nlc_apiKey = 'サービス資格情報内の apikey 属性値';
exports.nlc_url = 'サービス資格情報内の url 属性値';
exports.nlc_name = 'nlc2nlu';
exports.nlc_language = 'ja';

これで実行前の準備は完了しました。早速「データ学習」を実行してみます。「データ学習」は(前述の作業で用意した)学習データ CSV ファイルを指定して、$ node nlc create [csvfilename] を実行します。先ほどの手順で ../csvtool/csvfile.csv というファイルが作られている場合は以下のように指定して実行します:
$ node nlc create ../csvtool/csvfile.csv

これで指定した CSV ファイルを元にする学習が開始されます。この学習が完了すると問い合わせ(分類)ができるようになりますが、完了しているかどうかを確認するには以下のコマンドを実行します:
$ node nlc status

実行結果は以下のような JSON が表示されますが、この中の status 欄が "available" となっていれば学習は完了しています(データ量にもよりますが、自分が試した時はこうなるまでおよそ 10 分程度かかりました)。同時に表示されている classifier_id の値はこの後で使うので合わせてメモしておきましょう:
{
  "status": 200,
  "statusText": "OK",
  "headers": {
    "content-type": "application/json",
    "x-xss-protection": "1",
    "content-security-policy": "default-src 'none'",
    "x-content-type-options": "nosniff",
    "cache-control": "no-cache, no-store",
    "pragma": "no-cache",
    "expires": "0",
    "content-length": "428",
    "strict-transport-security": "max-age=31536000; includeSubDomains;",
    "x-dp-watson-tran-id": "e6168d25-5640-4790-a1f1-02ff089dd869",
    "x-request-id": "e6168d25-5640-4790-a1f1-02ff089dd869",
    "x-global-transaction-id": "e6168d25-5640-4790-a1f1-02ff089dd869",
    "server": "watson-gateway",
    "x-edgeconnect-midmile-rtt": "7",
    "x-edgeconnect-origin-mex-latency": "22",
    "date": "Tue, 24 Aug 2021 05:48:32 GMT",
    "connection": "close"
  },
  "result": {
    "classifier_id": "e87efex297-nlc-650",
    "name": "nlc2nlu",
    "language": "ja",
    "created": "2021-08-24T05:45:20.090Z",
    "url": "https://api.jp-tok.natural-language-classifier.watson.cloud.ibm.com/instances/f738a110-248b-419c-b771-8e6cbd45ee93/v1/classifiers/e87efex297-nlc-650",
    "status_description": "The classifier instance is now available and is ready to take classifier requests.",
    "status": "Available"
  }
}


学習が完了したら改めて日本語テキストを指定して、その内容がどのカテゴリーに属しているのかを分類してみましょう。日本語テキストと上述で確認した classifier_id 値を指定して、以下のようなコマンドを実行します:
$ node nlc classify [日本語テキスト] [classifier_id]

すると以下のような結果が表示されます。指定した日本語テキスト(「パッキャオがんばれ」)を学習データに含まれていたカテゴリー(今回作ったものだと「経済」、「IT」、「エンタメ」、「科学」、「スポーツ」)のどれに相当するものかを識別して、その可能性の高い順に結果を返しています。この例では「スポーツ」の可能性が 50% 強である、と判断されているようです:
$ node nlc classify パッキャオがんばれ e87efex297-nlc-650
{ "status": 200, "statusText": "OK", "headers": { "content-type": "application/json", "x-xss-protection": "1", "content-security-policy": "default-src 'none'", "x-content-type-options": "nosniff", "cache-control": "no-cache, no-store", "pragma": "no-cache", "expires": "0", "content-length": "679", "strict-transport-security": "max-age=31536000; includeSubDomains;", "x-dp-watson-tran-id": "102350df-e1d0-4568-ae10-6e72ba1b44b0", "x-request-id": "102350df-e1d0-4568-ae10-6e72ba1b44b0", "x-global-transaction-id": "102350df-e1d0-4568-ae10-6e72ba1b44b0", "server": "watson-gateway", "x-edgeconnect-midmile-rtt": "7", "x-edgeconnect-origin-mex-latency": "43", "date": "Tue, 24 Aug 2021 05:51:22 GMT", "connection": "close" }, "result": { "classifier_id": "e87efex297-nlc-650", "url": "https://api.jp-tok.natural-language-classifier.watson.cloud.ibm.com/instances/f738a110-248b-419c-b771-8e6cbd45ee93/v1/classifiers/e87efex297-nlc-650", "text": "パッキャオがんばれ", "top_class": "スポーツ", "classes": [ { "class_name": "スポーツ", "confidence": 0.5022989563107099 }, { "class_name": "エンタメ", "confidence": 0.20692538301831817 }, { "class_name": "経済", "confidence": 0.1200947580342344 }, { "class_name": "科学", "confidence": 0.11456564859743573 }, { "class_name": "IT", "confidence": 0.05611525403930185 } ] } }


作成した学習データを IBM Watson NLC から削除するには以下のコマンドを実行します:
$ node nlc delete [classifier_id]

ここまでのオペレーションで「データ学習」、「学習状況確認」、「問い合わせ(分類)」、「学習データ削除」の4つを NLC API を使って実現し、そのアプリケーションを実行する方法を紹介しました。


【nlu : NLU で操作】
では改めてこの nlc アプリケーションを NLU API で作り直し、同じ4つのオペレーションができることを確認してみます。先ほどダウンロードしたソースコードの nlu フォルダ内にある nlu/nlu.js ファイルを使って操作します(このコードの内容については後述します)。まずは csvgenerator.js の時と同様に  $ npm install を実行して依存ライブラリを導入します:
$ cd nlc

$ npm install

その後、node コマンドでこのファイルを実行します。この nlu.js もコマンドラインアプリケーションで、その実行時パラメータによって「データ学習」、「学習状況確認」、「問い合わせ(分類)」、「学習データ削除」の4つを行うことができます:
$ node nlu create [csvfilename]  ・・・データ学習

$ node nlu status  ・・・学習状況確認

$ node nlu analyze [日本語テキスト] [model_id]  ・・・問い合わせ(分類)

$ node nlu delete [model_id]  ・・・学習データ削除

※前述の NLC では問い合わせ時に "classify(分類)" という命令を指定していましたが、NLU では同じ作業を "analyze(解析)" と表現しているようです。またパラメータとして指定する ID も NLC では "classifier_id(分類ID)" だったのですが、NLU では "model_id(モデルID)" という表現になっていました。

では実際に nlu ツールを使いながら操作を確認してみましょう。まずは IBM Cloud にウェブブラウザでログインして、NLU インスタンスを作成し、「サービス資格情報」(を必要であれば作成して)の内容を確認します:
2021082401


この中に apikey 属性値と url 属性値が含まれているので、それらの値を nlu/settings.js ファイル内の exports.nlc_apiKey 値および exports.nlc_url 値にそれぞれコピーして保存します:
exports.nlu_apiKey = 'サービス資格情報内の apikey 属性値';
exports.nlu_url = 'サービス資格情報内の url 属性値';
exports.nlu_name = 'nlc2nlu';
exports.nlu_language = 'ja';

これで実行前の準備は完了しました。早速「データ学習」を実行してみます。「データ学習」は(前述の作業で用意した)学習データ CSV ファイルを指定して、$ node nlu create [csvfilename] を実行します。先ほどの手順で ../csvtool/csvfile.csv というファイルが作られている場合は以下のように指定して実行します:
$ node nlu create ../csvtool/csvfile.csv

これで指定した CSV ファイルを元にする学習が開始されます。この学習が完了すると問い合わせ(分類)ができるようになりますが、完了しているかどうかを確認するには以下のコマンドを実行します:
$ node nlu status

実行結果は以下のような JSON が表示されますが、この中の status 欄が "available" となっていれば学習は完了しています。同時に表示されている model_id の値はこの後で使うので合わせてメモしておきましょう。この辺りまでは前述の NLC の時とほぼ同様ですね:
{
  "status": 200,
  "statusText": "OK",
  "headers": {
    "x-powered-by": "Express",
    "content-type": "application/json; charset=utf-8",
    "content-length": "401",
    "etag": "W/\"191-CBnewThU0u/CjNzD01hil1wp9qo\"",
    "strict-transport-security": "max-age=31536000; includeSubDomains;",
    "x-dp-watson-tran-id": "15b238e1-d074-438d-8de5-44fcdfc163de",
    "x-request-id": "15b238e1-d074-438d-8de5-44fcdfc163de",
    "x-global-transaction-id": "15b238e1-d074-438d-8de5-44fcdfc163de",
    "server": "watson-gateway",
    "x-edgeconnect-midmile-rtt": "7",
    "x-edgeconnect-origin-mex-latency": "228",
    "date": "Tue, 24 Aug 2021 05:54:08 GMT",
    "connection": "close"
  },
  "result": {
    "models": [
      {
        "name": "nlc2nlu",
        "user_metadata": null,
        "language": "ja",
        "description": null,
        "model_version": "1.0.0",
        "version": "1.0.0",
        "workspace_id": null,
        "version_description": null,
        "status": "available",
        "notices": [],
        "model_id": "619ad785-0b0c-4981-8217-bd51064896a3",
        "features": [
          "classifications"
        ],
        "created": "2021-08-24T05:43:32Z",
        "last_trained": "2021-08-24T05:43:32Z",
        "last_deployed": "2021-08-24T05:50:10Z"
      }
    ]
  }
}

学習が完了したら改めて日本語テキストを指定して、その内容がどのカテゴリーに属しているのかを分類してみましょう。NLC では "classify" と指定していた部分は NLU では "analyze" となる点に注意してください。日本語テキストと上述で確認した model_id 値を指定して、以下のようなコマンドを実行します:
$ node nlu analyze [日本語テキスト] [model_id]

すると以下のような結果が表示されます。指定した日本語テキストを学習データに含まれていたカテゴリー(今回作ったものだと「経済」、「IT」、「エンタメ」、「科学」、「スポーツ」)のどれに相当するものかを識別して、その可能性の高い順に結果を返しています。同じ学習データで同じ問い合わせをして、こちらでも「スポーツ」の可能性が高いと判定されていますが、その確率や2位以下の結果には違いがあることがわかります:
$ node nlu analyze パッキャオがんばれ 619ad785-0b0c-4981-8217-bd51064896a3

{
  "status": 200,
  "statusText": "OK",
  "headers": {
    "server": "watson-gateway",
    "content-length": "499",
    "content-type": "application/json; charset=utf-8",
    "cache-control": "no-cache, no-store",
    "x-dp-watson-tran-id": "93978bcf-c49e-41e9-ad00-7f0000a34e15, 93978bcf-c49e-41e9-ad00-7f0000a34e15",
    "content-security-policy": "default-src 'none'",
    "pragma": "no-cache",
    "x-content-type-options": "nosniff",
    "x-frame-options": "DENY",
    "x-xss-protection": "1; mode=block",
    "strict-transport-security": "max-age=31536000; includeSubDomains;",
    "x-request-id": "93978bcf-c49e-41e9-ad00-7f0000a34e15",
    "x-global-transaction-id": "93978bcf-c49e-41e9-ad00-7f0000a34e15",
    "x-edgeconnect-midmile-rtt": "10",
    "x-edgeconnect-origin-mex-latency": "354",
    "date": "Tue, 24 Aug 2021 05:56:16 GMT",
    "connection": "close"
  },
  "result": {
    "usage": {
      "text_units": 1,
      "text_characters": 9,
      "features": 1
    },
    "language": "ja",
    "classifications": [
      {
        "confidence": 0.402721,
        "class_name": "スポーツ"
      },
      {
        "confidence": 0.326038,
        "class_name": "経済"
      },
      {
        "confidence": 0.314985,
        "class_name": "IT"
      },
      {
        "confidence": 0.255796,
        "class_name": "エンタメ"
      },
      {
        "confidence": 0.21978,
        "class_name": "科学"
      }
    ]
  }
}


作成した学習データを IBM Watson NLU から削除するには以下のコマンドを実行します:
$ node nlu delete [model_id]

ここまでのオペレーションで NLC 同様に NLU でも「データ学習」、「学習状況確認」、「問い合わせ(分類)」、「学習データ削除」の4つを API で実現し、そのアプリケーションを実行する方法を紹介しました。とりあえず、この4つのオペレーションについては NLC から NLU へ移行することはできそうだ、という感触が持てる結果になりました。


【API レベルの移行作業】
同じオペレーションを行う NLC のツールと NLU のツールを実際に Node.js + IBM Watson SDK で開発してわかったことを記載しておきます。

まず API に互換性はありません。IAM を使った認証部分についてはほぼ同じなのですが、今回行った4つのオペレーションを実現するためのそれぞれの API は NLC のものと NLU のものは全く異なります:
NLC の関数 オペレーションの種類 NLU の関数
IamAuthenticator() 認証 IamAuthenticator()
createClassifier() 分類器/モデルの作成 createClassificationModel()
listClassifiers() 作成した分類器/モデルの参照 listClassificationModels()
classify() 分類/解析 analyze()
deleteClassifier() 分類器/モデルの削除 deleteClassificationModel()


パラメータの指定方法など、詳しくはソースコード(nlc/nlc.js / nlu/nlu.js)を参照していただきたいのですが、大まかには上記表のような関数の違いがあります。特に Node.js + IBM Watson SDK を使っているアプリケーションにおいて NLC から NLU へ移植する場合は、表の左列にある関数を使っている箇所を右列の関数に置き換える、というのが大まかな流れになると思います(実際にはパラメータの指定方法だったり、返り値のフォーマットの違いもあったりするので、単なる文字列置換というわけにはいかないと思います)。また NLC では「分類器(Classifier)」と呼んでいたものが NLU だと「分類モデル(ClassficationModel)」と呼び名が変わっていたりもするので、資料を参照する場合の注意も必要だと思います。

加えて、上記のように NLC を使って問い合わせた結果と NLU に移植後に問い合わせた結果は必ずしも同じ結果とはいきません。このあたりの整合性についても移植の前後で意識しておく必要があります。

とはいえ、少なくとも API のレベルで NLC から NLU へアプリケーションを移植することは不可能ではなさそう、という感触も得ることができました。主要なオペレーションに関しては上述の表を使って関数を新しいものに置き換え、実行結果のフォーマットの違いをプログラミング内で吸収することができれば、ある程度の実現目途は立ちそうだと思っています。


今回の NLC のサービス終了自体は残念ではありますが、一方で見方を変えると、これまで有償サービスでしか提供されていなかった NLC の機能が、無料のライトプランで使える NLU でも利用できるようになった、とも言えます。API レベルでの互換性はありませんが、機能的には珍しくこれまでの機能の多くが移植されていて、「アプリケーションの作り変え」による対応が可能なレベルでマイグレーションができるようになっていると感じました。「料金的にも発展的なサービス統合」であると感じています。


Node.js 以外の開発言語での場合や、IBM Watson SDK の利用有無の違いをどこまで吸収できるかまでを調査したわけではないのですが、一部の言語については後述の参考資料の中でサンプルコードもあるようなので、別環境においてもぜひ挑戦していただき、情報が共有されていくことを願っています。



【参考資料】
https://cloud.ibm.com/docs/natural-language-classifier?topic=natural-language-classifier-migrating

https://cloud.ibm.com/apidocs/natural-language-understanding?code=node


このページのトップヘ