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

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

タグ:index

以前に 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 の1つが Cloudant です:
2016071301


ベースとなる製品はオープンソースの Apache CouchDB です。私自身も以前は疑問に思っていたのですが、たまに聞かれる質問の1つに「Cloudant と Apache CouchDB は何が違うのか?」があります。

Cloudant は(ソフトウェア単体版も存在していますが) DBaaS としてクラウドで提供されているとか、 IBM の管理下で利用できる(サーバーインフラを自分が管理しなくてよい)とか、運用面での違いは比較的わかりやすいのですが、機能面での決定的な違いについてあまり目にする機会がありませんでした。

その違いの1つが今回紹介する「全文検索」機能です。一般的に NoSQL DB はスケーラビリティにすぐれた大容量データの保存に向くストレージシステムですが、SQL のようなクエリー言語が使えないこともあり、柔軟な検索はあまり得意ではありません。全文検索に関しては SQL DB であれば、
> select * from xxx where name like '%テスト%'

のような指定で「name にテストという文字を含むもの」が検索できます。パフォーマンス対策とか色々考慮すべき点はありますが、処理そのものは単純だし簡単・便利に実現できてしまいます。

一方で、一般的な NoSQL DB には全文検索を実現するためのこのような便利な機能がありません。Apache CouchDB でもインデックスを作成することで「完全一致検索」は実現できますが、「全文検索」はそうはいきません。Apache Lucene や ElasticSearch などの全文検索エンジンを使って NoSQL DB とは別に全文検索機能を実装し、併用して運用していくことになります。この方法はデータの一元管理が難しく、また DB と検索エンジンの整合性も併せて管理する必要が生じるため、比較的運用負荷の高い実現方法になってしまいます。

実はこの点で Cloudant は Apache CouchDB とは違います。Cloudant の検索機能には Apache Lucene が初めから含まれており、特に Lucene の存在を意識することなく全文検索インデックスが使えるようになっています。その使い方を紹介します。


まずは Cloudant のインスタンスを用意します。IBM Bluemix アカウントをお持ちであれば、サービスとして Cloudant を1インスタンス用意いただくのが一番簡単だと思います。IBM Bluemix アカウントがない場合は http://cloudant.com/ に直接サインアップしていただいても構いません。以下は前者の前提で紹介します。

作成した Cloudant の環境変数を参照して、Cloudant サービスを利用するための接続情報を確認します:
2016071301


サービス接続情報の JSON を確認し、credentials.url の値をメモしておきます(後で使います)。ここでは credentials.url の値が以下のようになっていると仮定して続きを紹介します:
{
  "credentials": {
    "username": "USERNAME",
    "password": "PASSWORD",
    "host": "USERNAME.cloudant.com",
    "port": 443,
    "url": "https://USERNAME:PASSWORD@USERNAME.cloudant.com"
  }
}


まずは Cloudant のダッシュボード画面にアクセスしてみます。まだ何も作業していなければ特にデータベースも作られてなく、データベース一覧は空の状態のはずです。ここでは今回の作業用に1つデータベースを追加することにします。画面右上の "Create Database" をクリックします:
2016071302


新規に作成するデータベースの名称を入力するよう求められるので適当に(以下の例では "mydb" と)入力して "Create" をクリックします:
2016071303


mydb データベースが新規に作成されました。が、(当たり前ですが)まだこのデータベースには何のドキュメントも登録されていません:
2016071304


検索作業用にいくつかのドキュメントを追加してみましょう。+印から "New Doc" を選択します:
2016071305


以下の様なドキュメント編集画面が表示されるはずです。ここに登録するドキュメントの内容を JSON 形式で入力していきます:
2016071306


今回は以下の様に "name" と "desc" という2つのフィールドを持つドキュメントを生成することにします("_id" は初めから入っている値をそのまま使います):
{
 "_id": "XXXXXXXX....XXX",
 "name": "Natural Language Classifier",
 "desc": "自然言語テキストを学習してカテゴリを分類する"
}

この状態でドキュメントを保存します。"Create Document" と書かれたボタンをクリックします:
2016071307


ドキュメントが保存されました。mydb データベースのドキュメント一覧からも参照できますが、指定した "name" や "desc" の値はこの画面からは確認できません。ドキュメントの内容を確認するには該当ドキュメントの右上の鉛筆マークをクリックします:
2016071308


"name" や "desc" 、そして追加された "_rev" の値なども確認することができます。正しく格納されているようです。一度 "<" 印をクリックして元の画面に戻ります:
2016071309


同様にしていくつかのドキュメントを追加していきましょう。上記の作業を繰り返して、とりあえず以下の3つのドキュメントが登録された状態にしておきます:
#name の値desc の値
1Natural Language Classifier自然言語テキストを学習してカテゴリを分類する
2Personality Insights入力されたテキストから性格を分析する
3Tradeoff Analytics優先順位を意識したトレードオフ判断を視覚化する


ではこのデータベースに全文検索の索引を追加して全文検索ができるようにします。mydb のメニュー画面から Design Documents 横の+印をクリックし、サブメニューから "New Search Index" を選択します:
2016071301


索引に関する情報を入力する画面に切り替わります。ここではデザイン ID ("_design/" と書かれた右)に "mydbdoc" と、インデックス名("Index name" と書かれた下)に "mydbsearch" とそれぞれ入力します:
2016071302


続けて下にスクロールし、Search Index function の中身を以下のように書き換えます:
function(doc){
 if ('name' in doc) {
  var search_this = [doc.name, doc.desc];
  index('default', search_this.join(' '));
  index('name', doc.name, {store: 'yes'});
  index('desc', doc.desc, {store: 'yes'});
 }
}
  ↑ "name" というフィールドを持つドキュメントに対して、
   "name" と "desc" の2フィールドを対象に検索インデックスを作成するようにしています。


最後に Type には "Japanese" を選択して、一番下の "Create Document and Build Index" をクリックします。これで検索索引が作成され、かつ索引付けが実行されて検索が可能になります:
2016071303


元の画面に戻ると "Build Indexes" 欄が追加されており、その中に作成したインデックス名称(mydbsearch)が表示されているはずです。これを選択すると検索のプレビューを確認することができるので、本当にこれだけで全文検索が可能になったのか試してみましょう:
2016071304


画面右の検索ボックスに「テキスト」と入力してみます(上記3ドキュメントと同じデータを登録していれば、3つのうちの2つのドキュメントが「テキスト」という文字列を含んでいるので、正しく検索できれば結果は2ドキュメントになることが予想されます):
2016071305


全文検索が実行され、画面のように2ドキュメントが検索結果にリストされました。正しく全文検索が実行されたことが確認できました:
2016071306


ちなみに、この検索処理を API で実行する場合は以下の URL に対して GET リクエストを実行することになります:
https://USERNAME:PASSWORD@USERNAME.cloudant.com/mydb/_design/mydbdoc/_search/mydbsearch?q=テキスト

指定する URL は以下の形になっています:
(上述の環境変数で取得した青字部分)/(DB名)/_design/(デザインID)/_search/(インデックス名)?q=(検索文字列)

試しにこの文字列をブラウザのアドレスバーに入力して実行すると、同じ2件の検索結果が得られるはずです:
2016071307


というわけで、Cloudant サービスには標準で Lucene ベースの全文検索エンジンが搭載されており、Apache CouchDB 同様に簡単に使うことができる上、NoSQL DB に苦手な全文検索が簡単に実現・実装することができる、という特徴を持っていることが確認できました。

(参考)
https://cloudant.com/product/cloudant-features/cloudant-search/

IBM Bluemix を通じて DBaaS で提供されている NoSQL データベース Cloudant 。特に Bluemix の IoT Foundation などと親和性が高く、NodeRED などを使ってセンサーデバイスから送られてくる非構造化データを非常に簡単に格納することができるなど、SQL データベースが苦手とするようなケースでのデータストレージとして便利に使えます:
2016021201


ところで、データを Cloudant に格納する所までは(特に NodeRED を使うなどすれば)コーディングなどをほぼ意識することなく出来てしまうのですが、では実際に集まったデータを外部から利用するにはどうすればいいでしょうか? まあ「プログラミング」といえばプログラミングを行うのですが、SQL やら JDBC やらとは違う方法で各データレコードにアクセスする必要があり、その辺りをまとめてみます。

まずは今回扱う Cloudant 上のデータを用意します。実際にはどんなものでもいいのですが、ここでは以下の様なものとします。具体的には IBM Bluemix と NodeRED を使い、IBM IoT Foundation の MQTT ブローカー経由でラズベリーパイから送信されたデバイスデータを Cloudant 内のデータベースに格納したものを使います。この辺りの具体的な手順はこちらを参照ください:
Bluemix の Node-RED サービスで IoT アプリを作る(2/2)

2016021103


上記リンク先で解説した方法で集めたデータは "iotdata" という名前のデータベース内に格納されているものとします:
2016021101


個別のデータの中身はこのような感じです。"_id", "_rev" というデフォルトのキー値に加えて、ラズベリーパイから取り出した "myName", "cputemp(CPU温度)", "cpuload(CPU負荷率)", "sine(サインカーブの値)" という4つのキー値が各レコードに含まれて JSON 形式で格納されています:
2016021102


この iotdata データベースから目的のデータを取り出す API とその使い方を紹介していきます。その前に API 実行のために必要な情報を Bluemix 画面から取り出しておきます。Bluemix でこの Cloudant サービスをバインドしているランタイムプロジェクトから「環境変数」を参照して、この Cloudant サービスにアクセスするための credentials 情報を調べておきます。具体的には username, password, host, port, url の情報が API 実行時に必要になるため、メモするなどして控えておくようにします:
2016021104


(1) _id 値を指定してドキュメントを取り出す

NoSQL データベースではもっともシンプルな API です。目的のドキュメントを特定する _id 値を指定して、目的のドキュメントを取り出す、というものです。これは環境変数の url 値と、_id 値を指定して、以下の様な API を GET リクエストで実行します:
(https で始まる "url" の値)/(データベース名)/(_id の値)

例えば url の値が https://NNNN:PPPP@xx.xx.xx.xx/ であったとして、データベース名が iotdata、_id 値が 3bfeaf10db8b47a1021dc51397773a99 であれば以下の URL に GET アクセスすることになります:
https://NNNN:PPPP@xx.xx.xx.xx/iotdata/3bfeaf10db8b47a1021dc51397773a99

この URL をウェブブラウザのアドレス欄にそのまま打ち込んでアクセスすれば目的のドキュメントデータが JSON オブジェクトとして得られるはずです:
2016021105

↑目的のドキュメントが得られるはずです。ここまでは簡単。


(2) 特定のキーの値で検索してドキュメントを取り出す

_id 値がわかっていればドキュメントを取り出すことができることは分かりました。でも実際には _id 値がわかっているデータにアクセスする、という使い方はほとんどなく、_id 値ではない値に対して何らかの条件を与えて検索して、その結果のドキュメントのデータを取得する、というケースがほとんどのはずです。 この「検索」を NoSQL データベースではどのように行うか、というのが問題になります。

例えば「CPU 温度(cputemp)が 42 度より大きいレコードのみ取り出して、_id 値でソートする」という検索をするにはどうすればいいでしょう? SQL データベースであれば深く考えずに
select * from iotdata where cputemp > 42 order by _id asc
みたいに実行すればいいだけなのですが、NoSQL データベースでは(当たり前ですが) SQL は使えません。NoSQL データベースで検索するには検索条件を JSON で与えて API をポストする、という必要があります。そしてその前に検索するキー値(今回の例であれば cputemp)のインデックスを作成されている必要があります。念のため現在作成されているインデックスの一覧を確認してみます:
(https で始まる "url" の値)/(データベース名)/_index

上述の条件であれば、以下の URL に GET アクセスすることになります:
https://NNNN:PPPP@xx.xx.xx.xx/iotdata/_index


上記の URL にウェブブラウザでアクセス(GET リクエスト)すると、指定したデータベースに現在作成されているインデックスの一覧が JSON フォーマットで確認することができます:
2016021202

↑デフォルトでは "_id" キーを対象とした "type":"special" なインデックスだけが作成されていることがわかります。


では cputemp キーで検索できるよう、cputemp にインデックスを作成します。新規にインデックスを作成するには、同じ URL に、以下のような JSON データを POST します:
{
  "index": {
    "fields": ["cputemp"]
  },
  "name" : "cputemp-index",
  "type" : "json"
}

↑フィールドに cputemp を指定し、インデックス名を cputemp-index にしています(インデックス名は適当で構いません)。

ウェブブラウザでは POST ができないため、別途ツールを使って実行してください。以下は curl を使った場合のコマンドライン例です(実行結果を緑にしています):
$ curl -X POST https://NNNN:PPPP@xx.xx.xx.xx/iotdata/_index -d '{"index":{"fields":["cputemp"]},"name":"cputemp-index","type":"json"}'

{"result":"created","id":"_design/c233b8dd41b208ef1d9c8370f7ff3906857b0089","name":"cputemp-index"}

↑cputemp-index インデックスの作成が成功しました。この状態で再度上記のインデックス一覧確認 URL にアクセスすると、作成された cputemp-index が追加されて、以下の様な結果になります:
2016021201


cputemp のインデックスが作成されたので、このインデックスを使って CPU 温度でデータの検索を行います。例えば上記で紹介した「CPU 温度が 42 度より上なデータを検索」してみましょう。

この検索を行うための指定は以下の JSON になります:
{
  "selector": {
    "cputemp": {
      "$gt": 42
    }
  }
}

↑"cputemp"が42より大きい("$gt" は "Greater Than"の意)の条件を selector に指定しています。

この JSON データを本文として、以下の URL に POST することで検索を実行します:
(https で始まる "url" の値)/(データベース名)/_find

curl を使う場合は青のような入力になります。成功すると緑のような結果が返ってきます。cputemp が 42 より大きなものだけを取り出すことができました:
$ curl -X POST https://NNNN:PPPP@xx.xx.xx.xx/iotdata/_find -d '{"selector":{"cputemp":{"$gt":42}}}'

{"docs":[
{"_id":"*****","_rev":"xxxxx","myName":"myPi","cputemp":42.24,"cpuload":0,"sine":-0.32},
{"_id":"*****","_rev":"xxxxx","myName":"myPi","cputemp":42.24,"cpuload":0,"sine":-1},
   :
: {"_id":"*****","_rev":"xxxxx","myName":"myPi","cputemp":46.54,"cpuload":0.45,"sine":-0.75} ]}

クエリーには更に条件を加えることもできます。例えばこれは cputemp が 42 より大きなものを最初の10個だけ、_id と _rev と cputemp のフィールドだけ抜き出す、という検索をする場合の指示です:
{
  "selector": {
    "cputemp": {
      "$gt": 42
    }
  },
  "fields": ["_id","_rev","cputemp"],
  "limit": 10
}

これで検索すると検索結果には _id と _rev と cputemp だけが含まれたデータが最初の 10 件分だけ返されます。インデックスとこの検索条件を工夫することで色々なクエリーを実現することができるようになります。
$ curl -X POST https://NNNN:PPPP@xx.xx.xx.xx/iotdata/_find -d '{"selector":{"cputemp":{"$gt":42}},"fields":["_id","_rev","cputemp"],"limit":10}'

{"docs":[
{"_id":"*****","_rev":"xxxxx","cputemp":42.24},
{"_id":"*****","_rev":"xxxxx","cputemp":42.24},
   :
(10件分だけ)
   :
{"_id":"*****","_rev":"xxxxx","cputemp":46.54}
]}


ただ SQL データベースのように「インデックスさえ作れば SQL のような検索が自由にできる」と考えるべきではありません。インデックスを作らないと検索できないともいえるわけで、データリソース(データベースサイズ)への影響を考える必要も考えながら、最小限のインデックスを作るべきです。また SQL のように全文検索を得意としているわけではないので、場合によっては検索エンジンや SQL データベースと組み合わせて使う必要も出てきます。

その一方で分散データベースの得意分野といえるスケーリングや高可用性を活かした使い方ができるので、適材適所的な考えでシステムを構築していく中で、上記のような得意分野を活かした使い方をさせることで便利に使っていけると思っています。


なお、クエリーについてのより詳しい情報も含めた Cloudant の API に関してはこちらのリファレンスを参照ください:
IBM Cloudant Documentation - API Reference

このページのトップヘ