Indexar todos os dados usando a API de push do Azure AI Search
A API REST é a maneira mais flexível de enviar dados por push para um índice do Azure AI Search. Você pode usar qualquer linguagem de programação ou interativamente com qualquer aplicativo que possa postar solicitações JSON em um ponto de extremidade.
Aqui, você verá como usar a API REST com eficiência e explorar as operações disponíveis. Em seguida, você examinará o código do .NET Core e verá como otimizar a adição de grandes quantidades de dados por meio da API.
Operações de API REST com suporte
Há duas APIs REST com suporte fornecidas pela Pesquisa de IA. APIs de pesquisa e gerenciamento. Este módulo se concentra nas APIs REST de pesquisa que fornecem operações em cinco recursos de pesquisa:
Característica | Operações |
---|---|
Índice | Criar, excluir, atualizar e configurar. |
Documento | Obter, adicionar, atualizar e excluir. |
Indexador | Configure fontes de dados e agendamento em fontes de dados limitadas. |
Conjunto de habilidades | Obter, criar, excluir, listar e atualizar. |
Mapa de sinônimos | Obter, criar, excluir, listar e atualizar. |
Como chamar a API REST de pesquisa
Se você quiser chamar qualquer uma das APIs de pesquisa, precisará:
- Use o ponto de extremidade HTTPS (pela porta padrão 443) fornecida pelo serviço de pesquisa, você deve incluir um de versão da API no URI.
- O cabeçalho da solicitação deve incluir um atributo api-key.
Para localizar o ponto de extremidade, a versão da API e a chave de api, acesse o portal do Azure.
No portal, navegue até o serviço de pesquisa e selecione Gerenciador de Pesquisa. O ponto de extremidade da API REST está no campo url de solicitação de. A primeira parte da URL é o ponto de extremidade (por exemplo, https://azsearchtest.search.windows.net) e a cadeia de caracteres de consulta mostra o api-version
(por exemplo, api-version=2023-07-01-Preview).
Para localizar o api-key
à esquerda, selecione Chaves. A chave de administração primária ou secundária poderá ser usada se você estiver usando a API REST para fazer mais do que apenas consultar o índice. Se tudo o que você precisa é pesquisar um índice, você pode criar e usar chaves de consulta.
Para adicionar, atualizar ou excluir dados em um índice, você precisa usar uma chave de administração.
Adicionar dados a um índice
Use uma solicitação HTTP POST usando o recurso de índices neste formato:
POST https://[service name].search.windows.net/indexes/[index name]/docs/index?api-version=[api-version]
O corpo da solicitação precisa permitir que o ponto de extremidade REST saiba a ação a ser tomada no documento, qual documento aplicar a ação também e quais dados usar.
O JSON deve estar nesse formato:
{
"value": [
{
"@search.action": "upload (default) | merge | mergeOrUpload | delete",
"key_field_name": "unique_key_of_document", (key/value pair for key field from index schema)
"field_name": field_value (key/value pairs matching index schema)
...
},
...
]
}
Ação | Descrição |
---|---|
carregar | Semelhante a um upsert no SQL, o documento será criado ou substituído. |
mesclagem | A mesclagem atualiza um documento existente com os campos especificados. A mesclagem falhará se nenhum documento puder ser encontrado. |
mergeOrUpload | A mesclagem atualiza um documento existente com os campos especificados e o carrega se o documento não existir. |
excluir | Exclui todo o documento, você só precisa especificar o key_field_name. |
Se a solicitação for bem-sucedida, a API retornará um código de status 200.
Observação
Para obter uma lista completa de todos os códigos de resposta e mensagens de erro, consulte Adicionar, Atualizar ou Excluir Documentos (API REST do Azure AI Search)
Este exemplo JSON carrega o registro do cliente na unidade anterior:
{
"value": [
{
"@search.action": "upload",
"id": "5fed1b38309495de1bc4f653",
"firstName": "Sims",
"lastName": "Arnold",
"isAlive": false,
"age": 35,
"address": {
"streetAddress": "Sumner Place",
"city": "Canoochee",
"state": "Palau",
"postalCode": "1558"
},
"phoneNumbers": [
{
"phoneNumber": {
"type": "home",
"number": "+1 (830) 465-2965"
}
},
{
"phoneNumber": {
"type": "home",
"number": "+1 (889) 439-3632"
}
}
]
}
]
}
Você pode adicionar quantos documentos desejar na matriz de valores. No entanto, para obter um desempenho ideal, considere o envio em lote dos documentos em suas solicitações até um máximo de 1.000 documentos ou 16 MB no tamanho total.
Usar o .NET Core para indexar quaisquer dados
Para obter o melhor desempenho, use a biblioteca de clientes Azure.Search.Document
mais recente, atualmente versão 11. Você pode instalar a biblioteca de clientes com o NuGet:
dotnet add package Azure.Search.Documents --version 11.4.0
O desempenho do índice é baseado em seis fatores-chave:
- A camada de serviço de pesquisa e quantas réplicas e partições você habilitou.
- A complexidade do esquema de índice. Reduza quantas propriedades (pesquisáveis, facetas, classificáveis) cada campo tem.
- O número de documentos em cada lote, o melhor tamanho dependerá do esquema de índice e do tamanho dos documentos.
- Como sua abordagem é multithreaded.
- Tratamento de erros e limitação. Use uma estratégia de repetição de retirada exponencial.
- Onde seus dados residem, tente indexar seus dados como próximos ao índice de pesquisa. Por exemplo, execute uploads de dentro do ambiente do Azure.
Descubra o tamanho ideal do lote
Como trabalhar o melhor tamanho do lote é um fator-chave para melhorar o desempenho, vamos examinar uma abordagem no código.
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();
}
A abordagem é aumentar o tamanho do lote e monitorar o tempo necessário para receber uma resposta válida. O código faz loops de 100 a 1000, em 100 etapas de documento. Para cada tamanho de lote, ele gera o tamanho do documento, o tempo para obter uma resposta e o tempo médio por MB. A execução desse código fornece resultados como este:
No exemplo acima, o melhor tamanho do lote para taxa de transferência é 2.499 MB por segundo, 800 documentos por lote.
Implementa uma estratégia de repetição de retirada exponencial
Se o índice começar a limitar as solicitações devido a sobrecargas, ele responderá com um status 503 (solicitação rejeitada devido à carga pesada) ou 207 (alguns documentos falharam no lote). Você precisa lidar com essas respostas e uma boa estratégia é recuar. Recuar significa pausar por algum tempo antes de repetir sua solicitação novamente. Se você aumentar esse tempo para cada erro, fará o backup exponencialmente.
Examine este código:
// 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);
O código mantém o controle de documentos com falha em um lote. Se ocorrer um erro, ele aguardará um atraso e, em seguida, dobrará o atraso para o próximo erro.
Por fim, há um número máximo de tentativas e, se esse número máximo for atingido, o programa existirá.
Usar threading para melhorar o desempenho
Você pode concluir seu aplicativo de carregamento de documentos combinando a estratégia de retirada acima com uma abordagem de threading. Veja alguns exemplos de código:
public static async Task IndexDataAsync(SearchClient searchClient, List<Hotel> hotels, int batchSize, int numThreads)
{
int numDocs = hotels.Count;
Console.WriteLine("Uploading {0} documents...\n", numDocs.ToString());
DateTime startTime = DateTime.Now;
Console.WriteLine("Started at: {0} \n", startTime);
Console.WriteLine("Creating {0} threads...\n", numThreads);
// Creating a list to hold active tasks
List<Task<IndexDocumentsResult>> uploadTasks = new List<Task<IndexDocumentsResult>>();
for (int i = 0; i < numDocs; i += batchSize)
{
List<Hotel> hotelBatch = hotels.GetRange(i, batchSize);
var task = ExponentialBackoffAsync(searchClient, hotelBatch, i);
uploadTasks.Add(task);
Console.WriteLine("Sending a batch of {0} docs starting with doc {1}...\n", batchSize, i);
// Checking if we've hit the specified number of threads
if (uploadTasks.Count >= numThreads)
{
Task<IndexDocumentsResult> firstTaskFinished = await Task.WhenAny(uploadTasks);
Console.WriteLine("Finished a thread, kicking off another...");
uploadTasks.Remove(firstTaskFinished);
}
}
// waiting for the remaining results to finish
await Task.WhenAll(uploadTasks);
DateTime endTime = DateTime.Now;
TimeSpan runningTime = endTime - startTime;
Console.WriteLine("\nEnded at: {0} \n", endTime);
Console.WriteLine("Upload time total: {0}", runningTime);
double timePerBatch = Math.Round(runningTime.TotalMilliseconds / (numDocs / batchSize), 4);
Console.WriteLine("Upload time per batch: {0} ms", timePerBatch);
double timePerDoc = Math.Round(runningTime.TotalMilliseconds / numDocs, 4);
Console.WriteLine("Upload time per document: {0} ms \n", timePerDoc);
}
Esse código usa chamadas assíncronas para uma função ExponentialBackoffAsync
que implementa a estratégia de retirada. Você chama a função usando threads, por exemplo, o número de núcleos que seu processador tem. Quando o número máximo de threads tiver sido usado, o código aguardará a conclusão de qualquer thread. Em seguida, ele cria um novo thread até que todos os documentos sejam carregados.