Azure Cosmos DB入門(目次)

「Azure Cosmos DB入門」目次

DocumentDB が Azure Cosmos DB としてリニューアルされたので、改めてこのサービスの全体像を整理したコンテンツ(ブログ)を「Cosmos DB入門」として書こうと思います。
目次は以下の通り。順次コンテンツを追加していく予定です。
※ 2017/07/09に(1)~(8)のコンテンツが揃いました。今後もCosmos DBのトピックをブログに継続してエントリーしていきます。

  • Azure CosmosDB入門(1)
    • 1 Cosmos DBとは
      • 1.1 特徴
        • 1.1.1 グローバル分散可能
        • 1.1.2 柔軟なスケーリングが可能
        • 1.1.3 超高速な応答速度
        • 1.1.4 マルチデータモデル
        • 1.1.5 多様な一貫性モデル
        • 1.1.6 高いレベルのSLA
        • 1.1.7 Cosmos DBは自分たちが普通に使うものか?
      • 1.2 Hello Cosmos DB
        • 1.2.1 AzureポータルでCosmos DBを作成
        • 1.2.2 Azureポータルでデータを操作
        • 1.2.3 プログラムでデータを操作
  • Azure CosmosDB入門(2)
    • 2 Cosmos DBの主要概念
      • 2.1 データモデルとアクセスAPI
        • 2.1.1 atom-record-sequence(ARS)
        • 2.1.2 AzureポータルでCosmos DBを作成する
      • 2.2 各種データモデル
        • 2.2.1 「ドキュメント」モデル
        • 2.2.2 「グラフ」 モデル(2017/5現在Preview版)
        • 2.2.3 「テーブル」モデル(2017/5現在Preview版)
      • 2.3 パーティショニング
      • 2.4 RU(Request Unit)
        • 2.4.1 1RUとは?
        • 2.4.2 RUはコレクションに割り当てる
        • 2.4.3 RUという概念のすばらしさ
        • 2.4.4 予約RUの設定方法
        • 2.4.5 消費RUの確認方法
        • 2.4.6 RUの話は深い・・・(後半に続く)
  • Azure CosmosDB入門(3)
    • 3 Cosmos DBプログラミング ~ DocumentDB編(前編)
      • 3.1 まず、はじめに
      • 3.2 準備
      • 3.3 ドキュメントの作成・更新
      • 3.4 ドキュメントの検索
      • 3.5 ドキュメントの削除
      • 3.6 つづく・・・
      • 3.7 資料
  • Azure CosmosDB入門(4)
    • 4 Cosmos DBプログラミング ~ DocumentDB編(後編)
      • 4.1 サーバーサイド(データベースサイド)ロジック
      • 4.2 トランザクション
      • 4.3 ストアドプロシージャ
        • 4.3.1 Helloストアドプロシージャ
        • 4.3.2 期間指定で予約を削除するストアドプロシージャ
      • 4.4 トリガー
        • 4.4.1 プリ・トリガー(Pre-Trigger)
        • 4.4.2 ポスト・トリガー(Post-Trigger)
      • 4.5 ユーザー定義関数(UDF)
      • 4.6 資料
  • Azure CosmosDB入門(5)
    • 5 Cosmos DBプログラミング ~ MongoDB編
      • 5.1 はじめに
        • 5.1.1 Cosmos DB(MongoDBデータモデル)とMongoDB
      • 5.2 準備
        • 5.2.1 Cosmos DB(MongoDB)データベースアカウントの作成
        • 5.2.2 Visual Studio 2017プロジェクトの作成
      • 5.3 データベースとコレクションの作成
        • 5.3.1 Azureポータルで作成
        • 5.3.2 プログラムで作成
        • 5.3.3 mongoコマンドで作成
      • 5.4 ドキュメントの作成
        • 5.4.1 ドキュメントの一括投入 ~ Studio 3T for MongoDB(旧Mongo chef)
      • 5.5 ドキュメントの検索
        • 5.5.1 Mongoクエリー言語による検索
        • 5.5.2 LINQによる検索
      • 5.6 ドキュメントの更新
      • 5.7 ドキュメントの削除
      • 5.8 資料
      • 5.9 つづく・・・
  • Azure CosmosDB入門(6)
    • 6 Cosmos DBプログラミング ~ Gremlin編
      • 6.1 はじめに
        • 6.1.1 グラフ データモデルとは
        • 6.1.2 Gremlinとは
      • 6.2 準備
        • 6.2.1 Cosmos DB(Gremlin)データベースアカウントの作成
        • 6.2.2 Visual Studio 2017プロジェクトの作成
      • 6.3 データベースとコレクションの作成
        • 6.3.1 プログラムで作成
      • 6.4 グラフデータの操作
        • 6.4.1 Vertexの追加
        • 6.4.2 Edgeの追加
        • 6.4.3 Vertexの検索
        • 6.4.4 Edgeの検索
        • 6.4.5 Vertex~Edge~Vertexを辿る(1)
        • 6.4.6 Vertex~Edge~Vertexを辿る(2)
      • 6.5 まとめ
      • 6.6 資料
  • Azure CosmosDB入門(7)
    • 7 Cosmos DBプログラミング ~ Table編
      • 7.1 はじめに
        • Cosmos DB(Table) と Azure Table Storage
        • アクセス用クラスライブラリ
      • 7.2 Cosmos DB(Table)
        • 7.2.1 データベース構造
        • 7.2.2 Cosmos DB固有情報の設定
      • 7.3 準備
        • 7.3.1 Cosmos DB(Table)データベースアカウントの作成
        • 7.3.2 Visual Studio 2017プロジェクトの作成
      • 7.4 テーブルの作成
        • 7.4.1 Azureポータルでテーブルを作成
        • 7.4.2 コードでテーブルを作成
      • 7.5 テーブルの操作
        • 7.5.1 エンティティ型の定義
        • 7.5.2 エンティティの追加
        • 7.5.3 エンティティの一括追加
        • 7.5.4 エンティティの検索
        • 7.5.5 エンティティの更新
        • 7.5.6 エンティティの削除
      • 7.6 まとめ
      • 7.7 資料
  • Azure CosmosDB入門(8)
    • 8 Cosmos DBをもっと知りたい
      • 8.1 一貫性レベル(Consistency Level)
        • 8.1.1 Cosmos DBの提供する一貫性レベル(Consistency Level)
        • 8.1.2 既定の一貫性レベル
      • 8.2 RU(Request Unit)詳細
        • 8.2.1 RUの設定方法
        • 8.2.2 消費RUの確認方法
        • 8.2.3 RU/s超過時のレスポンスコード429
        • 8.2.4 リトライ動作の指定
        • 8.2.5 一貫性レベル(Consistency Level)による消費RUの違い
        • 8.2.6 RU/m (RU/分) の設定
      • 8.3 グローバルレプリケーション
      • 8.4 障害復旧およびフェールオーバー
        • 8.4.1 自動フェールオーバー
        • 8.4.2 手動フェールオーバー
      • 8.5 まとめ
        ※2017/07/09 目次更新

はじめに

先日 2017/5/10-12 に開催された Build 2017 において多数の新しい技術の発表がなされました。
その中の1つとして Azure Cosmos DB も発表されました。

Azure Cosmos DBは従来 DocumentDB として公開されていたサービスに置き換わるものです。厳密には「Cosmos DB が DocumentDB の機能を内包している」という関係性を持ちます。

Azure Cosmos DBは、2010年に「Project Florence」として開始されました。大規模アプリケーション開発の課題に対処するために、マイクロソフト社内向けに開発されていたサービスです。しかし、グローバルに分散されたアプリケーションの構築は、マイクロソフト以外の課題でもあるとの認識から2015年に「Azure DocumentDB」としてサービスリリースされました。
DocumentDBに多数の機能を追加し、そして2017/5/10に Azure Cosmos DB としてサービスリリースが行われました。

※これまでのDocumentDB情報

Cosmos DBは、従来のDocumentDBの知識やコードはそのまま利用可能です。大幅な機能追加によるリニューアルであり、従来のDocumentDBの機能や仕様がひっくり返されている部分はありません。
つまり、ネット上に公開されているDocumentDBの情報は(完全にマッチしないまでも)引き続きCosmos DBにおいて有効な情報です。

DocumentDBライブラリの「1クエリ」は「1HTTPリクエスト」ではない

豆知識的なメモ ブログです。

私自身、DocumentDBを学ぶ上で「あー、そうなんだぁ。ライブラリがいい意味でも、そうでない意味でも、面倒見てくれてるんだなぁ」と思った点について書いておきます。

※本記事では「FeedOptions.MaxItemCount」と「DocumentDBのREST API呼び出しにおけるHTTPヘッダー「x-ms-max-item-count」」について記述しています。

前提条件

以下のモデルクラスデータが 1,000件 、DocumentDBに保存されているものとします。

public class ProductItem
{
  [JsonProperty(PropertyName = "id")]
  public string Id { get; set; }

  public string Name { get; set; }

  public int Price { get; set; }

  public int StockNumber { get; set; }
}

また、ドキュメントは以下のデータベース・コレクションに保存されているものとします。

データベース名: ExampleDB1
コレクション名: ExampleCollection1

ライブラリを使って全件クエリー!

プロジェクトに対してNugetから「Microsoft.Azure.Document」を追加すると以下のようなコードで 1,000件のProductItemオブジェクト を取得することができます。

// リスト1
string EndpointUrl = "https://rddocdb.documents.azure.com:443/";
string PrimaryKey = "[プライマリキー]";

string DatabaseID = "ExampleDB1";
string CollectionID = "ExampleCollection1";

// DocumentClientオブジェクト初期化
DocumentClient client = new DocumentClient(new Uri(EndpointUrl), PrimaryKey);

// 全件取得
Uri documentCollectionUri = 
  UriFactory.CreateDocumentCollectionUri(DatabaseID, CollectionID);
var query = 
  client.CreateDocumentQuery<ProductItem>(documentCollectionUri);
// result変数に1,000件のデータが取得される
var result = query.ToList();

実際に発行されたHTTP

上記コードでは、最後の「result = query.ToList();」を実行したタイミングで DocumentDB へのクエリー要求が行われます。
処理完了後に result 変数には1,000件の ProductItemオブジェクト が取得されます。
FiddlerでHTTP通信を監視すると、以下のようなHTTP通信が行われていることを確認することができます。

f:id:daigo-knowlbo:20170509031327p:plain

10回の /docs へのリクエストが行われています。
また、各リクエスト時の リクエストチャージ(x-ms-request-charge)は 38.1 でした。全部で38.1×10のRUを消費。

つまり、ライブラリを利用するコードとしては「query.ToList()」の1文が、10回のHTTPリクエストに分割された、ということです。

x-ms-max-item-countの影響

全1,000件のデータが10回のHTTPリクエストに分割された理由、それはDocumentClient.CreateDocumentQuery()メソッド呼び出し時のFeedOptions引数に影響します。
リスト1ではCreateDocumentQuery()の第2引数を指定しませんでしたが、CreateDocumentQuery()には第2引数にFeedOptionsオブジェクトを指定することができます。
FeedOptions.MaxItemCountの値が1リクエストで取得する項目の最大件数になります(DocumentDBのREST API呼び出しにおけるHTTPヘッダー「x-ms-max-item-count」値)。
また、FeedOptions.MaxItemCountのデフォルト値は「100」となっています。
つまり、これを省略したリスト1では「1リクエストで取得する最大件数=100」で動作を行いました。

10回のリクエストの途中のHTTPヘッダーを見ると以下のようになっています。

f:id:daigo-knowlbo:20170509030905p:plain

上記キャプチャでは、HTTPレスポンスに「x-ms-continuation: {“token”:“qd4DAJ67FQDIAAAAAAAAAA==”,“range”:{“min”:“”,“max”:“FF”}}」が記述されています。
つまり、HTTPリクエスト・レスポンス間での特定クエリーに対するページング処理が挟み込まれています(10件ずつのページング)。

FeedOptions.MaxItemCountを指定する

では、以下のリスト2のように FeedOptions.MaxItemCount= 1000 を指定してみます。

// リスト2
string EndpointUrl = "https://rddocdb.documents.azure.com:443/";
string PrimaryKey = "[プライマリキー]";

string DatabaseID = "ExampleDB1";
string CollectionID = "ExampleCollection1";

// 1度の最大取得項目数は1,000
var feedOption = new FeedOptions() { MaxItemCount = 1000 };

// DocumentClientオブジェクト初期化
DocumentClient client = new DocumentClient(new Uri(EndpointUrl), PrimaryKey);

// 全件取得
Uri documentCollectionUri = 
  UriFactory.CreateDocumentCollectionUri(DatabaseID, CollectionID);
var query = 
  client.CreateDocumentQuery<ProductItem>(documentCollectionUri, feedOption);
var result = query.ToList();

リスト2を実行すると、以下のように1度のHTTPリクエストで1,000件のProductItemオブジェクトを取得することができます。

f:id:daigo-knowlbo:20170509031939p:plain

ちなみにこの時の消費RUは 380.96 となりました。

DocumentDB でページング処理を行う

「データを一覧してページング表示する」という要件は エンプラ系でも コンシューマー系でも 常にある要件です。
ということで、DocumentDBでデータのページング取得を行う方法を書いておこうと思います。

1. 前提

DocumentDBアカウントには、既に以下のデータが保存されている状態を前提とします。

データベース名: ExampleDB1
コレクション名: ExampleCollection1
ドキュメント: ProductItemドキュメントが1,000件保存されている

ProductItemは以下のようなデータクラスです。

// 保存するドキュメント
using Newtonsoft.Json;

namespace PagingExample
{
  public class ProductItem
  {
    [JsonProperty(PropertyName = "id")]
    public string Id { get; set; }

    public string Name { get; set; }

    public int Price { get; set; }

    public int StockNumber { get; set; }
  }
}

以下のような感じで、適当なデータが入っています。

f:id:daigo-knowlbo:20170507003752p:plain

2. DocumentDB操作管理クラスを用意

DocumentDBを操作する管理クラス(DocumentManager)の実装は以下の通りです。

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Linq;
using Microsoft.Azure.Documents.Client;
using System.IO;
using System.Threading.Tasks;

namespace PagingExample
{
  public class DocumentManager
  {
    private const string EndpointUrl = "https://rddocdb.documents.azure.com:443/";
    private const string PrimaryKey = "[マスターキー]";

    private const string DatabaseID = "ExampleDB1";
    private const string CollectionID = "ExampleCollection1";

    private DocumentClient client;

    // DocumentClientオブジェクトを初期化
    public async Task<bool> InitializeDocumentClient()
    {
      this.client = new DocumentClient(new Uri(DocumentManager.EndpointUrl), DocumentManager.PrimaryKey,
        new ConnectionPolicy
        {
          ConnectionMode = ConnectionMode.Gateway,
          ConnectionProtocol = Protocol.Https
        });

      return true;
    }

    // ProductItemを 10件 ずつ取得
    public async Task<(List<ProductItem>, string)> QueryProductItems(string continuration)
    {
      List<ProductItem> result = null;

      var feedOption = new FeedOptions() {
        MaxItemCount = 10,
        RequestContinuation = continuration };

      Uri documentCollectionUri = 
        UriFactory.CreateDocumentCollectionUri(
          DocumentManager.DatabaseID, DocumentManager.CollectionID);
      var query =
        this.client.CreateDocumentQuery<ProductItem>(
          documentCollectionUri, feedOption)
          .Where( p => p.Price > 700 )
          .AsDocumentQuery();

      var ret = await query.ExecuteNextAsync<ProductItem>();
      
      result = ret.ToList();
      string retContinuration = ret.ResponseContinuation;

      return (result, retContinuration);
    }
  }
}

InitializeDocumentClient()メソッド

DocumentClientオブジェクトを new してメンバーフィールドに保持します。

QueryProductItems()メソッド

引数 continuration は、DocumentDBに「どのページ(というかどの位置から継続抽出するか)」の状態文字列です。1ページ目の取得時には「空文字列」を設定します。
CreateDocumentQuery()を呼び出す際の引数「FeedOptions」で、「1度に取得する件数(MaxItemCount)」と「どのページ(どこから継続取得するかを表す状態文字列(RequestContinuation )」を指定します。
AsDocumentQuery()呼び出して、ExecuteNextAsync()呼び出した結果から、10件の抽出データ(ret.ToList()) と 次のページを読み込む為の継続取得状態文字列(ret.ResponseContinuation)が得られます。
これら2つのデータをタプルで戻り値として呼び出し元に返却しています。
また、サンプルなので Price > 700 という固定的なWhere条件を付加しています。

3. DocumentManagerを呼び出してページングデータ取得

では、前述で用意した DocumentManager を利用してProductItemデータを10件ずつページング取得したいと思います。
以下が実装になります。

var manager = new DocumentManager();
var initResult = await manager.InitializeDocumentClient();

// コンソール出力用ページ番号
int pageNo = 0;

// DocumentDBに投げるcontinuration値(1ページ目は空文字列)
string continuration = "";

do
{
  (List<ProductItem> items, string continuration) queryResult = 
    await manager.QueryProductItems(continuration);

  // 次のページを取得するためのcontinuration文字列取得
  continuration = queryResult.continuration;

  // 取得したデータをコンソール出力
  Console.WriteLine(string.Format("page {0}", pageNo));
  Console.WriteLine(string.Format("continuration {0}", continuration));
  foreach (var item in queryResult.items)
  {
    Console.WriteLine(string.Format("{0} {1}", item.Name, item.Price));
  }
  Console.WriteLine("-----");
  pageNo++;
} while (!string.IsNullOrEmpty(continuration)); // continurationが空だと終端ページに達したということ

データ抽出で得られた(Responseされた)Continuation値を、次のデータ抽出(Request)で利用する形です。

4. 実行結果

実行結果は以下の通りです。

page 0
continuration {"token":"+RID:mi01AOyHIgA7AAAAAAAAAA==#RT:1#TRC:10#FPC:AgEAAAB8AD8AABhwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABLcAHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAE=","range":{"min":"","max":"FF"}}
Product_8 800
Product_9 900
Product_18 800
Product_19 900
Product_28 800
Product_29 900
Product_38 800
Product_39 900
Product_48 800
Product_49 900
-----
page 1
continuration {"token":"+RID:mi01AOyHIgBtAAAAAAAAAA==#RT:2#TRC:20#FPC:AgEAAAB2AG8AAGDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHKsAccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAE=","range":{"min":"","max":"FF"}}
Product_58 800
Product_59 900
Product_68 800
Product_69 900
Product_78 800
Product_79 900
Product_88 800
Product_89 900
Product_98 800
Product_99 900
-----
page 2
continuration {"token":"+RID:mi01AOyHIgCfAAAAAAAAAA==#RT:3#TRC:30#FPC:AgEAAABuAJ+ANcABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMAB","range":{"min":"","max":"FF"}}
Product_108 800
Product_109 900
Product_118 800
Product_119 900
Product_128 800
Product_129 900
Product_138 800
Product_139 900
Product_148 800
Product_149 900
-----
page 3
continuration {"token":"+RID:mi01AOyHIgDRAAAAAAAAAA==#RT:4#TRC:40#FPC:AgEAAABoAN8ABhxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABI8AHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMAB","range":{"min":"","max":"FF"}}
Product_158 800
Product_159 900
Product_168 800
Product_169 900
Product_178 800
Product_179 900
Product_188 800
Product_189 900
Product_198 800
Product_199 900
-----
page 4
continuration {"token":"+RID:mi01AOyHIgADAQAAAAAAAA==#RT:5#TRC:50#FPC:AgEAAABiAA8BGHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHIMAccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMAB","range":{"min":"","max":"FF"}}
Product_208 800
Product_209 900
Product_218 800
Product_219 900
Product_228 800
Product_229 900
Product_238 800
Product_239 900
Product_248 800
Product_249 900
-----

...省略...

-----
page 18
continuration {"token":"+RID:mi01AOyHIgC-AwAAAAAAAA==#RT:19#TRC:190#FPC:AgEAAAAKAL+DA8ABBxxwwAE=","range":{"min":"","max":"FF"}}
Product_908 800
Product_909 900
Product_918 800
Product_919 900
Product_928 800
Product_929 900
Product_938 800
Product_939 900
Product_948 800
Product_949 900
-----
page 19
continuration
Product_958 800
Product_959 900
Product_968 800
Product_969 900
Product_978 800
Product_979 900
Product_988 800
Product_989 900
Product_998 800
Product_999 900
-----
続行するには何かキーを押してください . . .

5. HTTP通信内容

DocumentDBライブラリにより、裏側で具体的にどのようなREST API リクエストとレスポンスが行われたのかをFiddlerで確認します。

f:id:daigo-knowlbo:20170507012230p:plain

上記Fiddler画面から以下のことが分かります。

  • 「/dbs/ExampleDB1/colls/ExampleCollection1/docs」へのページごとのリクエストが行われた
  • FeedOptions.MaxItemCount / FeedOptions.RequestContinuation の値がHTTPリクエストヘッダでそれぞれ「x-ms-max-item-count」「x-ms-continuation」して指定されている
  • HTTPレスポンスヘッダ「x-ms-continuation」で、次のページを読み込むための継続状態文字列が返されている

資料

公式ドキュメントでは以下を見ればいいのかな。

docs.microsoft.com

Common DocumentDB REST request headers | Microsoft Docs

Common Azure Cosmos DB REST response headers | Microsoft Docs

DocumentDB のインデックス設定によるRUの変動について

DocumentDBのインデックスについて調べてのでブログに記しておこうと思います。
DocumentDBではデフォルト設定でも結構いい感じに動くようにインデックス設定が行われているのですが、カスタマイズ可能であり、これを最適化するとRUや(インデックス用)データサイズの節約につながります。

※RU = RequestUnit。DocumentDBにおける「単位時間(秒)あたりの処理数の抽象単位」。課金に大きく影響する数値です。

1. デフォルトのインデックス設定

DocumentDBでは、インデックスはコレクションに対して設定可能です。
またコレクションを作成すると、デフォルトでは、以下のようなインデックスポリシーが設定されます。

{
  "indexingMode": "consistent",
  "automatic": true,
  "includedPaths": [
    {
      "path": "/*",
      "indexes": [
        {
          "kind": "Range",
          "dataType": "Number",
          "precision": -1
        },
        {
          "kind": "Hash",
          "dataType": "String",
          "precision": 3
        }
      ]
    }
  ],
  "excludedPaths": []
}

これは、Azureポータル上で「対象DocumentDBアカウント → DataExploere → 対象データベース → 対象コレクション → Scale & Settings」で確認することができます。

f:id:daigo-knowlbo:20170505004326p:plain

「includedPaths」はインデックス適用対象のパス、「excludedPaths」はインデックス所帯対象のパス、という意味になります。
「path: “/*"」とは、「格納されたJSONデータ要素のすべて(配下の階層を含めてすべて)に対して」という意味を持ちます。
そして ”/*" に対して、「Range」「Hash」の2種類のインデックスが設定されています。
それぞれのインデックス種類の意味は、Azure Cosmos DB indexing policies | Microsoft Docsに記述されており、以下が対象箇所の引用になります。

Hash:
 Hash を /prop/? (または/) に使用して、以下のクエリを効率的に処理することができます。
 SELECT FROM collection c WHERE c.prop = “value”
 Hash を /props/[]/? (または / または /props/) に使用して、以下のクエリを効率的に処理することができます。
 SELECT tag FROM collection c JOIN tag IN c.props WHERE tag = 5

Range:
 Range を /prop/? (または/) に使用して、以下のクエリを効率的に処理することができます。
 SELECT FROM collection c WHERE c.prop = “value”
 SELECT FROM collection c WHERE c.prop > 5
 SELECT FROM collection c ORDER BY c.prop

インデックスの種類にはもう1つ「Spatial」があります。これはGEOクエリーを行う為のインデックスとなります。

つまり、コレクションに対するデフォルトのインデックス設定は、一般的な「文字列比較」や「数値比較」による抽出、また並べ替えのためのインデックスが、すべてのドキュメント要素に適用されることになります。

2. 下準備

さて、本投稿では「インデックスの最適化によるRUの変動」を調べるのですが、そのためのデータを準備したいと思います。
以下のPhotoItemクラスをコレクションに保存するドキュメントエンティティとします。

public class PhotoItem
{
  [JsonProperty(PropertyName = "id")]
  public string Id { get; set; }

  // タイトル
  public string Title { get; set; }

  // 「いいね」数
  public int LikeCount { get; set; }

  // 写真のURI
  public string PhotoUri { get; set; }

  // カテゴリ
  public string Category { get; set; }
}

データベース・コレクションは以下のDocumentManagerクラスのInitializeDocumentClient()メソッドで作成します。
データベース名は ExampleDB1、コレクション名は ExampleCollection1、パーティションキーとして Category を設定します。
CreateExampleDocument(PhotoItem photoItem)メソッドは、PhotoItemドキュメントを作成します。

public class DocumentManager
{
  private const string EndpointUrl = "https://rddocdb.documents.azure.com:443/";
  private const string PrimaryKey = "[プライマリキー]";

  private const string DatabaseID = "ExampleDB1";
  private const string CollectionID = "ExampleCollection1";

  private DocumentClient client;

  // データベースとコレクションを作成
  public async Task<bool> InitializeDocumentClient()
  {
    this.client = new DocumentClient(new Uri(DocumentManager.EndpointUrl), DocumentManager.PrimaryKey,
      new ConnectionPolicy
      {
        ConnectionMode = ConnectionMode.Gateway,
        ConnectionProtocol = Protocol.Https
      });

    // ExampleDB1 データベースを作成
    await this.client.CreateDatabaseIfNotExistsAsync(
      new Database { Id = DocumentManager.DatabaseID });

    // ExampleCollection1 コレクションを作成(パーティションキーはCategory)
    Collection<string> partitionPaths = new Collection<string>();
    partitionPaths.Add("/Category");
    var collection = new DocumentCollection { 
                Id = DocumentManager.CollectionID, 
                PartitionKey = new PartitionKeyDefinition() { Paths = partitionPaths } };
    collection = await this.client.CreateDocumentCollectionIfNotExistsAsync(
      UriFactory.CreateDatabaseUri(DocumentManager.DatabaseID),
      collection);

    return true;
  }
  
  // ドキュメントを作成
  public async Task<Document> CreateExampleDocument(PhotoItem photoItem)
  {
    // ドキュメント作成
    var doc = await this.client.CreateDocumentAsync(
      UriFactory.CreateDocumentCollectionUri(DocumentManager.DatabaseID, DocumentManager.CollectionID),
      photoItem);
    
    return doc;
  }

  ...省略

}

以下のコードで DocumentManagerクラス を呼び出してデータベース・コレクション・10,000件のドキュメントを作成します。

// DocumentManagerクラスを呼び出してデータベースを初期化
Random random = new Random();

var manager = new DocumentManager();
var initResult = await manager.InitializeDocumentClient();

string[] categories = { "sky", "sea", "mountain", "people" };

for (int i = 0; i < 10000; i++)
{
  PhotoItem item = new PhotoItem()
  {
    Id = i.ToString(),
    Title = string.Format("SampleTitle{0}", i.ToString()),
    LikeCount = random.Next(1000),
    PhotoUri = string.Format("https://xxxxxxx/{0}", i.ToString()),
    Category = categories[i%4]
  };
  var ret = await manager.CreateExampleDocument(item);
  System.Threading.Thread.Sleep(50); // 429しないように緩やかに追加する
  Console.WriteLine(i);
}

3. デフォルト状態での書き込みと読み込み

デフォルトのインデックス状態で書き込みと読み込みを確認してみます。

書き込み

10件のドキュメントを書き込んでみます。

Random random = new Random();

var manager = new DocumentManager();
var initResult = await manager.InitializeDocumentClient();

{
  string[] categories = { "sky", "sea", "mountain", "people" };
  for (int id = 10000; id < 10010; id++)
  {

    PhotoItem item = new PhotoItem()
    {
      Id = id.ToString(),
      Title = string.Format("SampleTitle{0}", id.ToString()),
      LikeCount = random.Next(1000),
      PhotoUri = string.Format("https://xxxxxxx/{0}", id.ToString()),
      Category = categories[id % 4]
    };
    var ret = await manager.CreateExampleDocument(item);
  }
}

上記ソースにより送信されたHTTPリクエストは以下のようになります(ここでは1件のみ抽出。続けて実施した10件の書き込みのRUは同一であった為)。

POST https://rddocdb-japaneast.documents.azure.com/dbs/ExampleDB1/colls/ExampleCollection1/docs HTTP/1.1
x-ms-documentdb-partitionkey: ["sea"]
x-ms-date: Thu, 04 May 2017 16:46:29 GMT
authorization: type%3dmaster%26ver%3d1.0%26sig%3deHQp5YAx4oaQAkFvxYn0vh9sMbLFqPliTss6TqfrT1Y%3d
x-ms-session-token: 0:10043
Cache-Control: no-cache
x-ms-consistency-level: Session
User-Agent: documentdb-dotnet-sdk/1.13.2 Host/32-bit MicrosoftWindowsNT/6.2.9200.0
x-ms-version: 2017-01-19
Accept: application/json
Host: rddocdb-japaneast.documents.azure.com
Content-Length: 109
Expect: 100-continue

{"id":"10001","Title":"SampleTitle10001","LikeCount":469,"PhotoUri":"https://xxxxxxx/10001","Category":"sea"}

そして、HTTPレスポンスは以下の通りです。

HTTP/1.1 201 Created
Cache-Control: no-store, no-cache
Pragma: no-cache
Transfer-Encoding: chunked
Content-Type: application/json
Server: Microsoft-HTTPAPI/2.0
Strict-Transport-Security: max-age=31536000
x-ms-last-state-change-utc: Thu, 04 May 2017 08:40:05.215 GMT
etag: "00003133-0000-0000-0000-590b5ae50000"
x-ms-resource-quota: documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;
x-ms-resource-usage: documentSize=6;documentsSize=3912;documentsCount=10031;collectionSize=6560;
x-ms-schemaversion: 1.3
x-ms-alt-content-path: dbs/ExampleDB1/colls/ExampleCollection1
x-ms-content-path: FvRbAKerSQA=
x-ms-quorum-acked-lsn: 10043
x-ms-current-write-quorum: 3
x-ms-current-replica-set-size: 4
x-ms-xp-role: 1
x-ms-request-charge: 6.1
x-ms-serviceversion: version=1.13.76.2
x-ms-activity-id: 6f53bfbf-d4b1-4ad4-9bf1-b4ce7367636b
x-ms-session-token: 0:10044
x-ms-gatewayversion: version=1.13.76.2
Date: Thu, 04 May 2017 16:46:28 GMT

...省略...

消費RUは・・・「x-ms-request-charge: 6.1」。
1件の書き込みRUは 6.1 でした。

読み込み

ドキュメントを読み込みを行います。
DocumentManagerクラスに検索用メソッドQueryPhotoItems()を追加します。
QueryPhotoItems()はかなり固定的な実装で、LikeCountが100より大きく、Categoryが sky のドキュメントを50件取得します。

public class DocumentManager {
 ...省略
  public async Task<List<PhotoItem>> QueryPhotoItems()
  {
    List<PhotoItem> result = null;

    var feedOption = new FeedOptions() { MaxItemCount = 50 };

    var query = this.client.CreateDocumentQuery<PhotoItem>(
      UriFactory.CreateDocumentCollectionUri(DocumentManager.DatabaseID, DocumentManager.CollectionID), feedOption)
      .Where(i => i.LikeCount > 100 && 
             i.Category == "sky")
      .Take(50);
    result = query.ToList();

    return result;
  }
}

DocumentManager.QueryPhotoItems()を呼び出す実装は以下の通り。

var manager = new DocumentManager();
var initResult = await manager.InitializeDocumentClient();
// LikeCountが>100のドキュメントを50件検索
var result = await manager.QueryPhotoItems();

上記で実行されるHTTPリクエストは以下の通りです。

POST https://rddocdb-japaneast.documents.azure.com/dbs/ExampleDB1/colls/ExampleCollection1/docs HTTP/1.1
x-ms-continuation: 
x-ms-documentdb-isquery: True
x-ms-max-item-count: 50
x-ms-documentdb-query-enablecrosspartition: False
x-ms-documentdb-query-iscontinuationexpected: False
x-ms-date: Thu, 04 May 2017 16:59:15 GMT
authorization: type%3dmaster%26ver%3d1.0%26sig%3dKF%2fbc5aVHlTffYtoHF4k5qzALl727%2bNdHlERf3plj7U%3d
Cache-Control: no-cache
x-ms-consistency-level: Session
User-Agent: documentdb-dotnet-sdk/1.13.2 Host/32-bit MicrosoftWindowsNT/6.2.9200.0
x-ms-version: 2017-01-19
Accept: application/json
Content-Type: application/query+json
Host: rddocdb-japaneast.documents.azure.com
Content-Length: 109
Expect: 100-continue

{"query":"SELECT TOP 50 * FROM root WHERE ((root[\"LikeCount\"] > 100) AND (root[\"Category\"] = \"sky\")) "}

HTTPレスポンスは以下の通りです。

HTTP/1.1 200 Ok
Cache-Control: no-store, no-cache
Pragma: no-cache
Transfer-Encoding: chunked
Content-Type: application/json
Server: Microsoft-HTTPAPI/2.0
Strict-Transport-Security: max-age=31536000
x-ms-last-state-change-utc: Thu, 04 May 2017 08:40:13.755 GMT
x-ms-resource-quota: documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;
x-ms-resource-usage: documentSize=6;documentsSize=4298;documentsCount=10040;collectionSize=6964;
x-ms-item-count: 50
x-ms-schemaversion: 1.3
x-ms-alt-content-path: dbs/ExampleDB1/colls/ExampleCollection1
x-ms-content-path: FvRbAKerSQA=
x-ms-xp-role: 2
x-ms-request-charge: 20.32
x-ms-serviceversion: version=1.13.76.2
x-ms-activity-id: 2f1f663c-09e2-4913-b20d-8ef5ac3d63c2
x-ms-session-token: 0:10052
x-ms-gatewayversion: version=1.13.76.2
Date: Thu, 04 May 2017 16:59:15 GMT

...省略...

x-ms-request-charge: 20.32。
消費RUは 20.32 でした。

4. LikeCount/Categoryのみのインデックス設定で書き込みと読み込み

インデックスを以下のように変更します。
LikeCount / Categoryのみにインデックスを適用するようにカスタマイズします。

{
  "indexingMode": "consistent",
  "automatic": true,
  "includedPaths": [
    {
      "path": "/LikeCount/*",
      "indexes": [
        {
          "kind": "Range",
          "dataType": "Number",
          "precision": -1
        }
      ]
    },
    {
      "path": "/Category/*",
      "indexes": [
        {
          "kind": "Hash",
          "dataType": "String",
          "precision": 3
        }
      ]
    }
  ],
  "excludedPaths": [
    {
      "path": "/*"
    }
  ]
}

設定はAzureポータルで「設定 → インデックス作成ポリシー」から変更することができます。

f:id:daigo-knowlbo:20170505020619p:plain

書き込み

先程と同様に10件のドキュメントを書き込んでみます。
HTTPリクエストは以下の通りです。

POST https://rddocdb-japaneast.documents.azure.com/dbs/ExampleDB1/colls/ExampleCollection1/docs HTTP/1.1
x-ms-documentdb-partitionkey: ["mountain"]
x-ms-date: Thu, 04 May 2017 17:03:09 GMT
authorization: type%3dmaster%26ver%3d1.0%26sig%3deHdnuMnm8pSUdfK4T0ItZPjQzGvmyzqBOEExfbDCuUw%3d
Cache-Control: no-cache
x-ms-consistency-level: Session
User-Agent: documentdb-dotnet-sdk/1.13.2 Host/32-bit MicrosoftWindowsNT/6.2.9200.0
x-ms-version: 2017-01-19
Accept: application/json
Host: rddocdb-japaneast.documents.azure.com
Content-Length: 114
Expect: 100-continue

{"id":"10010","Title":"SampleTitle10010","LikeCount":782,"PhotoUri":"https://xxxxxxx/10010","Category":"mountain"}

HTTPレスポンスは以下の通りです。

HTTP/1.1 201 Created
Cache-Control: no-store, no-cache
Pragma: no-cache
Transfer-Encoding: chunked
Content-Type: application/json
Server: Microsoft-HTTPAPI/2.0
Strict-Transport-Security: max-age=31536000
x-ms-last-state-change-utc: Thu, 04 May 2017 08:40:05.215 GMT
etag: "00004e33-0000-0000-0000-590b5ecd0000"
x-ms-resource-quota: documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;
x-ms-resource-usage: documentSize=6;documentsSize=4317;documentsCount=10040;collectionSize=6965;
x-ms-schemaversion: 1.3
x-ms-alt-content-path: dbs/ExampleDB1/colls/ExampleCollection1
x-ms-content-path: FvRbAKerSQA=
x-ms-quorum-acked-lsn: 10053
x-ms-current-write-quorum: 3
x-ms-current-replica-set-size: 4
x-ms-xp-role: 1
x-ms-request-charge: 5.52
x-ms-serviceversion: version=1.13.76.2
x-ms-activity-id: fedec436-d43f-479c-b8c7-8ce095d9dd07
x-ms-session-token: 0:10054
x-ms-gatewayversion: version=1.13.76.2
Date: Thu, 04 May 2017 17:03:08 GMT

...省略...

消費RUは・・・「x-ms-request-charge: 5.52」。
1件の書き込みRUは 5.52 でした。
デフォルトインデックス状態の 6.1 よりも小さな値になりました。

読み込み

ドキュメントを読み込みを行います。
先程と同様の読み込みを行います。

HTTPリクエストは以下の通りです。

POST https://rddocdb-japaneast.documents.azure.com/dbs/ExampleDB1/colls/ExampleCollection1/docs HTTP/1.1
x-ms-continuation: 
x-ms-documentdb-isquery: True
x-ms-max-item-count: 50
x-ms-documentdb-query-enablecrosspartition: False
x-ms-documentdb-query-iscontinuationexpected: False
x-ms-date: Thu, 04 May 2017 17:10:13 GMT
authorization: type%3dmaster%26ver%3d1.0%26sig%3dvO5eIEtwI7FZ3%2bD8d8ezEIvH3Hp9EuY1EJ8ZR0Rm2WE%3d
Cache-Control: no-cache
x-ms-consistency-level: Session
User-Agent: documentdb-dotnet-sdk/1.13.2 Host/32-bit MicrosoftWindowsNT/6.2.9200.0
x-ms-version: 2017-01-19
Accept: application/json
Content-Type: application/query+json
Host: rddocdb-japaneast.documents.azure.com
Content-Length: 109
Expect: 100-continue

{"query":"SELECT TOP 50 * FROM root WHERE ((root[\"LikeCount\"] > 100) AND (root[\"Category\"] = \"sky\")) "}

HTTPレスポンスは以下の通りです。

HTTP/1.1 200 Ok
Cache-Control: no-store, no-cache
Pragma: no-cache
Transfer-Encoding: chunked
Content-Type: application/json
Server: Microsoft-HTTPAPI/2.0
Strict-Transport-Security: max-age=31536000
x-ms-last-state-change-utc: Thu, 04 May 2017 08:40:13.755 GMT
x-ms-resource-quota: documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;
x-ms-resource-usage: documentSize=6;documentsSize=3929;documentsCount=10050;collectionSize=6597;
x-ms-item-count: 50
x-ms-schemaversion: 1.3
x-ms-alt-content-path: dbs/ExampleDB1/colls/ExampleCollection1
x-ms-content-path: FvRbAKerSQA=
x-ms-xp-role: 2
x-ms-request-charge: 20.32
x-ms-serviceversion: version=1.13.76.2
x-ms-activity-id: 2be70cc0-d86c-40ce-8f72-a2542c954ad9
x-ms-session-token: 0:10063
x-ms-gatewayversion: version=1.13.76.2
Date: Thu, 04 May 2017 17:10:13 GMT

...省略...

消費RUは 20.32 でした。
検索条件(LikeCount・Category)が、デフォルトインデックス時も、その後のカスタムインデックス時も共に対象となっている為に、読み込み時の消費RUが変化しませんでした。

4. 読み込みインデックスの効きを確認する

前述の確認では デフォルトインデックス時・カスタムインデックス時 共にインデックス対象の項目で検索してしまっていました。
検索条件を以下のようにした場合はどうでしょうか?

var query = this.client.CreateDocumentQuery<PhotoItem>(
  UriFactory.CreateDocumentCollectionUri(DocumentManager.DatabaseID, DocumentManager.CollectionID), feedOption)
  .Where(i => i.LikeCount > minLikeCount && 
              i.Category == "sky" && 
              i.Title == "SampleTitle0")
  .Take(50);

Title項目も検索対象としました(このクエリー条件は現実のビジネス的にはちょっと変な条件指定ですが・・・技術的検証としてとらえてください)。
Titleは「デフォルトインデックス時にはインデックス対象」「カスタムインデックス時にはインデックス対象外」となる項目です。

実施結果は以下のようになります。

デフォルトインデックス時(インデックス対象) : x-ms-request-charge: 5.96
カスタムインデックス時(インデックス対象外) : x-ms-request-charge: 204.98

RUに大きな違いが発生しました。
インデックス外の項目での検索の負荷の高さを確認することができます。

まとめ

DocumentDBではデフォルト状態で「すべての項目に対して Hash と Range のインデックスが設定されている」。
GEOクエリーを行わなければ、何もしないでもある程度いい感じに動いてくれる。
インデックスの最適化で消費RUの最適化が図れる。ただし、適切に実施しないと、消費RUに大きな差が生まれるので注意しなければならない。
といった感じでしょうか。

DocumentDBの(マスター)キーとauthorizationトークンについて

最近 DocumentDB について調べたり、使ったりしております。
DocumentDB は (たしか)2015年にGAしたと思うのですが、GAから日が経っているのに、想像したよりもネット上のブログなどでの資料が少ない気がしています(特に日本語)。
一方 本家のドキュメントは日本語版も含めかなりしっかりしている印象です。

まあ、大抵のエンプラ系システムであれば、NoSQLなんて使わず従来通りのSQL Server(もしくはSQL Database)でOKですからね。
普通の開発現場では、ちょー早くて、無制限にスケール可能で、でもデータ構造設計やパーティション分割やトランザクションや検索にコツが必要な DocumentDB(NoSQL) を欲するケースは少ないのでしょう・・・

でも、私個人的には、DocumentDBを含むクラウドネイティブな技術が好きなんですよね^^

と長くなりましたが本題へ・・・

(マスター)キーとauthorizationトークンの関係

このお話、DocumentDBをREST APIから使用している(使用したことがある)方は分かっていると思いますが、ライブラリ経由でしか使ったことがない方の場合は認識できていないかもしれない、という内容のお話です。
だからDocumentDBを利用した開発においては必須というわけではありません。

DocumentDBを利用する場合、以下の2パターンの方法があります。

  • REST APIを直に呼び出す
  • 各言語用ライブラリを使用する

大抵、後者の各言語用ライブラリを使う事と思います。
Visual Studio & .NET だったらNugetで「Microsoft.Azure.DocumentDB(.NET CoreならMicrosoft.Azure.DocumentDB.Core)」を追加するだけですね。
ライブラリはバックエンドでREST APIを呼び出してくれます。接続プロトコル(ConnectionPolicy.ConnectionProtocol)を HTTPS にしていれば、Fiddlerなんかでその通信内容を確認することができます。

で、DocumentDBに接続する場合には、DocumentClientクラスを以下のようにインスタンス化します。

var client = new DocumentClient(uri, authKey);

認証・認可のために第2引数に認証キー(authKey)を渡します(第1引数uriは、利用するDocumentDBアカウントのURIです)。
インスタンス化した DocumentClient に対して、CreateDocumentQuery()だったりCreateStoredProcedureQuery()だったりのメソッド呼び出しを行うことで対象のDocumentDBに対する操作を行うことができます。
「認証キー(authKey)」というのは、Azureポータルで確認可能なプライマリキー(セカンダリキー)です。
以下の画面キャプチャ。

f:id:daigo-knowlbo:20170504125630p:plain

ではDocumentClientがDocumentDBに対して操作を行う際にはどのような通信を行っているのか確認してみます。
まず、以下のようなDocumentDBの操作を行うクラスを用意します(DocumentManagerクラス)。

public class DocumentManager {
  private const string EndpointUrl = "https://rddocdb.documents.azure.com:443/";

  private const string PrimaryKey = "[(マスター)プライマリキー]";
  
  private const string DatabaseID = "ExampleDB1";
  private const string CollectionID = "ExampleCollection1";
  
  private DocumentClient client;
  
  public async Task<bool> Init() {
    // DocumentClientオブジェクト 作成
    this.client = new DocumentClient(new Uri(DocumentManager.EndpointUrl), DocumentManager.PrimaryKey,
      new ConnectionPolicy
      {
        ConnectionMode = ConnectionMode.Gateway,
        ConnectionProtocol = Protocol.Https
      });
    
    // ExampleDB1 作成
    var db = await this.client.CreateDatabaseIfNotExistsAsync(
      new Database { Id = DocumentManager.DatabaseID });
    
    // ExampleCollection1 作成
    var collection = await this.client.CreateDocumentCollectionIfNotExistsAsync(
      UriFactory.CreateDatabaseUri(DocumentManager.DatabaseID),
      collection);
    
    return true;
  }
  
  public async Task<bool> ReadDocumentCollection() {
    // コレクション情報を読み込む
    var collection = await this.client.ReadDocumentCollectionAsync(
      UriFactory.CreateCollectionUri(DocumentManager.DatabaseID, DocumentManager.CollectionID));

    return true;
  }
}

Init()メソッドは、以下の処理を行います。

  • DocumentClientオブジェクトを作成しprivateメンバーに保持
  • ExampleDB1データベースを作成
  • ExampleCollection1コレクションを作成

ReadDocumentCollection()メソッドは、Init()で作成されたExampleCollection1コレクション情報を読み取ります。

そして、以下のようにdocumentManagerを呼び出しましょう。

var documentManager = new DocumentManager();
var initResult = await documentManager.Init();
var readResult = await documentManager.ReadDocumentCollection();

最後の ReadDocumentCollection() 呼び出しに注目します。
Fiddlerで抜き取ったHTTPS通信は以下の通りです。

f:id:daigo-knowlbo:20170504133809p:plain

「/dbs/ExampleDB1/colls/ExampleCollection1」にGETリクエストを行っています。
つまり↓↓↓のREST APIが呼び出されています。
Get a Collection | Microsoft Docs
HTTP GETのリクエストヘッダで注目すべきは authorization です。
DocumentClientオブジェクト作成時に渡した認証キー(プライマリキー)を元にして authorization 値が生成されています。
厳密には認証キー(プライマリキー)を含む以下の要素から生成されています。

  • HTTP Verb(GETとかPOSTとか)
  • リソースタイプ(CollectionとかDocumentとかのアクセスの対象リソースタイプ)
  • リソースリンク(こんなの・・/dbs/ExampleDB1/colls/ExampleCollection1)
  • 日時(トークン生成日)
  • キー
  • キーのタイプ(masterとかresource)
  • トークンバージョン

日時は注意が必要で、トークン生成時に指定した日時と HTTPヘッダ x-ms-date 値がマッチしている必要があります。

上記からauthorizationトークンを生成するロジックは Access Control on Azure Cosmos DB Resources | Microsoft Docs に書かれておりまして、コードの引用が以下になります。

// authorizationトークンを作成
static string GenerateAuthToken(
  string verb, 
  string resourceType, 
  string resourceLink, 
  string date, 
  string key, 
  string keyType, 
  string tokenVersion)
{
  var hmacSha256 = new System.Security.Cryptography.HMACSHA256 { Key = Convert.FromBase64String(key) };

  verb = verb ?? "";
  resourceType = resourceType ?? "";
  resourceLink = resourceLink ?? "";

  string payLoad = string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}\n{1}\n{2}\n{3}\n{4}\n",
      verb.ToLowerInvariant(),
      resourceType.ToLowerInvariant(),
      resourceLink,
      date.ToLowerInvariant(),
      ""
  );

  byte[] hashPayLoad = hmacSha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(payLoad));
  string signature = Convert.ToBase64String(hashPayLoad);

  return System.Web.HttpUtility.UrlEncode(String.Format(System.Globalization.CultureInfo.InvariantCulture, "type={0}&ver={1}&sig={2}",
    keyType,
    tokenVersion,
    signature));
}

以下のようなコードで認証トークンを生成することができます。

var authorizationToken = 
  GenerateAuthToken(
    "get", 
    "colls", 
    "dbs/ExampleDB1/colls/ExampleCollection1", 
    "Tue, 02 May 2017 10:20:01 GMT", 
    "[プライマリキー]", 
    "master", 
    "1.0");

認証トークン生成して使ってみる

では認証トークンを生成して実際にREST呼び出ししてみましょう。
Fiddlerを使ってHTTPリクエストを投げたいと思います。

1.正常なケース

以下のリクエストを行います。
トークン生成に使用した 日時 と x-ms-date は同一でないといけない点には注意。
f:id:daigo-knowlbo:20170504141751p:plain

いい感じにレスポンスが返ってきました。
f:id:daigo-knowlbo:20170504141832p:plain

2.不正なauthorizationトークンを利用したケース

リクエスト内容とマッチしないauthorizationトークンを使ってリクエストを行います。
f:id:daigo-knowlbo:20170504141857p:plain

401 Unauthorized が返ってきます。
f:id:daigo-knowlbo:20170504141923p:plain

3.期限切れのauthorizationトークンを利用したケース

authorizationトークンは有効期限があります。
次は、有効期限切れになったauthorizationトークンでリクエストを行ってみます。
f:id:daigo-knowlbo:20170504141947p:plain

403 Forbidden が返ってきます。 f:id:daigo-knowlbo:20170504142012p:plain

bodyを見ると親切に以下のようなメッセージが書かれています。

{
  "code":"Forbidden",
  "message":"The authorization token is not valid at the current time. 
             Please create another token and retry 
             (token start time: Thu, 04 May 2017 00:57:01 GMT, 
              token expiry time: Thu, 04 May 2017 01:12:01 GMT, 
              current server time: Thu, 04 May 2017 05:04:48 GMT).\r\n
              ActivityId: f7c026e7-ddea-4c89-9331-4c9b24d50d9e"
}

つまり・・・↓↓↓と言っていますね。DocumentDB、超親切!!

そのトークンは現時刻では無効だよ。
別のトークン作ってリトライしてね。
そのトークンの開始時刻は「Thu, 04 May 2017 00:57:01 GMT」。無効になるのは「Thu, 04 May 2017 01:12:01 GMT」、今のサーバー時刻は「Thu, 04 May 2017 05:04:48 GMT」。

うん、つまりauthorizationトークンの有効期限は15分ってことですね。

まとめ

内容的に まとめ ってこともないのですが、いつも思うことはライブラリにラップされた内容だけでなくその裏側の本当の姿まで理解しておく事は重要だと思います。
そうすると、何か問題が起きた時に色々解決の糸口への道のりが短くなるのだと思っています。

Computer Vision APIでランドマーク認識する

本日は天気が良く、最近にしては珍しく予定が何もなかったのでプチドライブにいってきました。

我が家のある夢の国を出発し、永代橋を渡り、銀座を通り抜け、東京タワーへ、そしてレインボーブリッジ(下の一般道)を渡って夢の国に舞い戻る・・・という。

で、日本が誇るランドマーク「東京タワー」をカメラで撮ってきました。

ランドマークといえば先日2017/4/19に Cognitive Services のいくつかに機能が GA していました・・・(Face API / Computer Vision API / Content Moderator)

ということで、Computer Vision APIのランドマーク認識は、私の撮った写真をきちんと認識してくれるのかどうか試してみましょう!

Computer Vision APIよ、これが何か分かるか!?

f:id:daigo-knowlbo:20170423163959p:plain
※今日は、なんか、真ん中の展望台辺りから霧のようなものが噴霧されていました。

1. Azureに「Cognitive Services APIs」を作成

Cognitive Servicesを利用するためにはAzureに「Cognitive Services APIs」を作成する必要があります。
Azureポータルにログインします。
「+新規」をクリック。

f:id:daigo-knowlbo:20170423161222p:plain

Cognitive Services APIsを選択。

f:id:daigo-knowlbo:20170423161258p:plain

f:id:daigo-knowlbo:20170423161308p:plain

作成。

f:id:daigo-knowlbo:20170423161328p:plain

各種パラメータを適時設定します。

f:id:daigo-knowlbo:20170423161419p:plain
ここでは以下の設定にしました。

  • アカウント名: MyCognitive1
  • APIの種類: 今回はランドマーク認識を行うので「Computer Vision API」となります。
  • 場所: 日本のリージョンはまだないので「東南アジア」にしました。
  • 価格レベル: テストなので無料の F0 にしました(F0は、1分あたり20回・1ケ月あたり5000回の呼び出し制限)。

しばらく待つと作成が完了します。
以下はリソースの一覧画面で確認したところです。

f:id:daigo-knowlbo:20170423161851p:plain

MyCognitive1を選択し「概要」画面で「エンドポイント」をコピーしておきます。

f:id:daigo-knowlbo:20170423161943p:plain

「キー」を選択し「キー1」をコピーしておきます。

f:id:daigo-knowlbo:20170423162047p:plain

エンドポイントとキーはプログラムからAPI呼び出しを行う際に利用します。

2. コンソールアプリを作成

Visual Studio 2017(2015でもいいけど)を起動し、コンソールアプリケーションを作成します。
(前述で作成したCognitive Services(Computer Vision API)を呼び出すアプリです)

で、実装は単純に以下のサンプルをそのまま拝借させていただきました。

azure.microsoft.com

// https://azure.microsoft.com/ja-jp/blog/microsoft-cognitive-services-general-availability-for-face-api-computer-vision-api-and-content-moderator/から引用
// 名前空間、キー設定部分、エンドポイント指定部分を修正しています。

using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;

namespace RecognizinglandmarksExample
{
  static class Program
  {
    static void Main()
    {
      Console.Write("Enter image file path: ");
      string imageFilePath = Console.ReadLine();

      MakeAnalysisRequest(imageFilePath);

      Console.WriteLine("\n\nHit ENTER to exit...\n");
      Console.ReadLine();
    }

    static byte[] GetImageAsByteArray(string imageFilePath)
    {
      FileStream fileStream = new FileStream(imageFilePath, FileMode.Open, FileAccess.Read);
      BinaryReader binaryReader = new BinaryReader(fileStream);
      return binaryReader.ReadBytes((int)fileStream.Length);
    }

    static async void MakeAnalysisRequest(string imageFilePath)
    {
      var client = new HttpClient();

      // Request headers. Replace the second parameter with a valid subscription key.
      //client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", "putyourkeyhere");
      client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", "あなたのキーを設定");

      // Request parameters. You can change "landmarks" to "celebrities" on requestParameters and uri to use the Celebrities model.
      string requestParameters = "model=landmarks";
      //string uri = "https://westus.api.cognitive.microsoft.com/vision/v1.0/models/landmarks/analyze?" + requestParameters;
      string uri = "https://southeastasia.api.cognitive.microsoft.com/vision/v1.0/models/landmarks/analyze?" + requestParameters;
      
      Console.WriteLine(uri);

      HttpResponseMessage response;

      // Request body. Try this sample with a locally stored JPEG image.
      byte[] byteData = GetImageAsByteArray(imageFilePath);

      using (var content = new ByteArrayContent(byteData))
      {
        // This example uses content type "application/octet-stream".
        // The other content types you can use are "application/json" and "multipart/form-data".
        content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
        response = await client.PostAsync(uri, content);
        string contentString = await response.Content.ReadAsStringAsync();
        Console.WriteLine("Response:\n");
        Console.WriteLine(contentString);
      }
    }
  }
}

上記ソースから以下の行を修正します。

client.DefaultRequestHeaders.Add(“Ocp-Apim-Subscription-Key”, “あなたのキーを設定”);

「あなたのキーを設定」部分には、Azureポータルからの「Cognitive Services APIs」作成で得られた「キー1」に置き換えます。

もう1ヶ所、以下のエンドポイント部分も修正対象ですが、こちらは上記ソースでは東南アジア用に修正してあります。

string uri = “https://westus.api.cognitive.microsoft.com/vision/v1.0/models/landmarks/analyze?” + requestParameters;

ビルドしてコンソールアプリを作成します。
(このコンソールアプリでは、Vision APIの呼び出しに単純なHTTP POSTを行っているだけなので、特別なNugetパッケージの追加は必要ありません)

3. 実行

CTRL + F5で実行します。
画像ファイル名を聞かれるので、入力してEnter。

f:id:daigo-knowlbo:20170423163309p:plain

認識の結果がJSONで帰ってきます。

f:id:daigo-knowlbo:20170423163317p:plain

「Tokyo Tower」正解!そして confidence は 0.9743892 とのこと。

まとめ

本投稿は Cognitive Services の本当に触りの部分の薄っぺらな内容でしたが、顔認識・文字認識等々色々な機能が提供されています。BotとかMachine Learningとかと組み合わせるといろいろなことが出来そうですよね。

Global Azure Bootcamp 2017@Tokyo に行ってきた

本日(2017/4/22)、Global Azure Bootcamp 2017@Tokyoに参加してきました。

Global Azure Bootcamp 2017@Tokyo - connpass

ということで、ざっくり(長々?)個人的感想を垂れ流しておきます。

Azureって何よ2017年の最新情報をゆるまとめ

JAZUG女子部の安東 沙織さん・鈴木 可奈さんのセッション。

www.slideshare.net

Azureの概要、そして、この1年のAzureアップデートのまとめを、お二人の軽快な掛け合いトークで説明していただきました。
各技術に深入りするわけではなく、でも実際に日々Azureを使っている中でのポイント的なことであったり、不満点だったりについてお話していただきました。
あと、従来、Azure全体の説明を行う場合、「IaaSの説明をから入り、PaaSの説明に移る」ことが多いですが、「(お二人曰く)そろそろあえてPaaSを先に持ってきた」という点が「うん!いいね!」って思いました。
本スライドで、1年間をササーっと振り返り、箇条書き以上の内容は「ブチザッキ | いわゆる雑記」で、さらにここの技術を深掘る場合は公式Docや公式Blogへ、というところでしょうか。

Azure 2017年3月障害Deep Dive

harutamaさんのセッション。

docs.com

このセッション、事前に想像していたのと違っていて面白かったです。
2017年3月の障害に対するMSの報告文書「Root Cause Analysis」に記された「ストレージサービスのStream Manager」なるものの正体(実体)についてのDeepDiveでした。

ストレージサービスについては以下のようなことは、多くのAzure利用者が把握していること。

  • blobとかtableとかqueueとかfileとかの種類があるよね
  • LRSとかZRSとかGRSとかRA-GRSとかのレプリケーションオプションで耐障害性レベルが上がるよね
  • Premium Storage使うとVirtualMachine1台構成でも99.9%のSLAが付くよね

でも本セッションは、その先のストレージの内部構造についてのお話でした。 この内部構造は、Build 2016におけるセッションスライドとかMicrosoft Researchの論文とかをよく読むと紐解ける情報とのこと(確かそうだったよね・・・記憶あいまい気味)。
この手の話は実は大好きなので楽しかったです。
で、Azureは新機能連発なので最新ドキュメントにばかり目が行ってしまうのですが、過去の論文もしっかり読んでおくべきだと思いました。

bitFlyerを支える仕組み - Azure障害の中1時間未満でサービス復旧を実現-

竹井 悠人さん・萬藤 和久さんのセッション。

www.slideshare.net

ブロックチェーン・ビットコインのシステムを開発・運用しているbitFlyerさんのAzure障害との闘い(?)のご紹介でした。
時系列でAzure障害によるシステムの停止から別リージョンでの復旧の試み、3度の大規模障害から得られたノウハウ(障害対応中に問い合わせたマイクロソフトのサポートからも得られなかったノウハウ達)について説明していただきました。
ここら辺の話を聞きつつ、典型的エンプラの「ノウハウが蓄積されていない事には踏み入れない」精神だとAzureはないんだろうなぁ、とか思いました。私個人的には立ち向かい、それに見合うだけ得られるものを得ていくエンジニア人生を歩みたいです。

サポートエンジニアが紹介する、Azure Portal/CLI 使いこなし

田中孝桂さんのセッション。

docs.com

最近、個人的にも Azure CLI v2 をちょろちょろと使っていたので、でも、その時必要なコマンドのみで、Azure AutomationでVMの自動停止等々 細かなコマンドを知らなかったので、いろいろ勉強になりました。

Azure ML系 「practice over theory」

梅崎 猛さんのセッション。

docs.com

AzureML / Cognitive Service / Bot Framework with QnA / CNTK についてザザーと「こんなすごいのが、こんなに簡単に使えるよー」というビデオデモをたくさん流していただきました。
個人的には、Cognitive Serviceのいくつかは、数日前にGAしていたので、ちょうどお試し利用していたところでした。
AzureMLについては実は使ったことがなかったので、へー、こんな感じで使えるんだあ、と思いました。
Bot Framework with QnAなんかは、私が勤めている会社は自社サービスを主軸として製品サポート部門なんかもあるので、そこらへんにBotや問い合わせ文自動解析のCognitive Serviceがうまく使えればサポートコスト削減にならないだろうか、とぼんやり考えました(実際に実現するのはすんごい大変で、おそらく現段階では会社としては取り組まないだろうし、現実的ではないと思っています)。

DocumentDB DeepDive

近江 武一さんのセッション。

www.slideshare.net

私は最近、DocumentDBに はまって いて、公式ドキュメントを結構読み漁ったので、うんうんって思いながら聞くことができました。
また、以下のことは知らなかったので興味深かったです。

  • 「1KBデータだったら1RU消費」と思っていたのが、その物自体だけでないコストが発生して 2RU くらい消費する 。
  • RUはパーティションごとに等分されて割り当てられる。データが特定パーティションに偏ると、設定RUが無駄に死んだ状態になる。
  • パーティションが内部的には25個に分割されているみたい?(検証中?)

セッション内で、MSの松崎剛さんのブログが紹介されていましたが、このブログ、私は見逃していたのですが、内容的に非常に興味深い有益なものでした。

blogs.msdn.microsoft.com

松崎さんは実は弊社の ISVパートナー担当エバンジェリスト(?)(正式な立ち位置名称が分からないのですが・・・) をしていただいておりまして、いろいろ相談可能・・・なのですが・・・ここら辺のAzure周りのテクノロジーに対する取り組みは、私個人の活動であり、会社としてはあまりAzureには力を入れていないので、つまりアポできませんねぇ・・・

オルターブースさんのAzure事例

松村 優大さん、森田 邦裕さんのセッション。

docs.com

オリジナルソース(ドレッシング)をオーダーできるWebサイトを自社開発・運営されている株式会社オルターブースさんの事例紹介でした。
私は、このセッションを聞いてオルターブースさんのファンになりました。

技術に対して 挑戦的で熱い会社 であると感じたからです。

RCの段階で.NET Core採用しちゃったり、使ってみたい技術で行こうよ!って精神だったり(結果、今時の技術全部乗せ的な)、AzureとAWSの併用活用だったり、Slackへの障害通知、Fulentd→elastic search→kibanaな可視化ロギングだったり、Azure Container / Docker swarmで運用してたり、社員旅行と称してみんなで DE:CODE 2016 行くとか(半分冗談?半分ほんと?)。
私もソース、オーダーさせていただきます!

最後に

あー、あと、お声がけしたい方が何人かいたのですが、タイミングを逃し、そしてコミュ障パワーを発揮しお声がけできませんでした・・・
技術的に聞いてみたいこともあるので、また機会があったらお声がけさせていただきます!
この系統のイベントには今後も引き続き参加させていただきたいと思いますので、皆様よろしくお願いいたします。