教學課程:使用推送 API 將索引編製最佳化

Azure AI 搜尋服務支援兩種基本方法以供您將資料匯入到搜尋索引中:以程式設計方式將資料推送至索引,或將 Azure AI 搜尋服務索引子指向受支援的資料來源,以提取資料。

本教學課程介紹如何使用推送模型來提升資料編製索引的效率,方法是批次處理要求並使用指數輪詢重試策略。 您可以下載並執行範例應用程式。 本文會說明應用程式的重要層面,以及為資料編製索引時所應考量的因素。

本教學課程將使用 C# 和 Azure SDK for .NET 的 Azure.Search.Documents library 執行下列工作:

  • 建立索引
  • 測試各種批次大小,以判斷最有效率的大小
  • 非同步編製批次的索引
  • 使用多個執行緒來增加索引編製速度
  • 使用指數輪詢重試策略來重試失敗的文件

如果您沒有 Azure 訂用帳戶,請在開始前建立免費帳戶

必要條件

本教學課程需要下列服務和工具。

下載檔案

本教學課程的原始程式碼位於 Azure-Samples/azure-search-dotnet-samples GitHub 存放庫的 optimzize-data-indexing/v11 資料夾中。

主要考量

接下來會列出影響編製索引速度的因素。 詳細資訊可參閱編製大型資料集的索引

  • 服務層級和資料分割/複本的數目 - 新增分割區或升級服務層級都可增加索引編製速度。
  • 索引架構複雜度 - 新增欄位和欄位屬性會降低索引編製速度。 越小的索引編製速度越快。
  • 批次大小 - 最佳批次大小會根據索引結構描述與資料集而有所不同。
  • 執行緒/背景工作的數目 - 單一執行緒無法充分利用索引編製速度。
  • 重試策略 - 指數輪詢重試策略是最佳化索引編製的最佳做法。
  • 網路資料傳送速度 - 資料傳送速度可能會成為限制因素。 從您的 Azure 環境內為資料編製索引可增加資料傳送速度。

1 - 建立 Azure AI 搜尋服務

若要完成本教學課程,您必須有 Azure AI 搜尋服務 (可在入口網站中建立)。 建議您使用打算在生產環境中使用的相同層級,以便精確地測試索引編製速度並予以最佳化。

本教學課程使用金鑰式驗證。 複製管理員 API 金鑰,並貼上至 appsettings.json 檔案中。

  1. 登入 Azure 入口網站,並在搜尋服務的 [概觀] 頁面上取得 URL。 範例端點看起來會像是 https://mydemo.search.windows.net

  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 主控台應用程式會執行下列工作:

  • 根據 C# 旅館類別的資料結構 (也會參考地址類別) 建立新的索引。
  • 測試各種批次大小,以判斷最有效率的大小
  • 以非同步方式為資料編製索引
    • 使用多個執行緒來增加索引編製速度
    • 使用指數輪詢重試策略來重試失敗的項目

執行程式之前,請花一分鐘研讀此範例的程式碼及索引定義。 相關程式碼位於幾個檔案中:

  • Hotel.csAddress.cs 包含可定義索引的結構描述
  • DataGenerator.cs 包含一個簡單的類別,可讓您輕鬆地建立大量飯店資料
  • ExponentialBackoff.cs 包含可將索引編製程序最佳化的程式碼,如下文所述
  • Program.cs 包含的函式可建立和刪除 Azure AI 搜尋服務索引、為資料的批次編製索引,以及測試不同的批次大小

建立索引

此範例程式會使用 Azure SDK for .NET 來定義並建立 Azure AI 搜尋服務索引。 它會利用 FieldBuilder 類別從 C# 資料模型類別產生索引結構。

資料模型會透過旅館類別來定義,其中也包含對地址類別的參考。 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 檔案中會實作簡單的類別,以產生用於測試的資料。 此類別的唯一目的是要讓您輕鬆地產生具有唯一識別碼的大量文件以供編製索引。

若要取得有唯一識別碼的 100,000 家飯店清單,請執行下列幾行程式碼:

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

此範例中有兩種飯店大小可供進行測試:small (小型)large (大型)

索引的結構描述會影響索引編製速度。 因此,在完成本教學課程之後,請轉換此類別以產生最符合所需索引結構描述的資料。

4 - 測試批次大小

Azure AI 搜尋服務支援使用下列 API 來將單一或多個文件載入至索引:

以批次方式為文件編製索引會大幅提升索引編製效能。 這些批次最多可達 1000 個文件,或每批次最多 16 MB 左右。

為您的資料決定最佳批次大小是將索引編製速度最佳化的關鍵要素。 影響最佳批次大小的兩個主要因素如下:

  • 索引的結構描述
  • 資料的大小

由於最佳批次大小取決於索引和資料,因此最好的方法是測試不同批次大小,以判斷在您所處的情況下,能達到最快索引編製速度的大小。

下列函式示範用來測試批次大小的簡單方法。

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/s 的索引編製速度,來判斷哪些批次大小最有效率。

// 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,以及您想要針對每個批次大小進行測試的嘗試次數。 由於每個批次的索引編製時間可能會有一些變化,所以我們預設會針對每個批次進行三次嘗試,以讓結果更具統計意義。

await TestBatchSizesAsync(searchClient, numTries: 3);

在執行此函式時,您應該會在主控台中看到如下的輸出:

Output of test batch size function

請找出最有效率的批次大小,然後在本教學課程的下一個步驟中使用該批次大小。 您可能會在不同的批次大小中看到 MB/s 峰值。

5 - 為資料編製索引

由於我們已找出所要使用的批次大小,下一步就是開始為資料編製索引。 為了有效率地為資料編製索引,此範例會:

  • 使用多個執行緒/背景工作。
  • 實作指數輪詢重試策略。

取消註解第 41 到 49 行,然後重新執行程式。 在此次執行中,範例會產生並傳送批次的文件,如果您執行程式碼而不變更參數,則最多為 100,000 個。

使用多個執行緒/背景工作

為了充分利用 Azure AI 搜尋服務的索引編製速度,請使用多個執行緒將批次索引要求同時傳送給服務。

在上述重要考量中,有好幾個會影響最佳的執行緒數目。 您可以修改此範例,並使用不同的執行緒計數進行測試,以判斷您所處情況的最佳執行緒計數。 不過,只要您有數個同時執行的執行緒,應該就能利用絕大多數的效率增益。

當您增加點擊搜尋服務的要求時,您可能會遇到 HTTP 狀態碼,指出要求並未完全成功。 在索引編製期間,兩個常見的 HTTP 狀態碼如下:

  • 503 服務無法使用 - 此錯誤表示系統負載過重,因此目前無法處理要求。
  • 207多重狀態 - 此錯誤表示有些文件成功,但至少有一個文件失敗。

實作指數輪詢重試策略

如果發生失敗,系統應該就會使用指數輪詢重試策略來重試要求。

Azure AI 搜尋服務的 .NET SDK 會自動重試 503 和其他失敗的要求,但您應實作自己的邏輯才能重試 207。 您可以在重試策略中使用 Polly 等開放原始碼工具。

在此範例中,我們會實行自己的指數輪詢重試策略。 我們一開始會先定義一些變數,包括失敗要求的 maxRetryAttempts 和初始 delay

// 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 變數中。 這個變數很重要,因為其可讓您檢查批次中是否有任何文件失敗,如下所示。 如果有部份失敗,則會根據失敗的文件識別碼來建立新批次。

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 - 探索索引

您可以在執行程式之後,以程式設計方式或使用入口網站中的搜尋總管來探索已填入的搜尋索引。

程式設計方式

有兩個主要選項可用來檢查索引中的文件數目:文件計數 API取得索引統計資料 API。 這兩種途徑都需要一些額外時間來處理,因此如果一開始傳回的文件數目低於您原本的預期,請不必緊張。

文件計數

文件計數操作可在搜尋索引中抓取文件數目的計數:

long indexDocCount = await searchClient.GetDocumentCountAsync();

取得索引統計資料

取得索引統計資料操作會傳回目前索引的文件計數,以及儲存體使用量。 索引統計資料所需的更新時間會超過文件計數。

var indexStats = await indexClient.GetIndexStatisticsAsync(indexName);

Azure 入口網站

在 Azure 入口網站中,從左側瀏覽窗格的 [索引] 清單中尋找 optimize-indexing 索引。

List of Azure AI Search indexes

文件計數儲存體大小的根據是取得索引統計資料 API,而且可能需要花幾分鐘的時間才會更新。

重設並重新執行

在開發的早期實驗階段中,設計反覆項目最實用的方法是從 Azure AI 搜尋服務中刪除物件,並讓您的程式碼重建它們。 資源名稱是唯一的。 刪除物件可讓您使用相同的名稱加以重新建立。

本教學課程的範例程式碼會檢查是否有現有的索引,並將其刪除,以便您重新執行程式碼。

您也可以使用入口網站來刪除索引。

清除資源

如果您使用自己的訂用帳戶,當專案結束時,建議您移除不再需要的資源。 資源若繼續執行,將需付費。 您可以個別刪除資源,或刪除資源群組以刪除整組資源。

您可以使用左導覽窗格中的 [所有資源] 或 [資源群組] 連結,在入口網站中尋找和管理資源。

下一步

若要深入了解如何編製大量資料的索引,請嘗試下列教學課程。