次の方法で共有



November 2015

Volume 30 Number 12

Windows 10 - Search Indexer によるファイル操作の高速化

Adam Wilson | 年 11 月 2015

Search Indexer は、これまでさまざまなバージョンの Windows に搭載されてきました。エクスプローラーのライブラリ表示から、Internet Explorer のアドレス バーまで、あらゆる機能を強化してきたことに加え、スタート メニューや Outlook にも検索機能を提供します。Windows 10 では、デスクトップ コンピューターのユニバーサル Windows プラットフォーム (UWP) アプリだけでなく、すべての UWP アプリで Search Indexer の能力を利用できるようになります。これにより、Cortana の検索能力が向上するのはもちろんですが、最も重要なのは、アプリからファイル システムを操作するしくみが大幅に強化されることです。

Search Indexer により、ファイルの並べ替えやグループ化、ファイル システムの変更追跡など、興味深い操作をアプリから実行できるようになります。UWP アプリでは、名前空間 Windows.Storage と Windows.Storage.Search によって、Search Indexer のほとんどの API を使用できるようになります。アプリでは、ユーザーに優れたエクスペリエンスを提供するために、これまでも Search Indexer が使用されていました。今回は、Search Indexer を使用してファイル システムの変更の追跡方法や、ビューの迅速なレンダリング方法を調べ、アプリのクエリを強化する方法についての簡単なヒントをいくつか提示します。

ファイルとメタデータへの迅速なアクセス

ユーザーのデバイスには、大事な写真やお気に入りの曲など、数百~数千のメディア ファイルが保存されているのが普通です。どのようなプラットフォームでも、最も好まれるのは、デバイス上のファイルをすばやく反復処理し、それらのファイルに画期的な操作を実行できるアプリです。UWP は、フォーム ファクターを問わず、任意のデバイス上のファイルへのアクセスに使用できる一連のクラスを提供します。

Windows.Storage 名前空間には、ファイルやフォルダーにアクセスする基本クラスと、ほとんどのアプリが利用する基本操作が含まれています。しかし、アプリから大量のファイルやメタデータにアクセスする場合、こうしたクラスはユーザーが求めるパフォーマンス特性を提供しません。

たとえば StorageFolder.GetFilesAsync への呼び出しは、列挙するフォルダーを制御しないと、大惨事を招きます。ユーザーは 1 つのディレクトリに数億個ものファイルを置くことができます。しかし、ファイルごとに StorageFile オブジェクトの作成を試みると、瞬く間にメモリを使い果たしてしまいます。そこまで極端な例でなく、ファイルが数千個であっても、システムはその数千個のファイル ハンドルを作成してアプリ コンテナーにマーシャリングする必要があるため、呼び出しからの戻りが非常に遅くなります。アプリがこうした問題に陥るのを防ぐために、システムは StorageFileQueryResults クラスと StorageFolderQueryResults クラスを用意しています。

StorageFileQueryResults は、多くのファイルを処理するアプリを開発する場合、常に頼りになるクラスです。複雑な検索クエリの結果を巧みに列挙して変更するだけではなく、API が列挙要求を "*" に対するクエリとして扱うため、もっと平凡なケースにも機能します。

Search Indexer を利用できる場合は、アプリを高速化する最初のステップとしてこれを使用します。Search Indexer 担当のプログラム マネージャーとしての見解ではなく、これには論理的根拠があります。StorageFile オブジェクトと StorageFolder オブジェクトは Search Indexer を念頭に置いて設計されているため、このオブジェクトでキャッシュされるプロパティは Search Indexer からすばやく取得できます。Search Indexer を使用しない場合、システムはディスクとレジストリから値を検索しなくてはなりません。すると I/O が大量に行われ、アプリとシステム両方のパフォーマンスが低下します。

Search Indexer を確実に使用するには、QueryOptions オブジェクトを作成し、Search Indexer のみを使用するよう QueryOptions.IndexerOption プロパティを設定します。

QueryOptions options = new QueryOptions();
options.IndexerOption = IndexerOption.OnlyUseIndexer;

または、可能な場合に Search Indexer を使用するよう設定します。

options.IndexerOption = IndexerOption.UseIndexerWhenAvailable;

ファイル操作が遅くてもアプリがロックされたり、ユーザー エクスペリエンスが損なわれたりしない場合は、IndexerOption.UseIndexerWhenAvailable を使用することをお勧めします。このようにすると、ファイルの列挙に Search Indexer の使用を試みますが、必要に応じて非常に低速なディスク操作に切り替えます。IndexerOption.OnlyUseIndexer は、低速なファイル操作を待機するよりも結果を返さない方がよい場合に最適です。システムは Search Indexer が無効のときは結果を返しませんが、即座に戻るため、アプリのすばやい反応を保つことができます。

簡単な列挙だけのために QueryOptions オブジェクトを作成するのは、少々やり過ぎのように思えることもあります。このような場合、Search Indexer があれば不安に思う必要はありません。フォルダーのコンテンツを制御できる場合は、StorageFolder.GetItemsAsync を呼び出す意味があります。コード行の記述が容易になり、ディレクトリに少数のファイルしかない場合はパフォーマンスの問題が露呈しません。

また、不要な StorageFile オブジェクトや StorageFolder オブジェクトを作成しないようにすれば、ファイルの列挙が高速になります。たとえ Search Indexer を使用しているときでも StorageFile を開くと、システムはファイル ハンドルを作成し、プロパティ データをいくつか収集して、アプリのプロセスにマーシャリングしなければならなくなります。最初にこのようなオブジェクトを作成しないだけでも、多くの場合、こうしたプロセス間通信 (IPC) が持つ遅延の性質を避けることができます。

重要なのは、Search Indexer の支援がない StorageFileQueryResult オブジェクトは、内部で StorageFiles をまったく作成しないことです。StorageFiles の作成が必要になるのは、GetFilesAsync を通じて要求される場合です。それまでは、システムが比較的軽量のファイル リストのみをメモリに保持します。

多数のファイルを列挙する場合は、必要に応じてファイルのグループでページを切り替えるために、GetFilesAsync のバッチ処理機能を使用する方法がお勧めです。こうすると、次のグループが作成されるのを待つ間に、アプリはファイルでバックグラウンド処理を実行できます。図 1 のコードは、この簡単な例を示しています。

図 1 GetFilesAsync

uint index = 0, stepSize = 10;
IReadOnlyList<StorageFile> files = await queryResult.GetFilesAsync(index, stepSize);
index += 10;          
while (files.Count != 0)
{
  var fileTask = queryResult.GetFilesAsync(index, stepSize).AsTask();
  foreach (StorageFile file in files)
  {
    // Do the background processing here   
  }
  files = await fileTask;
  index += 10;
}

これは、多くの Windows アプリが既に使用しているコーディング パターンです。ステップ サイズに変化を持たせれば、アプリの最初のビューの応答性を高くするために適切な数の項目を取り出しながら、バックグラウンドで残りのファイルをすばやく準備できるようになります。

プロパティのプリフェッチも、アプリを簡単に高速化する有効な手段です。プロパティをプリフェッチすることで、アプリは特定のファイル プロパティに関心があることをシステムに通知できます。システムは、そのプロパティを Search Indexer から取得すると同時に、一連のファイルを列挙して StorageFile オブジェクトにキャッシュします。これにより、ファイルが返されるときにプロパティを断片的に収集する場合に比べれば、パフォーマンスが簡単に向上します。

プロパティ値のプリフェッチは QueryOptions オブジェクトで設定します。PropertyPrefetchOptions を使用することで、いくつか一般的なシナリオがサポートされますが、要求されるプロパティを、Windows でサポートされる値に合わせてアプリでカスタマイズすることもできます。コードは次のように簡単です。

QueryOptions options = new QueryOptions();
options.SetPropertyPrefetch(PropertyPrefetchOptions.ImageProperties,
  new String[] { });

上記の場合、アプリは ImageProperties を使用し、他のプロパティは使用しません。システムは、クエリ結果の列挙中に ImageProperties をメモリにキャッシュして、すぐに使えるようにします。

また、プリフェッチによってパフォーマンスを向上するには、プロパティをインデックスに格納する必要があります。こうしないと、システムは値を見つけるために依然としてファイルにアクセスしなければならず、その動作はきわめて遅くなります。Microsoft Windows デベロッパー センターのプロパティ システムに関するページ (bit.ly/1LuovhT、英語) では、Search Indexer で使用できるプロパティの情報が網羅されています。プロパティの説明の中で isColumn = true となっているものだけを探してください。これは、そのプロパティがプリフェッチ可能であることを示しています。

これらの強化点をすべて併用すれば、コードをはるかに高速にすることができます。簡単な例として、コンピューター上にあるすべての画像と、その高さを取得するアプリを作成しました。これは、フォト ビューアー アプリが、ユーザーの写真のコレクションを表示するために取るべき最初のステップです。

ファイルを列挙する 3 つの方法を試して、それらの違いをテストしました。最初のテストでは、Search Indexer を有効にして単純なコードを実行しました (図 2 参照)。2 つ目のテストでは、プロパティのプリフェッチとファイルのページ切り替えを実行するコードを使用しました (図 3 参照)。3 つ目のテストでもプロパティのプリフェッチとファイルのページ切り替えを実行しましたが、Search Indexer を無効にしました。コードは図 3 のものと同じですが、コメントに示したように 1 行だけ変更しています。

図 2 ライブラリを列挙する単純なコード

StorageFolder folder = KnownFolders.PicturesLibrary;
QueryOptions options = new QueryOptions(
  CommonFileQuery.OrderByDate, new String[] { ".jpg", ".jpeg", ".png" });          
options.IndexerOption = IndexerOption.OnlyUseIndexer;
StorageFileQueryResult queryResult = folder.CreateFileQueryWithOptions(options);
Stopwatch watch = Stopwatch.StartNew();          
IReadOnlyList<StorageFile> files = await queryResult.GetFilesAsync();
foreach (StorageFile file in files)
{                
  IDictionary<string, object> size =
    await file.Properties.RetrievePropertiesAsync(
    new String[] { "System.Image.VerticalSize" });
  var sizeVal = size["System.Image.VerticalSize"];
}           
watch.Stop();
Debug.WriteLine("Time to run the slow way: " + watch.ElapsedMilliseconds + " ms");

図 3 ライブラリの列挙を最適化したコード

StorageFolder folder = KnownFolders.PicturesLibrary;
QueryOptions options = new QueryOptions(
  CommonFileQuery.OrderByDate, new String[] { ".jpg", ".jpeg", ".png" });
// Change to DoNotUseIndexer for trial 3
options.IndexerOption = IndexerOption.OnlyUseIndexer;
options.SetPropertyPrefetch(PropertyPrefetchOptions.None, new String[] { "System.Image.VerticalSize" });
StorageFileQueryResult queryResult = folder.CreateFileQueryWithOptions(options);
Stopwatch watch = Stopwatch.StartNew();
uint index = 0, stepSize = 10;
IReadOnlyList<StorageFile> files = await queryResult.GetFilesAsync(index, stepSize);
index += 10;
// Note that I'm paging in the files as described
while (files.Count != 0)
{
  var fileTask = queryResult.GetFilesAsync(index, stepSize).AsTask();
  foreach (StorageFile file in files)
  {
// Put the value into memory to make sure that the system really fetches the property
    IDictionary<string,object> size =
      await file.Properties.RetrievePropertiesAsync(
      new String[] { "System.Image.VerticalSize" });
    var sizeVal = size["System.Image.VerticalSize"];                   
  }
  files = await fileTask;
  index += 10;
}
watch.Stop();
Debug.WriteLine("Time to run: " + watch.ElapsedMilliseconds + " ms");

プリフェッチを行ったコードとそうでないコードでは、パフォーマンスの違いが明らかです (図 4 参照)。

図 4 プリフェッチを行う場合と行わない場合の結果

テスト ケース (デスクトップ コンピューター上の 2,600 個の画像) サンプル数 10 個以上の平均実行時間
単純なコードで Search Indexer あり 9,318 ミリ秒
すべての最適化したコードで Search Indexer あり 5,796 ミリ秒
最適化したコードで Search Indexer なし 20,248 ミリ秒 (コールド起動 48,420 ミリ秒)

今回取り上げた簡単な最適化を適用するだけで、単純なコードのパフォーマンスが約 2 倍向上する可能性があり、パターンも既に実証されています。Search Indexer チームでは、Windows のあらゆるバージョンをリリースする前に、必ずアプリ チームと協力して、フォトや Groove ミュージックなどのアプリが可能な限り機能するようにしています。これらのパターンはこの試みから誕生したものです。UWP のリリース時に最初の UWP アプリのコードから直接持ってきたものなので、開発中のアプリに直接適用しても問題ありません。

ファイル システムでの変更の追跡

先ほど述べたように、1 か所ですべてのファイルを列挙するとリソースが大量に消費されます。多くの場合、ユーザーは古いファイルには関心がありません。必要なのは、撮影したばかりの写真、ダウンロードしたばかりの曲、最後に編集したドキュメントなどです。最新のファイルを先頭に表示するには、アプリでファイル システムの変更を簡単に追跡して、最後に作成または変更されたファイルを見つけます。

変更を追跡する方法は、アプリがバックグラウンドとフォアグラウンドのどちらで実行されているかによって異なります。アプリがフォアグラウンドにある場合、特定のクエリで変更が通知されるように、StorageFileQueryResult オブジェクトの ContentsChanged イベントを使用します。アプリがバックグラウンドにある場合は、何かが変更されたときに通知されるように、StorageLibraryContentChangedTrigger に登録します。これらはいずれも、何かが変更されたことをアプリに通知するだけの簡易通知なので、変更されたファイルについての情報は含まれません。

最近変更または作成されたファイルを見つけるために、システムは System.Search.GatherTime プロパティを提供します。このプロパティは、インデックスを付けた場所のすべてのファイルに設定され、Search Indexer がファイルへの変更を察知した最後の時間を追跡します。このプロパティは、システム クロックに基づいて絶えず更新されるため、アプリがこの値を信頼するかどうかは、システム時刻を手動で変更するユーザーや、タイム ゾーンの切り替え、サマータイムについてのよくある免責事項が当てはまります。

フォアグラウンドで変更追跡イベントに登録するのは簡単です。アプリで関心のあるスコープをカバーする StorageFileQueryResult オブジェクトを作成後、ContentsChanged イベントに登録するだけです。

StorageFileQueryResult resultSet = photos.CreateFileQueryWithOptions(option);
resultSet.ContentsChanged += resultSet_ContentsChanged;

このイベントは、結果セットで何かが変化したときに必ず発生します。その後、最近変更された 1 つまたは複数のファイルを見つけることができます。

バックグラウンドでの変更追跡は、これよりも若干複雑です。アプリは、デバイス上のライブラリでファイルが変更されたときに通知を受け取るよう登録できますが、複雑なクエリやスコープはサポートされません。つまり、アプリは本当に関心があるものが変更されたかどうかを確認するために、ちょっとした作業が必要になります。

余談ですが、興味深いことに、アプリがライブラリの変更についての通知に登録することしかできず、ファイルの種類に基づいていないのは、Search Indexer の設計が理由です。ディスク上のファイルの場所に基づいてクエリをフィルター処理するのは、ファイルの種類に基づいて一致をクエリするよりもはるかに高速です。最初のテストでデバイスのパフォーマンスが悪くなったのはこれが原因です。パフォーマンスのヒントについては後ほど取り上げますが、重要なのは、クエリ結果をファイルの場所に基づいてフィルター処理するのは、他のフィルター処理操作よりもきわめて高速になるという点です。

サンプル コードを使ってバックグラウンド タスクを登録する手順をブログ (bit.ly/1iPUVIo、英語) に掲載しましたが、ここではもう少し興味深い手順を紹介します。アプリでは、まずバックグラウンドのトリガーを作成しなければなりません。

StorageLibrary library =
  await StorageLibrary.GetLibraryAsync(KnownLibraryId.Pictures);
StorageLibraryContentChangedTrigger trigger =
  StorageLibraryContentChangedTrigger.Create(library);

複数の場所を追跡するアプリの場合は、ライブラリのコレクションからトリガーを作成することもできます。今回は、画像ライブラリのみを追跡します。1 つの場所の追跡は、アプリにとって最もよくあるシナリオの 1 つです。ただし、アプリには変更の追跡を試みるライブラリにアクセスできる適切な権限が必要です。アクセス権がないと、アプリで StorageLibrary オブジェクトの作成を試みると、システムはアクセス拒否例外を返します。

Windows モバイルを搭載する デバイスでは、システムが新しい画像を必ず画像ライブラリの場所に書き込むため、このシナリオが特に有効です。ユーザーが設定ページで、画像ライブラリのフォルダーを変更しても、問題は生じません。

アプリは、BackgroundExecutionManager を使用してバックグラウンド タスクを登録し、バックグラウンド タスクがアプリに組み込まれるようにしなければなりません。アプリがフォアグラウンドで実行中にバックグラウンド タスクがアクティブになることもあるため、すべてのコードで、ファイル アクセスやレジストリ アクセスが競合する可能性を認識しておく必要があります。

登録が完了したら、登録したライブラリに変更があるたびにアプリが呼び出されるようになります。この中には、アプリにとって関心がないファイルや、処理できないファイルが含まれていることがあります。その場合、バックグラウンド処理が無駄にならないように、バックグラウンド タスクがトリガーされたらすぐに制約の厳しいフィルターを適用するのが最適な方法です。

最後に変更または追加されたファイルを見つけるためには、Search Indexer に対してクエリを 1 つ実行するだけです。アプリにとって関心がある時間範囲を指定して、すべてのファイルを要求するだけです。他のクエリと同様、必要に応じて並べ替え機能やグループ化機能も利用できます。Search Indexer の内部では協定世界時が使用されるため、事前にすべての時間文字列を協定世界時に変換しておきます。次のようにクエリを構築します。

QueryOptions options = new QueryOptions();
DateTimeOffset lastSearchTime = DateTimeOffset.UtcNow.AddHours(-1);
// This is the conversion to Zulu time, which is used by the indexer
string timeFilter = "System.Search.GatherTime:>=" +
  lastSearchTime.ToString("yyyy\\-MM\\-dd\\THH\\:mm\\:ss\\Z")
options.ApplicationSearchFilter += timeFilter;

上記の例では、過去の時間すべてを範囲として結果を取得しようとしています。前回のクエリの時間範囲を保存しておいて、それをクエリに使用してもかまいませんが、いずれにせよすべての DateTimeOffset で機能します。ファイルの一覧が返されたら、その一覧を列挙したり、その一覧を使用して新たな追跡を開始することができます。

収集する時間範囲を使用するこれら 2 つの変更追跡方法を組み合わせれば、UWP アプリから簡単にファイル システムの変更を追跡して、ディスク上の変化に対応できるようになります。これらの API は Windows の歴史から見れば比較的新しいものですが、Windows 10 に搭載されているフォト アプリ、Groove ミュージック アプリ、OneDrive アプリ、Cortana アプリ、映画 & テレビ アプリで使用されています。このような優れたエクスペリエンスを提供していることがわかれば、作成するアプリにも安心してお使いいただけると思います。

一般的なベスト プラクティス

Search Indexer を使用するアプリには、バグを回避し、できる限り高速に動作するように、遵守すべき注意点がいくつかあります。この注意点を踏まえれば、アプリのパフォーマンスが重要な場面で、過度に複雑なクエリの実行を避け、プロパティの列挙を正しく使用し、インデックス化による遅延を把握することができるようになります。

クエリの設計方法が、パフォーマンスに大きな影響を及ぼすことがあります。Search Indexer が基盤となるデータベースに対してクエリを実行しているときに、情報がディスク上でレイアウトされる方法によって、高速になるクエリもあります。ファイルの場所に基づいたフィルター処理では、Search Indexer がインデックスの大部分をクエリからすばやく除去できるので、常に高速になります。場所に基づくフィルターは、クエリ条件を照合するときに取得および比較する文字列が少なくなるため、プロセッサ時間や I/O 時間を節約できます。

Search Indexer は正規表現を処理できますが、パフォーマンスを低下させることがわかっている正規表現もあります。Search Indexer のクエリに最も含めてはいけないのが、サフィックスの検索です。この検索では、特定の値で終わる条件をすべてクエリします。たとえば、「*tion」というクエリは、「tion」で終わる単語を含むドキュメントをすべて検索します。インデックスは、各トークンの先頭文字で並べ替えられるため、このようなクエリに一致する条件の検索は高速には実行されません。全インデックスを各トークンにデコードして検索条件と比較しなければならないため、きわめて低速になります。

列挙によってクエリを高速化することは可能ですが、多言語ビルドでは予期しない動作になることがあります。検索システムの作成経験がある開発者なら、列挙に基づく比較の方が、文字列の比較よりもはるかに高速であることを知っています。これは、Search Indexer にも当てはまります。プロパティ システムは、負荷が高い文字列比較を開始する前に、結果をフィルター処理して項目数を絞り込むために、多数の列挙を提供します。一般的には、音楽やドキュメントなど、アプリが処理できるファイルの種類で結果を絞り込むために System.Kind フィルターを使用します。

列挙を使用する場合は必ず把握しておかなければならない共通の誤解があります。ユーザーが音楽ファイルのみを探す場合、米国版の Windows では System.Kind:=music をクエリに追加すれば、問題なく検索結果が絞り込まれ、クエリが高速になります。これは他のいくつかの言語でも、おそらく国際化のテストに合格する程度には機能しますが、「music」を英語として認識できず、現地の言語で解析するシステムではテストに失敗します。

System.Kind など、列挙を正しい方法で使用するには、アプリが値を検索条件ではなく、列挙を意図していることを明確に示します。そのためには、enumeration#value 構文を使用します。たとえば、音楽のみに結果を絞り込む場合は、System.Kind:=System.Kind#Music とするのが正しい方法です。これは、Windows が対応しているすべての言語で機能し、システムが音楽ファイルと認識するファイルのみに結果を絞り込みます。

Advanced Query Syntax (AQS) を適切にエスケープすれば、ユーザーがクエリの問題を再現するのが難しくなくなります。AQS は、引用符やかっこを含め、クエリの処理に影響を及ぼす可能性がある多数の特性を所持しています。つまり、アプリは、このような文字を含む可能性があるクエリ条件をすべて慎重にエスケープする必要があります。たとえば、Document(8).docx を検索すると解析エラーになり、不適切な結果が返されるので、アプリではその条件を Document%288%29.docx とエスケープします。これにより、システムがかっこをクエリの一部として解析しなくなり、インデックス内で検索条件に一致する項目が返されます。

AQS のすべての特性の詳細と、クエリが適切かどうかを確認する方法については bit.ly/1Fhacfl (英語) を参照してください。このページには役立つ情報がたくさん提供されていて、今回紹介したヒントが詳しく説明されています。

インデックスの作成による遅延にも注意が必要です。インデックスの作成は瞬時には行われません。つまり、Search Indexer に基づいてインデックスや通知に表示される項目は、ファイルが書き込まれるタイミングより若干遅れます。標準システムの読み込みでは、この遅延は約 100 ミリ秒程度で、大半のアプリがファイル システムのクエリにかかる時間よりも短く、あまり顕著に感じられません。ただし、コンピューターの中で数千個のファイルを移動すると、Search Indexer の動作は明らかに遅くなります。

このような場合、アプリには 2 つの推奨事項があります。まず、アプリが最も関心のあるファイル システムに対して開いたクエリを保持します。通常、アプリの検索対象のファイル システムの場所で StorageFileQueryResult オブジェクトを作成することで、これを行います。アプリがクエリを開いていることを Search Indexer が確認すると、他のすべてのスコープよりも優先して、このスコープのインデックスを作成します。ただし、これを必要以上に大きなスコープには実行しないようにしてください。Search Indexer は、変更をできる限りすばやく処理するために、システムのバックオフとユーザーのアクティブな通知を配慮しなくなるため、これが発生している間、ユーザーが感じるぐらいにシステムのパフォーマンスが低下します。

もう 1 つの推奨方法は、システムがファイル操作を行っていることをユーザーに通知することです。Cortana など、アプリ UI の上部にメッセージを表示するアプリもあれば、複雑なクエリの実行を停止して、その操作の簡易版を提示するアプリもあります。作成しているアプリに合ったエクスペリエンスを適宜選択してください。

まとめ

今回は、Windows 10 の Search Indexer と Windows Storage API を利用して可能なことを簡単に取り上げました。クエリを使用してアプリのアクティブ化にコンテキストを渡す方法の詳細や、バックグラウンド タスクをトリガーするコード サンプルについては、チームのブログ (bit.ly/1iPUVIo、英語) を参照してください。Search Indexer チームは、検索 API を使いやすくするために、絶えず開発者と連携しています。API の機能や、必要な追加機能について、ぜひご意見をお寄せください。


Adam Wilsonは、Windows Developer Ecosystem and Platform チームのプログラム マネージャーです。Windows Search Indexer とプッシュ通知の信頼性向上に取り組んでいます。以前は、Windows Phone 8.1 のストレージ API を担当していました。連絡先は adwilso@microsoft.com (英語のみ) です。

この記事のレビューに協力してくれた技術スタッフの Sami Khoury に心より感謝いたします。
Sami Khoury は、Windows Developer Ecosystem and Platform チームのエンジニアです。Windows Search Indexer の開発リーダーです。