2015年8月12日水曜日

Cloudant(CouchDB)を操作する際の最低限のおさえどころ


CouchDBはJSON形式でデータの入出力が行えるNoSQLである。
CloudantはCouchDBをベースに作られたクラウドで利用できるDBaaS(distributed database as a service)である。
現在CloudantはIBMが提供しているbluemixのサービスとして利用できる。

ここではCloudantの使い方をまとめておく。
アカウントは取得済みであることを前提とする。

登録が終わるとusernameとpasswordが払い出される。
以下手順内では${username}、${password}変数部分は各自のものに読み替えていただきたい。



◆ WEBコンソール
ブラウザを開いてアクセスできることを確認する。
https://${username}:${password}@${username}.cloudant.com/dashboard.html"



◆ データベースの操作
WEBのコンソールからも操作はできるが、REST経由で操作できるためコマンドラインで手順を残しておく。

(全データベースの取得)
$ curl -X GET "https://${username}:${password}@${username}.cloudant.com/_all_dbs"


(データベースの新規作成)
$ db="testdb"
$ curl -X PUT "https://${username}:${password}@${username}.cloudant.com/${db}"


(削除)
$ curl -X DELETE "https://${username}:${password}@${username}.cloudant.com/${db}"



◆ jsonデータ(ドキュメント)の操作
リレーショナルなデータベースではないためid検索が基本である。
サーチインデックスを付与しない場合、直接それ以外の属性(下記例ではname, ageなど)による検索はできない。
投入する値がユニークであれば、"_id"キーにデータを入れるのも手だろう。
idやサーチインデックスについては後述。

(1ドキュメントを投入する場合)
$ doc='{
  "name": "aaa",
  "age": "20",
  "hobbies": {
    "1st": "computer",
    "2nd": "ferret"
  },
  "timestamp": "1438354800"
}'

$ curl -v \
-H "Content-type: application/json" \
-X POST \
-d "${doc}" \
https://${username}:${password}@${username}.cloudant.com/${db}/


(2つ以上のドキュメントはbulk投入できる)
jsonをここではdoc.jsonというファイル名で保存したとする。
{
    "docs": [
        {
            "name": "aaa",
            "age": "20",
            "hobbies": {
                "1st": "computer",
                "2nd": "ferret"
            },
            "timestamp": "1438354800"
        },
        {
            "name": "bbb",
            "age": "30",
            "hobbies": {
                "1st": "food",
                "2nd": "book"
            },
            "timestamp": "1438354860"
        },
        {
            "name": "ccc",
            "age": "40",
            "hobbies": {
                "1st": "car",
                "2nd": "movie"
            },
            "timestamp": "1438354920"
        }
    ]
}

$ curl -v \
-H "Content-type: application/json" \
-X POST \
-d @doc.json \
"https://${username}:${password}@${username}.cloudant.com/${db}/_bulk_docs"



◆ ドキュメントの操作

(全てのドキュメントを取得)
$ curl -X GET "https://${username}:${password}@${username}.cloudant.com/${db}/_all_docs"

結果にはidだけ渡される。ドキュメントその物を含んで欲しいときはinclude_docsフラグを付与する。
$ curl -X GET "https://${username}:${password}@${username}.cloudant.com/${db}/_all_docs?include_docs=true"

?limit=***
を付与することで取得するドキュメント数を制限できる。


(個別のドキュメントの取得)
$ curl -X GET "https://${username}:${password}@${username}.cloudant.com/${db}/${_id}"


(更新)
まずidとrev番号を確認する。
先の個別のドキュメントの取得時の結果の_id、_rev属性の値として確認できる。
それをjsonファイルに追加して対応する。それ以外は更新も新規投入と同じである。
revision番号は更新のたびに変わるため、その都度確認すること。

$ doc='{
"_id": "***",
"_rev": "***",
"name": "aaa",
"age": "20",
"hobbies": {
"1st": "computer",
"2nd": "ferret"
},
"timestamp": "1438354800"
}'

$ curl -v \
-H "Content-type: application/json" \
-X POST \
-d "${doc}" \
https://${username}:${password}@${username}.cloudant.com/${db}/


(削除)
idをurlパスに、rev番号をurlパラメータにしてdeleteメソッドを呼ぶ。

$ curl -X DELETE https://${username}:${password}@${username}.cloudant.com/${db}/${_id}?rev=${_rev}



◆ インデックスの作成と検索
cloudantはLuceneベースの検索ができる。つまり全文検索である。ランク付き検索、強力な照会タイプ、結果のブックマーキング、ファセット検索、フィールド検索などの機能も備えている。ややこしい点の一つなのだが、NoSQL DB とは別に全文検索機能を実装しているわけである。


(作成)
WEBコンソールから
Databases -> testdb
All Documents -> New Search Index

入力ボックスで指示していく(_design配下でインデックスは管理される)。
・ Save to Design Document: _design/newDesignDoc ※任意の名称である

・ index name: newSearch ※任意の名称である

・ Search index function: 
function (doc) {
  if (doc.name && doc.hobbies) {
    index("name", doc.name, {"store":true});
    index("hobby_1st", doc.hobbies["1st"], {"store":true});
    index("hobby_2nd", doc.hobbies["2nd"], {"store":true});
    index("timestamp", doc.timestamp, {"store":true});
    return
  }
}

・ Analyzer: Single

・ Type: Standard

インデックスは既に投入済みのドキュメントにも有効である。


(検索)
$ curl -X GET "https://${username}:${password}@${username}.cloudant.com/${db}/_design/newDesignDoc/_search/newSearch?q=name:aaa"

$ curl -X GET "https://${username}:${password}@${username}.cloudant.com/${db}/_design/newDesignDoc/_search/newSearch?q=hobby_1st:computer"

$ curl -X GET "https://${username}:${password}@${username}.cloudant.com/${db}/_design/newDesignDoc/_search/newSearch?q=timestamp:1438354*"


storeオプションの有無で検索結果に値を含むかどうかが変わる。
デフォルトではfalseである。
trueにすることで、include_docsを使わずに欲しい情報を取得することができるようになる。 

nameキーで検索した結果をそれぞれ見てみる。

・storeがtrueの場合
{"total_rows":1,"bookmark":"g2wAAAABaANkAB9kYmNvcmVAZGIxMi5pYm0wMDkuY2xvdWRhbnQubmV0bAAAAAJuBAAAAADAbgQA____32poAkY_06N6AAAAAGEAag","rows":[{"id":"3f8705b307d03b42a4fd027d49b58436","order":[0.3068528175354004,0],"fields":{"name":"aaa"}}]}

・storeがfalseの場合
{"total_rows":1,"bookmark":"g2wAAAABaANkAB9kYmNvcmVAZGIxMi5pYm0wMDkuY2xvdWRhbnQubmV0bAAAAAJuBAAAAADAbgQA____32poAkY_06N6AAAAAGEAag","rows":[{"id":"3f8705b307d03b42a4fd027d49b58436","order":[0.3068528175354004,0],"fields":{}}]}



◆ map/reduceの利用1
先の全文検索のためのインデックスによる検索する手段とは別に、map/reduceを使い結果を求めることもできる。こちらがより純粋なNo SQLとしての用途である。

(作成)
WEBコンソールから
Databases -> testdb
newDesignDoc(作成したインデックス名) -> New View

入力ボックスで指示していく。
・ Design Document: _design/newDesignDoc ※すでに作成済みのものを利用

・ index name: calc ※任意の名称である

・ Map function
function(doc) {
  emit(doc._id, 1);
}

mapフェーズでは"doc._id"をキーとして数字1を出力し、
reduceフェーズでそれを集約する("doc._name"でも構わない。目的に応じてmap対象を選べばよい)。

・ Reduce function: custom ※この中でsum関数を書くことにする
function(keys, values, rereduce) {
  if (rereduce) {
    return sum(values)
  } else {
    return values.length
  }
}


reduceの選択肢の中にすでにsum関数は用意されている。
また、用意されているreduceの中に"_stat"という関数がある。
_statは_sum, _count, _min, _max, _sumsqrの結果をまとめて出力する関数である。
reducerに_statを選んでクエリを投げると以下の結果を得られる。

$ curl -X GET "https://${username}:${password}@${username}.cloudant.com/${db}/_design/newDesignDoc/_view/calc"
{"rows":[
{"key":null,"value":{"sum":3,"count":3,"min":1,"max":1,"sumsqr":3}}
]}


◆ map/reduceの利用2
単純に特定のkeyに対して検索をしたい場合は、reduceは不要である。
例えば、created_at、subject、body要素を含んでいるデータがあり、subjectが"AAA"だけの、日付による検索をし、subjectとbodyを結果として取得したい場合は以下のようにmap関数を登録しておくだけでよいだろう。

function (doc) {
  msec = Date.parse(doc.created_at);
  if (msec) {
    if (doc.subject == "AAA") {
      emit([date.getFullYear(),
            date.getMonth()+1,
            date.getDate(),
            date.getHours(),
            date.getMinutes(),
            date.getSeconds()
          ],
          [doc.subject,
           doc.body
          ]);
    }
  }
}

$ curl -G \
"https://${username}:${password}@${username}.cloudant.com/${db}/_design/${design}/_view/${mapper}" \
--data-urlencode "startkey=[2015,8,10,12,0]" \
--data-urlencode "endkey=[2015,8,31,12,0]"



~参考~
IBM Cloudant 公式マニュアル

CloudantDBとSQL系DBの違い

Cloudantに格納したデータを可視化
日付期間でのデータ検索方法など参考になる