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 行くとか(半分冗談?半分ほんと?)。
私もソース、オーダーさせていただきます!

最後に

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

DocumentDBのgeospatial機能で地理的検索(位置情報検索)を行う

結構以前からあったものだと思うのですが、Azure DocumentDBの「geospatial(地理空間検索)」機能を使ってみたいと思います。
緯度・経度情報をDocumentDBに保存しておき、以下のような操作を行うことが可能です。

  1. 2点間の距離の算出
  2. 1つの地理的領域が、別のもう1つの領域内に存在するかのチェック
  3. 2つの地理的領域の重なりのチェック

ここでは「地理的領域」という言葉を使用しましたが、DocumentDBでは緯度・経度情報を「点」「ライン」「ポリゴン(領域)」で保持することができます。
これらの緯度・経度情報に対してクエリーをかけることができます。

こんなアプリを作る

本投稿では、上記1番目の「2点間の距離の算出」を利用して以下のようなWebアプリを作成します。

  • 災害時避難施設情報をDocumentDBに保存しておく
  • Webアプリを作成。JavaScriptで現在位置情報を自動取得 → 近隣の避難施設を検索する
  • 避難施設の場所はGooogle Mapsへのリンクを配置することで表示する

画面キャプチャは以下の通りです。

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

「避難施設を検索」をクリックすると・・・

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

「地図を表示」をクリックすると・・・Google Mapsが別ウィンドウ表示される。

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

ソースは以下の github に上げてあります。

github.com

実装の構成は「1ソリューション・3プロジェクト構成」です。
プロジェクト構成は以下の通り。

・CreateEvacuationFacilityDb - DocumentDBデータを作成するコンソールアプリケーションプロジェクト
・EvacuationFacilityLib - DocumentDB操作を行う為のリポジトリクラスとモデルクラスを含むライブラリプロジェクト
・EvacuationFacilityWeb - 災害時避難施設を検索するWebアプリケーションプロジェクト

では以下に、ソースの解説を含めて作成手順を記述いたします。

1. DocumentDBデータを準備する

まず、データソースとなるDocumentDBデータを作成します。

DocumentDB Emulatorを起動

Azure上にDocumentDBを作成してもよいのですが、今回はローカルでお手軽にテスト可能な DocumentDB Emulator を利用します。
DocumentDB Emulatorを起動しておきましょう。タスクトレイにブルーのアイコンが現れますね。

災害時避難施設情報XMLを取得する

災害時避難施設情報は 国土数値情報ダウンロードサービス国土数値情報 避難施設データの詳細 からダウンロードすることができるのでこれを利用します。
平成24年のデータで古い気もしますが、全国の災害時避難施設の名称・住所・緯度経度情報 等々 が得られます。
今回は、データが大きいので、東京 のデータだけを取得しました。
ダウンロードファイルを解凍すると以下のようなイメージです。

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

この中から「P20-12_13.xml」を利用します。

避難施設情報XMLからDocumentDB保存用データ型に変換

XMLデータをDocumentoDBデータ保存用の型に変換する処理を用意します。
ダウンロードした P20-12_13.xml は以下のようなスキーマです。

// P20-12_13.xml
<?xml version="1.0" encoding="utf-8"?>
<ksj:Dataset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ksj="http://nlftp.mlit.go.jp/ksj/schemas/ksj-app" gml:id="P20Dataset" xsi:schemaLocation="http://nlftp.mlit.go.jp/ksj/schemas/ksj-app KsjAppSchema-P20-v1_0.xsd">
  <gml:description>国土数値情報 避難施設 インスタンス文書</gml:description>
  <gml:boundedBy>
    <gml:EnvelopeWithTimePeriod srsName="JGD2000 / (B, L)" frame="GC / JST">
      <gml:lowerCorner>20.0 123.0</gml:lowerCorner>
      <gml:upperCorner>46.0 154.0</gml:upperCorner>
      <gml:beginPosition calendarEraName="西暦">1900</gml:beginPosition>
      <gml:endPosition indeterminatePosition="unknown"/>
    </gml:EnvelopeWithTimePeriod>
  </gml:boundedBy>
  <!--図形-->
  <gml:Point gml:id="pt100001">
    <gml:pos>35.686898 139.739538</gml:pos>
  </gml:Point>
  <gml:Point gml:id="pt100002">
    <gml:pos>35.697347 139.760433</gml:pos>
  </gml:Point>
  ... 省略 ...
  <ksj:EvacuationFacilities gml:id="P20_100001">
    <ksj:position xlink:href="#pt100001"/>
    <ksj:administrativeAreaCode codeSpace="AdministrativeAreaCode.xml">13101</ksj:administrativeAreaCode>
    <ksj:name>いきいきプラザ一番町</ksj:name>
    <ksj:address>東京都千代田区一番町12-2</ksj:address>
    <ksj:facilityType>指定避難所、二次避難所</ksj:facilityType>
    <ksj:seatingCapacity>-1</ksj:seatingCapacity>
    <ksj:facilityScale>-1</ksj:facilityScale>
    <ksj:hazardClassification>
      <ksj:Classification>
        <ksj:earthquakeHazard>false</ksj:earthquakeHazard>
        <ksj:tsunamiHazard>false</ksj:tsunamiHazard>
        <ksj:windAndFloodDamage>false</ksj:windAndFloodDamage>
        <ksj:volcanicHazard>false</ksj:volcanicHazard>
        <ksj:other>false</ksj:other>
        <ksj:notSpecified>true</ksj:notSpecified>
      </ksj:Classification>
    </ksj:hazardClassification>
  </ksj:EvacuationFacilities>
  <ksj:EvacuationFacilities gml:id="P20_100002">
    <ksj:position xlink:href="#pt100002"/>
    <ksj:administrativeAreaCode codeSpace="AdministrativeAreaCode.xml">13101</ksj:administrativeAreaCode>
    <ksj:name>お茶の水小学校</ksj:name>
    <ksj:address>東京都千代田区猿楽町1-1-1</ksj:address>
    <ksj:facilityType>指定避難所、地区救援センター</ksj:facilityType>
    <ksj:seatingCapacity>-1</ksj:seatingCapacity>
    <ksj:facilityScale>-1</ksj:facilityScale>
    <ksj:hazardClassification>
      <ksj:Classification>
        <ksj:earthquakeHazard>false</ksj:earthquakeHazard>
        <ksj:tsunamiHazard>false</ksj:tsunamiHazard>
        <ksj:windAndFloodDamage>false</ksj:windAndFloodDamage>
        <ksj:volcanicHazard>false</ksj:volcanicHazard>
        <ksj:other>false</ksj:other>
        <ksj:notSpecified>true</ksj:notSpecified>
      </ksj:Classification>
    </ksj:hazardClassification>
  </ksj:EvacuationFacilities>
  ... 省略 ...
</ksj:Dataset>

上記XMLデータを元に、以下のようなJSONスキーマデータに変換してDocumentDBに保存したいと思います。

{
  "ID": "pt100001",
  "Latitude": 35.686898,
  "Longitude": 139.739538,
  "Location": {
    "type": "Point",
    "coordinates": [
      139.739538,
      35.686898
    ]
  },
  "Name": "いきいきプラザ一番町",
  "Address": "東京都千代田区一番町12-2",
  "FacilityType": "指定避難所、二次避難所"
}

ということで、上記JSONスキーマに該当するモデルクラスとして、以下の EvacuationFacilityInfo クラスを用意します。
「public Point Location」プロパティがポイントで、Point型は「Microsoft.Azure.Documents.Spatial名前空間の型」になります。
この値が地理的データクエリーの対象となります。

// EvacuationFacilityLib\Models\EvacuationFacilityInfo.cs
using Microsoft.Azure.Documents.Spatial;

namespace EvacuationFacilityLib.Models
{
  public class EvacuationFacilityInfo
  {
    // ID
    public string ID { get; set; }
    // 緯度
    public double Latitude { get; set; }
    // 経度
    public double Longitude { get; set; }
    // DocumentDBクエリー用のPoint型
    public Point Location { get; set; }
    // 施設名
    public string Name { get; set; }
    // 住所
    public string Address { get; set; }
    // 施設タイプ
    public string FacilityType { get; set; }
  }
}

XMLデータを EvacuationFacilityInfo オブジェクト配列にコンバートするロジックを実装します(GmlToCustomFormatConvertorクラス)。
実装はごちゃごちゃしていますが、「XPathでクエリーしてモデルクラスに一生懸命変換する」というごり押し実装なので説明は省きます。

// CreateEvacuationFacilityDb\GmlToCustomFormatConvertor.cs
using System.Collections.Generic;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Linq;
using Microsoft.Azure.Documents.Spatial;
using EvacuationFacilityLib.Models;

namespace CreateEvacuationFacilityDb
{
  public class GmlToCustomFormatConvertor
  {
    public static List<EvacuationFacilityInfo> Load(string xmlFilePath)
    {
      var result = new List<EvacuationFacilityInfo>();

      var xDoc = XDocument.Load(xmlFilePath);

      var nsmgr = new XmlNamespaceManager(new NameTable());
      nsmgr.AddNamespace("gml", "http://www.opengis.net/gml/3.2");
      nsmgr.AddNamespace("ksj", "http://nlftp.mlit.go.jp/ksj/schemas/ksj-app");
      nsmgr.AddNamespace("xlink", "http://www.w3.org/1999/xlink"); 

      var points = xDoc.XPathSelectElements("//gml:Point", nsmgr);
      foreach (var point in points)
      {
        var evacuationFacilityInfo = new EvacuationFacilityInfo();

        //var id = point.Attribute("gml:id").Value;
        evacuationFacilityInfo.ID = point.FirstAttribute.Value;
        
        //var pos = point.XPathSelectElement("gml:pos");
        string latitudeAndLongitude = point.Value;
        double dLatitude = double.Parse(latitudeAndLongitude.Split(' ')[0]);
        double dLongitude = double.Parse(latitudeAndLongitude.Split(' ')[1]);
        evacuationFacilityInfo.Latitude = dLatitude;
        evacuationFacilityInfo.Longitude = dLongitude;
        evacuationFacilityInfo.Location = new Point(dLongitude, dLatitude);

        var evacuationFacility = 
          xDoc.XPathSelectElement("//ksj:EvacuationFacilities/ksj:position[@xlink:href='#" + evacuationFacilityInfo.ID + "']", nsmgr).Parent;
        evacuationFacilityInfo.Name = 
          evacuationFacility.XPathSelectElement("ksj:name", nsmgr).Value;
        evacuationFacilityInfo.Address = 
          evacuationFacility.XPathSelectElement("ksj:address", nsmgr).Value;
        evacuationFacilityInfo.FacilityType = 
          evacuationFacility.XPathSelectElement("ksj:facilityType", nsmgr).Value;

        result.Add(evacuationFacilityInfo);
      }

      return result;
    }
  }
}

EvacuationFacilityInfoオブジェクトをDocumentDBに保存

XMLをコンバートして得られたEvacuationFacilityInfoオブジェクトをDocumentDBに保存するリポジトリークラスも作成しましょう。
データベースの作成・コレクションの作成・ドキュメントの保存・ドキュメントの地理的検索まで実装してしまいます。

// EvacuationFacilityInfoRepository.cs
#define LOCAL_DB

using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;

using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;

using Microsoft.Azure.Documents.Spatial;

using EvacuationFacilityLib.Models;

namespace EvacuationFacilityLib.Repositories
{
  public class EvacuationFacilityInfoRepository
  {
    /// <summary>
    /// 任意のデータベースID
    /// </summary>
    private static readonly string DatabaseId = "EvacuationFacilityInfoDb";

    /// <summary>
    /// 任意のコレクションID
    /// </summary>
    private static readonly string CollectionId = "EvacuationFacilityInfoCollection";

    /// <summary>
    /// エンドポイント
    /// </summary>
#if LOCAL_DB
    private static readonly string EndPoint = "https://localhost:8081/";
#else
    private static readonly string EndPoint = "[your endpoint]";
#endif

    /// <summary>
    /// 認証キー(固定)
    /// </summary>
#if LOCAL_DB
      private static readonly string AuthKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
#else
    private static readonly string AuthKey = "[your auth key]";
#endif

    private static DocumentClient client;


    /// <summary>
    /// EvacuationFacilityInfoオブジェクトを保存します。
    /// </summary>
    public static void SaveEvacuationFacilityInfos(List<EvacuationFacilityInfo> evacuationFacilityInfos)
    {
      foreach (var info in evacuationFacilityInfos)
      {
        client.UpsertDocumentAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId), info);
      }
    }

    /// <summary>
    /// 緯度経度に対する距離範囲に存在するデータ(避難施設)を取得します。
    /// </summary>
    public static List<EvacuationFacilityInfo> Search(double longitude, double latitude, int distance)
    {
      List<EvacuationFacilityInfo> result = null;

      var query = 
        client.CreateDocumentQuery<EvacuationFacilityInfo>(
          UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId),
          new FeedOptions { MaxItemCount = -1 })
          .Where(i => i.Location.Distance(new Point(longitude, latitude)) < distance);

      result = query.ToList();

      return result;
    }

    /// <summary>
    /// データベース・コレクションの初期化を行います。
    /// </summary>
    public static void Initialize(bool initDatabase = false)
    {
      client = new DocumentClient(new Uri(EndPoint), AuthKey, new ConnectionPolicy { EnableEndpointDiscovery = false });
      if (initDatabase)
      {
        CreateDatabaseIfNotExistsAsync().Wait();
        CreateCollectionIfNotExistsAsync().Wait();
      }
    }

    /// <summary>
    /// 存在しなければデータベースを作成します。
    /// </summary>
    /// <returns></returns>
    private static async Task CreateDatabaseIfNotExistsAsync()
    {
      try
      {
        await client.ReadDatabaseAsync(UriFactory.CreateDatabaseUri(DatabaseId));
      }
      catch (DocumentClientException e)
      {
        if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
          await client.CreateDatabaseAsync(new Database { Id = DatabaseId });
        }
        else
        {
          throw;
        }
      }
      catch
      {
        throw;
      }
    }

    /// <summary>
    /// 存在しなければコレクションを作成します。
    /// </summary>
    /// <returns></returns>
    private static async Task CreateCollectionIfNotExistsAsync()
    {
      try
      {
        await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId));
      }
      catch (DocumentClientException e)
      {
        if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
          // DataType.Point に対して SpatialIndex 付加してコレクションを作成
          await client.CreateDocumentCollectionAsync(
            UriFactory.CreateDatabaseUri(DatabaseId),
            new DocumentCollection { Id = CollectionId, IndexingPolicy = new IndexingPolicy(new SpatialIndex(DataType.Point)) },
            new RequestOptions { OfferThroughput = 1000 });
        }
        else
        {
          throw;
        }
      }
    }
  }
}

DatabaseId / CollectionIdは任意ですが、ここではそれぞれ「EvacuationFacilityInfoDb」「EvacuationFacilityInfoCollection」としました。
EndPointはローカルのDocumentDB Emulatorを使用するため「https://localhost:8081/」となります。AuthKeyもEmulatorである為、固定値となります。
エミュレータではなくAzure DocumentDBを利用する場合には、適切なEndPointとAuthKeyを指定してください(あなたのAzureポータルから確認可能)。
Initialize()メソッドで接続用DocumentClientオブジェクトを作成します。引数initDatabaseがtrueの場合は、データベース・コレクションの作成も行います。
CreateCollectionIfNotExistsAsync()メソッドではコレクションを作成します。ここでポイントがあります。地理的クエリーを行う為に、インデックスポリシーとしてSpatialIndexを指定してます。
SaveEvacuationFacilityInfos()メソッドは、EvacuationFacilityInfoオブジェクト配列をDocumentDBに保存します。
Search()メソッドは、データの中から、引数で指定した緯度経度に対する距離範囲内の位置にある避難施設を検索します。
以下の様に距離範囲のクエリー条件を記述することができます。

.Where(i => i.Location.Distance(new Point(longitude, latitude)) < distance);

Microsoft.Azure.Documents.Spatial.Pointクラスのコンストラクタ引数は、(緯度, 経度)ではなく (経度, 緯度) なのでご注意を。

コンバート及びDocumentDB作成処理の呼び出し

上記で用意した各クラス・メソッドを呼び出して「XMLデータからEvacuationFacilityInfoオブジェクトに変換、さらにDocumentDBに保存」までを行うコンソールアプリ「CreateEvacuationFacilityDb」のMain()メソッドを実装します。

// CreateEvacuationFacilityDb\Program.cs
using System.Collections.Generic;

using EvacuationFacilityLib.Models;
using EvacuationFacilityLib.Repositories;

namespace CreateEvacuationFacilityDb
{
  class Program
  {
    static void Main(string[] args)
    {
      // DocumentDB初期化
      EvacuationFacilityInfoRepository.Initialize(true);
      
      // XMLをロードしてカスタムクラスコレクションに変換
      // xmlのパスは書き換えてください!
      List<EvacuationFacilityInfo> evacuationFacilityInfoList = 
        GmlToCustomFormatConvertor.Load(@"J:\Projects\Github\research\DocumentDB\GIS\P20-12_13.xml");

      // カスタムクラスコレクションをDocumentDBに保存
      EvacuationFacilityInfoRepository.SaveEvacuationFacilityInfos(evacuationFacilityInfoList);
    }

  }
}

読み込むXMLファイルのパスは皆さんの環境に合わせて書き換えてください。
CreateEvacuationFacilityDbコンソールアプリを実行すると、DocumentDBにデータが作成・格納されます。

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

2. Webアプリを作成する

EvacuationFacilityWebアプリを作成します。

DocumentDB接続を初期化

まずGlobal.asax.csのStartで「EvacuationFacilityInfoRepository.Initialize()」を呼び出し、DocumentClientオブジェクトを初期化します。

// EvacuationFacilityWeb\Global.asax.cs
public class MvcApplication : System.Web.HttpApplication
{
  protected void Application_Start()
  {
    // DocumentDB初期化
    EvacuationFacilityInfoRepository.Initialize();

    AreaRegistration.RegisterAllAreas();
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
  }

コントローラークラスを実装

コントローラクラスを実装します。
検索画面表示の為のHTTP GET用Index()メソッド、「避難施設を検索」クリック時のHTTP POST用Index()メソッドを実装します。
POST時には検索結果をViewBagにしまって resultビュー を表示します。

// EvacuationFacilityWeb\Controllers\HomeController.cs
using System.Web.Mvc;

using EvacuationFacilityLib.Repositories;

namespace EvacuationFacilityWeb.Controllers
{
  public class HomeController : Controller
  {
    public ActionResult Index()
    {
      return View();
    }

    [HttpPost]
    public ActionResult Index(double latitude, double longitude, int distance)
    {
      ViewBag.EvacuationFacilities = EvacuationFacilityInfoRepository.Search(longitude, latitude, distance);

      return View("result");
    }
  }
}

ビューを実装

Index及びResultのビュー定義は以下の通りです。
実行時の緯度経度は navigator.geolocation.getCurrentPosition() で取得します。

// EvacuationFacilityWeb\Views\Home\Index.cshtml
<script type="text/javascript">
  if ("geolocation" in navigator) {
    navigator.geolocation.getCurrentPosition(function (position) {
      // POST用
      $('#latitude').val(position.coords.latitude);
      $('#longitude').val(position.coords.longitude);
      // 表示用
      $('#spanLatitude').html(position.coords.latitude);
      $('#spanLongitude').html(position.coords.longitude);

      $('#currentLocationGmapUrl').attr('href', 'https://www.google.co.jp/maps/place/' + position.coords.latitude + '+' + position.coords.longitude);
    },
    function (err) {
      alert(err.code + ': ' + err.message);
    });
  } else {
    alert('geolocation is not supported.');
}

</script>

<style>
.row {
  padding-top: 5px;
  padding-bottom: 5px;
}
</style>

@using (Html.BeginForm())
{
<div class="row">
  <div class="col-xs-2"></div>
  <div class="col-xs-8">
    近隣の避難施設を検索します。
  </div>
  <div class="col-xs-2"></div>
</div>
<div class="row">
  <div class="col-xs-5 text-right">
    現在地:
  </div>
  <div class="col-xs-7">
    緯度<span id="spanLatitude"></span> 経度:<span id="spanLongitude"></span>
    <br />
    <a id="currentLocationGmapUrl" target="_blank">地図を表示</a>
  </div>
</div>
<div class="row">
  <div class="col-xs-5 text-right">
    距離範囲:
  </div>
  <div class="col-xs-7">
      <select id="distance" name="distance">
        <option value="500">0.5 km 範囲内</option>
        <option value="1000">1 km 範囲内</option>
        <option value="3000">3 km 範囲内</option>
        <option value="5000">5 km 範囲内</option>
      </select>
  </div>
</div>
<div class="row">
  <div class="col-xs-5 text-right">

  </div>
  <div class="col-xs-7">
    <input type="hidden" id="latitude" name="latitude" />
    <input type="hidden" id="longitude" name="longitude" />
    <input type="submit" value="避難施設を検索" class="btn btn-default" />
  </div>
</div>
}
<hr />
「navigator.geolocation.getCurrentPosition()」で現在位置を取得するので、LANだとIPが降られた場所で遠くになるかも・・・

以下は resultビュー です。
検索結果はHomeControllerにより ViewBag.EvacuationFacilities にEvacuationFacilityInfo配列として格納されています。
それらのデータを一覧レンダリングします。 Google Mapsへのリンクはソースにあるように緯度・経度 をクエリーパラメータとすることで実装します。

// EvacuationFacilityWeb\Views\Home\result.cshtml
<h2>検索結果</h2>

<a href="./">戻る</a>

@if (this.ViewBag.EvacuationFacilities != null)
{
  <p>@this.ViewBag.EvacuationFacilities.Count 件</p>
  foreach (EvacuationFacilityLib.Models.EvacuationFacilityInfo facility in this.ViewBag.EvacuationFacilities)
  {
    <p>名称:@facility.Name</p>
    <p>住所:@facility.Address</p>
    <p>施設タイプ:@facility.FacilityType</p>
    <p><a target="_blank" href='@System.String.Format("https://www.google.co.jp/maps/place/{0}+{1}", facility.Latitude, facility.Longitude)'>地図を表示</a></p>
    <hr />
    
  }
}

まとめ

緯度・軽度からのクエリーというのは、なかなか色々なところで使えそうですね。
特にスマホアプリ系では利用用途が多そうに思います。スマホネイティブ(Xamarin Forms含め)なら位置情報もより正確に取得出来、マップを含めたアプリ連携がよりスムーズです。
さらに技術を進めると単純な位置関係だけでなく、道路を考慮した実移動距離となってきますが、Azureに限らずどんどん進化していくような気がしています。

ということで、今回、ソースの解説は結構 雑い かったと思いますが、一式をgithubにあげたので、そちらを見ていただければ幸いです。

Visual Studio for Mac から ASP.NET CoreアプリをAzureにパブリッシュ!

Visual Studio for MacASP.NET CoreアプリをAzure App Service にパブリッシュしてみたいと思います。

では早速。(Visual Studio for MacGUIとウィザードが自動出力するコードのみを使ったノンコーディング操作で行きます。)

1. プロジェクトの作成

Visual Studio for Macを起動し、ASP.NET Core Webアプリケーションを作成します。
メニュー「ファイル → 新しいソリューション」を選択します。
「新しいプロジェクト」ダイアログで「.NET Core → App → ASP.NET Core Web App」を選択して「次へ」をクリックします。

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

プロジェクト名・ソリューション名を入力して「作成」をクリックします。

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

カラカラとNugetパッケージの復元が行われるので、完了するまで少し待ちます・・・

2. Azureにパブリッシュ

ソリューションからプロジェクト名を右マウスクリックし「Publish → Publish to Azure」をクリックします。

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

パブリッシュ済みのAzure App Serviceの一覧がリストされます。
今回は新規に追加したいので左下の「ニュース」をクリックします(ニュース・・・というか 新規 だと思う・・・)。

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

(そして更に、また謎の日本語のダイアログが表示されます)

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

Android サービス」・・・実際にはAzure App Serviceが作成されるのでご心配なく・・・
「作成済み」ボタン・・・「作成」と認識してください。
(この辺りは2017/4/14時点なので、そのうち正しい日本語にローカライズされるでしょう・・・)
まあ、Azureではおなじみの アプリ名 や リソースグループ名 価格プラン 等を入力します。
ちなみに今回は「プロジェクト名」は CoreWebApp1 としたのですが、Azureにパブリッシュする「アプリ名」は RdCoreWebApp1 としています。この名称は異なっていてもOKです。

パブリッシュが完了すると、ブラウザで対象アプリが実行されます。

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

3. Azureポータルで確認

Azureポータルに行き、今回パブリッシュしたアプリ(リソースグループ)を確認したのが以下の画像キャプチャです。

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

4. まとめ

はい!
サクサクっとVisual Studio for MacからAzureにパブリッシュできました!
Visual Studio for Macが Preview から 正式版 になるのが待ち遠しいです。
そして各種環境の.NET Standard 2.0実装がリリースされれば、更にWindowsMacの敷居が低くなるでしょう^^

Azure FunctionsをVS2017でローカル実行&デバッグしてパブリッシュする

MSDNブログ(↓↓↓)で Azure Functions 関連の記事を見たので自分でやってみました。

blogs.msdn.microsoft.com

上記ブログに記述されている事、そして今回自分で試した事を要約すると以下の内容です。

  1. Azure FunctionsをVisual Studio 2017で開発&デバッグする(つまりローカル実行するということ)
  2. Azure Functionsをクラスライブラリとしてビルド&パブリッシュする

1 は、まあ 開発効率を上げるということですね。
Azure Functions自体はAzureポータルでコーディングできますが、本格的実装はローカルで行いたいですよね。

2 は、クラスライブラリとしてアセンブリ(dll)にしてデプロイすることで実行パフォーマンスを上げる意味を持ちます。
Azure Functionsは5分間非アクティブであるとアイドル状態となり、次の実行時には再度メモリ上に展開されます。C# / JavaScriptコードがデプロイされている場合、アセンブリへのコンパイル作業から再実行され起動パフォーマンスが落ちてしまいます。

では、以下に手順を・・・

ちなみに、Visual Studioとの統合ツール「Visual Studio Tools for Azure Functions」がまだ 2017 対応できていないらしく、そのため以下のような手動による操作がいくつか発生します。でも、あと数週間もすればツールがリリースされるみたいです。

Azure Functions CLI のインストール

Azure Functions CLI をインストールします。
以下のnpmコマンドでグローバル領域にインストールします。

npm i -g azure-functions-cli

www.npmjs.com

Visual Studio 2017 でWebプロジェクトを作成

Visual Studio 2017を起動します。
メニュー「ファイル → 新規作成 → プロジェクト」を選択します。
プロジェクトテンプレートは「Web → ASP.NET Web アプリケーション(.NET Framework)」、プロジェクト名はここでは「AzureFunctionsWeb」としました。

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

「空」テンプレートを選択します。

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

プロジェクトのプロパティを表示します(ソリューションエクスプローラからプロジェクトをマウス右ボタンクリックメニューで「プロパティ」を選択)。

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

Webタブを選択し、以下の設定を行います。
* 「外部プログラムを起動する」を選択。
* パスは「C:\Users\「【ユーザー名】\AppData\Roaming\npm\node_modules\azure-functions-cli\bin\func.exe」とする。
* 「コマンドライン引数」は「host start」とする。
* 「作業ディレクトリ」は【プロジェクトフォルダ】とする。

設定を行った画面は以下の通り。

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

Azure Portal上で Azure Functions を作成

Visual Studio Tools for Azure Functionsがリリースされれば、VSからプロジェクトを生成できると思いますが、それが今はないのでポータルで作成して、ローカルVSにベースとなるソースを落としてきます)

Azure Functions 作成画面を表示

https://functions.azure.com/signinをブラウザで開きます。
「New function app」の「Name」に作成するファンクション名を入力します。ここでは「RyuichiFunction0916」としました。
「Region」は「Japan East」としました。
「Create + get started」をクリックします。

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

「WebHook + API」「C#」を選択し、「この関数を作成する」ボタンをクリックします。

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

作成完了。

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

Azure Functions ソースのダウンロード

左下の「Function App の設定」をクリックします。

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

「Kuduに移動」をクリックします。

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

「site」を選択します。

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

「wwwroot」のダウンロードアイコンをクリックします。

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

ダウンロードしたzipのイメージは以下の通り。

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

WebプロジェクトにFunctionsソースを追加

先程作成したWebプロジェクト(AzureFunctionsWeb)フォルダにzipの内容を解凍します。

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

追加のファイルをプロジェクトに含めます。
また「run.csx」ファイルを「run.cs」にリネームします。

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

run.csを以下のように修正します。
run.csxでは単純に run() だけが定義されていますが、csコードにする為「名前空間・クラス」でくくります。
また必要な using を追加します。
レスポンス文字列も若干修正を加えました。

// run.cs
using Microsoft.Azure.WebJobs.Host;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

namespace AzureFunctionsWeb
{
  public class ExampleFunction
  {
    public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
    {
      log.Info("C# HTTP trigger function processed a request.");

      // parse query parameter
      string name = req.GetQueryNameValuePairs()
        .FirstOrDefault(q => string.Compare(q.Key, "name", true) == 0)
        .Value;

      // Get request body
      dynamic data = await req.Content.ReadAsAsync<object>();

      // Set name to query string or body data
      name = name ?? data?.name;

      return name == null
        ? req.CreateResponse(HttpStatusCode.BadRequest, 
                             "Please pass a name on the query string or in the request body")
        : req.CreateResponse(HttpStatusCode.OK,
                             "Hello " + 
                              name + 
                              ". I'm assembly!!");
    }
  }
}

プロジェクトにNugetパッケージを追加

Nugetで以下のパッケージを追加します。

function.jsonの調整

function.jsonに「scriptFile」「entryPoint」キーを追加します。
scriptFileにはアセンブリ名を、entryPointには名前空間/クラス付きのRun()メソッドの完全名を記述します。

// function.json
{
  "scriptFile": "..\\bin\\AzureFunctionsWeb.dll",
  "entryPoint": "AzureFunctionsWeb.ExampleFunction.Run",
  "disabled": false,
  "bindings": [
    {
      "authLevel": "function",
      "name": "req",
      "type": "httpTrigger",
      "direction": "in"
    },
    {
      "name": "$return",
      "type": "http",
      "direction": "out"
    }
  ]
}

ローカル実行

Visual StudioでF5クリックにより実行を行います。
以下のようなコマンドプロンプトが表示され Function がリクエスト待ち(実行待ち)状態となります。

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

コマンドプロンプト表示上に、以下の記述を確認することができます。

Job host started
Http Function HttpTriggerCSharp1: http://localhost:7071/api/HttpTriggerCSharp1

このURLがFunctionの受け口のURLになります。

サンプル(ExampleFunction)は「name」というキーをパラメータとして受け取ります。その為、以下のURLをブラウザでアクセスしてみましょう。

http://localhost:7071/api/HttpTriggerCSharp1?name=daigo

Functionが実行され以下のようなレスポンスが得られます。

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

ブレークポイントもきちんと止まります。

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

Azure FunctionsへのPublish

次に、ローカルで実装を行った Function を AzureにPublishします。
コマンドプロンプトを起動します。

作成したWebプロジェクトフォルダ(F:\Projects\Research\AzureFunctionsWeb\AzureFunctionsWeb)に移動します。

以下のコマンドを実行してCLIからAzureにログインします。

func azure login  

Azureへのログイン画面が表示されるので、ログインを行います。

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

以下のコマンドで、現在PublishされているFunctionの一覧を確認します。

func azure functionapp list

現在の私の環境では2つのFunctionがPublishされていました。
RyuichiFunction0916の方が、今回作成したものです。

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

以下のコマンドによりFunctionをローカルからAzureへPublishします。
(RyuichiFunction0916部分は適時 Function 名に置き換えます)

func azure functionapp publish RyuichiFunction0916

正常にPublishされた結果画面が以下になります。

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

Azure Functionsを実行

Azureポータルで「RyuichiFunction0916」を表示。
「開発」タブを選択、「テスト」タブを選択、「要求本文」をテスト文字列に修正、「実行ボタン」クリック、を行います。
以下のように「出力」に「"Hello Daigo. I’m assembly!!“」が表示されました。
VS2017上で実装したロジックが実行されていることが確認できます。

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

まとめ

VS2017用「Visual Studio Tools for Azure Functions」がリリースされれば、上記のようなゴニョゴニョした作業はバックエンドで自動化されると思います。ただ、CLIなんかで、裏の動きを知っておくのは良いことだと思っています。
また、Azure functionsやマイクロサービスについては、私自身はまだ実運用の場面では使ったことがないので、色々な知見を経験者の方から学びたいと思っております。
WebHookを絡めてGithubだったりSlackだったりと連携する実装なんかも比較的容易に出来るようなので、色々試してみたいと思います。

Azure VM 2台 を超簡単にLoad Balancer構成してみた

先日、Azureの障害祭りがあったということで・・・というわけではないですが(笑)
2台構成の Azure Virtual Machine(高可用性)を、「超簡単に」組むというのをやってみました。

また、今更どうこう言うまでもなく ResourceManager ベースになってから、Azureの構成は、よりオートメーション化が可能になっています。

Azure QuickStart Template

GithubのAzure QuickStart Templateというところに、かなり使えるテンプレート(json構成ファイル)がたくさん落ちています。

github.com

もう山のようにいろんなテンプレートが落ちていますね。
これらをベースにjsonファイルいじって、自分の必要とする構成をカスタムに構成すれば工数の削減につながりますね。
また、ドキュメント読んだだけじゃ、よくわからなかったなぁ・・というようなものも、実際に動くテンプレートサンプルを使うと非常に理解しやすいですね。

2台構成&LoadBalancerのテンプレート

上記 Azure QuickStart Template の中から今回使うのは「2台構成&LoadBalancerのテンプレート」ということで以下になります。

github.com

上記URLを開くと、親切にも以下の赤枠にあるように「Deploy to Azure」というリンクが用意されています。

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

リンク先は以下の通りで、Githubに置いてある azuredeploy.json を Azureポータルに食わせるリンクになっています。

https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Fazure-quickstart-templates%2Fmaster%2F201-2-vms-loadbalancer-lbrules%2Fazuredeploy.json

デプロイ

「Deploy to Azure」リンクをクリックすると、Azure Portalに移動し、構成を組むためのパラメータの入力画面になります。
パラメータを適当に埋めます。

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

ここではリージョンは西日本にしました。
また、VMサイズは、このテンプレートではデフォルトで Standard_D1 になっています。
「購入」ボタンをクリックしましょう。

「リソース グループ」を確認すると、「Win2VM(先程のパラメータで入力したリソースグループ名)」が作成されています。

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

VMが作成されるまで数分かかると思うので、作成が完了するまで待ちましょう。

構成を確認する

Win2VMリソースグループは以下のような構成です。

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

以下のような、2台のVMロードバランサーする最低限の構成がしっかり組まれています。

  • 可用性セット
  • ロードバランサー
  • パブリックIP
  • Virtual Machine + Disk + NIC 2つ
  • 仮想ネットワーク
  • ストレージアカウント

myAvSet(可用性セット)

myAvSet(可用性セット)は、以下のような構成になっています。

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

障害ドメインは2つ、更新ドメインも2つ、それぞれのドメインに対してVMが割り当てられています。
AzureポータルのGUI上から可用性セットを作成すると「障害ドメイン 2つ、更新ドメイン 5つ」がデフォルトになっています。
この構成JSONをベースにして3台以上の構成をとる場合には適時 障害ドメイン及び更新ドメインを拡張する必要があります。
更新ドメインが2つのまま3台目のVMを追加すると、1つ目のVM(myVM0)と同一更新ドメインに配置されてしまいます。つまり、メンテナンス時に2つのVMが同時に再起動される可能性が発生してしまいます。

ロードバランサーのプローブ

ロードバランサーのプローブは、TCPでポート80、5秒ごとのチェック、以上閾値は2、で構成されています。

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

Virtual Machine のIISを構成

作成された Virtual MachineにはまだIISが構成されていません。
2つのVMにそれぞれIISを構成しましょう。
myVM0 を選択し、「接続」をクリックするとリモートデスクトップ接続ファイルがダウンロードされます。

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

通常のWindows Serverへのリモートデスクトップになりますので、IISを構成しましょう。

まあ、例のコレですね・・・(途中端折ります)

f:id:daigo-knowlbo:20170321015434p:plain
f:id:daigo-knowlbo:20170321015544p:plain
f:id:daigo-knowlbo:20170321015552p:plain
f:id:daigo-knowlbo:20170321015602p:plain

※myVM1にも同様にIISを構成しましょう。

LoadBalancerの動きを確認する

これでクライアントからのHTTPリクエストに対して、「2台に負荷分散&1台に障害が発生したらもう1台に振り分け」という高可用性なサーバー構成が完了しました。

本当に障害発生時にもう片方にリクエストが振り分けられるかどうか確認してみます。
myVM0 の c:\inetpub\wwwroot に、以下の index.html を配置します。

Hello. I'm myVm0.

myVM1 の c:\inetpub\wwwroot には、以下の index.html を配置します。

Hello. I'm myVm1.

URL(index.html)にアクセスるする

では、作成した index.html にアクセスしてみましょう。
ブラウザで、URLにアクセスします。
URLのIPアドレスロードバランサー(myLB)の「パブリックIPアドレス」で確認することができます。
ここでは「52.175.152.10」でした。

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

http://52.175.152.10」にブラウザでアクセスすると・・・私の環境では myVM0 につながりました。

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

では myVM0 のIISを停止してみます。

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

再び「http://52.175.152.10」をアクセスすると・・・

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

myVM1 につながりました!OKです!

まとめ

一昔前はクラスタリング構成のサーバーを組む(もしくは実証試験する)なんて、物理サーバー2台用意して、ケーブルつないで、OSメディア用意して・・・なんて大ごとでしたが、Azureのようなクラウド環境を利用するとサクサクっと社会人1年目でも簡単に組めちゃう素晴らしき時代ですね!
ただ、そのベースにある技術を理解しておくことが重要だと思います!(って人に言える程、私自身が有識者ではないが・・・)

Azure DocumentDB Emulatorを使ってみた。で、.NET Coreから操作した話。

技術者としての尊敬の対象である Scott Hanselman 氏のブログで「Azure DocumentDB Emulator」についての記事が書かれていたので、自分でも使ってみました。

www.hanselman.com

こんなことをやった

「Azure DocumentDB」は、もはや、広く知られた Azureが提供する NoSQL データベースです。
いわゆる「ちょー早くて、ちょースケーラブルで、RDBの概念は捨ててから使ってね」っていうデータベースですね。
NoSQLは、その登場以来、どこにどう使うべきか?という部分についての議論・検討が行われ、まだまだ一般化していないように思います。
私自身も実は会社の業務としてNoSQLを採用したプロジェクトに関わった事はありません・・・

まあ、それはさておき(また、本投稿はNoSQLの本質を議論する趣旨ではないので・・・)、今回は以下のことを試してみたので、それらをまとめておこうと思います。

  • 「Azure DocumentDB Emulator」をインストールしてみた
  • 「.NET Core Consoleアプリ」から「Azure DocumentDB Emulator」に接続してデータ操作してみた
  • 「Azure DocumentDB(クラウド)」から「Azure DocumentDB Emulator(ローカル)」にデータをエクスポート(リストア)してみた
  • データのコンソール出力にはSerilogを使った

「Azure DocumentDB Emulator」のインストール

さあ、今回の主役である「Azure DocumentDB Emulator」をインストールします。
この「Azure DocumentDB Emulator」は、2016/11にリリースされました。
これは クラウドのAzure DocumentDB をローカルPC上でエミュレートするソフトウェアです。オフライン状態でも、Azure DocumentDB開発が可能になります。
以下のリンクページ 上部の「Download the Emulator」をクリックするとインストーラをダウンロードする事が出来ます。
(ちなみにこれはWindows版のみでMac版は残念ながらありません)

docs.microsoft.com

ダウンロードした msi ファイルは、ダブルクリックし「Next」や「OK」や、指示通りにクリックしていればインストール完了します。
インストールが完了すると、自動的に起動し、タスクトレイに以下のようなアイコンが表示されます(スタートメニューから「DocumentDB Emulator」を選択しても明示的に起動する事が出来ます)。

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

また、このアイコンをマウス右クリックすると以下のようなメニューが表示されます。

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

「Open Data Explorer...」をクリックすると以下のような管理画面が表示されます(インストール直後は、この画面も自動で表示されるはずです)。

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

見てわかるように「https://localhost:8081/_explorer/index.html#」という8081ポートでホストされた管理コンソール画面となります。
「.NET / .NET Core / Java...」等のタブが有り、ここに表示されている「Code Samples」をクリックすると、何も考えなくても「ビルド→実行」が可能なサンプルプロジェクトをダウンロード可能です。
画面上部の「Explorer」タブをクリックすると、DocumentDB Emulatorに保存されているデータベース内容を確認する事が出来ます(以下の画面)。
初期状態では 空 になります。

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

「.NET Core Consoleアプリ」+「Azure DocumentDB Emulator」でデータ保存・読み込み

では、せっかくなので最新の Visual Studio 2017 RC + .NET Core を使って DocumentDB Emulator に接続してみます。

①プロジェクトの作成

Visual Studio 2017 RCを起動し、メニュー「ファイル→新規作成→プロジェクト」を選択します。

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

ここではシンプルに、テンプレートとして「Console App(.NET Core)」を選択し、プロジェクト名は「DocDbExampleCoreConsole」としました。

②Nuget参照の追加

次にNugetで必要なライブラリへの参照を追加します。
メニュー「プロジェクト→Nuget パッケージの管理」を選択します。
まず必要なのは「Microsoft.Azure.DocumentDB.Core」になります。

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

さらに、これは本投稿の本質ではありませんが、コンソールへのデータログ出力用に「Serilog」および、そのコンソールSinkである「Serilog.Sinks.Console」も追加します。

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

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

③モデルクラスの作成

DocumentDBに保存するモデルクラスを作成します。
ブログの投稿内容を想定したモデルを作成することとします。
メニュー「プロジェクト→クラスの追加」を選択し、表示された「新しい項目の追加」ウィンドウで「コード→クラス」を選択し、名前は「BlogPost.cs」とします。

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

実装は以下のとおりであり、ブログ投稿を表す「BlogPostクラス」、ブログ投稿者を表す「Userクラス」、ブログ投稿へのコメントを表す「Commentクラス」から構成されます。

// BlogPost.cs
using System.Collections.Generic;
using Newtonsoft.Json;

namespace DocDbExampleCoreConsole
{
  /// <summary>
  /// ブログポスト(1投稿)を表すクラスです。
  /// </summary>
  public class BlogPost
  {
    [JsonProperty(PropertyName = "id")]
    public string Id { get; set; }

    [JsonProperty(PropertyName = "title")]
    public string Title { get; set; }

    [JsonProperty(PropertyName = "contents")]
    public string Contents { get; set; }

    [JsonProperty(PropertyName = "author")]
    public User Author { get; set; }

    [JsonProperty(PropertyName = "comments")]
    public List<Comment> Comments { get; set; }
  }

  /// <summary>
  /// ブログ投稿者を表すクラスです。
  /// </summary>
  public class User
  {
    [JsonProperty(PropertyName = "id")]
    public string Id { get; set; }

    [JsonProperty(PropertyName = "name")]
    public string Name { get; set; }
  }

  /// <summary>
  /// ブログ投稿へのコメントを表すクラスです。
  /// </summary>
  public class Comment
  {
    [JsonProperty(PropertyName = "id")]
    public string Id { get; set; }

    [JsonProperty(PropertyName = "text")]
    public string Text { get; set; }
  }
}

リポジトリクラスの作成

モデルクラスをDocumentDBに出し入れするためのデータベースアクセス用のリポジトリクラスを作成します。
メニュー「プロジェクト→クラスの追加」を選択し、表示された「新しい項目の追加」ウィンドウで「コード→クラス」を選択し、名前は「BlogPostRepository.cs」とします。

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

実装は以下の通りです。

// BlogPostRepository.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;
using Microsoft.Azure.Documents.Linq;

namespace DocDbExampleCoreConsole
{
  public class BlogPostRepository
  {
    /// <summary>
    /// 任意のデータベースID
    /// </summary>
    private static readonly string DatabaseId = "Blog";

    /// <summary>
    /// 任意のコレクションID
    /// </summary>
    private static readonly string CollectionId = "BlogPost";

    /// <summary>
    /// エンドポイント
    /// </summary>
    private static readonly string EndPoint = "https://localhost:8081/";

    /// <summary>
    /// 認証キー(固定)
    /// </summary>
    private static readonly string AuthKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";

    private static DocumentClient client;

    /// <summary>
    /// ブログポストデータを作成します。
    /// </summary>
    /// <param name="blogPost"></param>
    /// <returns></returns>
    public static async Task<Document> CreateBlobPostAsync(BlogPost blogPost)
    {
      return await client.CreateDocumentAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId), blogPost);
    }

    /// <summary>
    /// すべてのブログポストデータを取得します。
    /// </summary>
    /// <returns></returns>
    public static async Task<IEnumerable<BlogPost>> GetAllBlogPostsAsync()
    {
      IDocumentQuery<BlogPost> query = client.CreateDocumentQuery<BlogPost>(
        UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId),
        new FeedOptions { MaxItemCount = -1 })
        .AsDocumentQuery();

      List<BlogPost> results = new List<BlogPost>();
      while (query.HasMoreResults)
      {
        results.AddRange(await query.ExecuteNextAsync<BlogPost>());
      }

      return results;
    }

    /// <summary>
    /// データベース・コレクションの初期化を行います。
    /// </summary>
    public static void Initialize()
    {
      client = new DocumentClient(new Uri(EndPoint), AuthKey, new ConnectionPolicy { EnableEndpointDiscovery = false });
      CreateDatabaseIfNotExistsAsync().Wait();
      CreateCollectionIfNotExistsAsync().Wait();
    }
    
    /// <summary>
    /// 存在しなければデータベースを作成します。
    /// </summary>
    /// <returns></returns>
    private static async Task CreateDatabaseIfNotExistsAsync()
    {
      try
      {
        await client.ReadDatabaseAsync(UriFactory.CreateDatabaseUri(DatabaseId));
      }
      catch (DocumentClientException e)
      {
        if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
          await client.CreateDatabaseAsync(new Database { Id = DatabaseId });
        }
        else
        {
          throw;
        }
      }
    }

    /// <summary>
    /// 存在しなければコレクションを作成します。
    /// </summary>
    /// <returns></returns>
    private static async Task CreateCollectionIfNotExistsAsync()
    {
      try
      {
        await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId));
      }
      catch (DocumentClientException e)
      {
        if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
          await client.CreateDocumentCollectionAsync(
            UriFactory.CreateDatabaseUri(DatabaseId),
            new DocumentCollection { Id = CollectionId },
            new RequestOptions { OfferThroughput = 1000 });
        }
        else
        {
          throw;
        }
      }
    }
  }
}

リポジトリクラス実装のポイントは以下の通りです。

  • DatabaseId / CollectionId
    これらは任意の名称をつけます。ここでは「Blog」「BlogPost」としました。

  • EndPoint
    エンドポイントはデフォルトでは「https://localhost:8081/」となります。

  • AuthKey
    Azure DocumentDB Emulator では認証キーは固定で「C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==」と決められています。

  • Microsoft.Azure.Documents.Client.DocumentClient
    DocumentDBへの接続クライアント(繋ぎ役)は「DocumentClientクラス」となります。本サンプルでは Initialize() メソッド内で初期化を行っています。

  • Initialize() / CreateDatabaseIfNotExistsAsync() / CreateCollectionIfNotExistsAsync()
    データベースは、データ操作を行う前に初期作成を行う必要があります(RDBでいうところのCREATE DATABASE / CREATE TABLE)。これに該当する処理をCreateDatabaseIfNotExistsAsync() / CreateCollectionIfNotExistsAsync()で実装しています。両メソッドは Initialize() から呼び出されています。つまり、初期化処理として BlogPostRepository.Initialize() を呼び出す必要があります。

  • CreateBlobPostAsync(BlogPost blogPost)
    ブログポスト(ドキュメント)をデータベース コレクション」に追加します(RDBのINSERT)。

  • GetAllBlogPostsAsync()
    登録されたすべてのブログポスト(ドキュメント)を取得しています(RDBのSELECT)。

⑤main()の実装(リポジトリクラスの呼び出し)

用意したリポジトリクラス(BlogPostRepository)を利用して、BlogPostモデルクラスのデータベースへの出し入れ処理を実装します。
以下がコンソールアプリケーションのmain処理の実装になります。

// Program.cs
using System.Threading.Tasks;
using System.Collections.Generic;

using Serilog;

namespace DocDbExampleCoreConsole
{
  class Program
  {

    private IEnumerable<BlogPost> blogPosts = null;

    static void Main(string[] args)
    {
      Program program = new Program();

      // データベースを初期化
      program.InitializeDatabase();

      // データを挿入
      program.InitializeData().Wait();

      // データを抽出
      program.GetAllBlogPosts().Wait();

      // 抽出したデータをコンソール出力
      Log.Logger = new LoggerConfiguration()
        .WriteTo.Console()
        .CreateLogger();
      foreach (var blogPost in program.blogPosts)
      {
        Log.Information("{@BlogPost}", blogPost);
      }

      System.Console.ReadLine();
    }

    /// <summary>
    /// データベース・コレクションを初期化します。
    /// </summary>
    private void InitializeDatabase()
    {
      // データベース・コレクションを作成
      BlogPostRepository.Initialize();
    }

    /// <summary>
    /// BlogPostサンプルデータを作成します。
    /// </summary>
    /// <returns></returns>
    private async Task InitializeData()
    {
      // 50件、テスト投稿を作成
      for (int i = 0; i < 50; i++)
      {
        BlogPost blogPost = new BlogPost() {
          Title = string.Format("Title {0}", i),
          Contents = string.Format("Hi! This contents no is '{0}'.", i),
          Author = new User() { Name = "nanasi san" },
          };
        blogPost.Comments = new List<Comment>();
        blogPost.Comments.Add(new Comment() { Text = "Good job!!" });

        await BlogPostRepository.CreateBlobPostAsync(blogPost);
      }
    }

    /// <summary>
    /// すべてのBlogPostデータを取得します。
    /// </summary>
    /// <returns></returns>
    private async Task GetAllBlogPosts()
    {
      this.blogPosts = await BlogPostRepository.GetAllBlogPostsAsync();
    }
  }
}

mainクラス実装のポイントは以下の通りです。

  • program.InitializeDatabase();
    リポジトリークラスの呼び出しにより、データベース・コレクションを作成しています。

  • program.InitializeData().Wait();
    リポジトリークラスの呼び出しにより、BlogPostサンプルデータを50件作成しています。

  • program.GetAllBlogPosts().Wait();
    リポジトリークラスの呼び出しにより、データベースコレクションに登録された全てのBlogPostデータ抽出しています。

  • ログ出力
    抽出の結果として得られたデータは「IEnumerable<BlogPost>」型です。
    個別データである BlogPost オブジェクトをSerilogでコンソール出力しています。
    Serilogを使用する理由は、オブジェクトのログ出力に際し、子プロパティを含めた形で自動でJSON形式出力してくれる機能を活用する為です。

以下がDocDbExampleCoreConsoleコンソールアプリケーションの実行結果となります。

2016-12-08 02:37:27 [Information] BlogPost { Id: "43eb6293-c532-46dd-969d-e6e4258bcd7d", Title: "Title 0", Contents: "Hi・・This contents no is '0'.", Author: User { Id: null, Name: "nanasi san" }, Comments: [Comment { Id: null, Text: "Good job!!" }] }
2016-12-08 02:37:27 [Information] BlogPost { Id: "86380c10-c45e-46e5-9320-a432be8e838f", Title: "Title 1", Contents: "Hi・・This contents no is '1'.", Author: User { Id: null, Name: "nanasi san" }, Comments: [Comment { Id: null, Text: "Good job!!" }] }
2016-12-08 02:37:27 [Information] BlogPost { Id: "77350d85-6e11-49ac-9f52-d14729cb41b4", Title: "Title 2", Contents: "Hi・・This contents no is '2'.", Author: User { Id: null, Name: "nanasi san" }, Comments: [Comment { Id: null, Text: "Good job!!" }] }
2016-12-08 02:37:27 [Information] BlogPost { Id: "e151fef2-74d8-4735-a89a-971603169c36", Title: "Title 3", Contents: "Hi・・This contents no is '3'.", Author: User { Id: null, Name: "nanasi san" }, Comments: [Comment { Id: null, Text: "Good job!!" }] }
2016-12-08 02:37:27 [Information] BlogPost { Id: "580291de-c99a-4893-b416-48954847413a", Title: "Title 4", Contents: "Hi・・This contents no is '4'.", Author: User { Id: null, Name: "nanasi san" }, Comments: [Comment { Id: null, Text: "Good job!!" }] }
2016-12-08 02:37:27 [Information] BlogPost { Id: "a80815e6-5d03-49e9-b12e-a2892f504dbd", Title: "Title 5", Contents: "Hi・・This contents no is '5'.", Author: User { Id: null, Name: "nanasi san" }, Comments: [Comment { Id: null, Text: "Good job!!" }] }
...以下省略

⑥「Azure DocumentDB Emulator」のExplorerからデータを確認

「Azure DocumentDB Emulator」の管理画面である「https://localhost:8081/_explorer/index.html#」から投入されたBlogPostデータを参照してみます。
以下の画面のようにデータを確認する事が出来ます。

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

ちなみにタスクトレイアイコン マウス右ボタン「Reset Data...」をクリックすると、簡単にきれいさっぱりデータが消去されるのでお気を付けください。

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

「Azure DocumentDB」から「Azure DocumentDB Emulator」にデータを(エクスポート)リストア

最後にクラウド上の Azure DocumentDB から Azure DocumentDB Emulator にデータリストアする手順を説明しておきます。

「本来の実装はクラウドのAzure DocumentDBで行っているが、出先等で一時的にローカルエミュレータ開発をしたい」
「今までEmulatorが無かったから、常にクラウドに接続していたけれど、クラウドエミュレータを平行利用開発したい」

等々の場合に有効かと・・・

以下のページから「Azure DocumentDB Data Migration Tool」をダウンロードします。

Download Azure DocumentDB Data Migration Tool from Official Microsoft Download Center

ダウンロードしたzipファイルを任意のフォルダに解凍し dtui.exe を起動します。

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

「Nextボタン」をクリックします。

表示された以下のウィンドウにて、Import from は「DocumentDB」、ConnectionString は「Azure上のDocumentDBへの接続文字列(Database=xxx も忘れずに)」、Collection は「インポート対象のコレクション名」を入力して「Nextボタン」をクリックします。

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

以下の画面において、export先(つまり、ここではローカルの Azure DocumentDB Emulator)の設定を行います。
ConnectionString は、EndPoint=「https://localhost:8081」・AccountLey=「固定値」Database=「任意(ここではBlog)」となります。
入力したら「Nextボタン」をクリックします。

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

以下のウィンドウが表示されたら「Next」ボタンをクリックします(必要であれば「エラーログファイル」の設定も行って)。

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

設定内容の最終確認を行う以下の画面が表示されいます。
問題なければ「Next」ボタンをクリックします。

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

以上で「クラウド Azure DocumentDB」→「ローカル Azure DocumentDB Emulator」へのデータのリストアが完了しました。

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

まとめ

NoSQLが登場したのはいつだったでしょうか・・・RedisやCassandra、memcashedに興奮し、迷走し・・・Azure黎明期にも当時のNoSQL(DoucmentDBという名称ではなかったと思う・・・)に興奮した記憶があります。
ビジネスアプリケーションにおいては、まだまだNoSQLの導入は遅れているように思います。同時にNoSQLがRDBにとって代わる技術では無い事も事実です。
RDBの堅牢なデータ管理とNoSQLのパフォーマンスを適材適所に組み合わせたシステムの開発、という提案を開発者として行っていきたいものです。

で、まあ新しい技術への探求は相変わらず楽しいのですよね^^