Azure AI 검색에서 동시성 관리
인덱스 및 데이터 원본과 같은 Azure AI 검색 리소스를 관리할 때는 리소스를 안전하게 업데이트해야 합니다. 특히 애플리케이션의 여러 구성 요소가 리소스에 동시에 액세스할 때는 안전한 업데이트가 더욱 중요합니다. 두 클라이언트가 조정 없이 리소스를 동시에 업데이트하면 경합 상태가 발생할 수 있습니다. 이를 방지하기 위해 Azure AI 검색은 낙관적 동시성 모델을 사용합니다. 이 모델에서는 리소스가 잠기지 않습니다. 대신에 요청을 작성할 수 있도록 리소스 버전을 식별하는 ETag가 모든 리소스에 포함되어 있으므로 실수로 인한 덮어쓰기를 방지할 수 있습니다.
작동 방식
낙관적 동시성은 인덱스, 인덱서, 데이터 원본, 기술 세트, synonymMap 리소스에 쓰기 작업을 수행하는 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와 함께 요청이 실패합니다. .NET SDK를 사용하는 경우 이 실패는 IsAccessConditionFailed()
확장 메서드가 true를 반환하는 예외로 나타납니다.
참고 항목
동시성의 메커니즘은 한 가지뿐입니다. 리소스 업데이트에 사용되는 API 또는 SDK에 관계없이 항상 사용됩니다.
예시
다음 코드는 업데이트 작업에 대한 낙관적 동시성을 보여 줍니다. 이전 업데이트에 의해 개체의 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.windows.net/");
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" 인덱스를 가져와 업데이트 작업의 개체 버전을 확인한 다음 조건이 실패하면 예외를 throw합니다. 그런 후에 작업을 최대 3회까지 다시 시도하는데, 이때 먼저 서버에서 인덱스를 검색하여 최신 버전을 가져옵니다.
private static void EnableSynonymsInHotelsIndexSafely(SearchServiceClient serviceClient)
{
int MaxNumTries = 3;
for (int i = 0; i < MaxNumTries; ++i)
{
try
{
Index index = serviceClient.Indexes.Get("hotels");
index = AddSynonymMapsToFields(index);
// The IfNotChanged condition ensures that the index is updated only if the ETags match.
serviceClient.Indexes.CreateOrUpdate(index, accessCondition: AccessCondition.IfNotChanged(index));
Console.WriteLine("Updated the index successfully.\n");
break;
}
catch (Exception e) when (e.IsAccessConditionFailed())
{
Console.WriteLine($"Index update failed : {e.Message}. Attempt({i}/{MaxNumTries}).\n");
}
}
}
private static Index AddSynonymMapsToFields(Index index)
{
index.Fields.First(f => f.Name == "category").SynonymMaps = new[] { "desc-synonymmap" };
index.Fields.First(f => f.Name == "tags").SynonymMaps = new[] { "desc-synonymmap" };
return index;
}