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

Azure AI 検索では、データを検索インデックスにインポートするために、2 つの基本的な手法がサポートされています。プログラムでインデックスにデータを "プッシュする" 方法と、サポートされるデータ ソースで Azure AI 検索のインデクサーを指定してデータを "プルする" 方法です。

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

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

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

前提条件

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

  • Azure サブスクリプション。 アカウントがない場合は、無料アカウントを作成することができます。

  • Visual Studio (任意のエディション)。 サンプル コードと手順については、無料の Community エディションでテストされています。

ファイルのダウンロード

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

重要な考慮事項

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

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

ステップ 1: Azure AI 検索サービスを作成する

このチュートリアルを最後まで行うには、Azure AI 検索サービスが必要です。これは、Azure portal で作成するか、現在のサブスクリプションで既存のサービスを見つけることができます。 インデックスの作成速度を正確にテストして最適化できるよう、運用環境で使用するのと同じレベルを使用することをお勧めします。

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

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

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

    HTTP エンドポイントと API キーの場所のスクリーンショット。

ステップ 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 コンソール アプリは次のタスクを実行します。

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

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

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

インデックスの作成

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

データ モデルは 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);

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

バッチ サイズ テスト関数の出力のスクリーンショット。

どのバッチ サイズが最も効率的であるかを見極め、そのバッチ サイズをチュートリアルの次の手順で使用します。 バッチ サイズが異なっていても、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);

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

インデックス データ関数の出力を示すスクリーンショット。

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

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

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

ステップ 6: インデックスを調べる

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

プログラム

インデックス内のドキュメント数は、主に 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 インデックスを見つけます。

Azure AI 検索のインデックスの一覧を示すスクリーンショット。

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

リセットして再実行する

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

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

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

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

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

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

次のステップ

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