Modellare tipi di dati complessi in Azure AI Search
I set di dati esterni usati per popolare un indice di Azure AI Search possono presentarsi in diverse forme. In alcuni casi. includono sottostrutture gerarchiche o annidate. Alcuni esempi possono includere più indirizzi per un singolo cliente, più colori e dimensioni per un singolo prodotto, più autori di un singolo libro e così via. Nel gergo di modellazione, queste strutture possono essere definite tipi di dati complessi, composti, compositi o aggregati. Il termine usato da Azure AI Search per questo concetto è tipo complesso. In Azure AI Search, i tipi complessi vengono modellati usando campi complessi. Un campo complesso è un campo contenente elementi figlio (campi secondari) che possono essere di qualsiasi tipo di dati, inclusi altri tipi complessi. Questa operazione funziona in modo analogo ai tipi di dati strutturati in un linguaggio di programmazione.
I campi complessi rappresentano un singolo oggetto nel documento o una matrice di oggetti, a seconda del tipo di dati. I campi di tipo Edm.ComplexType
rappresentano singoli oggetti, mentre i campi di tipo Collection(Edm.ComplexType)
rappresentano matrici di oggetti.
Azure AI Search supporta in modo nativo tipi e raccolte complessi. Questi tipi consentono di modellare quasi tutte le strutture JSON in un indice di Azure AI Search. Nelle versioni precedenti delle API di Azure AI Search, era possibile importare solo set di righe. Nella versione più recente, l'indice può ora corrispondere più precisamente ai dati di origine. In altre parole, se i dati di origine hanno tipi complessi, anche l'indice può avere tipi complessi.
Per iniziare, è consigliabile usare il set di dati Hotels, che può essere caricato nella procedura guidata Importa dati nel portale di Azure. La procedura guidata rileva tipi complessi nell'origine e suggerisce uno schema di indice basato sulle strutture rilevate.
Nota
Il supporto per i tipi complessi è diventato disponibile a livello generale a partire da api-version=2019-05-06
.
Se la soluzione di ricerca è basata su soluzioni alternative precedenti di set di dati bidimensionati in una raccolta, è necessario modificare l'indice in modo da includere tipi complessi supportati nella versione più recente dell'API. Per altre informazioni sull'aggiornamento delle versioni dell'API, vedere Eseguire l'aggiornamento alla versione più recente dell'API REST o eseguire l'aggiornamento alla versione più recente di .NET SDK.
Esempio di struttura complessa
Il documento JSON seguente è costituito da campi semplici e campi complessi. I campi complessi, ad esempio Address
e Rooms
, dispongono di sottocampi.
Address
ha un singolo set di valori per tali sottocampi, poiché si tratta di un singolo oggetto nel documento. Al contrario, Rooms
dispone di più set di valori per i relativi sottocampi, uno per ogni oggetto dell'insieme.
{
"HotelId": "1",
"HotelName": "Stay-Kay City Hotel",
"Description": "Ideally located on the main commercial artery of the city in the heart of New York.",
"Tags": ["Free wifi", "on-site parking", "indoor pool", "continental breakfast"],
"Address": {
"StreetAddress": "677 5th Ave",
"City": "New York",
"StateProvince": "NY"
},
"Rooms": [
{
"Description": "Budget Room, 1 Queen Bed (Cityside)",
"RoomNumber": 1105,
"BaseRate": 96.99,
},
{
"Description": "Deluxe Room, 2 Double Beds (City View)",
"Type": "Deluxe Room",
"BaseRate": 150.99,
}
. . .
]
}
Creare campi complessi
Come per qualsiasi definizione di indice, è possibile usare la portale di Azure, l'API REST o .NET SDK per creare uno schema che include tipi complessi.
Altri SDK di Azure forniscono campioni in Python, Java e JavaScript.
Accedere al portale di Azure.
Nella pagina Panoramica del servizio di ricerca, selezionare la scheda Indici.
Aprire un indice esistente o creare un nuovo indice.
Selezionare la scheda Campi e quindi selezionare Aggiungi campo. Verrà aggiunto un campo vuoto. Se si usa una raccolta di campi esistenti, scorrere verso il basso per configurare il campo.
Assegnare un nome al campo e impostarne il tipo su
Edm.ComplexType
oCollection(Edm.ComplexType)
.Selezionare i puntini di sospensione all'estrema destra, quindi selezionare Aggiungi campo o Aggiungi sottocampo e quindi assegnare attributi.
Limiti di raccolta complessi
Durante l'indicizzazione, è possibile avere un massimo di 3.000 elementi in tutte le raccolte complesse all'interno di un singolo documento. Un elemento di una raccolta complessa è un membro di tale raccolta. Per Rooms (l'unica raccolta complessa nell'esempio hotel), ogni camera è un elemento. Nell'esempio precedente, se "Stay-Kay City Hotel" aveva 500 camere, il documento dell'hotel avrebbe 500 elementi della stanza. Per raccolte complesse annidate, viene conteggiato anche ogni elemento annidato, oltre all'elemento esterno (padre).
Questo limite si applica solo a raccolte complesse e non a tipi complessi (ad esempio Address) o raccolte di stringhe (ad esempio Tag).
Aggiornare campi complessi
Tutte le regole di reindicizzazione generalmente applicabili a campi si applicano allo stesso modo a campi complessi. L'aggiunta di un nuovo campo a un tipo complesso non richiede una ricompilazione dell'indice, ma la maggior parte delle altre modifiche richiede una ricompilazione.
Aggiornamenti strutturali alla definizione
È possibile aggiungere nuovi campi secondari a un campo complesso in qualsiasi momento senza la necessità di una ricompilazione dell'indice. Ad esempio, l'aggiunta di "ZipCode" a Address
o "Servizi" a Rooms
è consentita, proprio come l'aggiunta di un campo di primo livello a un indice. I documenti esistenti hanno un valore Null per nuovi campi fino a quando questi campi non vengono popolati esplicitamente aggiornando i dati.
Si noti che all'interno di un tipo complesso, ogni sottocampo ha un tipo e può avere attributi, proprio come i campi di primo livello
Aggiornamenti dei dati
L'aggiornamento di documenti esistenti in un indice con l'azione upload
funziona allo stesso modo per i campi complessi e quelli semplici: tutti i campi vengono sostituiti. Tuttavia, merge
(o mergeOrUpload
quando applicato a un documento esistente) non funziona allo stesso modo in tutti i campi. In particolare, merge
non supporta l'unione di elementi all'interno di una raccolta. Questa limitazione è presente per raccolte di tipi primitivi e raccolte complesse. Per aggiornare una raccolta, è necessario recuperarne il valore completo, apportare modifiche e quindi includere la nuova raccolta nella richiesta DELL'API Index.
Cercare campi complessi nelle query di testo
Le espressioni di ricerca in formato libero funzionano come previsto con tipi complessi. Se un campo o un sottocampo ricercabile in qualsiasi parte di un documento è corrispondente, il documento stesso è corrispondente.
Le query diventano più complesse e sfaccettate quando si hanno più termini e operatori e alcuni termini hanno nomi di campo specificati, come possibile con la sintassi Lucene. Ad esempio, questa query tenta di trovare una corrispondenza tra due termini, "Portland" e "OR", rispetto a due sottocampi del campo Indirizzo:
search=Address/City:Portland AND Address/State:OR
Le query di questo tipo sono non correlate per la ricerca a tutto testo, a differenza dei filtri. Nei filtri, le query sui sottocampi di una raccolta complessa vengono correlate usando variabili di intervallo in any
o all
. La query Lucene precedente restituisce documenti contenenti sia "Portland, Maine" che "Portland, Oregon", insieme ad altre città in Oregon. Ciò si verifica perché ogni clausola si applica a tutti i valori del relativo campo nell'intero documento, quindi non esiste un concetto di "sottodocumento corrente". Per altre informazioni su questo argomento, vedere Informazioni sui filtri di raccolta OData in Azure AI Search.
Cercare campi complessi nelle query RAG
Un criterio RAG passa i risultati della ricerca a un modello di chat per l'intelligenza artificiale generativa e la ricerca conversazionale. Per impostazione predefinita, i risultati della ricerca passati a un LLM sono un set di righe bidimensionale. Tuttavia, se l'indice ha tipi complessi, la query può fornire tali campi se si converte prima i risultati della ricerca in JSON e quindi si passa json all'LLM.
Un esempio parziale illustra la tecnica:
- Indicare i campi desiderati nel prompt o nella query
- Assicurarsi che i campi siano ricercabili e recuperabili nell'indice
- Selezionare i campi per i risultati della ricerca
- Formattare i risultati come JSON
- Inviare la richiesta di completamento della chat al provider di modelli
import json
# Query is the question being asked. It's sent to the search engine and the LLM.
query="Can you recommend a few hotels that offer complimentary breakfast? Tell me their description, address, tags, and the rate for one room they have which sleep 4 people."
# Set up the search results and the chat thread.
# Retrieve the selected fields from the search index related to the question.
selected_fields = ["HotelName","Description","Address","Rooms","Tags"]
search_results = search_client.search(
search_text=query,
top=5,
select=selected_fields,
query_type="semantic"
)
sources_filtered = [{field: result[field] for field in selected_fields} for result in search_results]
sources_formatted = "\n".join([json.dumps(source) for source in sources_filtered])
response = openai_client.chat.completions.create(
messages=[
{
"role": "user",
"content": GROUNDED_PROMPT.format(query=query, sources=sources_formatted)
}
],
model=AZURE_DEPLOYMENT_MODEL
)
print(response.choices[0].message.content)
Per l'esempio end-to-end, vedere Avvio rapido: Ricerca generativa (RAG) con dati di base da Ricerca di intelligenza artificiale di Azure.
Selezionare campi complessi
Il parametro $select
viene usato per scegliere quali campi vengono restituiti nei risultati della ricerca. Per utilizzare questo parametro per selezionare campi secondari specifici di un campo complesso, includere il campo padre e il sottocampo separati da una barra (/
).
$select=HotelName, Address/City, Rooms/BaseRate
I campi devono essere contrassegnati come Recuperabili nell'indice se si desidera che siano presenti tra i risultati della ricerca. In un'istruzione $select
è possibile usare solo i campi contrassegnati come Recuperabile.
Filtrare, eseguire facet e ordinare campi complessi
La stessa sintassi di percorso OData usata per i filtri e le ricerche a campi può essere usata anche per eseguire facet, ordinare e selezionare campi in una richiesta di ricerca. Per i tipi complessi, si applicano regole che determinano quali sottocampi possono essere contrassegnati come ordinabili o facetable. Per altre informazioni su queste regole, vedere le Informazioni di riferimento sull'API Crea Indice.
Sottocampi in facet
Qualsiasi sottocampo può essere contrassegnato come facetable a meno che non sia di tipo Edm.GeographyPoint
o Collection(Edm.GeographyPoint)
.
I conteggi dei documenti restituiti nei risultati del facet vengono calcolati per il documento padre (un hotel), non per i documenti secondari in una raccolta complessa (sale). Si supponga, ad esempio, che un hotel abbia 20 camere di tipo "suite". Dato questo parametro facet facet=Rooms/Type
, il numero di facet sarà 1 per l'hotel, non 20 per le camere.
Ordinamento di campi complessi
Le operazioni di ordinamento si applicano ai documenti (Hotel) e non ai documenti secondari (Stanze). Quando si dispone di una raccolta di tipi complessa, ad esempio Camere, è importante tenere presente che non è possibile effettuare ordinamenti di alcun tipo su Stanze. Infatti, non è possibile ordinare alcuna raccolta.
Le operazioni di ordinamento funzionano quando i campi hanno un singolo valore per ogni documento, indipendentemente dal fatto che il campo sia un campo semplice o un sottocampo in un tipo complesso. Ad esempio, Address/City
può essere ordinato perché esiste un solo indirizzo per hotel, quindi $orderby=Address/City
ordina gli hotel in base alla città.
Applicare filtri a campi complessi
È possibile fare riferimento a sottocampi di un campo complesso in un'espressione di filtro. È sufficiente usare la stessa sintassi del percorso OData usata per eseguire il faceting, ordinare e selezionare campi. Ad esempio, il filtro seguente restituisce tutti gli hotel in Canada:
$filter=Address/Country eq 'Canada'
Per filtrare in base a un campo di raccolta complesso, è possibile usare un'espressione lambda con gli operatori any
eall
. In tal caso, la variabile di intervallo dell'espressione lambda è un oggetto con campi secondari. È possibile fare riferimento a tali campi secondari con la sintassi del percorso OData standard. Ad esempio, il filtro seguente restituisce tutti gli hotel con almeno una camera deluxe e tutte le camere per non fumatori:
$filter=Rooms/any(room: room/Type eq 'Deluxe Room') and Rooms/all(room: not room/SmokingAllowed)
Come per i campi semplici di primo livello, i sottocampi semplici di campi complessi possono essere inclusi in filtri solo se l'attributo filtrabile è impostato su true
nella definizione dell'indice. Per altre informazioni, vedere le informazioni di riferimento sull’API Crea Indice.
Soluzione alternativa per il limite di raccolta complesso
Tenere presente che Ricerca intelligenza artificiale di Azure limita gli oggetti complessi in una raccolta a 3.000 oggetti per documento. Il superamento di questo limite comporta il messaggio seguente:
A collection in your document exceeds the maximum elements across all complex collections limit.
The document with key '1052' has '4303' objects in collections (JSON arrays).
At most '3000' objects are allowed to be in collections across the entire document.
Remove objects from collections and try indexing the document again."
Se sono necessari più di 3.000 elementi, è possibile inviare tramite pipe (|
) o usare qualsiasi forma di delimitatore per delimitare i valori, concatenarli e archiviarli come stringa delimitata. Non esiste alcuna limitazione al numero di stringhe archiviate in una matrice. L'archiviazione di valori complessi come stringhe ignora la limitazione complessa della raccolta.
Per illustrare, si supponga di avere una "searchScope
matrice con più di 3.000 elementi:
"searchScope": [
{
"countryCode": "FRA",
"productCode": 1234,
"categoryCode": "C100"
},
{
"countryCode": "USA",
"productCode": 1235,
"categoryCode": "C200"
}
. . .
]
La soluzione alternativa per archiviare i valori come stringa delimitata potrebbe essere simile alla seguente:
"searchScope": [
"|FRA|1234|C100|",
"|FRA|*|*|",
"|*|1234|*|",
"|*|*|C100|",
"|FRA|*|C100|",
"|*|1234|C100|"
]
L'archiviazione di tutte le varianti di ricerca nella stringa delimitata è utile negli scenari di ricerca in cui si desidera cercare gli elementi che hanno solo "FRA" o "1234" o un'altra combinazione all'interno della matrice.
Ecco un frammento di formattazione del filtro in C# che converte gli input in stringhe ricercabili:
foreach (var filterItem in filterCombinations)
{
var formattedCondition = $"searchScope/any(s: s eq '{filterItem}')";
combFilter.Append(combFilter.Length > 0 ? " or (" + formattedCondition + ")" : "(" + formattedCondition + ")");
}
L'elenco seguente fornisce input e stringhe di ricerca (output) affiancate:
Per il codice di contea "FRA" e il codice prodotto "1234", l'output formattato è
|FRA|1234|*|
.Per il codice prodotto "1234", l'output formattato è
|*|1234|*|
.Per il codice di categoria "C100", l'output formattato è
|*|*|C100|
.
Specificare il carattere jolly (*
) solo se si implementa la soluzione alternativa per la matrice di stringhe. In caso contrario, se si usa un tipo complesso, il filtro potrebbe essere simile all'esempio seguente:
var countryFilter = $"searchScope/any(ss: search.in(countryCode ,'FRA'))";
var catgFilter = $"searchScope/any(ss: search.in(categoryCode ,'C100'))";
var combinedCountryCategoryFilter = "(" + countryFilter + " and " + catgFilter + ")";
Se si implementa la soluzione alternativa, assicurarsi di testare in modo extentutivo.
Passaggi successivi
Provare il set di dati Hotel nella procedura guidata Importare dati. Per accedere ai dati, sono necessarie le informazioni di connessione di Azure Cosmos DB fornite nel file leggimi.
Con queste informazioni, il primo passaggio della procedura guidata consiste nel creare una nuova origine dati di Azure Cosmos DB. Più avanti nella procedura guidata, alla pagina dell'indice di destinazione, verrà visualizzato un indice con tipi complessi. Creare e caricare questo indice e quindi eseguire query per comprendere la nuova struttura.