Indexación de datos mediante la API de inserción de Búsqueda de Azure AI

Completado

La API de REST es la manera más flexible de insertar datos en un índice de Búsqueda de Azure AI. Puede usar cualquier lenguaje de programación o hacerlo de forma interactiva con cualquier aplicación que pueda publicar solicitudes JSON en un punto de conexión.

Aquí, verá cómo usar la API de REST de forma eficaz y explorar las operaciones disponibles. A continuación, verá el código de .NET Core y verá cómo optimizar la adición de grandes cantidades de datos a través de la API.

Operaciones de la API de REST admitidas

Hay dos API de REST compatibles proporcionadas por Búsqueda de AI. API de búsqueda y de administración. Este módulo se centra en las API de REST de búsqueda que proporcionan operaciones en cinco características de búsqueda:

Característica Operations
Índice Crear, eliminar, actualizar y configurar.
Documento Obtener, agregar, actualizar y eliminar.
indizador Configuración de orígenes de datos y programación en orígenes de datos limitados.
Conjunto de aptitudes Obtener, crear, eliminar, enumerar y actualizar.
Mapa de sinónimos Obtener, crear, eliminar, enumerar y actualizar.

Cómo llamar a la API de REST de búsqueda

Si desea llamar a cualquiera de las API de búsqueda, debe:

  • Usar el punto de conexión HTTPS (en el puerto predeterminado 443) proporcionado por el servicio de búsqueda; debe incluir una versión de API en el URI.
  • El encabezado de solicitud debe incluir un atributo api-key.

Para buscar el punto de conexión, la versión de api y la clave de API, vaya a Azure Portal.

A screenshot of search explorer and finding the endpoint and api version.

En el portal, vaya al servicio de búsqueda y seleccione Explorador de búsqueda. El punto de conexión de la API de REST se encuentra en el campo URL de solicitud. La primera parte de la dirección URL es el punto de conexión (por ejemplo, https://azsearchtest.search.windows.net), y la cadena de consulta muestra api-version (por ejemplo, api-version=2023-07-01-Preview).

A screenshot of the keys section of a search service.

Para buscar api-key a la izquierda, seleccione Claves. La clave de administrador principal o secundaria se puede usar si usa la API de REST para hacer más acciones además de consultar el índice. Si solo necesita buscar en un índice, puede crear y usar claves de consulta.

Para agregar, actualizar o eliminar datos de un índice, debe usar una clave de administrador.

Adición de datos a un índice

Use una solicitud HTTP POST con la característica de índices en este formato:

POST https://[service name].search.windows.net/indexes/[index name]/docs/index?api-version=[api-version] 

El cuerpo de la solicitud debe informar al punto de conexión de REST de la acción que se debe realizar en el documento, en qué documento se debe aplicar la acción y qué datos se deben usar.

El JSON debe tener este 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)  
        ...  
    },  
    ...  
  ]  
}
Acción Descripción
upload De manera parecida a un upsert en SQL, el documento se creará o se reemplazará.
merge La combinación actualiza un documento existente con los campos especificados. Si no se encuentra ningún documento, se producirá un error en la combinación.
mergeOrUpload La combinación actualiza un documento existente con los campos especificados y lo carga si el documento no existe.
delete Elimina todo el documento, solo tiene que especificar key_field_name.

Si la solicitud se realiza correctamente, la API devolverá un código de estado 200.

Nota:

Para obtener una lista completa de todos los códigos de respuesta y los mensajes de error, consulte Adición, actualización o eliminación de documentos (API de REST de Búsqueda de Azure AI).

En este ejemplo, JSON carga el registro del cliente en la unidad 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"
          }
        }
      ]
    }
  ]
}

Puede agregar tantos documentos en la matriz de valores como desee. Sin embargo, para un rendimiento óptimo, considere la posibilidad de procesar por lotes los documentos de las solicitudes hasta un máximo de 1000 documentos o un tamaño total de 16 MB.

Uso de .NET Core para indexar los datos

Para obtener el mejor rendimiento, use la biblioteca cliente de Azure.Search.Document más reciente, que actualmente es la versión 11. Puede instalar la biblioteca cliente con NuGet:

dotnet add package Azure.Search.Documents --version 11.4.0

El rendimiento del índice se basa en seis factores clave:

  • El nivel de servicio de la búsqueda y cuántas réplicas y particiones ha habilitado.
  • La complejidad del esquema del índice. Reduzca el número de propiedades (buscables, clasificables y ordenables) que tiene cada campo.
  • El número de documentos de cada lote; el mejor tamaño dependerá del esquema del índice y del tamaño de los documentos.
  • En qué grado es multiproceso el enfoque.
  • Control de los errores y la limitación. Use una estrategia de reintento de retroceso exponencial.
  • Donde residen los datos, intente indexar los datos de la forma más similar posible al índice de búsqueda. Por ejemplo, ejecute cargas desde dentro del entorno de Azure.

Determinación del tamaño de lote óptimo

Dado que determinar el mejor tamaño de lote es un factor clave para mejorar el rendimiento, echemos un vistazo a un enfoque en el 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();
}

El enfoque consiste en aumentar el tamaño del lote y supervisar el tiempo necesario para recibir una respuesta válida. El código recorre de 100 a 1000, en 100 pasos del documento. Para cada tamaño de lote, genera el tamaño del documento, el tiempo para obtener una respuesta y el tiempo medio por MB. La ejecución de este código proporciona resultados como los siguientes:

A screenshot of the output from the code above.

En el ejemplo anterior, el mejor tamaño de lote para el rendimiento es de 2,499 MB por segundo y 800 documentos por lote.

Implementación de una estrategia de reintento de retroceso exponencial

Si el índice comienza a limitar las solicitudes debido a sobrecargas, responde con un estado 503 (solicitud rechazada debido a una carga pesada) o 207 (algunos documentos del lote generaron errores). Tiene que controlar estas respuestas y una buena estrategia es el retroceso. El retroceso consiste en detenerse durante algún tiempo antes de volver a intentar la solicitud. Si aumenta este tiempo para cada error, realizará un retroceso exponencial.

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);

El código realiza un seguimiento de los documentos con errores de un lote. Si se produce un error, espera un retraso y, a continuación, duplica el retraso para el siguiente error.

Por último, hay un número máximo de reintentos y, si se alcanza este número máximo, el programa existe.

Uso de subprocesos para mejorar el rendimiento

Puede completar la aplicación de carga de documentos mediante la combinación de la estrategia de retroceso anterior con un enfoque de subprocesos. A continuación, se ponen ejemplos 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);
        }

Este código usa llamadas asincrónicas a una función ExponentialBackoffAsync que implementa la estrategia de retroceso. La función se llama mediante subprocesos; por ejemplo, el número de núcleos que tiene el procesador. Cuando se ha usado el número máximo de subprocesos, el código espera a que finalice cualquier subproceso. A continuación, crea un nuevo subproceso hasta que se cargan todos los documentos.