Управление параллелизмом в Поиск с использованием ИИ Azure

Примечание.

Поиск с использованием ИИ Azure доступна через портал Azure, REST API и Azure SDKs. Он также лежит в основе Foundry IQ — управляемого слоя знаний, который преобразует корпоративный контент в многократно используемые базы знаний с учетом разрешений доступа для агентов на портале Microsoft Foundry.

При управлении Поиск с использованием ИИ Azure ресурсами, такими как индексы и источники данных, важно безопасно обновлять ресурсы, особенно если ресурсы доступны одновременно различными компонентами приложения.

  • Операции обновления ресурсов могут не завершиться немедленно. Например, обновление индекса или индексатора может занять несколько секунд. Обновления ресурсов сериализуются, что означает, что несколько операций обновления могут не выполняться одновременно в одном ресурсе.
  • Когда два клиента одновременно обновляют ресурс без координации, возможна ситуация, которую называют состояние гонки. Один клиент может запустить операцию обновления, а другой клиент получает конфликтную ошибку. Чтобы предотвратить это, Поиск с использованием ИИ Azure поддерживает модель параллелизма оптимистической конкуренции. При этом в отношении ресурса нет никаких блокировок. Вместо этого существует ETag для каждого ресурса, который определяет версию ресурса, чтобы можно было сформулировать запросы, которые избегают случайной перезаписи.

Принцип работы

Оптимистичная параллельность реализуется с помощью проверок условий доступа при вызовах API, которые записывают индексы, индексаторы, источники данных, наборы навыков, базы знаний и карты синонимов.

Все ресурсы имеют тег сущности (ETag), который предоставляет сведения о версии объекта. Если сначала проверить ETag, то можно избежать параллельных обновлений в стандартном рабочем процессе (получение, локальное изменение, обновление), убедившись, что ETag ресурса соответствует локальной копии.

Каждый раз при обновлении ресурса его ETag изменяется автоматически. При реализации управления параллелизмом все, что необходимо сделать, это поместить предварительное условие в запрос на обновление. Этим условием должно быть требование, чтобы удаленный ресурс имел такой же ETag, как и копия ресурса, измененного вами на клиенте. Если другой процесс изменяет удаленный ресурс, ETag не соответствует предварительным условию, и запрос завершается ошибкой HTTP 412 или 409. Если вы используете пакет SDK .NET, этот сбой манифестирует как исключение, в котором метод расширения IsAccessConditionFailed() возвращает значение true.

Примечание.

Для параллелизма существует только один механизм. Он всегда используется независимо от того, какой API или пакет SDK используется для обновлений ресурсов. Начиная с 18 июля 2025 г. Поиск с использованием ИИ Azure начал применять сериализацию для операций создания и обновления индекса, чтобы обеспечить согласованность и надежность.

Пример

Следующий код демонстрирует оптимистическое параллелизм для операции обновления. Второе обновление завершается сбоем, так как предыдущим обновлением изменяется 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 в индекс, который уже существует.

Фрагмент кода получает индекс с примером отелей, проверяет версию объекта в операции обновления, вызывает исключение, если условие завершается сбоем, а затем повторяет операцию (до трех раз), начиная с извлечения индекса с сервера, чтобы получить последнюю версию.

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

    for (int i = 0; i < MaxNumTries; ++i)
    {
        try
        {
            SearchIndex index = indexClient.GetIndex("hotels-sample");
            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;
}

См. также