管理 Azure AI 搜索资源(如索引和数据源)时,请务必安全地更新资源,尤其是在应用程序的不同组件并发访问资源时。
- 资源更新作可能不会立即完成。 例如, 更新索引 或 索引器 可能需要几秒钟才能完成。 资源更新已 序列化,这意味着多个更新作可能不会在同一资源上同时运行。
- 当两个客户端同时在不协调的情况下更新资源时,可能会有 争用条件 。 一个客户端可以启动更新操作,而另一个客户端可能会收到冲突错误。 为防止这种情况,Azure AI 搜索支持 乐观并发模型。 资源上没有锁。 相反,每个资源都有一个 ETag,用于标识资源版本,以便可以制定避免意外覆盖的请求。
工作原理
乐观并发是通过 API 调用中对索引、索引器、数据源、技能集、知识库和同义词映射资源进行条件访问检查来实现的。
所有资源都有 一个实体标记(ETag)
提供对象版本信息。 通过首先检查 ETag,可以确保资源的 ETag 与本地副本匹配,从而避免典型工作流(获取、修改本地、更新)中的并发更新。
REST API 在请求标头上使用 ETag 。
用于 .NET 的 Azure SDK 通过 accessCondition 类设置 ETag,并设置资源上的 If-Match | If-Match-None 标头 。 使用 ETag 的对象(如 SynonymMap.ETag 和 SearchIndex.ETag)具有 accessCondition 类。
每次更新资源时,其 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;
}