Indicizzare tutti i dati usando l'API push di Ricerca intelligenza artificiale di Azure
L'API REST è il modo più flessibile per eseguire il push dei dati in un indice di Ricerca intelligenza artificiale di Azure. È possibile usare qualsiasi linguaggio di programmazione o in modo interattivo con qualsiasi app in grado di inviare richieste JSON a un endpoint.
In questa unità verrà illustrato come usare l'API REST in modo efficace ed esplorare le operazioni disponibili. Si esaminerà quindi il codice .NET Core e si vedrà come ottimizzare l'aggiunta di grandi quantità di dati tramite l'API.
Operazioni dell'API REST supportate
Sono disponibili due API REST supportate dalla ricerca di intelligenza artificiale. API di ricerca e gestione. Questo modulo è incentrato sulle API REST di ricerca che forniscono operazioni su cinque funzionalità di ricerca:
Caratteristica / Funzionalità | Operazioni |
---|---|
Indice | Creare, eliminare, aggiornare e configurare. |
Documento | Ottenere, aggiungere, aggiornare ed eliminare. |
Indicizzatore | Configurare origini dati e pianificazione in origini dati limitate. |
Set di competenze | Ottenere, creare, eliminare, elencare e aggiornare. |
Mappa di sinonimi | Ottenere, creare, eliminare, elencare e aggiornare. |
Come chiamare l'API REST di ricerca
Per chiamare una delle API di ricerca necessarie:
- Usare l'endpoint HTTPS (sulla porta predefinita 443) fornito dal servizio di ricerca, è necessario includere un versione api nell'URI.
- L'intestazione della richiesta deve includere un attributo chiave API.
Per trovare l'endpoint, la versione api e la chiave API, passare al portale di Azure.
Nel portale passare al servizio di ricerca e quindi selezionare Esplora ricerche. L'endpoint dell'API REST si trova nel campo URL richiesta. La prima parte dell'URL è l'endpoint (ad esempio https://azsearchtest.search.windows.net) e la stringa di query mostra il api-version
(ad esempio api-version=2023-07-01-Preview).
Per trovare il api-key
a sinistra, selezionare chiavi . La chiave di amministrazione primaria o secondaria può essere usata se si usa l'API REST per eseguire più di una semplice query sull'indice. Se è sufficiente eseguire ricerche in un indice, è possibile creare e usare chiavi di query.
Per aggiungere, aggiornare o eliminare dati in un indice è necessario usare una chiave di amministrazione.
Aggiungere dati a un indice
Usare una richiesta HTTP POST usando la funzionalità indici in questo formato:
POST https://[service name].search.windows.net/indexes/[index name]/docs/index?api-version=[api-version]
Il corpo della richiesta deve informare l'endpoint REST dell'azione da eseguire sul documento, il documento da applicare anche all'azione e i dati da usare.
Il codice JSON deve essere in questo 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)
...
},
...
]
}
Azione | Descrizione |
---|---|
caricare | Analogamente a un upsert in SQL, il documento verrà creato o sostituito. |
unione | Unisci aggiorna un documento esistente con i campi specificati. L'unione avrà esito negativo se non è possibile trovare alcun documento. |
mergeOrUpload | Unisci aggiorna un documento esistente con i campi specificati e lo carica se il documento non esiste. |
eliminare | Elimina l'intero documento, è sufficiente specificare il key_field_name. |
Se la richiesta ha esito positivo, l'API restituirà un codice di stato 200.
Nota
Per un elenco completo di tutti i codici di risposta e i messaggi di errore, vedere aggiungere, aggiornare o eliminare documenti (API REST di Ricerca di intelligenza artificiale di Azure)
Questo esempio JSON carica il record del cliente nell'unità precedente:
{
"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"
}
}
]
}
]
}
È possibile aggiungere il numero desiderato di documenti nella matrice di valori. Tuttavia, per ottenere prestazioni ottimali, prendere in considerazione l'invio in batch dei documenti nelle richieste fino a un massimo di 1.000 documenti o 16 MB di dimensioni totali.
Usare .NET Core per indicizzare i dati
Per ottenere prestazioni ottimali, usare la libreria client Azure.Search.Document
più recente, attualmente versione 11. È possibile installare la libreria client con NuGet:
dotnet add package Azure.Search.Documents --version 11.4.0
Le prestazioni dell'indice si basano su sei fattori chiave:
- Il livello di servizio di ricerca e il numero di repliche e partizioni abilitate.
- Complessità dello schema dell'indice. Ridurre il numero di proprietà (ricercabile, tabella visibile, ordinabile) di ogni campo.
- Il numero di documenti in ogni batch, le dimensioni migliori dipendono dallo schema dell'indice e dalle dimensioni dei documenti.
- Qual è il modo in cui il multithreading è l'approccio.
- Gestione degli errori e della limitazione. Usare una strategia di ripetizione dei tentativi di backoff esponenziale.
- Dove risiedono i dati, provare a indicizzare i dati come vicini all'indice di ricerca. Ad esempio, eseguire i caricamenti dall'interno dell'ambiente Azure.
Ottimizzare le dimensioni del batch
Quando si lavora al meglio le dimensioni del batch è un fattore chiave per migliorare le prestazioni, si esaminerà un approccio nel codice.
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();
}
L'approccio consiste nell'aumentare le dimensioni del batch e monitorare il tempo necessario per ricevere una risposta valida. Il codice esegue cicli da 100 a 1000, in 100 passaggi del documento. Per ogni dimensione del batch, restituisce le dimensioni del documento, il tempo necessario per ottenere una risposta e il tempo medio per MB. L'esecuzione di questo codice offre risultati simili al seguente:
Nell'esempio precedente le dimensioni batch migliori per la velocità effettiva sono 2,499 MB al secondo, 800 documenti per batch.
Implementare una strategia di ripetizione dei tentativi con backoff esponenziale
Se l'indice inizia a limitare le richieste a causa di overload, risponde con uno stato 503 (richiesta rifiutata a causa di un carico elevato) o 207 (alcuni documenti non riusciti nel batch). È necessario gestire queste risposte e una buona strategia è il backoff. Il back-off significa sospendere per qualche tempo prima di riprovare la richiesta. Se si aumenta questo tempo per ogni errore, si tornerà in modo esponenziale.
Esaminare questo codice:
// 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);
Il codice tiene traccia dei documenti non riusciti in un batch. Se si verifica un errore, attende un ritardo e quindi raddoppia il ritardo per l'errore successivo.
Infine, esiste un numero massimo di tentativi e, se questo numero massimo viene raggiunto, il programma esiste.
Usare il threading per migliorare le prestazioni
È possibile completare l'app di caricamento dei documenti combando la strategia di backoff precedente con un approccio di threading. Ecco un codice di esempio:
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);
}
Questo codice usa chiamate asincrone a una funzione ExponentialBackoffAsync
che implementa la strategia di backoff. Si chiama la funzione usando thread, ad esempio il numero di core del processore. Quando è stato usato il numero massimo di thread, il codice attende il completamento di qualsiasi thread. Crea quindi un nuovo thread finché non vengono caricati tutti i documenti.