Esercitazione: Ottimizzare l'indicizzazione con l'API push

Ricerca di intelligenza artificiale di Azure supporta due approcci di base per l'importazione dei dati in un indice di ricerca: eseguire il push dei dati nell'indice a livello di codice o puntare un indicizzatore di Ricerca intelligenza artificiale di Azure a un'origine dati supportata per eseguire il pull dei dati.

Questa esercitazione illustra come indicizzare in modo efficiente i dati usando il modello push inviando in batch le richieste e usando una strategia di ripetizione dei tentativi di backoff esponenziale. È possibile scaricare ed eseguire l'applicazione di esempio. Questo articolo illustra gli aspetti chiave dell'applicazione e quali fattori considerare durante l'indicizzazione dei dati.

Questa esercitazione usa C# e la libreria Azure.Search.Documents di Azure SDK per .NET per eseguire le attività seguenti:

  • Creare un indice
  • Testare diverse dimensioni di batch per determinare quelle più efficienti
  • Indicizzare i batch in modo asincrono
  • Usare più thread per aumentare la velocità di indicizzazione
  • Usare una strategia di ripetizione di tentativi di backoff esponenziale per eseguire nuovi tentativi con i documenti che presentano errori

Se non si ha una sottoscrizione di Azure, creare un account gratuito prima di iniziare.

Prerequisiti

Per questa esercitazione sono necessari i servizi e gli strumenti seguenti.

Scaricare i file

Il codice sorgente per questa esercitazione si trova nella cartella optimize-data-indexing/v11 nel repository GitHub Azure-Samples/azure-search-dotnet-samples .

Considerazioni essenziali

I fattori che influiscono sulla velocità di indicizzazione sono elencati di seguito. Per altre informazioni, vedere Indicizzare set di dati di grandi dimensioni.

  • Livello di servizio e numero di partizioni/repliche : l'aggiunta di partizioni o l'aggiornamento del livello aumenta la velocità di indicizzazione.
  • Complessità dello schema dell'indice: l'aggiunta di campi e proprietà dei campi riduce la velocità di indicizzazione. Gli indici più piccoli sono più veloci da indicizzare.
  • Dimensioni dei batch: le dimensioni ottimali dei batch variano in base allo schema dell'indice e al set di dati.
  • Numero di thread/ruoli di lavoro : un singolo thread non sfrutta appieno le velocità di indicizzazione.
  • Strategia di ripetizione dei tentativi: una strategia di ripetizione dei tentativi di backoff esponenziale è una procedura consigliata per l'indicizzazione ottimale.
  • Velocità di trasferimento dei dati di rete: la velocità di trasferimento dei dati può essere un fattore limitante. Indicizzare i dati all'interno dell'ambiente Azure per aumentare la velocità di trasferimento dei dati.

1 - Creare servizio di ricerca di intelligenza artificiale di Azure

Per completare questa esercitazione, è necessario un servizio di ricerca di intelligenza artificiale di Azure, che è possibile creare nel portale. È consigliabile usare lo stesso livello che si prevede di usare nell'ambiente di produzione, per poter testare e ottimizzare la velocità di indicizzazione in modo accurato.

Questa esercitazione usa l'autenticazione basata su chiave. Copiare una chiave API amministratore da incollare nel file appsettings.json .

  1. Accedere al portale di Azure e nella pagina panoramica del servizio di ricerca ottenere l'URL. Un endpoint di esempio potrebbe essere simile a https://mydemo.search.windows.net.

  2. In Impostazioni>Chiavi ottenere una chiave amministratore per diritti completi sul servizio. Sono disponibili due chiavi amministratore interscambiabili, fornite per continuità aziendale nel caso in cui sia necessario eseguire il rollover di una di esse. È possibile usare la chiave primaria o secondaria nelle richieste per l'aggiunta, la modifica e l'eliminazione di oggetti.

    Get an HTTP endpoint and access key

2 - Configurare l'ambiente

  1. Avviare Visual Studio e aprire OptimizeDataIndexing.sln.
  2. In Esplora soluzioni aprire il file appsettings.json per aggiungervi le informazioni sulla connessione.
{
  "SearchServiceUri": "https://{service-name}.search.windows.net",
  "SearchServiceAdminApiKey": "",
  "SearchIndexName": "optimize-indexing"
}

3 - Esplorare il codice

Dopo l'aggiornamento di appsettings.json, il programma di esempio in OptimizeDataIndexing.sln sarà pronto per la compilazione e l'esecuzione.

Questo codice è derivato dalla sezione C# di Avvio rapido: Ricerca full-text con gli SDK di Azure. In tale articolo sono disponibili informazioni più dettagliate sulle nozioni di base per l'uso di .NET SDK.

Questa semplice app console in C#/.NET esegue le attività seguenti:

  • Crea un nuovo indice basato sulla struttura dei dati della classe Hotel in C# (che fa anche riferimento alla classe Address)
  • Testa diverse dimensioni di batch per determinare quelle più efficienti
  • Indicizza i dati in modo asincrono
    • Usando più thread per aumentare la velocità di indicizzazione
    • Usando una strategia di ripetizione dei tentativi con backoff esponenziale per gli elementi con errori

Prima di eseguire il programma, esaminare il codice e le definizioni di indice per questo esempio. Il codice rilevante si trova in diversi file:

  • Hotel.cs e Address.cs contengono lo schema che definisce l'indice
  • DataGenerator.cs contiene una classe semplice per facilitare la creazione di grandi quantità di dati degli alberghi
  • ExponentialBackoff.cs contiene codice per ottimizzare il processo di indicizzazione come descritto in questo articolo
  • Program.cs contiene funzioni che creano ed eliminano l'indice di Ricerca intelligenza artificiale di Azure, indicizza i batch di dati e testano dimensioni batch diverse

Creazione dell'indice

Questo programma di esempio usa Azure SDK per .NET per definire e creare un indice di Ricerca di intelligenza artificiale di Azure. Sfrutta la FieldBuilder classe per generare una struttura di indice da una classe del modello di dati C#.

Il modello di dati è definito dalla classe Hotel, che contiene anche riferimenti alla classe Address. FieldBuilder esegue il drill-down attraverso più definizioni di classi per generare una struttura dei dati complessa per l'indice. I tag di metadati vengono usati per definire gli attributi di ogni campo, ad esempio se è ordinabile o ricercabile.

I frammenti di codice seguenti del file Hotel.cs illustrano come sia possibile specificare un singolo campo e un riferimento a un'altra classe di modello di dati.

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

Nel file Program.cs l'indice è definito con un nome e una raccolta di campi generata dal metodo FieldBuilder.Build(typeof(Hotel)) e viene quindi creato come segue:

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

Generazione dei dati

Nel file DataGenerator.cs viene implementata una classe semplice per generare i dati per i test. L'unico scopo di questa classe è facilitare la creazione di un numero elevato di documenti con ID univoco per l'indicizzazione.

Per ottenere un elenco di 100.000 hotel con ID univoci, eseguire le righe di codice seguenti:

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

A scopo di test, in questo esempio sono disponibili due tipi di albergo: small e large.

Lo schema dell'indice ha un effetto sulle velocità di indicizzazione. Per questo motivo, è opportuno convertire questa classe per generare dati che corrispondano meglio allo schema di indice previsto dopo l'esecuzione di questa esercitazione.

4 - Testare le dimensioni dei batch

Ricerca di intelligenza artificiale di Azure supporta le API seguenti per caricare singoli o più documenti in un indice:

L'indicizzazione dei documenti in batch migliorerà significativamente le prestazioni di indicizzazione. I batch possono contenere fino a 1000 documenti oppure fino a circa 16 MB per batch.

Determinare le dimensioni ottimali dei batch per i dati è fondamentale per ottimizzare la velocità di indicizzazione. I due fattori principali che influiscono sulle dimensioni ottimali dei batch sono i seguenti:

  • Schema dell'indice
  • Dimensioni dei dati

Dato che le dimensioni ottimali dei batch dipendono dall'indice e dai dati, l'approccio migliore consiste nel testare dimensioni di batch diverse per determinare quali garantiscono la velocità di indicizzazione più elevata per lo scenario specifico.

La funzione seguente illustra un approccio semplice per testare le dimensioni dei batch.

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

Dato che non tutti i documenti sono delle stesse dimensioni (nonostante in questo esempio lo siano), si stimano le dimensioni dei dati inviati al servizio di ricerca. A tale scopo si usa la funzione seguente, che prima converte l'oggetto in JSON e quindi ne determina le dimensioni in byte. Questa tecnica consente di determinare le dimensioni di batch più efficienti in termini di velocità di indicizzazione in 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;
}

La funzione richiede un SearchClient più il numero di tentativi da testare per ogni dimensione del batch. Poiché potrebbe verificarsi una variabilità nei tempi di indicizzazione per ogni batch, ogni batch viene provato tre volte per impostazione predefinita per rendere i risultati più significativi statisticamente.

await TestBatchSizesAsync(searchClient, numTries: 3);

Quando si esegue la funzione, nella console verrà visualizzato un output simile al seguente:

Output of test batch size function

Identificare le dimensioni di batch più efficienti e quindi usare tali dimensioni nel passaggio successivo dell'esercitazione. È possibile che si verifichi un plateau in MB/s tra dimensioni batch diverse.

5 - Indicizzare dati

Ora che sono state identificate le dimensioni di batch che si intende usare, il passaggio successivo consiste nell'iniziare a indicizzare i dati. Per indicizzare i dati in modo efficiente, questo esempio:

  • Usa più thread/ruoli di lavoro.
  • Implementa una strategia di ripetizione dei tentativi con backoff esponenziale.

Rimuovere il commento dalle righe da 41 a 49 e rieseguire e il programma. In questa esecuzione, l'esempio genera e invia batch di documenti, fino a 100.000 se si esegue il codice senza modificare i parametri.

Usare più thread/ruoli di lavoro

Per sfruttare al meglio le velocità di indicizzazione di Ricerca intelligenza artificiale di Azure, usare più thread per inviare contemporaneamente richieste di indicizzazione batch al servizio.

Alcune delle considerazioni chiave indicate in precedenza possono influire sul numero ottimale di thread. È possibile modificare questo esempio ed eseguire test con diversi conteggi dei thread per determinare il conteggio ottimale per lo scenario specifico. Con l'esecuzione simultanea di diversi thread, comunque, dovrebbe essere possibile sfruttare la maggior parte dei vantaggi in termini di efficienza.

Man mano che si aumentano le richieste che raggiunge il servizio di ricerca, è possibile che vengano visualizzati codici di stato HTTP che indicano che la richiesta non ha avuto esito positivo. Durante l'indicizzazione, i due codici di stato HTTP comuni sono i seguenti:

  • 503 - Servizio non disponibile. Questo errore indica che il sistema è in sovraccarico e al momento la richiesta non può essere elaborata.
  • 207 - Multi-Status. Questo errore indica che alcuni documenti hanno avuto esito positivo, ma almeno uno ha avuto esito negativo.

Implementare una strategia di ripetizione dei tentativi con backoff esponenziale

Se si verifica un errore, le richieste dovranno essere ripetute usando una strategia di ripetizione dei tentativi con backoff esponenziale.

.NET SDK di Ricerca intelligenza artificiale di Azure ritenta automaticamente i tentativi 503 e altre richieste non riuscite, ma è necessario implementare la propria logica per riprovare 207. Gli strumenti open source, ad esempio Polly , possono essere utili in una strategia di ripetizione dei tentativi.

In questo esempio si implementa una strategia di ripetizione dei tentativi con backoff esponenziale. Per iniziare, definire alcune variabili, tra cui e l'iniziale maxRetryAttemptsdelay per una richiesta non riuscita:

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

I risultati dell'operazione di indicizzazione sono archiviati nella variabile IndexDocumentResult result. Questa variabile è importante perché consente di verificare se i documenti nel batch presentano errori, come illustrato di seguito. Se si verifica un errore parziale, viene creato un nuovo batch in base all'ID dei documenti non riusciti.

È anche necessario rilevare le eccezioni RequestFailedException, perché indicano che la richiesta non è completamente riuscita e dovrà essere ritentata.

// 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 questo punto si esegue il wrapping del codice di backoff esponenziale in una funzione per facilitarne la chiamata.

Viene quindi creata un'altra funzione per gestire i thread attivi. Per semplicità, questa funzione non è inclusa, ma è disponibile in ExponentialBackoff.cs. La funzione può essere chiamata con il comando seguente, dove hotels rappresenta i dati da caricare, 1000 la dimensione dei batch e 8 il numero di thread simultanei:

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

Quando si esegue la funzione, verrà visualizzato un output simile al seguente:

Output of index data function

Quando un batch di documenti ha esito negativo, viene visualizzato un errore che indica l'errore e che verrà eseguito un nuovo tentativo:

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

Al termine dell'esecuzione della funzione, è possibile verificare che tutti i documenti siano stati aggiunti all'indice.

6 - Esplorare l'indice

È possibile esplorare l'indice di ricerca popolato dopo che il programma è stato eseguito a livello di codice o usando Esplora ricerche nel portale.

A livello di codice

Per verificare il numero di documenti in un indice sono disponibili due opzioni principali: l'API di conteggio dei documenti e l'API di recupero delle statistiche dell'indice. Entrambi i percorsi richiedono tempo per l'elaborazione, quindi non essere allarmati se il numero di documenti restituiti inizialmente è inferiore a quello previsto.

Conteggio documenti

L'operazione di conteggio dei documenti recupera un conteggio del numero di documenti in un indice di ricerca:

long indexDocCount = await searchClient.GetDocumentCountAsync();

Ottenere le statistiche di un indice

L'operazione di recupero delle statistiche dell'indice restituisce il numero di documenti per l'indice corrente, nonché l'utilizzo dello spazio di archiviazione. L'aggiornamento delle statistiche dell'indice richiederà più tempo rispetto all'aggiornamento del numero di documenti.

var indexStats = await indexClient.GetIndexStatisticsAsync(indexName);

Azure portal

In portale di Azure, dal riquadro di spostamento sinistro e trovare l'indice di ottimizzazione nell'elenco Indici.

List of Azure AI Search indexes

Il conteggio dei documenti e le dimensioni Archiviazione si basano sull'API Get Index Statistics e possono richiedere alcuni minuti per l'aggiornamento.

Reimpostare ed eseguire di nuovo

Nelle prime fasi sperimentali dello sviluppo, l'approccio più pratico per l'iterazione di progettazione consiste nell'eliminare gli oggetti da Ricerca di intelligenza artificiale di Azure e consentire al codice di ricompilarli. I nomi di risorsa sono univoci. L'eliminazione di un oggetto consente di ricrearlo usando lo stesso nome.

Il codice di esempio per questa esercitazione verifica la presenza di indici esistenti e li elimina affinché sia possibile eseguire nuovamente il codice.

Per eliminare gli indici è anche possibile usare il portale.

Pulire le risorse

Quando si lavora nella propria sottoscrizione, alla fine di un progetto è opportuno rimuovere le risorse che non sono più necessarie. Le risorse che rimangono in esecuzione hanno un costo. È possibile eliminare risorse singole oppure gruppi di risorse per eliminare l'intero set di risorse.

Per trovare e gestire le risorse nel portale, usare il collegamento Tutte le risorse o Gruppi di risorse nel riquadro di spostamento a sinistra.

Passaggi successivi

Per altre informazioni sull'indicizzazione di grandi quantità di dati, provare l'esercitazione seguente.