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

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

タグ:cloudant

IBM Bluemix からも提供されている、スケーラブルな NoSQL DBaaS である Cloudant 。データの読み書きには REST API が提供されており、ウェブアプリケーションだけでなく、スマホのネイティブアプリなど色々なアプリケーションから利用することができます。
2017032101


この Cloudant はいわゆる「JSON ドキュメント」を格納するデータベースなのですが、バイナリデータ(というかファイル)を扱う機能("attachement")も持っています。

以下にバイナリデータを格納する方法を紹介します。まずは JSON データを無視して「バイナリデータだけを新たに格納する場合」は以下のような JSON データを新規作成します(赤字はコメント):
{
  "_id": "D001",  Cloudant 上でのドキュメントID、省略した場合は作成時に自動的に割り振られる
  "_attachments": { この _attachments オブジェクトがバイナリ保存時の肝
    "A001": {     任意に付ける Attachment 名、取り出し時のURLに指定する
      "content_type": "image/jpeg",  バイナリデータの Content-Type
      "data": (バイナリデータを Base64 エンコードしたテキストデータ)
    }
  }
}

上記のデータを(普通の JSON ドキュメントと同様に)以下の URL に対して POST リクエストすると、このバイナリデータを含むドキュメント(と attachment )が新規に Cloudant 内に作成されます:
https://(Cloudant のホスト名)/(データベース名)


また、作成したバイナリデータを取り出す場合は、以下の URL に対して GET リクエストを実行します:
https://(Cloudant のホスト名)/(データベース名)/(ドキュメント ID)/(Attachment 名)


バイナリデータだけのドキュメントを作成する場合は上記の方法でした。一方、バイナリデータも含む JSON データを保存する場合は、以下のような JSON データを用意して、同様に POST します:
{
  "_id": "D001",
  "myname": "abc",   この2つの値が普通の JSON データとして扱われる部分
  "myvalue": 123,
  "_attachments": {
    "A001": { 
      "content_type": "image/jpeg",  
      "data": (バイナリデータを Base64 エンコードしたテキストデータ)
    }
  }
}

取得時には content_type で指定したデータ型が有効になってレスポンスが返ってきます。なので、例えばバイナリデータとして JPEG 画像データを格納し、その際の cotent_type 値に "image/jpeg" などの正しい型が指定されていれば、取得 URL にブラウザでアクセスすればそのまま画像を表示することができます:
2017032102

(↑ Cloudant 上に格納した画像データを直接 URL 指定で表示している様子)


一般的にはファイルなどのバイナリデータをネット上のストレージに格納する場合は Object Storage などを使うことが多いと思っています。が、Content-Type を意識して取り出したり、「(添付ファイルなどの)JSON ドキュメントに紐付いたバイナリデータ」として利用する場合に便利な機能だと思っています。


なお、Cloudant の Attachment 関連 API についてはこちらを参照ください:
https://docs.cloudant.com/attachments.html
 

以前に Cloudant データベースにインデックスを作成する方法を紹介しました:
Cloudant 内のデータをインデックスして検索する


上記エントリの (2) で紹介した方法は単独のフィールド(cputemp)にインデックスを作成して、数値が一定値よりも大きいレコードを探す、というクエリーができるようにしたものでした。

では例えば、複数フィールドで検索条件を指定するようなクエリーを実行したい場合はどのようにすればいいでしょうか? 例えば以下のようなレコードが Cloudant のデータベース db に入っているものとします:
IDNAMEAGE
1鈴木20
2鈴木41
3佐藤20
4田中35
5田中15
: : :

(↑ RDB っぽく書いてますが、実際には JSON フォーマットで {"ID":1, "NAME":"鈴木", "AGE":20} のように格納されているとします)


この時に「30歳以上の鈴木さん」とか「20歳の田中さん(実際には見つからない)」という条件でレコードを検索したい場合、どのようなインデックスを作って、どのように検索すればいいでしょうか?


まず、NAME と AGE の2つのフィールドにまたがるインデックスを用意する必要があります。Cloudant の管理画面から目的のデータベースを選び、"Design Documents" の右にある + をクリックして、"Query Indexes" を選択してください:
2016101001


Index の定義画面が表示されたら、テキストフィールド内に以下の値を記述します:
{
  "index": {
    "fields": [
      {
        "name": "NAME",
        "type": "string"
      },
      {
        "name": "AGE",
        "type": "number"
      }
    ]
  },
  "type": "text",
  "name": "db-index"
}

string 型の NAME と、number 型の AGE を指定して db-index という名前のインデックスを作ります。入力後に "Create Index" ボタンをクリックすると実際にインデックスが作成されます:

2016101002


インデックス作成後、実際にクエリーを実行してみましょう。例えば「30歳以上の鈴木さん」を検索するには以下のような POST リクエストを発行します(curl で実行する場合の例):
$ curl -X POST https://username:password@XXX-bluemix.cloudant.com/db/_find -d '{"selector":{"NAME":{"$eq":"鈴木"},"AGE":{"$ge":30}}}'
(Cloudant のサーバーが XXX-bluemix.cloudant.com 、ユーザー名が username 、パスワードが password であるものと仮定しています)

同様にして「20歳の田中さん」を検索する場合はこのようなリクエストを発行することになります:
$ curl -X POST https://username:password@XXX-bluemix.cloudant.com/db/_find -d '{"selector":{"NAME":{"$eq":"田中"},"AGE":{"$eq":20}}}'

それぞれ _find にセレクター(selector) を指定して POST リクエストを発行しています。そしてそのセレクターの中で存在するインデックスを使った検索条件を指定して、クエリーを実行しています。その際にあらかじめ作成しておいたインデックス(db-index)が使われて、想定したレコードを検索することができるようになる、というものです。

この「想定するクエリーを実行するためのインデックスをあらかじめ用意しておく」というのが RDB に比べてちと面倒で、クエリーの柔軟性にかける部分ではありますが、Cloudant はこういった方法でクエリーに対応しています。


IBM Bluemix からも提供されている NoSQL 型データベースの DBaaS "Cloudant" の開発者向けエディションが Docker イメージでの無料提供が開始されました:
https://hub.docker.com/r/ibmcom/cloudant-developer/

2016091004


1インスタンスでスケーリングなし&サポートもなし、という条件は付きますが、IBM Bluemix などのアプリケーション開発者がローカル環境だけでシングルテナントの Cloudant の環境を使うことができるようになります。以下、その手順を紹介します。

まず利用にあたり、docker 1.9 以上が必要です。RHEL/CentOS の 6.x の場合は docker 1.7 までしかサポートされていないため、バージョン7以上を用意する必要があります。

docker 環境が用意できたら、以下のコマンドで開発者向け Cloudant のイメージを pull します:
# docker pull ibmcom/cloudant-developer

そして以下のコマンドでローカル環境上にコンテナを作り、起動します:
# docker run --privileged --detach --volume cloudant:/srv --name cloudant-developer --publish 8080:80 --hostname cloudant.dev ibmcom/cloudant-developer

初回起動時のみ、ライセンス規約に同意する必要があります。以下のコマンドを実行してライセンス画面を表示します:
# docker exec -ti cloudant-developer cast license --console

すると以下の様なライセンス内容がコンソールに表示されます。同意する場合は "1" を入力してください:

2016091001


また、これも最初の1回だけ利用前にデータベースを初期化する必要があります。初期化は以下のコマンドを実行します:
# docker exec cloudant-developer cast database init -v -y -p pass

これで準備は完了です、簡単ですね。実際にローカル環境の Cloudant を利用するにはウェブブラウザで以下のアドレスにアクセスします:
http://(docker が動いているマシンのIPアドレス):8080/dashboard.html

すると以下の様なログインが画面が現れます。デフォルトでは
 Username = admin
 Password = pass
という ID & パスワードがセットされているので、この値を入力してログインしてください:

2016091002


ログインが成功すると、見慣れた Cloudant のダッシュボード画面が表示されます。後はいつもと一緒です。これでいつでもローカルで手軽に使える Cloudant 環境が用意できました:

2016091003

先日、Cloudant のデータベースのバックアップ(というかダンプというか、スナップショットというか、、)を撮る方法をこちらで紹介しました:
Cloudant のデータベースダンプとリストア

この中では /_all_docs?include_docs=true に GET アクセスすることで指定データベース内の全ドキュメントを中身ごと取り出す方法でダンプを取得する方法を紹介しました。

併せて、取得したダンプファイルを /_bulk_docs に POST してリストアする、という方法も紹介しました。この方法で確かにデータそのものは指定データベースに入るのですが、JSON の構造が少し変わってしまいます。「リストア」という観点では不十分でした。

というわけで、今回は JSON の構造を変えずにリストアする方法を紹介します。といってもコマンドでどうにかするのではなく、取得したダンプファイルを読み込んで、少しフォーマットを変更した上で同じ /_bulk_docs コマンドでリストアする、という方法です。フォーマットを変更する箇所はプログラミングでツール化しました(赤字はコメント):
public class DbRestore {
  static String c_baseurl = "https://username.cloudant.com"; //. Cloudant サーバー
  static String c_username = "username"; //. ユーザー名
  static String c_password = "password"; //. パスワード
  static String c_db = "newdb"; //. リストア先データベース名(作成しておく必要があります)
  static String c_dumpfile = "olddb.dump"; //. 読み込むダンプファイル名

  public static void main(String[] args) {
    try{
      //. ダンプファイルを読み込む
      String lines = "";
      BufferedReader br = new BufferedReader( new FileReader( new File( c_dumpfile ) ) );
      String line = br.readLine();
      while( line != null ){
        lines += ( line + "\n" );
        line = br.readLine();
      }
      br.close();

      //. ダンプファイル内のドキュメントデータを1つずつ読み込み、リストア用にフォーマットを変更
      String docs2 = "{\"docs\":[\n";
      JSONParser parser = new JSONParser();
      JSONObject obj = ( JSONObject )parser.parse( lines );
      JSONArray docs = ( JSONArray )obj.get( "docs" );
      for( int i = 0; i < docs.size(); i ++ ){
        JSONObject doc0 = ( JSONObject )docs.get( i );
        JSONObject doc = ( JSONObject )doc0.get( "doc" );
        JSONObject d = ( JSONObject )doc.get( "d" );
        if( i > 0 ){ docs2 += ","; }
        docs2 += "{\"d\":" + d.toJSONString() + "}\n";
      }
      docs2 += "]}";

      //. リストア用に変更したダンプデータを /_bulk_docs にポスト
      String url = c_baseurl + "/" + c_db + "/_bulk_docs";
      HttpClient client = new HttpClient();
      byte[] b64data = Base64.encodeBase64( ( c_username + ":" + c_password ).getBytes() );
      PostMethod post = new PostMethod( url );
      post.setRequestHeader( "Authorization", "Basic " + new String( b64data ) );
      post.setRequestHeader( "Content-Type", "application/json" );
      post.setRequestBody( docs2 );
      int sc = client.executeMethod( post );
      String result = post.getResponseBodyAsString();

      //. 結果確認
      System.out.println( "Status = " + sc );
      System.out.println( "Result = " + result );
    }catch( Exception e ){
      e.printStackTrace();
    }
  }
}


このコードは JSON-SimpleApache HTTP Client ライブラリを使って /_bulk_docs にダンプデータをポストする処理を Java で実装しています。ダンプデータをそのままポストすると情報は再現できるのですが、データフォーマットが変わってしまいます。その問題を回避すべく、ポスト前に必要なフォーマット変換を行っていて、ダンプ前のデータフォーマットが変わらないように(_id 値や _rev 値は変わります)ポストしています。

このコードの c_dumpfile に前回の方法でダンプしたファイル名を指定し、Java で動かせばダンプファイルの中身をリストアできるはずです。


Cloudant のような NoSQL データベースの場合、バックアップは一般的には複製機能によって実現するケースが多いのかもしれません。要は生きたデータベースを2系統以上用意して、生きたデータベースにバックアップする、という考え方です。これだとメインDBに障害があってもすぐに切り替えて稼働ができます。

ただ、例えばデータベースの引越しが目的の場合や、あるタイミングでのスナップショットを取るような場合などでは、一旦データベースのダンプを取得して、引越先でリストアしたい、というケースもあります。このようなケースでは生きたデータベースに複製することが必ずしも正しい回答にはなりません。


そういった場合のダンプは以下のコマンドで取得することができます:
$ curl -X GET https://(username):(password)@(hostname)/(dbname)/_all_docs?include_docs=true > file.dump

上記例では file.dump というファイル名を指定してダンプ結果を取得するように指示しています。ダンプというか、データベースの _all_docs(全ドキュメント) に include_docs_true(中身ごと取得)オプションを付けて GET する、というよく考えたらごく普通のコマンドをそのまま実行しています。

取得したダンプファイル(file.dump)を使ってリストアする場合はちょっと準備が必要です。まず上記コマンドで取得したダンプファイルをテキストエディタで開き、1行目の "rows" を "docs" に書き換えて保存します:
{"total_rows":100,"offset":0,"rows":[ 
  :
  :

  ↓ "rows" を "docs" に書き換えて保存
{"total_rows":100,"offset":0,"docs":[ : :

編集後に次のコマンドを実行します:
$ curl -d @file.dump -H "Content-Type: application/json" -X POST https://(username):(password)@(hostname)/(dbname)/_bulk_docs

こちらはデータベースに対して、中身を JSON ファイルで指定して POST しているだけです。

これで手動でバックアップができるので、後はこれを cron などで自動化したり、取得したダンプファイルを外部のオブジェクトストレージとかに転送する仕組みまで作ってしまえば自動バックアップが実現できそうです。

(2016/Jul/30 追記)
上記方法でもリストアできますが、元のデータベース内と比べて JSON ドキュメントのフォーマットが変わってしまいます。フォーマットを変えずにリストアする場合の方法は別のエントリで紹介しています:
Cloudant のバックアップデータをリストアする





 

このページのトップヘ