在 Azure AI 搜索中管理并发

管理 Azure AI 搜索资源(如索引和数据源)时,请务必安全地更新资源,尤其是在应用程序的不同组件并发访问资源时。 如果两个客户端同时在不协调的情况下更新资源,则竞争条件是可能的。 为防止这种情况,Azure AI 搜索使用 乐观并发模型。 资源上没有锁。 相反,每个资源都有一个 ETag,用于标识资源版本,以便可以制定避免意外覆盖的请求。

工作原理

乐观并发是通过 API 调用中写入索引、索引器、数据源、技能集、知识代理和 SynonymMap 资源的访问条件检查来实现的。

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

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

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

注释

并发机制只有一种。 无论使用哪个 API 或 SDK 来更新资源,它始终会被使用。

从 2025 年 7 月 18 日开始,Azure AI 搜索开始强制执行索引创建和更新作的序列化,以确保一致性和可靠性。 此更改与长期最佳做法以及服务中其它操作的既定行为保持一致。 随着各地区的推出逐步推进,在发送并发或时间接近的请求时,你可能会开始遇到409冲突错误。 若要避免这些错误,请确保不会并行发送操作,并遵循本文件中的并发指南。

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;
}

另请参阅