チュートリアル:プッシュ API を使用してインデックス作成を最適化する

Azure AI Search は、検索インデックスにデータをインポートする 2 つの基本的な手法をサポートしています。1 つは、プログラムを使用してインデックスにデータを "プッシュ" する方法、もう 1 つは、サポート対象データ ソースで Azure AI Search のインデクサーをポイントしてデータを "プル" する方法です。

このチュートリアルでは、プッシュ モデルを使用して効率的にデータのインデックスを作成する方法について説明します。その際、要求はバッチで処理し、エクスポネンシャル バックオフの再試行戦略を使用しています。 サンプル アプリケーションをダウンロードし、実行できます。 この記事では、アプリケーションの主な特徴と、データのインデックスを作成する際に考慮すべき事柄について説明します。

このチュートリアルでは、C# と、Azure SDK for .NET の Azure.Search.Documents ライブラリを使って、以下のタスクを実行します。

  • インデックスを作成する
  • さまざまなバッチ サイズをテストして最も効率的なサイズを特定する
  • バッチに非同期でインデックスを付ける
  • 複数のスレッドを使用してインデックスの作成速度を高める
  • エクスポネンシャル バックオフの再試行戦略を使用して、失敗したドキュメントを再試行する

Azure サブスクリプションをお持ちでない場合は、開始する前に 無料アカウント を作成してください。

前提条件

このチュートリアルには、次のサービスとツールが必要です。

ファイルのダウンロード

このチュートリアルのソース コードは、Azure-Samples/azure-search-dotnet-samples GitHub リポジトリの optimize-data-indexing/v11 フォルダーにあります。

重要な考慮事項

インデックス作成の速度に影響する要因を次に示します。 詳細については、大規模なデータ セットのインデックス作成に関する記事を参照してください。

  • サービス レベルとパーティションとレプリカの数 - パーティションを追加するか、レベルをアップグレードすると、インデックス作成の速度が向上します。
  • インデックス スキーマの複雑さ - フィールドとフィールド プロパティを追加すると、インデックス作成の速度が低下します。 インデックスが小さいほど、インデックス作成は速くなります。
  • バッチ サイズ - 最適なバッチ サイズは、インデックス スキーマとデータセットによって異なります。
  • スレッドまたはワーカーの数 - シングル スレッドでは、インデックスの作成速度が十分に発揮されません。
  • 再試行戦略 - エクスポネンシャル バックオフの再試行戦略は、インデックス作成を最適化するためのベスト プラクティスです。
  • ネットワーク データ転送速度 - データ転送速度が制限要因になる場合があります。 データのインデックス作成を Azure 環境内から行えば、データの転送速度が上がります。

1 - Azure AI Search サービスを作成する

このチュートリアルをすべて実行するには、ポータルで作成できる Azure AI Search Service が必要です。 インデックスの作成速度を正確にテストして最適化できるよう、運用環境で使用するのと同じレベルを使用することをお勧めします。

このチュートリアルでは、キーベースの認証を使います。 管理者の API キーをコピーして、appsettings.json ファイルに貼り付けます。

  1. Azure portal にサインインし、ご使用の検索サービスの [概要] ページで、URL を入手します。 たとえば、エンドポイントは https://mydemo.search.windows.net のようになります。

  2. [設定]>[キー] で、サービスに対する完全な権限の管理キーを取得します。 管理キーをロールオーバーする必要がある場合に備えて、2 つの交換可能な管理キーがビジネス継続性のために提供されています。 オブジェクトの追加、変更、および削除の要求には、主キーまたはセカンダリ キーのどちらかを使用できます。

    Get an HTTP endpoint and access key

2 - 環境を設定する

  1. Visual Studio を起動し、OptimizeDataIndexing.sln を開きます。
  2. ソリューション エクスプローラーで appsettings.json を開き、接続情報を指定します。
{
  "SearchServiceUri": "https://{service-name}.search.windows.net",
  "SearchServiceAdminApiKey": "",
  "SearchIndexName": "optimize-indexing"
}

3 - コードを調べる

appsettings.json を更新したら、OptimizeDataIndexing.sln のサンプル プログラムをビルドして実行できます。

このコードは、「クイック スタート: Azure SDK を使用したフルテキスト検索」の C# セクションから派生しています。 .NET SDK の取り扱いの基本については、そちらの記事でさらに詳しい情報をご覧いただけます。

この単純な C# または .NET コンソール アプリは次のタスクを実行します。

  • (Address クラスも参照する) C# Hotel クラスのデータ構造に基づいて新しいインデックスを作成する
  • さまざまなバッチ サイズをテストして最も効率的なサイズを特定する
  • データのインデックスを非同期的に作成する
    • 複数のスレッドを使用してインデックスの作成速度を高める
    • エクスポネンシャル バックオフの再試行戦略を使用して、失敗した項目を再試行する

プログラムを実行する前に、時間を取って、このサンプルのコードとインデックスの定義を確認しましょう。 関連するコードはいくつかのファイルにあります。

  • Hotel.csAddress.cs には、インデックスを定義するスキーマが含まれています。
  • DataGenerator.cs には、大量のホテル データを簡単に作成できる単純なクラスが含まれています。
  • ExponentialBackoff.cs には、この記事で説明するインデックス作成プロセスを最適化するためのコードが含まれています
  • Program.cs には、Azure AI Search のインデックスの作成と削除、バッチ単位でのデータのインデックス作成、さまざまなバッチ サイズのテストを行う関数が含まれています。

インデックスを作成する

このサンプル プログラムでは、Azure SDK for .NET を使用して、Azure AI Search のインデックスを定義して作成します。 FieldBuilder クラスを利用して、C# データ モデル クラスからインデックス構造を生成します。

データ モデルは、Hotel クラスによって定義されています。Hotel クラスには、Address クラスへの参照も含まれています。 FieldBuilder は、複数のクラス定義をドリルダウンして、このインデックスの複雑なデータ構造を生成します。 メタデータ タグは、検索や並べ替えが可能かどうかなど、各フィールドの属性を定義するために使用されます。

Hotel.cs ファイルの次のスニペットは、単一のフィールドと、別のデータ モデル クラスへの参照を指定する方法を示しています。

. . .
[SearchableField(IsSortable = true)]
public string HotelName { get; set; }
. . .
public Address Address { get; set; }
. . .

Program.cs ファイルでは、インデックスが、FieldBuilder.Build(typeof(Hotel)) メソッドで生成された名前とフィールド コレクションを使用して定義され、次のように作成されます。

private static async Task CreateIndexAsync(string indexName, SearchIndexClient indexClient)
{
    // Create a new search index structure that matches the properties of the Hotel class.
    // The Address class is referenced from the Hotel class. The FieldBuilder
    // will enumerate these to create a complex data structure for the index.
    FieldBuilder builder = new FieldBuilder();
    var definition = new SearchIndex(indexName, builder.Build(typeof(Hotel)));

    await indexClient.CreateIndexAsync(definition);
}

データを生成する

DataGenerator.cs ファイルには、テスト用のデータを生成するための単純なクラスが実装されています。 インデックス作成用に一意の ID を持った大量のドキュメントを簡単に作成できるようにすることが、このクラスの唯一の目的です。

たとえば、一意の ID を持った 100,000 件のホテルのリストを取得する場合、次のコード行を実行します。

long numDocuments = 100000;
DataGenerator dg = new DataGenerator();
List<Hotel> hotels = dg.GetHotels(numDocuments, "large");

このサンプルのテスト用に用意されたホテルには、smalllarge の 2 つのサイズがあります。

インデックスのスキーマは、インデックス作成の速度に影響します。 そのため、このチュートリアルを実行し終えたら、意図したインデックス スキーマに最適なデータを生成するようこのクラスを変換することをお勧めします。

4 - バッチ サイズをテストする

Azure AI Search は、1 つまたは複数のドキュメントをインデックスに読み込む次の API をサポートしています。

ドキュメントのインデックスをバッチ単位で作成することによって、インデックス作成のパフォーマンスが大幅に向上します。 バッチの最大サイズは、1,000 ドキュメントまたは約 16 MB です。

実際のデータに最適なバッチ サイズを見極めることが、インデックスの作成速度を最適化するうえで重要な要素となります。 最適なバッチ サイズは主に、次の 2 つの要因によって左右されます。

  • インデックスのスキーマ
  • データのサイズ

最適なバッチ サイズはインデックスとデータによって異なるため、さまざまなバッチ サイズをテストしながら、実際のシナリオにおいてインデックスの作成速度が最速となるサイズを見極めるのが最善のアプローチとなります。

次の関数は、バッチ サイズをテストするための単純なアプローチを示したものです。

public static async Task TestBatchSizesAsync(SearchClient searchClient, int min = 100, int max = 1000, int step = 100, int numTries = 3)
{
    DataGenerator dg = new DataGenerator();

    Console.WriteLine("Batch Size \t Size in MB \t MB / Doc \t Time (ms) \t MB / Second");
    for (int numDocs = min; numDocs <= max; numDocs += step)
    {
        List<TimeSpan> durations = new List<TimeSpan>();
        double sizeInMb = 0.0;
        for (int x = 0; x < numTries; x++)
        {
            List<Hotel> hotels = dg.GetHotels(numDocs, "large");

            DateTime startTime = DateTime.Now;
            await UploadDocumentsAsync(searchClient, hotels).ConfigureAwait(false);
            DateTime endTime = DateTime.Now;
            durations.Add(endTime - startTime);

            sizeInMb = EstimateObjectSize(hotels);
        }

        var avgDuration = durations.Average(timeSpan => timeSpan.TotalMilliseconds);
        var avgDurationInSeconds = avgDuration / 1000;
        var mbPerSecond = sizeInMb / avgDurationInSeconds;

        Console.WriteLine("{0} \t\t {1} \t\t {2} \t\t {3} \t {4}", numDocs, Math.Round(sizeInMb, 3), Math.Round(sizeInMb / numDocs, 3), Math.Round(avgDuration, 3), Math.Round(mbPerSecond, 3));

        // Pausing 2 seconds to let the search service catch its breath
        Thread.Sleep(2000);
    }

    Console.WriteLine();
}

このサンプルではすべてのドキュメントが同じサイズですが、実際にはそうとは限らないため、検索サービスに送信するデータのサイズを見積もります。 そのために使用しているのが以下の関数です。オブジェクトを JSON に変換したうえで、そのサイズをバイト単位で調べます。 この手法によって、インデックスの作成速度 (MB/秒) の観点から最も効率のよいバッチ サイズを特定することができます。

// Returns size of object in MB
public static double EstimateObjectSize(object data)
{
    // converting object to byte[] to determine the size of the data
    BinaryFormatter bf = new BinaryFormatter();
    MemoryStream ms = new MemoryStream();
    byte[] Array;

    // converting data to json for more accurate sizing
    var json = JsonSerializer.Serialize(data);
    bf.Serialize(ms, json);
    Array = ms.ToArray();

    // converting from bytes to megabytes
    double sizeInMb = (double)Array.Length / 1000000;

    return sizeInMb;
}

この関数には、SearchClient に加え、バッチ サイズごとのテストの試行回数を指定する必要があります。 各バッチのインデックス作成時間にはばらつきがあることが考えられるため、統計的に有意な結果を得るために、各バッチは既定で 3 回試行されます。

await TestBatchSizesAsync(searchClient, numTries: 3);

この関数を実行すると、次のような出力がコンソールに表示されます。

Output of test batch size function

どのバッチ サイズが最も効率的であるかを見極め、そのバッチ サイズをチュートリアルの次の手順で使用します。 バッチ サイズが異なっていても、1 秒あたりのバイト数 (MB) が頭打ちになっていることがわかります。

5 - データのインデックスを作成する

使用するバッチ サイズを特定できたので、データのインデックス作成を開始します。 データのインデックスを効率よく作成するために、このサンプルでは、次のことを行っています。

  • 複数のスレッド (ワーカー) を使用する。
  • エクスポネンシャル バックオフの再試行戦略を実装する。

41 行目から 49 行目までをコメント解除し、プログラムを再実行します。 この実行では、パラメーターを変更せずにコードを実行した場合、サンプルにより、最大 100,000 個のドキュメントのバッチが生成され、送信されます。

複数のスレッド (ワーカー) を使用する

Azure AI Search のインデックス作成速度を最大限に引き出すには、おそらく複数のスレッドを使用して、インデックス作成要求のバッチをサービスに対して同時に送信します。

上記の重要な考慮事項の一部は、最適なスレッド数に影響する可能性があります。 さまざまなスレッド数でこのサンプルを変更、テストすることによって、実際のシナリオに最適なスレッド数を見極めてください。 ただし、複数のスレッドを同時に実行すれば、効率向上の利点はおおよそ活かすことができるはずです。

検索サービスに対する要求を増やしていくと、要求が完全には成功しなかったことを示す HTTP 状態コードが返されることがあります。 インデックスの作成時によく発生する HTTP 状態コードは次の 2 つです。

  • 503 Service Unavailable - このエラーは、システムが過負荷の状態にあり、この時点では要求を処理できないことを示します。
  • 207 Multi-Status - このエラーは、ドキュメントの一部は成功しましたが、少なくとも 1 つが失敗したことを示します。

エクスポネンシャル バックオフの再試行戦略を実装する

失敗した要求は、エクスポネンシャル バックオフの再試行戦略を使用して再試行する必要があります。

503 などで失敗した要求は、Azure AI Search の .NET SDK によって自動的に再試行されますが、207 には、独自の再試行ロジックを実装することをお勧めします。 Polly などのオープンソース ツールは、再試行戦略で役立ちます。

このサンプルでは、エクスポネンシャル バックオフの再試行戦略を独自に実装します。 まず、失敗した要求の maxRetryAttemptsdelay (初期延期期間) を含め、いくつかの変数を定義します。

// Create batch of documents for indexing
var batch = IndexDocumentsBatch.Upload(hotels);

// Create an object to hold the result
IndexDocumentsResult result = null;

// Define parameters for exponential backoff
int attempts = 0;
TimeSpan delay = delay = TimeSpan.FromSeconds(2);
int maxRetryAttempts = 5;

インデックス作成捜査の結果は変数 IndexDocumentResult result に格納されます。 下の画像のようにバッチに含まれるドキュメントが失敗したかどうかを確認できるため、この変数は重要です。 部分的に失敗している場合、失敗したドキュメントの ID に基づいて新しいバッチが作成されます。

RequestFailedException 例外は要求が完全に失敗しており、再試行が必要であることを示すため、これもキャッチする必要があります。

// Implement exponential backoff
do
{
    try
    {
        attempts++;
        result = await searchClient.IndexDocumentsAsync(batch).ConfigureAwait(false);

        var failedDocuments = result.Results.Where(r => r.Succeeded != true).ToList();

        // handle partial failure
        if (failedDocuments.Count > 0)
        {
            if (attempts == maxRetryAttempts)
            {
                Console.WriteLine("[MAX RETRIES HIT] - Giving up on the batch starting at {0}", id);
                break;
            }
            else
            {
                Console.WriteLine("[Batch starting at doc {0} had partial failure]", id);
                Console.WriteLine("[Retrying {0} failed documents] \n", failedDocuments.Count);

                // creating a batch of failed documents to retry
                var failedDocumentKeys = failedDocuments.Select(doc => doc.Key).ToList();
                hotels = hotels.Where(h => failedDocumentKeys.Contains(h.HotelId)).ToList();
                batch = IndexDocumentsBatch.Upload(hotels);

                Task.Delay(delay).Wait();
                delay = delay * 2;
                continue;
            }
        }

        return result;
    }
    catch (RequestFailedException ex)
    {
        Console.WriteLine("[Batch starting at doc {0} failed]", id);
        Console.WriteLine("[Retrying entire batch] \n");

        if (attempts == maxRetryAttempts)
        {
            Console.WriteLine("[MAX RETRIES HIT] - Giving up on the batch starting at {0}", id);
            break;
        }

        Task.Delay(delay).Wait();
        delay = delay * 2;
    }
} while (true);

ここでは、エクスポネンシャル バックオフのコードを呼び出しやすいよう関数にラップしています。

さらに、アクティブなスレッドを管理するための関数も別途作成します。 その関数は、簡潔にするためにここでは省略していますが、ExponentialBackoff.cs でご覧いただけます。 その関数は、次のコマンドで呼び出すことができます。hotels はアップロードするデータで、1000 はバッチ サイズ、また 8 は、コンカレント スレッド数です。

await ExponentialBackoff.IndexData(indexClient, hotels, 1000, 8);

この関数を実行すると、次のような出力が表示されます。

Output of index data function

ドキュメントのバッチが失敗すると、処理に失敗したこと、またそのバッチが再試行されていることを示すエラーが出力されます。

[Batch starting at doc 6000 had partial failure]
[Retrying 560 failed documents]

関数の実行が完了したら、すべてのドキュメントがインデックスに追加されたことを確認できます。

6 - インデックスを調べる

プログラムの実行後に、ポータルの Search エクスプローラーまたはプログラムを使用して、設定された検索インデックスを確認できます。

プログラム

インデックス内のドキュメント数は、主に 2 つの方法で確認できます。Count Documents API を使用する方法と Get Index Statistics API を使用する方法です。 どちらのパスも処理に時間がかかるため、最初に返されるドキュメント数が予想より少なくても心配する必要はありません。

ドキュメントのカウント

Count Documents 操作は、検索インデックス内のドキュメントの数を取得します。

long indexDocCount = await searchClient.GetDocumentCountAsync();

インデックス統計の取得

Get Index Statistics 操作は、現在のインデックスに対するドキュメントの数と記憶域の使用状況を返します。 インデックスの統計は、ドキュメント数よりも更新に時間がかかります。

var indexStats = await indexClient.GetIndexStatisticsAsync(indexName);

Azure portal

Azure portal の左側のナビゲーション ウィンドウにある [インデックス] 一覧で optimize-indexing インデックスを見つけます。

List of Azure AI Search indexes

[ドキュメント数] と [ストレージ サイズ] は、Get Index Statistics API から得られるため、更新に数分かかる場合があります。

リセットして再実行する

開発の初期の実験的な段階では、設計反復のための最も実用的なアプローチは、Azure AI Search からオブジェクトを削除してリビルドできるようにすることです。 リソース名は一意です。 オブジェクトを削除すると、同じ名前を使用して再作成することができます。

このチュートリアルのサンプル コードでは、コードを再実行できるよう、既存のインデックスをチェックしてそれらを削除しています。

インデックスは、ポータルを使用して削除することもできます。

リソースをクリーンアップする

所有するサブスクリプションを使用している場合は、プロジェクトの終了時に、不要になったリソースを削除することをお勧めします。 リソースを実行したままにすると、お金がかかる場合があります。 リソースは個別に削除することも、リソース グループを削除してリソースのセット全体を削除することもできます。

ポータルの左側のナビゲーション ウィンドウにある [すべてのリソース] または [リソース グループ] リンクを使って、リソースを検索および管理できます。

次のステップ

大量のデータのインデックス作成の詳細については、次のチュートリアルを試してください。