目次


Hyperledger Fabric で CouchDB のインデックスを設定し、高速な検索を実現する

Hyperledger Fabric ではステート DB として CouchDB を使用することができ、格納した JSON データに対して多彩な検索を行うことができます。
しかし、検索性能を上げるためにはインデックスを適切に設定する必要があります。
本稿では、Hyperledger Fabric SDK for Node.js を用いてチェーンコードをインストールする際に、インデックスを設定する方法について説明します。
また、インデックスが適切に設定されたか確認する方法についても紹介します。

はじめに

Hyperledger Fabric (以下、Fabric) ではステート DB として CouchDB を使用することができます。
CouchDB では JSON データを格納し、それらに対する多彩な検索を行うことができます。Fabric ではこれをリッチ・クエリと呼んでいます。
しかし、CouchDB も基本的にはキー・バリュー・ストア (KVS) であり、検索性能を上げるためにはインデックスを適切に設定する必要があります。

本稿の執筆にあたっては Fabric の v1.2.1 で動作確認を行いました。v.1.3.0 においても同様の方法が適用できます。

ステート DB として CouchDB を使用する場合にインデックスを設定する方法については、公式ドキュメントのこちらに記載があります。
そこではサンプルコードとして Fabric samples にある marble の例 があげられています。

しかし、そこに書かれている方法はチェーンコードのソースがピアにすでに存在する場合にしか使用できません。
Hyperledger Fabric SDK for Node.js (以下、Fabric SDK) などを使用して外部からチェーンコードをインストールする際には異なる方法が必要です。
本稿では Fabric SDK を用いてチェーンコードをインストールおよびインスタンス化する際に、インデックスを設定する方法について説明します。

また、CouchDB のユーザ・インタフェースである Fauxton を利用して、インデックスが適切に設定されたか確認する方法についても紹介します。
それに加えて、CouchDB のインデックスの使用に慣れていない読者のために、インデックスの使用にあたって注意する点についても記載しています。

目次:

準備

CouchDB を使用するように Fabric を設定し、ネットワークを立ち上げる

docker-compose ファイルで設定する必要があります。上記公式ドキュメントを参考にしてください。
設定が完了したらネットワークを立ち上げて下さい。

リッチ・クエリを行うチェーンコードを用意する

前述した marble を使うか、GetQueryResult メソッドを呼び出すチェーンコードを自作して下さい。

Fauxton を使用する

インデックスの設定および検索時のインデックスの使用について確認するには、CouchDB のユーザインタフェースである Fauxton を使用すると便利です。
使用する Docker Compose ファイルにおいて、ある CouchDB コンテナに関する設定が次のようになっているとします:

couchdb01:
    container_name: couchdb01
    image: hyperledger/fabric-couchdb
    environment:
      - COUCHDB_USER=
      - COUCHDB_PASSWORD=
    ports:
      - 7084:5984

この場合、ブラウザで localhost:7084/_utils にアクセスすると Fauxton が表示されます。

 

画面にはデータベースの一覧が表示されています。
すでにネットワーク上でチェーンコードが動いてる場合には "<チャネル名>_<チェーンコード ID>" という名前のデータベースがあります。
それを選択するとドキュメント (CouchDB ではあるキーに対するエントリのことを指す) の一覧が見られます。

 

リッチ・クエリ使用時のインデックスの使用を確認する

CouchDB のリッチ・クエリとインデックス

CouchDB は JSON が格納できるドキュメント・ストアと呼ばれる KVS です。
一般に、KVS が高速に行えるデータアクセスは次の 2 種類に限られます: ある単独キーを指定しての 1 エントリの読み込みと、キーの範囲を指定して複数のエントリを読み込むレンジ・クエリです (チェーンコードでは GetState() と GetStateByRange() )。
CouchDB はこれに加えて、オブジェクトのフィールド値に関する条件を指定した検索 (チェーンコードでは GetQueryResult()) が行なえます。

例えば「income フィールドの値が 1 千万以上かつ age が 35 以下」のような条件です。このような問い合わせをリッチ・クエリと呼びます。
上記の条件で問い合わせを行うには、Go 言語のチェーンコードの場合、以下のようにメソッドを呼び出します:

stub.GetQueryResult(`{
    "selector": {
        "income": {
            "$gt": 10000000
        },
        "age": {
            "$lte": 35
        }
    }
}`)

また、「income フィールドの値が 1 千万以上または age が 35 以下」という条件で検索することもできます。

stub.GetQueryResult(`{
    "selector": {
        "$or": [
            {
                "income": {
                    "$gt": 10000000
                }
            },
            {
                "age": {
                    "$lte": 35
                }
            }
        ]
    }
}`)

他にも多彩な条件が指定できます。
クエリのフォーマットについては、CouchDB の _find メソッドの説明をご覧ください。

リッチ・クエリは便利ですが、効率の良い検索のためには予め適切なインデックスを設定しておく必要あります。

インデックスが設定されていない場合の警告メッセージ

ピアのログで確認する

用意したチェーンコードをインストールしてインスタンス化し、リッチ・クエリを行うトランザクションを起動します。
インデックスを設定せずに検索を行った場合、ピアでは以下のようなログが出力されます。なお、ログレベルを DEBUG に設定しておく必要があります。

2018-09-08 18:18:53.623 UTC [chaincode] handleMessage -> DEBU 8ad [7fc8f0a5] Fabric side handling ChaincodeMessage of type: GET_QUERY_RESULT in state ready
2018-09-08 18:18:53.624 UTC [chaincode] HandleTransaction -> DEBU 8ae [7fc8f0a5] handling GET_QUERY_RESULT from chaincode
2018-09-08 18:18:53.624 UTC [statecouchdb] applyAdditionalQueryOptions -> DEBU 8af Rewritten query: {"limit":10000,"selector":{"MemberEntity":""},"skip":0}
2018-09-08 18:18:53.624 UTC [couchdb] QueryDocuments -> DEBU 8b0 Entering QueryDocuments()  query={"limit":10000,"selector":{"MemberEntity":""},"skip":0}
2018-09-08 18:18:53.624 UTC [couchdb] handleRequest -> DEBU 8b1 Entering handleRequest()  method=POST  url=http://couchdb01:5984/mychannel_mycc/_find
2018-09-08 18:18:53.624 UTC [couchdb] handleRequest -> DEBU 8b2 HTTP Request: POST /mychannel_mycc/_find HTTP/1.1 | Host: couchdb01:5984 | User-Agent: Go-http-client/1.1 | Content-Length: 55 | Accept: application/json | Content-Type: application/json | Accept-Encoding: gzip |  | 
2018-09-08 18:18:53.673 UTC [couchdb] handleRequest -> DEBU 8b3 Exiting handleRequest()
2018-09-08 18:18:53.674 UTC [couchdb] QueryDocuments -> DEBU 8b4 HTTP/1.1 200 OK
Transfer-Encoding: chunked
Cache-Control: must-revalidate
Content-Type: application/json
Date: Sat, 08 Sep 2018 18:18:53 GMT
Server: CouchDB/2.1.1 (Erlang OTP/18)
X-Couch-Request-Id: afbed5d455
X-Couchdb-Body-Time: 0

73
{"docs":[
],
"bookmark": "nil",
"warning": "no matching index found, create an index to optimize query time"}


0

2018-09-08 18:18:53.674 UTC [couchdb] QueryDocuments -> DEBU 8b5 Exiting QueryDocuments()
2018-09-08 18:18:53.674 UTC [statecouchdb] ExecuteQuery -> DEBU 8b6 Exiting ExecuteQuery
2018-09-08 18:18:53.674 UTC [chaincode] HandleGetQueryResult -> DEBU 8b7 Got keys and values. Sending RESPONSE
2018-09-08 18:18:53.675 UTC [chaincode] HandleTransaction -> DEBU 8b8 [7fc8f0a5] Completed GET_QUERY_RESULT. Sending RESPONSE

QueryDocuments メソッドのログに注目して下さい。
ピアはまずチェーンコードから来たリッチ・クエリの一部を書き換えます (4行目)。
次に handleRequest メソッドで、CouchDB の _find メソッドを呼び出します (5行目)。
その結果が HTTP レスポンスとして返ります (8行目)。
適切なインデックスが存在しないか、検索で使用されなかった場合はレスポンスに "no matching index found, create an index to optimize query time" という警告が含まれています (21行目)。

Fauxton で確認する

この警告は Fauxton からも確認できます。
Faxton の UI でデータベースを選択し、左側にある "Run A Query with Mango" というタブをクリックします。

 

適当なクエリを入力して "Run Query" を押すと検索が実行されます。
検索が完了すると、ボタンの下に "Executed in 9 ms" などのように実行時間が表示されます。
そこに "!" のマークが表示されている場合は、マウスオーバーすることにより "no matching index found, ..." の警告を見ることができます。

 

インデックスを適切に設定することにより、この警告が表示されないようにすることが本稿の目的です。

インデックス作成ファイルの形式

インデックスを作成するためには、チェーンコードのインストール時にインデックス作成ファイルを指定します。
これはチェーンコードのメタデータに含められ、ソースとともにピアに格納されます。
そしてインスタンス化する際にはそれをもとにインデックスが作成されます。

設定ファイルのフォーマットについては、前述の公式ドキュメントの説明にあります。
例えば、以下のような JSON ファイルを作って "indexOwner.json" などの名前で保存しておきます。

{"index": {"fields": ["docType", "owner"]}, "ddoc": "indexOwnerDoc", "name": "indexOwner", "type": "json"}

インストール時におけるインデックス作成ファイルの指定方法

インデックス作成ファイルをインストール時に指定する方法については注意が必要です。

公式ドキュメントではチェーンコードを Go 言語で書く場合について説明しており、そこではインデックス作成ファイルを META-INF ディレクトリ以下に配置し、ディレクトリをソースファイル (.go ファイル) と同じディレクトリに置くように書かれています。
この方法は、ソースコードディレクトリがピアのファイルシステム上に存在する場合にのみ動作する方法です。
Fabric samples にあるサンプルはほとんどそのような構成になっています。

しかし、Fabric SDK を使用してチェーンコードをピアの外部から与える場合には、インストールするメソッド、installChaincode() を呼ぶ際に、インデックス作成ファイルを明示的に指定する必要があります。
Fabric SDK の公式ドキュメントによると、META-INF ディレクトリへの絶対パスを ChaincodeInstallRequest オブジェクトの metadataPath フィールドに指定します。

インデックスが使用されていることを確認する

ピアのログで確認する

インデックスが正しく作成された場合には、ピアのログに以下のように表示されます。
チェーンコードのインストール時には、メタデータファイルの処理について次のようなログが表示されます:

2018-09-09 04:58:38.971 UTC [chaincode-metadata] GetMetadataAsTarEntries -> DEBU 28b Wrote file to statedb tar: META-INF/statedb/couchdb/indexes/indexAccomodation.json
2018-09-09 04:58:38.971 UTC [chaincode-metadata] GetMetadataAsTarEntries -> DEBU 28c Created metadata tar
2018-09-09 04:58:38.972 UTC [chaincode-metadata] processIndexMap -> DEBU 28d Found index field name: "Id"
2018-09-09 04:58:38.972 UTC [chaincode-metadata] processIndexMap -> DEBU 28e Found index field name: "Name"
2018-09-09 04:58:38.972 UTC [chaincode-metadata] processIndexMap -> DEBU 28f Found index field name: "Phone"
2018-09-09 04:58:38.972 UTC [chaincode-metadata] validateIndexJSON -> DEBU 290 Found index object: "ddoc":"indexAccomodationDoc"
2018-09-09 04:58:38.972 UTC [chaincode-metadata] validateIndexJSON -> DEBU 291 Found index object: "name":"indexAccomodation"
2018-09-09 04:58:38.972 UTC [chaincode-metadata] validateIndexJSON -> DEBU 292 Found index object: "type":"json"

さらに、インスタンス化の際にはインデックスが作成されたというログが出力されます。

2018-09-09 05:12:25.388 UTC [chaincode-metadata] GetMetadataAsTarEntries -> DEBU 3b8 Wrote file to statedb tar: META-INF/statedb/couchdb/indexes/indexAccomodation.json
2018-09-09 05:12:25.388 UTC [chaincode-metadata] GetMetadataAsTarEntries -> DEBU 3b9 Created metadata tar
2018-09-09 05:12:25.388 UTC [couchdb] CreateIndex -> DEBU 3ba Entering CreateIndex()  indexdefinition={
    "index": {
        "fields": [
            "Id",
            "Name",
            "Phone"
        ]
    },
    "ddoc": "indexAccomodationDoc",
    "name": "indexAccomodation",
    "type": "json"
}
2018-09-09 05:12:25.388 UTC [couchdb] handleRequest -> DEBU 3bb Entering handleRequest()  method=POST  url=http://couchdb01:5984/mychannel_mycc/_index
2018-09-09 05:12:25.389 UTC [couchdb] handleRequest -> DEBU 3bc HTTP Request: POST /mychannel_mycc/_index HTTP/1.1 | Host: couchdb01:5984 | User-Agent: Go-http-client/1.1 | Content-Length: 201 | Accept: application/json | Content-Type: application/json | Accept-Encoding: gzip |  | 
2018-09-09 05:12:25.470 UTC [gossip/election] waitForInterrupt -> DEBU 3bd [133 55 245 197 141 7 42 21 191 91 203 184 116 16 34 237 193 167 140 32 13 183 232 84 176 253 43 71 219 97 130 166] : Exiting
2018-09-09 05:12:25.470 UTC [gossip/election] IsLeader -> DEBU 3be [133 55 245 197 141 7 42 21 191 91 203 184 116 16 34 237 193 167 140 32 13 183 232 84 176 253 43 71 219 97 130 166] : Returning true
2018-09-09 05:12:25.471 UTC [gossip/election] waitForInterrupt -> DEBU 3bf [133 55 245 197 141 7 42 21 191 91 203 184 116 16 34 237 193 167 140 32 13 183 232 84 176 253 43 71 219 97 130 166] : Entering
2018-09-09 05:12:25.499 UTC [couchdb] handleRequest -> DEBU 3c0 Exiting handleRequest()
2018-09-09 05:12:25.500 UTC [couchdb] CreateIndex -> INFO 3c1 Created CouchDB index [indexAccomodation] in state database [mychannel_mycc] using design document [_design/indexAccomodationDoc]

なお、最後のメッセージに含まれるデザインドキュメントの場所は、リッチクエリで明示的に指定する必要がある場合には必要になります。

Fauxton で確認する

作成されたインデックスは Fauxton からも確認できます。
チェーンコードのインスタンス化後に、対象の "<チャネル名>_<チェーンコード ID>" データベースを開きます。
ドキュメントの一覧が表示されるので、そこにデザインドキュメント _design/<インデックスドキュメント名> が登録されていることを確認してください。

 

このドキュメントにはインデックス名や設定したフィールド名などが格納されています。
このドキュメントが存在していない場合は、インデックスの作成に失敗しています。

さらに、作成したインデックスはインデックス一覧からも確認できます。
ドキュメント一覧が表示されている状態から、ブラウザの URL の _all_docs 以下の部分を _index に書き換えます。
すると作成されているインデックスの一覧が表示されます。

 

デフォルトのインデックスが必ず設定されており、それに加えてさきほど作成したインデックスの内容が表示されます。
項目が一部省略されている場合は、右上の JSON ボタンを押すとデータの全体を見ることができます。

インデックスを使用した検索を行う

ほとんどの場合、使用するインデックスを明示的に指定する必要はありません。
何も指定せずに GetQueryResult メソッドを呼べば CouchDB が適切なインデックスを使用して検索を行います。

使用するインデックスを明示してリッチ・クエリを行う

同様の条件で複数のインデックスが作成されている場合 (例えば、対象とするフィールドは同じだが、昇順と降順それぞれのインデックスが存在する) など、明示的に使用するインデックスを指定したい場合もあります。
その場合には、CouchDB で直接クエリするときと同様に、使用するデザインドキュメント名を use_index フィールドに指定して GetQueryResult() を呼びます。

iter, err := stub.GetQueryResult(
    `{
      "selector": {
        "Id": "0123",
        "Name": "Hotel California",
        "Phone": "0123456789"
      },
      "use_index": "indexAccomodationDoc"
    }`)

1 つのインデックスドキュメントに複数のインデックスが含まれる場合は "use_index": ["<インデックスドキュメント名>", "<インデックス名>"] のように両者を指定することもできます。

なお、適用できないインデックス名をクエリで指定すると GetQueryResult() はエラーを返します。
したがって、必要な場合を除いてはインデックス名を指定しないことをおすすめします。

インデックス作成のコツ

ここでは Fabric の解説から離れて、CouchDB のインデックス作成のコツを紹介します。
すでにご存知の場合はスキップしていただいて結構です。
以下では、Fauxton でインデックスの挙動を確認する方法について紹介します。

インデックスとして以下のもの、つまり、ID、Name、Phone の 3 フィールドについてのインデックス、が作成されているとして話をすすめます。
また、これらのフィールドを持つ JSON データが多数格納されているとします。
なお、いずれのフィールドも文字列が格納されることを想定しています。

{
    "index": {
        "fields": [
            "Id",
            "Name",
            "Phone"
        ]
    },
    "ddoc": "indexAccomodationDoc",
    "name": "indexAccomodation",
    "type": "json"
}

これまで説明したように、Fauxton で注目しているチェーンコードのデータベースを開き、Run A Query with Mango のタブをクリックします。
ここではリッチ・クエリを入力して実行することができるとともに、実行計画を表示する Explain コマンドが実行できます。
これにより、作成したインデックスが実際に使用されるか、データの検索が効率よく行えるか、を手軽に確認することができます。

効率よく検索できる、フィールド完全一致クエリ

まず、これらのフィールドを完全一致で検索するクエリを実行してみます。
以下のクエリを入力して Explain ボタンを押すと、右に実行計画が表示されます。

{
   "selector": {
      "Id": "0123",
      "Name": "Hotel California",
      "Phone": "0123456789"
   }
}

実行計画でまず注目するべきは index フィールドです。
ここにはクエリ実行に使用されるインデックス名とドキュメント名が表示されます。
今回の実行計画では indexAccomodation および _design/indexAccomodationDoc が表示されています。
つまり、use_index を指定しなくてもインデックスが自動的に使用されることがわかります。

次に mrargs フィールドに注目します。
ここで start_key および end_key フィールド値に具体的な値が表示され、全レコード検索でないように見える場合には、インデックスを用いたレンジクエリとして効率よく処理されることを意味しています。
今回の例では、これらのフィールド値は以下のようになっています:

  "start_key": [
   "0123",
   "Hotel California",
   "0123456789"
  ],
  "end_key": [
   "0123",
   "Hotel California",
   "0123456789",
   "<MAX>"
  ],

効率よく検索できない例 1

一方、正規表現によるマッチング ($regex) など、レンジクエリの組み合わせとして表現できないようになクエリについては、インデックスを使っても効率よく検索できません。
たとえば、フィールド "Id" の前方一致、"Name" の部分一致、"Phone" の部分一致、というクエリを考えます:

{
   "selector": {
      "Id": {
         "$regex": "^0123"
      },
      "Name": {
         "$regex": "Hotel California"
      },
      "Phone": {
         "$regex": "042"
      }
   }
}

この場合、実行計画から、作成したインデックスが使われるものの、start_key が []、end_key が ["<MAX>"] となって、全レコードをスキャンして検索していることがわかります。

効率よく検索できない例 2

もう 1 つ注意したいのは、フィールド値の完全一致を行う場合であっても、次のクエリのように一部のフィールドについての条件を含んでいない場合です:

{
   "selector": {
      "Id": "0123",
      "Name": "Hotel California"
   }
}

この場合、実行計画結果の index フィールド値は次のようになります。
ddoc フィールド値が null、name フィールド値が _all_docs であり、これはインデックスが使われないことを意味しています。

"index": {
  "ddoc": null,
  "name": "_all_docs",
  "type": "special",
  "def": {
   "fields": [
    {
     "_id": "asc"
    }
   ]
  }
}

また、CouchDB の制限により、$in や $or を用いた検索についても、現時点ではインデックスが使用されません。

まとめ

この記事では、Hyperledger Fabric のステート DB として CouchDB を使用する際に、インデックスを指定する方法および動作を確認する方法を紹介しました。
使用にあたってはインデックスを適切に設定する必要がありますが、CouchDB のユーザインタフェースである Fauxton を使うことによって設定の正しさを手軽に確認できます。
これにより複雑な検索を可能にしつつ、高い検索性能が実現できます。
データ構造が複雑なアプリケーションの作成などに、ぜひお役立てください。


ダウンロード可能なリソース

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Cloud computing
ArticleID=1063696
ArticleTitle=Hyperledger Fabric で CouchDB のインデックスを設定し、高速な検索を実現する
publish-date=12062018