Partilhar via


Tutorial: Otimizar a indexação usando a API push

O Azure AI Search suporta dois métodos básicos para importar dados para um índice de pesquisa: enviar os seus dados programaticamente para o índice ou puxar os seus dados apontando um indexador para uma fonte de dados suportada.

Este tutorial explica como indexar dados de forma eficiente usando o modelo push enviando solicitações em lote e usando uma estratégia de repetição de backoff exponencial. Pode descarregar e executar a aplicação de exemplo. Este tutorial também explica os principais aspetos do aplicativo e quais fatores considerar ao indexar dados.

Neste tutorial, você usa C# e a biblioteca Azure.Search.Documents do SDK do Azure para .NET para:

  • Criar um índice
  • Teste vários tamanhos de lote para determinar o tamanho mais eficiente
  • Indexar lotes de forma assíncrona
  • Use vários threads para aumentar as velocidades de indexação
  • Utilize uma estratégia de novos tentativas com backoff exponencial para tentar novamente documentos com falha.

Pré-requisitos

Transferir ficheiros

O código-fonte deste tutorial está na pasta optimize-data-indexing/v11 no repositório GitHub Azure-Samples/azure-search-dotnet-scale .

Considerações principais

Os seguintes fatores afetam as velocidades de indexação. Para obter mais informações, consulte Indexar grandes conjuntos de dados.

  • Nível de preços e número de partições/réplicas: Adicionar partições ou atualizar o seu nível aumenta a velocidade de indexação.
  • Complexidade do esquema de índice: Adicionar campos e propriedades de campo reduz as velocidades de indexação. Índices menores são mais rápidos para indexar.
  • Tamanho do lote: o tamanho ideal do lote varia com base no esquema de índice e no conjunto de dados.
  • Número de threads/trabalhadores: um único thread não aproveita ao máximo as velocidades de indexação.
  • Estratégia de tentativa de repetição: Uma estratégia de recuo exponencial é uma prática recomendada para uma indexação ótima.
  • Velocidades de transferência de dados de rede: As velocidades de transferência de dados podem ser um fator limitante. Indexe dados de dentro do seu ambiente do Azure para aumentar as velocidades de transferência de dados.

Criar um serviço de pesquisa

Este tutorial requer um serviço Azure AI Search, que você pode criar no portal do Azure. Também pode encontrar um serviço existente na sua subscrição atual. Para testar e otimizar com precisão as velocidades de indexação, recomendamos usar o mesmo escalão de preços que planeia usar em produção.

Este tutorial usa autenticação baseada em chave. Copie uma chave de API de administrador para colar no appsettings.json ficheiro.

  1. Entre no portal do Azure e selecione seu serviço de pesquisa.

  2. No painel esquerdo, selecione Visão Geral e copie o endpoint. Deve estar neste formato: https://my-service.search.windows.net

  3. No painel esquerdo, selecione Chaves de Definições> e copie uma chave de administrador para obter todos os direitos no serviço. Há duas chaves de administrador intercambiáveis, fornecidas para continuidade de negócios no caso de você precisar rolar uma. Podes usar qualquer uma das teclas nos pedidos para adicionar, modificar ou eliminar objetos.

    Captura de tela dos locais de ponto de extremidade HTTP e chave de API.

Configurar o ambiente

  1. Abra o arquivo OptimizeDataIndexing.sln no Visual Studio.

  2. No Explorador de Soluções, edite o appsettings.json ficheiro com a informação de ligação que recolheu no passo anterior.

    {
      "SearchServiceUri": "https://{service-name}.search.windows.net",
      "SearchServiceAdminApiKey": "",
      "SearchIndexName": "optimize-indexing"
    }
    

Explore o código

Depois de atualizares appsettings.json, o programa OptimizeDataIndexing.sln de exemplo deverá estar pronto para ser construído e executado.

Esse código é derivado da seção C# de Início Rápido: Pesquisa de Texto Completo, que fornece informações detalhadas sobre o básico de trabalhar com o SDK do .NET.

Este aplicativo de console C#/.NET simples executa as seguintes tarefas:

  • Cria um novo índice com base na estrutura de dados da classe C# Hotel (que também faz referência à Address classe)
  • Testa vários tamanhos de lote para determinar o tamanho mais eficiente
  • Indexa dados de forma assíncrona
    • Usando vários threads para aumentar as velocidades de indexação
    • Usando uma estratégia de tentativas de backoff exponencial para repetir itens falhados

Antes de executares o programa, dedica um minuto a estudar o código e as definições de índice para este exemplo. O código relevante está em vários ficheiros:

  • Hotel.cs e Address.cs contêm o esquema que define o índice
  • DataGenerator.cs contém uma classe simples para facilitar a criação de grandes quantidades de dados hoteleiros
  • ExponentialBackoff.cs contém código para otimizar o processo de indexação conforme descrito neste artigo
  • Program.cs contém funções que criam e eliminam o índice Azure AI Search, indexam lotes de dados e testam diferentes tamanhos de lote

Criar o índice

Este programa de exemplo usa o SDK do Azure para .NET para definir e criar um índice do Azure AI Search. Ele aproveita a FieldBuilder classe para gerar uma estrutura de índice a partir de uma classe de modelo de dados C#.

O modelo de dados é definido pela Hotel classe, que também contém referências à Address classe. FieldBuilder Detalha várias definições de classe para gerar uma estrutura de dados complexa para o índice. As tags de metadados são usadas para definir os atributos de cada campo, como se é pesquisável ou classificável.

Os seguintes excertos do Hotel.cs ficheiro especificam um único campo e uma referência a outra classe de modelo de dados.

. . .
[SearchableField(IsSortable = true)]
public string HotelName { get; set; }
. . .
public Address Address { get; set; }
. . .

No Program.cs ficheiro, o índice é definido com um nome e uma coleção de campos gerados pelo FieldBuilder.Build(typeof(Hotel)) método, e depois criado da seguinte forma:

private static async Task CreateIndexAsync(string indexName, SearchIndexClient indexClient)
{
    // Create a new search index structure that matches the properties of the Hotel class.
    // The Address class is referenced from the Hotel class. The FieldBuilder
    // will enumerate these to create a complex data structure for the index.
    FieldBuilder builder = new FieldBuilder();
    var definition = new SearchIndex(indexName, builder.Build(typeof(Hotel)));

    await indexClient.CreateIndexAsync(definition);
}

Gerar dados

Uma classe simples é implementada no DataGenerator.cs ficheiro para gerar dados para testes. O objetivo dessa classe é facilitar a geração de um grande número de documentos com uma ID exclusiva para indexação.

Para obter uma lista de 100.000 hotéis com IDs exclusivos, execute o seguinte código:

long numDocuments = 100000;
DataGenerator dg = new DataGenerator();
List<Hotel> hotels = dg.GetHotels(numDocuments, "large");

Há dois tamanhos de hotéis disponíveis para teste nesta amostra: pequenos e grandes.

O esquema do índice afeta as velocidades de indexação. Depois de concluir este tutorial, considere converter essa classe para gerar os dados que melhor correspondem ao esquema de índice pretendido.

Tamanhos dos lotes de teste

Para carregar um ou vários documentos em um índice, o Azure AI Search dá suporte às seguintes APIs:

A indexação de documentos em lotes melhora significativamente o desempenho da indexação. Esses lotes podem ter até 1.000 documentos ou até cerca de 16 MB por lote.

Determinar o tamanho de lote ideal para seus dados é um componente fundamental para otimizar as velocidades de indexação. Os dois principais fatores que influenciam o tamanho ideal do lote são:

  • O esquema do seu índice
  • O tamanho dos seus dados

Como o tamanho de lote ideal depende do seu índice e dos seus dados, a melhor abordagem é testar diferentes tamanhos de lote para determinar o que resulta nas velocidades de indexação mais rápidas para o seu cenário.

A função a seguir demonstra uma abordagem simples para testar tamanhos de lote.

public static async Task TestBatchSizesAsync(SearchClient searchClient, int min = 100, int max = 1000, int step = 100, int numTries = 3)
{
    DataGenerator dg = new DataGenerator();

    Console.WriteLine("Batch Size \t Size in MB \t MB / Doc \t Time (ms) \t MB / Second");
    for (int numDocs = min; numDocs <= max; numDocs += step)
    {
        List<TimeSpan> durations = new List<TimeSpan>();
        double sizeInMb = 0.0;
        for (int x = 0; x < numTries; x++)
        {
            List<Hotel> hotels = dg.GetHotels(numDocs, "large");

            DateTime startTime = DateTime.Now;
            await UploadDocumentsAsync(searchClient, hotels).ConfigureAwait(false);
            DateTime endTime = DateTime.Now;
            durations.Add(endTime - startTime);

            sizeInMb = EstimateObjectSize(hotels);
        }

        var avgDuration = durations.Average(timeSpan => timeSpan.TotalMilliseconds);
        var avgDurationInSeconds = avgDuration / 1000;
        var mbPerSecond = sizeInMb / avgDurationInSeconds;

        Console.WriteLine("{0} \t\t {1} \t\t {2} \t\t {3} \t {4}", numDocs, Math.Round(sizeInMb, 3), Math.Round(sizeInMb / numDocs, 3), Math.Round(avgDuration, 3), Math.Round(mbPerSecond, 3));

        // Pausing 2 seconds to let the search service catch its breath
        Thread.Sleep(2000);
    }

    Console.WriteLine();
}

Como nem todos os documentos têm o mesmo tamanho (embora estejam nesta amostra), estimamos o tamanho dos dados que enviamos para o serviço de pesquisa. Você pode fazer isso usando a seguinte função que primeiro converte o objeto em JSON e, em seguida, determina seu tamanho em bytes. Esta técnica permite-nos determinar quais os tamanhos de lote mais eficientes em termos de velocidades de indexação MB/s.

// Returns size of object in MB
public static double EstimateObjectSize(object data)
{
    // converting object to byte[] to determine the size of the data
    BinaryFormatter bf = new BinaryFormatter();
    MemoryStream ms = new MemoryStream();
    byte[] Array;

    // converting data to json for more accurate sizing
    var json = JsonSerializer.Serialize(data);
    bf.Serialize(ms, json);
    Array = ms.ToArray();

    // converting from bytes to megabytes
    double sizeInMb = (double)Array.Length / 1000000;

    return sizeInMb;
}

A função requer um SearchClient mais o número de tentativas que você gostaria de testar para cada tamanho de lote. Como pode haver variabilidade nos tempos de indexação para cada lote, tente cada lote três vezes por padrão para tornar os resultados estatisticamente mais significativos.

await TestBatchSizesAsync(searchClient, numTries: 3);

Ao executar a função, você verá uma saída no console semelhante ao exemplo a seguir:

Captura de ecrã da saída da função de tamanho de lote de teste.

Identifique qual tamanho de lote é mais eficiente e use esse tamanho de lote na próxima etapa deste tutorial. Você pode observar uma estabilização em MB/s ao longo de diferentes tamanhos de lote.

Indexar os dados

Agora que você identificou o tamanho do lote que pretende usar, o próximo passo é começar a indexar os dados. Para indexar dados de forma eficiente, este exemplo:

  • Usa múltiplos tópicos/tarefas
  • Implementa uma estratégia de retentativa com recuo exponencial

Descomente as linhas 41 a 49 e, em seguida, execute novamente o programa. Nessa execução, o exemplo gera e envia lotes de documentos, até 100.000 se você executar o código sem alterar os parâmetros.

Usar vários threads/trabalhadores

Para aproveitar as velocidades de indexação do Azure AI Search, use vários threads para enviar solicitações de indexação em lote simultaneamente para o serviço.

Várias das principais considerações podem afetar o número ideal de threads. Você pode modificar este exemplo e testar com diferentes contagens de threads para determinar a contagem de threads ideal para seu cenário. No entanto, desde que você tenha vários threads funcionando simultaneamente, você deve ser capaz de aproveitar a maioria dos ganhos de eficiência.

À medida que você aumenta as solicitações que chegam ao serviço de pesquisa, você pode encontrar códigos de status HTTP indicando que a solicitação não foi totalmente bem-sucedida. Durante a indexação, dois códigos de status HTTP comuns são:

  • 503 Serviço indisponível: Este erro significa que o sistema está sob carga pesada e o seu pedido não pode ser processado neste momento.
  • 207 Multi-Status: Este erro significa que alguns documentos foram bem-sucedidos, mas pelo menos um falhou.

Implementar uma estratégia de repetição com recuo exponencial

Se ocorrer uma falha, você deve repetir as solicitações usando uma estratégia de repetição de backoff exponencial.

O SDK .NET do Azure AI Search tenta automaticamente 503s e outras solicitações com falha, mas você deve implementar sua própria lógica para repetir 207s. Ferramentas de código aberto como Polly podem ser úteis em uma estratégia de repetição.

Neste exemplo, implementamos a nossa própria estratégia de repetição com backoff exponencial. Começamos por definir algumas variáveis, incluindo o maxRetryAttempts e o delay inicial para uma solicitação com falha.

// Create batch of documents for indexing
var batch = IndexDocumentsBatch.Upload(hotels);

// Create an object to hold the result
IndexDocumentsResult result = null;

// Define parameters for exponential backoff
int attempts = 0;
TimeSpan delay = delay = TimeSpan.FromSeconds(2);
int maxRetryAttempts = 5;

Os resultados da operação de indexação são armazenados na variável IndexDocumentResult result. Essa variável permite verificar se os documentos no lote falharam, como mostrado no exemplo a seguir. Se houver uma falha parcial, um novo lote será criado com base na ID dos documentos com falha.

RequestFailedException As exceções também devem ser capturadas, pois indicam que o pedido falhou completamente, devendo ser tentado novamente.

// Implement exponential backoff
do
{
    try
    {
        attempts++;
        result = await searchClient.IndexDocumentsAsync(batch).ConfigureAwait(false);

        var failedDocuments = result.Results.Where(r => r.Succeeded != true).ToList();

        // handle partial failure
        if (failedDocuments.Count > 0)
        {
            if (attempts == maxRetryAttempts)
            {
                Console.WriteLine("[MAX RETRIES HIT] - Giving up on the batch starting at {0}", id);
                break;
            }
            else
            {
                Console.WriteLine("[Batch starting at doc {0} had partial failure]", id);
                Console.WriteLine("[Retrying {0} failed documents] \n", failedDocuments.Count);

                // creating a batch of failed documents to retry
                var failedDocumentKeys = failedDocuments.Select(doc => doc.Key).ToList();
                hotels = hotels.Where(h => failedDocumentKeys.Contains(h.HotelId)).ToList();
                batch = IndexDocumentsBatch.Upload(hotels);

                Task.Delay(delay).Wait();
                delay = delay * 2;
                continue;
            }
        }

        return result;
    }
    catch (RequestFailedException ex)
    {
        Console.WriteLine("[Batch starting at doc {0} failed]", id);
        Console.WriteLine("[Retrying entire batch] \n");

        if (attempts == maxRetryAttempts)
        {
            Console.WriteLine("[MAX RETRIES HIT] - Giving up on the batch starting at {0}", id);
            break;
        }

        Task.Delay(delay).Wait();
        delay = delay * 2;
    }
} while (true);

A partir daqui, envolva o código de backoff exponencial em uma função para que ele possa ser facilmente chamado.

Outra função é então criada para gerenciar os threads ativos. Para simplificar, essa função não está incluída aqui, mas pode ser encontrada em ExponentialBackoff.cs. Você pode chamar a função usando o seguinte comando, onde hotels são os dados que queremos carregar, 1000 é o tamanho do lote e 8 é o número de threads simultâneos.

await ExponentialBackoff.IndexData(indexClient, hotels, 1000, 8);

Quando você executa a função, você deve ver uma saída semelhante ao exemplo a seguir:

Captura de tela que mostra a saída de uma função de dados de índice.

Quando um lote de documentos falha, um erro é impresso indicando a falha e que o lote está sendo repetido.

[Batch starting at doc 6000 had partial failure]
[Retrying 560 failed documents]

Depois que a função terminar de ser executada, você poderá verificar se todos os documentos foram adicionados ao índice.

Explore o índice

Depois que o programa terminar a execução, você poderá explorar o índice de pesquisa preenchido programaticamente ou usando o explorador de Pesquisa no portal do Azure.

Programaticamente

Há duas opções principais para verificar o número de documentos em um índice: a API Count Documents e a Get Index Statistics API. Ambos os caminhos requerem tempo para serem processados, por isso não se assuste se o número de documentos devolvidos for inicialmente inferior ao esperado.

Contar documentos

A operação Contar documentos recupera uma contagem do número de documentos em um índice de pesquisa.

long indexDocCount = await searchClient.GetDocumentCountAsync();

Obter estatísticas de índice

A operação Obter estatísticas de índice retorna uma contagem de documentos para o índice atual, além do uso de armazenamento. As estatísticas de índice levam mais tempo para serem atualizadas do que a contagem de documentos.

var indexStats = await indexClient.GetIndexStatisticsAsync(indexName);

portal do Azure

No portal do Azure, no painel esquerdo, localize o índice de indexação otimizada na lista Índices .

Mostra de ecrã que mostra uma lista de índices do Azure AI Search.

A contagem de documentos e o tamanho do armazenamento são baseados na API Get Index Statistics e podem levar vários minutos para serem atualizados.

Repor e executar novamente

Nos estágios experimentais iniciais de desenvolvimento, a abordagem mais prática para iteração de design é excluir os objetos da Pesquisa de IA do Azure e permitir que seu código os reconstrua. Os nomes dos recursos são exclusivos. Quando elimina um objeto, pode recriá-lo com o mesmo nome.

O código de exemplo para este tutorial verifica os índices existentes e os exclui para que você possa executar novamente o código.

Você também pode usar o portal do Azure para excluir índices.

Limpar recursos

Quando estiver a trabalhar na sua própria subscrição, no final de um projeto, é uma boa ideia remover os recursos de que já não necessita. Os recursos que deixar em funcionamento podem lhe custar dinheiro. Pode eliminar recursos individualmente ou eliminar o grupo de recursos para eliminar todo o conjunto de recursos.

Você pode localizar e gerenciar recursos no portal do Azure, usando o link Todos os recursos ou Grupos de recursos no painel de navegação esquerdo.

Próximo passo

Para saber mais sobre como indexar dados de grandes quantidades, tente o seguinte tutorial: