教程:使用推送 API 优化索引编制

Azure AI 搜索支持采用两种基本方法将数据导入到搜索索引中:一种方法是以编程方式将数据推送到索引中;另一种方法是将 Azure AI 搜索索引器指向受支持的数据源来拉取数据。

本教程介绍了如何使用推送模型通过分批处理请求并使用指数回退重试策略来高效地为数据编制索引。 你可以下载并运行示例应用程序。 本文介绍了该应用程序的主要方面,以及为数据编制索引时要考虑的因素。

此教程使用 C# 和 Azure SDK for .NET 中的 Azure.Search.Documents 库来执行以下任务:

  • 创建索引
  • 测试各种批大小以确定最高效的大小
  • 以异步方式索引批
  • 使用多个线程以提高索引编制速度
  • 使用指数退避重试策略重试失败的文档

先决条件

本教程需要以下服务和工具。

下载文件

本教程的源代码位于 Azure-Samples/azure-search-dotnet-scale GitHub 存储库的 optimize-data-indexing/v11 文件夹中。

重要注意事项

接下来列出了影响索引速度的因素。 要了解详细信息,请参阅为大型数据集编制索引

  • 服务层级和分区/副本数:添加分区或升级层级会提高索引编制速度
  • 索引架构复杂性:添加字段和字段属性会降低索引编制速度。 索引越小,索引速度越快。
  • 批大小:最佳批大小因索引架构和数据集而异
  • 线程数/工作器数:单个线程不能充分利用索引编制速度
  • 重试策略:指数退避重试策略是优化索引的最佳做法
  • 网络数据传输速度:数据传输速度可能是一个限制因素。 从 Azure 环境中为数据编制索引可以提高数据传输速度。

第 1 步:创建 Azure AI 搜索服务

若要完成本教程,需要一个 Azure AI 搜索服务,你可以在 Azure 门户中创建该服务,或者在当前订阅下找到现有服务。 建议使用打算在生产中使用的同一层级,以便能够准确地测试和优化索引编制速度。

此教程使用基于密钥的身份验证。 复制管理员 API 密钥以粘贴到 appsettings.json 文件中

  1. 登录到 Azure 门户。 在搜索服务“概述”页上,获取终结点 URL。 示例终结点可能类似于 https://mydemo.search.azure.cn

  2. 在“设置”>“密钥”中,获取有关该服务的完全权限的管理员密钥 。 有两个可交换的管理员密钥,为保证业务连续性而提供,以防需要滚动一个密钥。 可以在请求中使用主要或辅助密钥来添加、修改和删除对象。

    HTTP 终结点和 API 密钥位置的屏幕截图。

第 2 步:设置环境

  1. 启动 Visual Studio 并打开“OptimizeDataIndexing.sln”。

  2. 在解决方案资源管理器中,打开“appsettings.json”以提供该服务的连接信息。

{
  "SearchServiceUri": "https://{service-name}.search.azure.cn",
  "SearchServiceAdminApiKey": "",
  "SearchIndexName": "optimize-indexing"
}

第 3 步:浏览代码

更新 appsettings.json 后,OptimizeDataIndexing.sln 中的示例程序应当已做好了生成和运行准备工作。

此代码派生自快速入门:使用 Azure SDK 进行全文搜索的 C# 部分。 可以在那篇文章中更详细地了解使用 .NET SDK 的基础知识。

此简单的 C#/.NET 控制台应用程序执行以下任务:

  • 基于 C# Hotel 类(此类还引用 Address 类)的数据结构创建新索引
  • 测试各种批大小以确定最高效的大小
  • 以异步方式为数据编制索引
    • 使用多个线程以提高索引编制速度
    • 使用指数回退重试策略重试失败的项

运行该程序之前,请抽时间研究此示例的代码和索引定义。 相关代码存在于多个文件中:

  • Hotel.cs 和 Address.cs 包含用于定义索引的架构
  • DataGenerator.cs 包含一个简单的类,可轻松创建大量的酒店数据
  • ExponentialBackoff.cs 包含用于优化索引编制过程的代码,如此文章中所述
  • Program.cs 包含用于创建和删除 Azure AI 搜索索引、为各批数据编制索引以及测试不同批大小的函数

创建索引

此示例程序使用 Azure SDK for .NET 来定义和创建 Azure AI 搜索索引。 它利用 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

索引的架构对索引速度有影响。 因此,在完成本教程之后,有必要对该类进行转换以生成与预期的索引架构最匹配的数据。

第 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/秒的索引编制速度来确定哪些批大小是最高效的。

// 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);

运行此函数时,控制台中应会显示如下所示的输出:

测试批大小函数的输出的屏幕截图。

确定最高效的批大小,然后在教程的下一步中使用该批大小。 你可能会发现不同批大小的索引编制速度(MB/秒)差异不大。

第 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 中。 此变量很重要,因为它允许你检查批次中是否有任何文档失败,如以下示例所示。 如果发生部分失败,将基于失败的文档 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 步:浏览索引

在程序运行后,你可以采用编程方式或者使用门户中的搜索浏览器来浏览已填充的搜索索引。

采用编程方式

可以使用两个主要选项来检查索引中的文档数:对文档计数 API获取索引统计信息 API。 这两个路径都需要花费时间进行处理,因此,如果返回的文档数最初低于你预计的值,请不要惊慌。

计数文档

“对文档计数”操作检索某个搜索索引中文档的计数:

long indexDocCount = await searchClient.GetDocumentCountAsync();

获取索引统计信息

“获取索引统计信息”操作会返回当前索引的文档计数以及存储使用情况。 索引统计信息更新需要花费比文档计数更新更多的时间。

var indexStats = await indexClient.GetIndexStatisticsAsync(indexName);

Azure 门户

在 Azure 门户中的左侧导航窗格中,在“索引”列表中找到“optimize-indexing”索引

显示 Azure AI 搜索索引列表的屏幕截图。

“文档计数”和“存储大小”基于获取索引统计信息 API,可能需要花费几分钟时间进行更新。

重置并重新运行

在开发的前期试验阶段,设计迭代的最实用方法是,删除 Azure AI 搜索中的对象,并允许代码重新生成它们。 资源名称是唯一的。 删除某个对象后,可以使用相同的名称重新创建它。

本教程的示例代码会检查现有索引并将其删除,使你能够重新运行代码。

还可以使用门户来删除索引。

清理资源

在自己的订阅中操作时,最好在项目结束时删除不再需要的资源。 持续运行资源可能会产生费用。 可以逐个删除资源,也可以删除资源组以删除整个资源集。

可以使用左侧导航窗格中的“所有资源”或“资源组”链接 ,在门户中查找和管理资源。

下一步

若要详细了解如何为大量数据编制索引,请尝试以下教程。