在 Azure AI 搜索中管理并发

管理 Azure AI 搜索资源(如索引和数据源)时,请务必安全地更新资源,尤其是在应用程序的不同组件并发访问资源时。

  • 资源更新作可能不会立即完成。 例如, 更新索引索引器 可能需要几秒钟才能完成。 资源更新已 序列化,这意味着多个更新作可能不会在同一资源上同时运行。
  • 当两个客户端同时在不协调的情况下更新资源时,可能会有 争用条件 。 一个客户端可以启动更新操作,而另一个客户端可能会收到冲突错误。 为防止这种情况,Azure AI 搜索支持 乐观并发模型。 资源上没有锁。 相反,每个资源都有一个 ETag,用于标识资源版本,以便可以制定避免意外覆盖的请求。

工作原理

乐观并发是通过 API 调用中对索引、索引器、数据源、技能集、知识库和同义词映射资源进行条件访问检查来实现的。

所有资源都有 一个实体标记(ETag)

提供对象版本信息。 通过首先检查 ETag,可以确保资源的 ETag 与本地副本匹配,从而避免典型工作流(获取、修改本地、更新)中的并发更新。

每次更新资源时,其 ETag 都会自动更改。 实现并发管理时,你所做的一切都是在更新请求上设置先决条件,该请求要求远程资源具有与在客户端上修改的资源副本相同的 ETag。 如果另一个进程更改远程资源,ETag 与前置条件不匹配,并且请求失败并出现 HTTP 412 或 409。 如果您使用 .NET SDK,错误会表现为一个异常,该异常是在 IsAccessConditionFailed() 扩展方法返回 true 时发生。

注释

并发机制只有一种。 无论使用哪个 API 或 SDK 来更新资源,它始终会被使用。 从 2025 年 7 月 18 日开始,Azure AI 搜索开始强制执行索引创建和更新作的序列化,以确保一致性和可靠性。

Example

以下代码演示更新操作的乐观并发。 第二次更新失败了,因为对象的 ETag 在先前的更新中已被变更。 更具体地说,当请求标头中的 ETag 不再与对象的 ETag 匹配时,搜索服务将返回状态代码 400(错误请求),并且更新失败。

using Azure;
using Azure.Search.Documents;
using Azure.Search.Documents.Indexes;
using Azure.Search.Documents.Indexes.Models;
using System;
using System.Net;
using System.Threading.Tasks;

namespace AzureSearch.SDKHowTo
{
    class Program
    {
        // This sample shows how ETags work by performing conditional updates and deletes
        // on an Azure Search index.
        static void Main(string[] args)
        {
            string serviceName = "PLACEHOLDER FOR YOUR SEARCH SERVICE NAME";
            string apiKey = "PLACEHOLDER FOR YOUR SEARCH SERVICE ADMIN API KEY";

            // Create a SearchIndexClient to send create/delete index commands
            Uri serviceEndpoint = new Uri($"https://{serviceName}.search.azure.cn/");
            AzureKeyCredential credential = new AzureKeyCredential(apiKey);
            SearchIndexClient adminClient = new SearchIndexClient(serviceEndpoint, credential);

            // Delete index if it exists
            Console.WriteLine("Check for index and delete if it already exists...\n");
            DeleteTestIndexIfExists(adminClient);

            // Every top-level resource in Azure Search has an associated ETag that keeps track of which version
            // of the resource you're working on. When you first create a resource such as an index, its ETag is
            // empty.
            SearchIndex index = DefineTestIndex();

            Console.WriteLine(
                $"Test searchIndex hasn't been created yet, so its ETag should be blank. ETag: '{index.ETag}'");

            // Once the resource exists in Azure Search, its ETag is populated. Make sure to use the object
            // returned by the SearchIndexClient. Otherwise, you will still have the old object with the
            // blank ETag.
            Console.WriteLine("Creating index...\n");
            index = adminClient.CreateIndex(index);
            Console.WriteLine($"Test index created; Its ETag should be populated. ETag: '{index.ETag}'");

            // ETags prevent concurrent updates to the same resource. If another
            // client tries to update the resource, it will fail as long as all clients are using the right
            // access conditions.
            SearchIndex indexForClientA = index;
            SearchIndex indexForClientB = adminClient.GetIndex("test-idx");

            Console.WriteLine("Simulating concurrent update. To start, clients A and B see the same ETag.");
            Console.WriteLine($"ClientA ETag: '{indexForClientA.ETag}' ClientB ETag: '{indexForClientB.ETag}'");

            // indexForClientA successfully updates the index.
            indexForClientA.Fields.Add(new SearchField("a", SearchFieldDataType.Int32));
            indexForClientA = adminClient.CreateOrUpdateIndex(indexForClientA);

            Console.WriteLine($"Client A updates test-idx by adding a new field. The new ETag for test-idx is: '{indexForClientA.ETag}'");

            // indexForClientB tries to update the index, but fails due to the ETag check.
            try
            {
                indexForClientB.Fields.Add(new SearchField("b", SearchFieldDataType.Boolean));
                adminClient.CreateOrUpdateIndex(indexForClientB);

                Console.WriteLine("Whoops; This shouldn't happen");
                Environment.Exit(1);
            }
            catch (RequestFailedException e) when (e.Status == 400)
            {
                Console.WriteLine("Client B failed to update the index, as expected.");
            }

            // Uncomment the next line to remove test-idx
            //adminClient.DeleteIndex("test-idx");
            Console.WriteLine("Complete.  Press any key to end application...\n");
            Console.ReadKey();
        }

        private static void DeleteTestIndexIfExists(SearchIndexClient adminClient)
        {
            try
            {
                if (adminClient.GetIndex("test-idx") != null)
                {
                    adminClient.DeleteIndex("test-idx");
                }
            }
            catch (RequestFailedException e) when (e.Status == 404)
            {
                //if an exception occurred and status is "Not Found", this is working as expected
                Console.WriteLine("Failed to find index and this is because it's not there.");
            }
        }

        private static SearchIndex DefineTestIndex() =>
            new SearchIndex("test-idx", new[] { new SearchField("id", SearchFieldDataType.String) { IsKey = true } });
    }
}

设计模式

实现乐观并发的设计模式应包括一个循环,用于重试访问条件检查、访问条件测试,以及(可选)在尝试重新应用更改之前检索更新的资源。

此代码片段演示如何将 synonymMap 添加到已存在的索引。

该代码片段获取 hotels-sample-index 索引,在更新操作中检查对象版本,如果条件失败,则会引发异常,然后重试操作(最多三次),从服务器检索索引以获取最新版本。

private static void EnableSynonymsInHotelsIndexSafely(SearchIndexClient indexClient)
{
    int MaxNumTries = 3;

    for (int i = 0; i < MaxNumTries; ++i)
    {
        try
        {
            SearchIndex index = indexClient.GetIndex("hotels-sample-index");
            index = AddSynonymMapsToFields(index);

            // The onlyIfUnchangedcondition ensures that the index is updated only if the ETags match.
            indexClient.CreateOrUpdateIndex(index, onlyIfUnchanged: true);

            Console.WriteLine("Updated the index successfully.\n");
            break;
        }
        catch (RequestFailedException e) when (e.Status == 412)
        {
            Console.WriteLine($"Index update failed : {e.Message}. Attempt({i}/{MaxNumTries}).\n");
        }
    }
}

private static SearchIndex AddSynonymMapsToFields(SearchIndex index)
{
    index.Fields.First(f => f.Name == "category").SynonymMapNames.Add("desc-synonymmap");
    index.Fields.First(f => f.Name == "tags").SynonymMapNames.Add("desc-synonymmap");
    return index;
}

另请参阅