しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

Azure Storage Blob Index (Preview) を試してみた

去年の Ignite で Mark Russinovich がセッションで話していたという Blob Index がプレビューとして公開されました。セッションでは Blob Quick Query も紹介されていましたが、こっちは公開されていないようです。

これまでも Blob はメタデータとしてコンテンツ以外に属性を持たせることが出来ていましたが、Blob Index は Tags が新規追加され、その値によって Blob Container 横断でクエリが書ける機能です。

プレビューとして利用可能なリージョンはフランスだけなので、Resource Provider の登録後に新しくストレージアカウントを作成すれば、即使える状態になっています。

Azure Portal でのサポートも追加されているので、アップロード時に Tags を追加できます。

f:id:shiba-yan:20200510022046p:plain

コンテナーを選択すると Filter が増えていますが、これが Blob Index を使ったフィルタリングになっています。API 的には == 以外も使えるはずですが、Azure Portal では固定になっています。

f:id:shiba-yan:20200511162733p:plain

Tags は 1 Blob あたり 10 個まで付けることが出来、キーと値それぞれにサイズ上限があるので設計時には注意したいです。特に全て文字列として扱われる点は注意したいです。

複雑なクエリを書くことも無理なので、上手く Azure Cognitive Search と使い分ける必要がありそうです。

C# SDK を使って Tags をセットする

Azure Storage SDK v12 のプレビュー版に Blob Index への対応が行われているので、ドキュメントに従って特定バージョンのパッケージをインストールすれば使えます。

ドキュメントでは Visual Studio の設定からパッケージソースを追加しろとありますが、グローバルに設定してしまうので nuget.config を用意した方がいろいろと便利です。

最近は dotnet new nuget を使えばひな型を作成できるので簡単です。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <!--To inherit the global NuGet package sources remove the <clear/> line below -->
    <clear />
    <add key="nuget" value="https://api.nuget.org/v3/index.json" />
    <add key="azure-sdk" value="https://azuresdkartifacts.blob.core.windows.net/azure-sdk-for-net/index.json" />
  </packageSources>
</configuration>

これで特定のプロジェクトに対してのみパッケージソースを追加できました。

サンプルとしてどのような情報を Blob Index Tags に追加しようかと考えたのですが、あまり良いものが思いつかなかったので位置情報付きの写真に対して Reverse Geocoding を実行して国と州を取得し、その情報と撮影日を Blob Index Tags に追加することにしました。

class Program
{
    static async Task Main(string[] args)
    {
        var connectionString = "DefaultEndpointsProtocol=https;AccountName=****;AccountKey=****;EndpointSuffix=core.windows.net";

        var serviceClient = new BlobServiceClient(connectionString);

        var container = serviceClient.GetBlobContainerClient("photos");

        foreach (var file in Directory.GetFiles(@"C:\Users\shibayan\Documents\images", "*.jpg"))
        {
            var blob = container.GetBlobClient(Guid.NewGuid() + Path.GetExtension(file));

            var reader = new ExifReader(file);

            reader.GetTagValue(ExifTags.DateTimeOriginal, out string dateTimeOriginal);

            var date = DateTime.ParseExact(dateTimeOriginal, new[] { "yyyy:MM:dd HH:mm:ss.fff", "yyyy:MM:dd HH:mm:ss" }, CultureInfo.InvariantCulture, DateTimeStyles.None);

            var (state, country) = await ReverseGeocodingAsync(reader.GetLatitude(), reader.GetLongitude());

            var options = new UploadBlobOptions
            {
                Tags = new Dictionary<string, string>
                {
                    { "Type", Path.GetExtension(file).ToLower() },
                    { "Date", date.ToString("yyyy-MM-dd") },
                    { "State", state },
                    { "Country", country }
                }
            };

            await blob.UploadAsync(file, options);
        }
    }

Reverse Geocoding 周りや Exif の処理に関しては Blob Index Tags と関係ないので省略しています。

Blob の作成と同時に Blob Index Tags の追加を行っていますが、後から Blob Index Tags だけ更新することも出来るので、上手く使い分けていきましょう。Case-sensitive なのでキーと値の作成には注意します。

とりあえず Azure Portal から Blob Index を使ったフィルタリングを試してみました。前に Surface Pro X を買うためにハワイに行った時の写真だけを絞り込んでみます。

f:id:shiba-yan:20200510021543p:plain

適当な Blob を開いてみたところ、ちゃんとハワイの写真でした。フィルタリングは一瞬で行われます。

次は 2019/03/19 にアメリカで撮影した写真のフィルタリングを行いました。この日はレドモンドで MVP Global Summit が開催されていたので、写真がそれなりにあります。

f:id:shiba-yan:20200510021551p:plain

フィルタリング結果を選択すると、Blob Index Tags の確認が出来ます。正しい写真が返ってきています。

f:id:shiba-yan:20200510021603p:plain

現実的には Azure Portal からのフィルタリングは行わないし、今の Azure Portal では特定のコンテナー内でしかフィルタリングが行えないので、SDK を使ってフィルタリングを実施してみます。

C# SDK を使って Tags でフィルタリング

フィルタリングのクエリは SQL っぽいよく見るやつですが、クオートの書き方が少し特殊です。

class Program
{
    static async Task Main(string[] args)
    {
        var connectionString = "DefaultEndpointsProtocol=https;AccountName=****;AccountKey=****;EndpointSuffix=core.windows.net";

        var serviceClient = new BlobServiceClient(connectionString);

        var query = @"""Country"" = 'United States' AND ""State"" = 'Hawaii'";

        Console.WriteLine($"Query: {query}");
        Console.WriteLine("================================================================");

        await foreach (var result in serviceClient.FindBlobsByTagsAsync(query))
        {
            Console.WriteLine($"Result: {nameof(result.ContainerName)} = {result.ContainerName}, {nameof(result.Name)} = {result.Name}");
        }
    }
}

Blob Index のクエリではキー名はダブルクオートで括る必要があり、値はシングルクオートで括る必要があります。キーと値を分けるための仕様かと思いましたが、左右入れ替えるとエラーになるので謎です。

フィルタリング結果は IAsyncEnumerable<T> を使って取得しています。

f:id:shiba-yan:20200511165743p:plain

Azure Portal を使った時と同じ結果が返ってきました。正しくフィルタリングは動作しています。

現状の Blob Index には残念な仕様が多くあるので、以下のセクションに目を通しておくとよいと思います。自分は先に読んでいなかったので割と時間を無駄にした感があります。

具体的には一部のオペレーターを同時に使うことが出来ない問題があります。例えば以下のようなクエリはエラーになるので Blob Index では現在使うことが出来ません。

"Country" = 'United States' AND "Date" >= '2019-03-19'

一番クエリとしては使いたい系だとは思いますが、これはエラーになるので諦めるしかないです。

Lifecycle Management

設定した Blob Index Tags は Lifecycle Management のフィルタリングにも使えるので、これまで以上に柔軟な制御が行えるようになりました。

f:id:shiba-yan:20200510022259p:plain

Lifecycle Management では利用できるオペレーターが増えているので、通常の Blob に対するフィルタリングより柔軟な定義が書けるようになっています。

考えられるユースケース

これまでも Blob のメタデータを Cosmos DB に入れて検索に使っていた場合には、数に寄りますが Cosmos DB を排除して全て Blob Index に移行することも出来そうです。

とはいえ Blob Index は補助的に使うのにとどめておいた方が良い感じですね。やはりセカンダリインデックス的な。これまで Blob Storage ではコンテナーとファイルパスを工夫して、日付や ID / パーティションキーでアクセス出来るようにしてきたと思います。その設計と Blob Index を組み合わせるのが最適でしょう。

基本はパス設計で解決しつつ、処理ステータスなどの変更可能な状態に関しては Blob Index に、不変かつクエリが必要ないものはメタデータに入れるといった設計上の工夫が必要だろうと感じました。

Lifecycle Management と組み合わせて、ステータスが完了なっていて変更から 30 日経過した Blob を自動削除や Cool Storage に移動といった処理も書けるので、結構使い道はあると思います。これまでは処理が完了した時には別コンテナーに移動していたと思いますが、Blob Index を使うと必要なさそうです。

Change Feed と組み合わせると結構面白そうな気がするんですが、同時に使えるリージョンが無いので GA かもうちょっとプレビュー対象のリージョンが広がるまで待つ必要があります。