2015/06/07

CloudantDB

NoSQL系のDBが増えてきて最近 CloudantDB触る機会があって面白いと思ったのでここでメモを書くことにした。 まだまだわからないことだらけなのでこれをまたアップデートしていきたい。

CloudantDBとSQL系DBの違い

  • SQL系:
    一つのDBの中に複数のテーブルがありえる。テーブルの中にデータがある。テーブルにはスキーマが必須なのでデータに型がある。 検索するにはSQL Queryで書く
  • CloudantDB:
    一つのDBの中に沢山のJSONドキュメントがある。スキーマがない。それぞれのドキュメントにはJSONである以外の制約がないからドキュメントごと全く異なる情報を持つことができる。 ドキュメントIDで検索することができるがsqlクエリみたいなものを実現するにはsearch indexを事前に作る必要がある。クエリーはCloudant Queryといい Lucene Queryベースだそうである。

登録

まず登録しアカウント名とパスワードを取得し試しに幾つかのDBを作る。


簡単なAPIを使う

簡単の為にターミナルでcurlを使う。そして、ユーザー認証が必要だから次の変数を定義する。(本当はcurlの-uオプションで自分のパスワードを入力した方がbashのhistoryに残らなくてセキュリティ的にいいけどここでは最もシンプルな方法で紹介することにする)
CLOUDANT='https://アカウント名:パスワード@アカウント名.cloudant.com'
  • 😵 curlの復習:
    curl <url> \
        -X <method> \
        -H <header> \
        -d <data>
    
    *メソッドを指定しない場合 通常はGETとなる。

DB一覧を取得

これは一番簡単なAPIで上記のアカウント名とパスワードを試せる。
curl ${CLOUDANT}/_all_dbs
["_users","big","myanswers"]

DBを作成

car_answersというDBを用意する。 Cloudantのコンソルを使って5つのドキュメントを作ることができるが、ここでAPIで行う
curl ${CLOUDANT}/car_answers -X PUT
{"ok":true}

複数のドキュメントをアップロード

上記で作成したDBに次の5つのドキュメントをアップロードする。バルクというAPIを使うために一つのオブジェクトにdocsというキーにドキュメントの配列を定義する必要がある。ここでは_idを自分で指定しているけれども これは後で検索するから見やすさのためだけである。指定しなくてよい。Cloudantがユニークなものを生成してくれて それらをリスポンスに返ってくる。
json='{
    "docs": [
        {
            "_id": "1",
            "created_at": "2015-05-15 00:00:00",
            "maker": "Mercedes Benz",
            "モデル": "A 180",
            "answers": {
                "評価": "★★★",
                "その他": "特になし"
            }
        },
        {
          "_id": "2",
          "created_at": "2015-05-16 00:00:00",
          "maker": "Toyota",
          "モデル": "レクサス",
          "answers": {
              "評価":"★★★★",
              "その他":"よかった"    
          }
        },
        {
          "_id": "3",
          "created_at": "2015-05-17 00:00:00",
          "maker": "Honda",
          "モデル": "フィット",
          "answers": {
              "評価":"★★★★★",
              "その他":"すぐ購入したいと思います"    
          }
        },
        {
          "_id": "4",
          "created_at": "2015-05-18 00:00:00",
          "maker": "Mercedes Benz",
          "モデル": "E 250 CABRIOLET",
          "answers": {
              "評価":"★★",
              "その他":"イメージしていたものと少し違った"    
          }
        },
        {
          "_id": "5",
          "other": "他とちがうもの"
        }
    ]
}'
そして、この大きなオブジェクトをリクエストのボディーに入れて送信
curl ${CLOUDANT}/car_answers/_bulk_docs -X POST -H "Content-Type: application/json" -d "$json"
[{"id":"1","rev":"1-a408ef0c09d5b0aaec96b87e04e049de"},{"id":"2","rev":"1-615734de2db3b4717544f2dfd0e01bec"},{"id":"3","rev":"1-fd7d17addf8149d0c522368e69bda27c"},{"id":"4","rev":"1-d0be948351c84706c5febea1a1622700"},{"id":"5","rev":"1-4d8829cf3843c4d7a0b623bcced3a620"}]

単品でドキュメントをアップロード

バルクではなく、単品でドキュメントをアップロードもちろん可能。バルクと同じくidrevを指定しなくてよくてCloudantはユニークなものを生成してくれて 生成されたidrev番号がリスポンスに返ってくる。
json='{
  "created_at": "2016-07-14 00:00:00",
  "maker": "Android",
  "モデル": "N",
  "answers": {
    "評価": "★★★★★",
    "その他": "特になし"
  }
}'
curl ${CLOUDANT}/car_answers/ -X POST -H "Content-Type: application/json" -d "$json"
{"ok":true,"id":"c4d9e99028d5b4016ef008768bfee9a6","rev":"1-8e040f92ceea84466d2a90b5507f93b1"}

ドキュメント一覧を取得

例えば、car_answersというDBにある全ドキュメントを取得するには_all_docsを使って取れる
curl ${CLOUDANT}/car_answers/_all_docs
{"total_rows":5,"offset":0,"rows":[
{"id":"1","key":"1","value":{"rev":"1-a408ef0c09d5b0aaec96b87e04e049de"}},
{"id":"2","key":"2","value":{"rev":"1-615734de2db3b4717544f2dfd0e01bec"}},
{"id":"3","key":"3","value":{"rev":"1-fd7d17addf8149d0c522368e69bda27c"}},
{"id":"4","key":"4","value":{"rev":"1-d0be948351c84706c5febea1a1622700"}},
{"id":"5","key":"5","value":{"rev":"1-4d8829cf3843c4d7a0b623bcced3a620"}}
]}
デフォルトではリスポンスのrowsにドキュメントのidだけ渡される。ドキュメントその物を含んで欲しいときはinclude_docsフラグが便利
curl ${CLOUDANT}/car_answers/_all_docs?include_docs=true
デフォルトでは一つのリクエストで最大取得可能なドキュメントの数は25だが、それをlimitで200まで変更可
curl ${CLOUDANT}/car_answers/_all_docs?include_docs=true&limit=200
ドキュメント取得の詳細:docs.cloudant.com/database.html#get-documents

特定なドキュメントを取得

ドキュメントのidがわかれば そのドキュメントを簡単に取得できる。例えば、car_answersidが3のドキュメントを取る:
curl ${CLOUDANT}/car_answers/3
{"_id":"3","_rev":"1-fd7d17addf8149d0c522368e69bda27c","created_at":"2015-05-17 00:00:00","maker":"Honda","モデル":"フィット","answers":{"評価":"★★★★★","その他":"すぐ購入したいと思います"}}
条件を指定してドキュメントの取得は次に説明する。

ドキュメントを条件を指定して検索

CloudantのDBを検索するには検索インデックス(略してインデックス)が必要。

インデックスについて

インデックスとは

条件を指定して検索を実現するのは検索インデックスである。DBに入っているドキュメントはどんなものかCloudantにはわからない、まして、全ドキュメントは同じフォーマットである保証もない。キーワードで検索したいときに ドキュメントのどのフィルドをどのように扱えばいいかを決めるのは検索インデックスである。検索インデックスは必ずデザインドキュメントの中にあり、デザインドキュメントに複数の検索インデックスが存在しえる。

インデックスの作成

他のドキュメントと同じくAPIで作成可能だが 今回はCloudantDBのコンソルで作成する。car_answersというDBを選択しAll Design Docsの➕ボタンを選択しNew Search Indexを選ぶ。そしてCreate Search Indexという画面が表示される。


Create Search Indexの入力フィルドの説明
  • Save to Design Document
    どこのデザインドキュメントに保存するかを選択できるプルダウンメニューがあるが、一個もないときにデザインドキュメント作成用の入力フィルドも表示される。全てのデザインドキュメントが"_design/"で始まるのでそれは固定となっている
  • Index name
    検索インデックスの名前
  • Search index function
    これはインデックス化するに使う関数。入力としてはドキュメントが渡される。出力はない。インデックス化したいドキュメントのパラメーターをindex(...)でインデックスに入れることが役割。極普通のjavascriptの関数。使い方についてはこのあと説明する。
  • Analyzer
    インデックスに入れたテキスト(この場合はdoc.maker)をどういう風に区切って扱うかを決めるのはアナライザーだ。多分Tokenizerという言い方をする方がすぐわかりやすい
    例えばドキュメント1を処理した場合は"Mercedes Benz"というストリングがインデックスされる。"Mercedes Benz"を一つのストリングとして扱う("Keyword")か英語の文法を使って単語で分けるか日本語の文法で分けるかUnicode Text Segmentationアルゴリズム("Standard")で分けるかなど。アナライザーによって検索でドキュメントがヒットするかしないか変わる。より詳細は[Cloudant/For Developers/Search Indexes/Analyzers]を参照。
    関数ごとに一個のアナライザーを使う場合はSingle。インデックス化されるパラメーターごとにアナライザーを使う場合はMultipleを選択。

ドキュメントの検索・初級編

Analyzerはstandardでsingleな次のインデックスを作る(上記の画像と同じインデックス)
// インデックス名:car
function (doc) {
    index("brand", doc.maker);
}

curl ${CLOUDANT}/car_answers/_design/search/_search/car?q=brand:Honda
{"total_rows":1,"bookmark":"g2wAAAABaANkAB5kYmNvcmVAZGI4LmlibTAwOS5jbG91ZGFudC5uZXRsAAAAAmJgAAAAYn____9qaAJGP9OjegAAAABhAGo","rows":[{"id":"3","order":[0.3068528175354004,0],"fields":{}}]}
URL部意味
car_answersDBの名前
_design/searchデザインドキュメントのID
_search検索時に使う決まり
car検索インデックスの名前
qCloudantQuery用のパラメーター
brand:"Honda"CloudantQueryそのもの

他に試してみる
curl ${CLOUDANT}/car_answers/_design/search/_search/car?q=brand:Mercedes%20Benz
{"total_rows":2,"bookmark":"g1AAAACOeJzLYWBgYMpgTmGQS0lKzi9KdUhJMtXLTMo1MLDUS87JL01JzCvRy0styQGpy2MBkgwNQOr____zszKY3OznnJMDiSUyoJphQcCMBxAz_qOakQUAaqAqIg","rows":[{"id":"1","order":[0.028130024671554565,0],"fields":{}},{"id":"4","order":[0.028130024671554565,0],"fields":{}}]}
見つかった!ドキュメント1と4が返ってきている!
_searchでもinclude_docsオプションも使えるしlimitも使える。しかしbashなので&などをエンコードしないといけないしその次にURLエンコードしないといけないしまだ使っていないけどCloudantQueryの中でエスケープしないといけないものもあるのでこれから-vオプションを使う。実際に行われたリクエストがわかるから。
curl -v ${CLOUDANT}/car_answers/_design/search/_search/car?q=brand:Mercedes%20Benz\&limit=1\&include_docs=true
> GET /car_answers/_design/search/_search/car?q=brand:Mercedes%20Benz&limit=1&include_docs=true HTTP/1.1
...
{"total_rows":2,"bookmark":"g2wAAAABaANkAB5kYmNvcmVAZGI1LmlibTAwOS5jbG91ZGFudC5uZXRsAAAAAm4EAAAAAIBuBAD___-famgCRj-czh4AAAAAYQBq","rows":[{"id":"1","order":[0.028130024671554565,0],"fields":{},"doc":{"_id":"1","_rev":"1-a408ef0c09d5b0aaec96b87e04e049de","created_at":"2015-05-15 00:00:00","maker":"Mercedes Benz","モデル":"A 180","answers":{"評価":"★★★","その他":"特になし"}}}]}
定義された検索インデックス関数ではこれ以上大したものができないのでもうすこし拡張していく。

ドキュメントを検索・中級編

同じデザインドキュメントの中で新しい検索インデックスcar2を作成。同じくAnalyzerをstandardにする。
// インデックス名:car2
function (doc) {
    
    if (typeof doc.maker === 'undefined') {
        // makerというフィルドをもっていないドキュメントを相手にしない. 
        //(なんだかの理由でmakerをもっているもっていないドキュメントがある)
        return;
    }
    
    if (doc.created_at) {
        // 日付で検索したい。例: q=created_at:2015-09-25 11:30:58 ただし日付の部分をエスケープが必要
        index("created_at", doc.created_at, {"store": true});

        // 日付で楽に検索したい。例: q=20150925113058
        index("created", parseInt(doc.created_at.replace(/[^0-9]/g, ''), 10), {"store": true});
    }
    
    if (doc["モデル"]) {
        // 日本語キーで検索は可能だがエスケープが必要。これで楽にできる。「モデル」の代わりに「model」で検索可
        index("model", doc["モデル"], {"store": true});
    }

    if (doc.answers && doc.answers["評価"]) {
        // 評価で検索したい。例:q=stars:★★★ ただし★をエスケープしなければならない
        var stars = doc.answers["評価"];
        index("stars", stars, {"store": true});

        // 評価で楽に検索したい。例:q=stars_num:3
        index("stars_num", (stars.match(/★/g) || []).length);
    }

    if (doc.answers["その他"]) {
        // コメントなどで検索したい
        index("comment", doc.answers["その他"], {"store": true, "index": false});
    }
}
注意点
Javascriptの標準の挙動だが、関数のどっかでExceptionが発生するとそれ以降は実行されなくて インデックス化されないので 検索でヒットしないことがある。関数のよくチェックすること。

index関数の第三パラメーターについて

詳細は[Cloudant/For Developers/Search Indexes/Options]に書かれてあるけど、まとめると次のようになる
キー 意味 デフォルト
store 検索結果に値を含むかどうか。 false
index 検索インデックスに入れるかどうか。 true
facet faceting機能をオンにするかどうか。今回は使わない false
boost 検索結果の中で優先度を上げる係数。今回は使わない 1.0

いろいろと検索してみる

まずは({"store": true}を付けた)モデルで検索すると
curl -v ${CLOUDANT}/car_answers/_design/search/_search/car2?q=model:CABRIOLET
> GET /car_answers/_design/search/_search/car2?q=model:CABRIOLET HTTP/1.1
...
{"total_rows":1,"bookmark":"g2wAAAABaANkAB5kYmNvcmVAZGI4LmlibTAwOS5jbG91ZGFudC5uZXRsAAAAAm4EAAAAAOBuBAD_____amgCRj_Do3oAAAAAYQBq","rows":[{"id":"4","order":[0.1534264087677002,0],"fields":{"stars":"★★","model":"E 250 CABRIOLET","comment":"イメージしていたものと少し違った","created_at":"2015-05-18 00:00:00"}}]}
今までと違ってより詳細な情報を取得していることがわかる。それはstore:trueのお陰。これでinclude_docsを使わずに欲しい情報だけを取ることができる。
CloudantDBのコンソルで インデックスを簡単に試すことができる。URL全部書く必要がなくて Cloudant Queryの部分のみ、いろいろ試すには便利だね
エスケープについては便利かどうかを試したい。
おなじく
curl ${CLOUDANT}/car_answers/_design/search/_search/car2?q=stars_num:2
{"total_rows":1,"bookmark":"g2wAAAABaANkAB5kYmNvcmVAZGI3LmlibTAwOS5jbG91ZGFudC5uZXRsAAAAAm4EAAAAAOBuBAD_____amgCRj_wAAAAAAAAYQBq","rows":[{"id":"4","order":[1.0,0],"fields":{"stars":"★★","model":"E 250 CABRIOLET","comment":"イメージしていたものと少し違った","created_at":"2015-05-18 00:00:00","created":20150518000000.0}}]}

Official docsメモ
The following characters require escaping if you want to search on them;
+ - && || ! ( ) { } [ ] ^ " ~ * ? : \ /
Escape these with a preceding backslash character. https://cloudant.com/for-developers/search/

日本語で検索

ここからエスケープがかなりわけがわからなくなるのでcurlの--data-urlencodeオプションでクエリエンコーディングをする。そして-Gオプションで urlに付ける。さらに-vでverboseをオンにして実際に叩かれたURLの確認ができる。
model:"フィット"の検索はこんな感じ
curl -v -G ${CLOUDANT}/car_answers/_design/search/_search/car2 --data-urlencode 'q=model:"フィット"'
> GET /car_answers/_design/search/_search/car2?q=model%3A%22%E3%83%95%E3%82%A3%E3%83%83%E3%83%88%22 HTTP/1.1
...
{"total_rows":1,"bookmark":"g2wAAAABaANkAB5kYmNvcmVAZGI3LmlibTAwOS5jbG91ZGFudC5uZXRsAAAAAmJgAAAAYn____9qaAJGP9OjegAAAABhAGo","rows":[{"id":"3","order":[0.3068528175354004,0],"fields":{"stars":"★★★★★","model":"フィット","comment":"すぐ購入したいと思います","created_at2":"2015-05-1700:00:00","created_at":"2015-05-17 00:00:00","created":20150517000000.0}}]}
stars:"★★"の検索は次の通りのはずだがうまくいかない。
curl -v -G ${CLOUDANT}/car_answers/_design/search/_search/car2 --data-urlencode 'q=stars:"★★"'
> GET /car_answers/_design/search/_search/car2?q=stars%3A%22%E2%98%85%E2%98%85%22 HTTP/1.1

{"total_rows":0,"bookmark":"g2o","rows":[]}
その理由はAnalyserのstandardにある。AnalyzerについてCloudant Analizersにの述べているように standardで"★★"というストリングをトークン化しようとするとトークンの数がゼロなのでstandardで記号を決してヒットしない。別のインデックスを作っておく必要がある。この場合はkeywordもしくはwhitespaceがいいかと思う。

レンジ検索・数字編

数字は割りと簡単で問題なし
curl -v -G ${CLOUDANT}/car_answers/_design/search/_search/car2 --data-urlencode 'q=created:[20150501000000 TO 20160705235959]'
> GET /car_answers/_design/search/_search/car2?q=created%3A%5B20150501000000%20TO%2020160705235959%5D HTTP/1.1

{"total_rows":4,"bookmark":"g1AAAAELeJzLYWBgYMlgTmGQS0lKzi9KdUhJMtHLTMo1MLDUS87JL01JzCvRy0styQGqY0pkSJL___9_VgaTm_0HBjBIZEDVbIZTc1ICkEyqJ6Aft-V5LCD1DUAKaMR88twAMeMBxAw0d2QBADHUTsY","rows":[{"id":"2","order":[1.0,0],"fields":{"stars":"★★★★","model":"レクサス","stars2":4.0,"comment":"よかった","created_at":"2015-05-16 00:00:00","created":20150516000000.0}},{"id":"3","order":[1.0,0],"fields":{"stars":"★★★★★","model":"フィット","stars2":5.0,"comment":"すぐ購入したいと思います","created_at":"2015-05-17 00:00:00","created":20150517000000.0}},{"id":"1","order":[1.0,0],"fields":{"stars":"★★★","model":"A 180","stars2":3.0,"comment":"特になし","created_at":"2015-05-15 00:00:00","created":20150515000000.0}},{"id":"4","order":[1.0,0],"fields":{"stars":"★★","model":"E 250 CABRIOLET","stars2":2.0,"comment":"イメージしていたものと少し違った","created_at":"2015-05-18 00:00:00","created":20150518000000.0}}]}

レンジ検索・ストリング編

この辺になると またエスケープがややこしすぎてわけわからなすぎる。検索したいのは2015-05-01 00:00:00ストリングなので"で囲む。内容の:-をエスケープが必要。しかし 上記と同じレンジを指定しているのに結果が違う。なぜだろう。スペースが怪しいと思う。
結果が違う理由はAnalizerのstandardによってトークン化された結果が違うからだ。数字をトークン化しようとすると同じ数字になるけど、数字でない場合はスペースやさまざまな記号で別れたりする。
この場合は完全マッチの検索に近いからkeywordwhitespaceがいいかもしれないね。AnalyzerについてCloudant Analizersに書いた。 とりあえず有効なqueryのようだが 期待しているものではない
curl -v -G ${CLOUDANT}/car_answers/_design/search/_search/car2 --data-urlencode 'q=created_at:["2015\-05\-01 00\:00\:00" TO "2016\-07\-05 23\:59\:59"]'
> GET /car_answers/_design/search/_search/car2?q=created_at%3A%5B%222015%5C-05%5C-01%2000%5C%3A00%5C%3A00%22%20TO%20%222016%5C-07%5C-05%2023%5C%3A59%5C%3A59%22%5D HTTP/1.1

{"total_rows":2,"bookmark":"g1AAAACPeJzLYWBgYMpgTmGQT0lKzi9KdUhJMjTRy0zKNTCw1EvOyS9NScwr0ctLLckBKcxjAZIMC4DU____92dlMLnZf2AAg0QGkCFycEPMCZjxAGLGfxQzGLMAarcpYw","rows":[{"id":"7fbfa0bbc27b8ab664d64cde5d00b421","order":[1.0,0],"fields":{"stars":"★★★★★","model":"N","stars2":5.0,"comment":"特になし","created_at":"2016-07-14 00:00:00","created":20160714000000.0}},{"id":"c4d9e99028d5b4016ef008768bfee9a6","order":[1.0,1],"fields":{"stars":"★★★★★","model":"N","stars2":5.0,"comment":"特になし","created_at":"2016-07-14 00:00:00","created":20160714000000.0}}]}

ドキュメントの検索・上中級(TODO)

* Multiple Analizersを持つインデックスでの検索("★"などの記号で検索したい)
* 検索: index:falseなどについて(検索キーワードとして使いたくないけど(store:trueに合わせて使うと)と結果に出て欲しいとき)
* 検索: ranges, [], (), AND, OR, などについて

CloudantQueryでエスケープについて

StackoverflowのEscaping Lucene Characters using Javascript Regexの回答で大抵カバーできるけど、下記では\と/とも対応している。
function luceneEscaped(input) {
    // http://stackoverflow.com/questions/26431958/ + own additions
    // + - && || ! ( ) { } [ ] ^ " ~ * ? : \ /
    input = input.replace(/([\!\*\+\&\|\(\)\[\]\{\}\^\~\?\:\"\-\\\\\/])/g, "\\$1")
    return '"' + input + '"';
}

0 comments :