Modéliser des types de données complexes dans Recherche Azure AI
Les jeux de données externes utilisés pour remplir un index Recherche Azure AI peuvent avoir différentes formes. Ils incluent parfois des sous-structures hiérarchiques ou imbriquées. Des exemples incluent les adresses multiples pour un même client, les couleurs et les tailles multiples pour un même produit, les auteurs multiples pour un même livre, etc. En termes de modélisation, ces structures peuvent être désignées sous le nom de types de données complexes, composées, composites or agrégées. Le terme utilisé par Recherche Azure AI pour ce concept est type complexe. Dans Recherche Azure AI, les types complexes sont modélisés à l’aide de champs complexes. Un champ complexe est un champ qui contient des enfants (sous-champs) pouvant correspondre à n’importe quel type de données, notamment d’autres types complexes. Ceci fonctionne d’une manière similaire aux types de données structurées dans un langage de programmation.
Les champs complexes représentent un objet unique dans le document ou un tableau d’objets, selon le type de données. Les champs de type Edm.ComplexType
représentent des objets uniques, alors que des champs de type Collection(Edm.ComplexType)
représentent des tableaux d’objets.
Recherche Azure AI prend nativement en charge les types et les collections complexes. Ces types vous permettent de modéliser presque n’importe quelle structure JSON dans un index Recherche Azure AI. Dans les versions précédentes des API de Recherche Azure AI, seuls les jeux de lignes aplaties pouvaient être importés. Dans la version la plus récente, votre index peut mieux correspondre aux données sources. En d’autres termes, si vos données sources contiennent des types complexes, votre index peut également contenir des types complexes.
Pour commencer, nous vous recommandons le jeu de données d’hôtels, que vous pouvez charger dans l’Assistant Importer des données du portail Azure. L’Assistant détecte les types complexes dans la source et suggère un schéma d’index basé sur les structures détectées.
Remarque
La prise en charge des types complexes a commencé à être généralement disponible dans api-version=2019-05-06
.
Si votre solution de recherche est basée sur des solutions de contournement antérieures de jeux de données aplatis d’une collection, vous devez modifier votre index pour inclure des types complexes pris en charge dans la dernière version d’API. Pour plus d’informations sur la mise à niveau des versions d’API, consultez Mettre à niveau vers la dernière version de l’API REST ou Mettre à niveau vers la dernière version du kit de développement logiciel (SDK) .NET.
Exemple de structure complexe
Le document JSON suivant est composé de champs simples et de champs complexes. Les champs complexes, tels que Address
et Rooms
, comportent des sous-champs. Address
contient un seul jeu de valeurs pour ces sous-champs, puisqu’il s’agit d’un objet unique dans le document. En revanche, Rooms
a plusieurs ensembles de valeurs pour ses sous-champs, soit un pour chaque objet dans la collection.
{
"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,
}
. . .
]
}
Créer des champs complexes
Comme avec n’importe quelle définition d’index, vous pouvez utiliser le Portail Azure, l’API REST, ou le Kit de développement logiciel (SDK) .NET pour créer un schéma incluant des types complexes.
D’autres kits de développement logiciel (SDK) Azure fournissent des exemples dans Python, Java et JavaScript.
Connectez-vous au portail Azure.
À partir de la page Vue d’ensemble du service de recherche, sélectionnez l’onglet Index.
Ouvrez un index existant ou créez un nouvel index.
Sélectionnez l’onglet champs, puis sélectionnez Ajouter un champ. Un champ vide est ajouté. Si vous utilisez une collection de champs existante, faites défiler vers le bas pour définir le champ.
Donnez un nom au champ et définissez le type sur
Edm.ComplexType
ouCollection(Edm.ComplexType)
.Sélectionnez les ellipses à l’extrême droite, puis sélectionnez Ajouter un champ ou Ajouter un sous- champ, puis affectez des attributs.
Limites des collections complexes
Pendant l’indexation, vous pouvez avoir un maximum de 3 000 éléments dans toutes les collections complexes d’un même document. Un élément d’une collection complexe est un membre de cette collection. Pour Chambres (la seule collection complexe dans l’exemple Hôtel), chaque chambre est un élément. Dans l’exemple ci-dessus, si l’hôtel « Stay-Kay-City » contient 500 chambres, le document de l’hôtel aurait 500 éléments de chambre. Pour les collections complexes imbriquées, chaque élément imbriqué est également compté, en plus de l’élément externe (parent).
Cette limite s’applique uniquement aux collections complexes, et non aux types complexes (tels que l’adresse) ou aux collections de chaînes (telles que les balises).
Mettre à jour des champs complexes
Toutes les règles de réindexation qui s’appliquent à des champs s’appliquent en général toujours aux champs complexes. Contrairement à la plupart des modifications, l’ajout d’un nouveau champ à un type complexe ne nécessite pas de reconstruction d’index.
Mises à jour structurelles de la définition
Vous pouvez ajouter des sous-champs à un champ complexe à tout moment, sans qu’une reconstruction d’index soit nécessaire. Par exemple, l’ajout de « ZipCode » à Address
ou d’« Amenities » (infrastructures) à Rooms
est autorisé, tout comme l’ajout d’un champ de niveau supérieur à un index. Les documents existants ont une valeur Null pour les nouveaux champs jusqu'à ce que vous remplissiez explicitement ces champs en mettant à jour de vos données.
Notez qu’au sein d’un type complexe, chaque sous-champ a un type et peut avoir des attributs, comme c’est le cas pour les champs de niveau supérieur
Mises à jour des données
La mise à jour de documents existants dans un index avec l’action upload
fonctionne de la même façon pour les champs complexes et simples : tous les champs sont remplacés. Toutefois, merge
(ou mergeOrUpload
lorsqu’il est appliqué à un document existant) ne fonctionne pas de la même façon dans tous les champs. Plus précisément, merge
ne prend pas en charge la fusion d’éléments dans une collection. Cette limitation existe pour les collections de types primitifs et les collections complexes. Pour mettre à jour une collection, vous devez récupérer la valeur de la collection complète, apporter des modifications, puis inclure la nouvelle collection dans la requête de l’API d’index.
Rechercher des champs complexes dans des requêtes de texte
Les expressions de recherche de forme libre fonctionnent comme prévu avec des types complexes. Si un champ ou un sous-champ de recherche correspond, n’importe où dans un document, le document lui-même est une correspondance.
Les requêtes sont plus nuancées lorsque vous avez plusieurs termes et opérateurs, et certains termes ont des noms de champs spécifiés, comme cela est possible avec la syntaxe Lucene. Par exemple, cette requête essaie de faire correspondre deux termes, « Portland » et « OR » à deux sous-champs du champ adresse :
search=Address/City:Portland AND Address/State:OR
Des requêtes de ce type sont sans corrélation pour la recherche en texte intégral, contrairement aux filtres. Dans les filtres, les requêtes relatives aux sous-champs d’une collection complexe sont corrélées à l’aide de variables de portée dans any
ou all
. La requête Lucene ci-dessus retourne des documents contenant « Portland, Maine » et « Portland, Oregon », ainsi que d’autres villes d’Oregon. Cela est dû au fait que chaque clause s’applique à toutes les valeurs de son champ dans le document entier. Il n’existe donc pas de concept de « sous-document actuel ». Pour plus d’informations à ce sujet, consultez Présentation des filtres de collection OData dans Recherche Azure AI.
Rechercher des champs complexes dans des requêtes RAG
Un modèle RAG transmet les résultats de recherche à un modèle de conversation pour la recherche d’IA générative et conversationnelle. Par défaut, les résultats de recherche passés à un LLM sont un ensemble de lignes aplati. Toutefois, si votre index a des types complexes, votre requête peut fournir ces champs si vous convertissez d’abord les résultats de la recherche en JSON, puis le transmettez au LLM.
Un exemple partiel illustre la technique :
- Indiquez les champs souhaités dans l’invite ou dans la requête
- Vérifiez que les champs peuvent faire l’objet d’une recherche et peuvent être récupérés dans l’index
- Sélectionnez les champs des résultats de la recherche
- Formatez les résultats en JSON
- Envoyez la demande d’achèvement de conversation au fournisseur de modèles
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)
Pour obtenir l’exemple de bout en bout, consultez Démarrage rapide : Recherche générative (RAG) avec des données de base à partir de Recherche Azure AI.
Sélectionner des champs complexes
Le paramètre $select
permet de choisir quels champs retourner dans les résultats de la recherche. Pour utiliser ce paramètre et sélectionner des sous-champs spécifiques d’un champ complexe, incluez le champ parent et le sous-champ séparés par une barre oblique (/
).
$select=HotelName, Address/City, Rooms/BaseRate
Les champs doivent être marqués comme récupérables dans l’index si vous souhaitez les afficher dans les résultats de la recherche. Seuls les champs marqués comme récupérable peuvent être utilisés dans une instruction $select
.
Filtrer et trier des champs complexes et activer des facettes pour les champs complexes
La même syntaxe de chemin OData utilisée pour le filtrage et les recherches par champ peut également être utilisée pour activer des facettes, trier et sélectionner dans le cadre d’une requête de recherche. Pour les types complexes, des règles sont appliquées pour définir quels sous-champs peuvent être marqués triables ou dotés de choix multiples. Pour plus d’informations sur ces règles, consultez la référence Créer une API d’index.
Sous-champs à choix multiples
Des choix multiples peuvent être activés pour n’importe quel sous-champ, sauf s’il est de type Edm.GeographyPoint
ou Collection(Edm.GeographyPoint)
.
Le nombre de documents retournés dans les résultats des choix multiples activés est calculé pour le document parent (un hôtel), et non pour les sous-documents dans une collection complexe (des chambres). Par exemple, supposez qu'un hôtel a 20 suites. Étant donné ce paramètre de choix multiples facet=Rooms/Type
, le nombre de choix sera de un pour l’hôtel, et non 20 pour les chambres.
Tri de champs complexes
Les opérations de tri s’appliquent aux documents (hôtels) et non aux sous-documents (chambres). Lorsque vous avez une collection de type complexe comme des chambres, il est important de savoir que vous ne pouvez pas du tout trier les chambres. En fait, vous ne pouvez trier aucune collection.
Les opérations de tri fonctionnent lorsque les champs ont une valeur unique pour chaque document, que le champ soit un champ simple ou un sous-champ dans un type complexe. Par exemple, Address/City
peut être trié, car il n’y a qu’une seule adresse par hôtel. $orderby=Address/City
triera donc les hôtels par ville.
Filtrage sur des champs complexes
Vous pouvez faire référence à des sous-champs d’un champ complexe dans une expression de filtre. Utilisez simplement la même syntaxe de chemin OData qui est utilisé pour l’activation de facettes, le tri et la sélection de champs. Par exemple, le filtre suivant retourne tous les hôtels au Canada :
$filter=Address/Country eq 'Canada'
Pour filtrer sur un champ de collection complexe, vous pouvez utiliser une expression lambdaavec les opérateursany
et all
. Dans ce cas, la variable de portée de l’expression lambda est un objet avec des sous-champs. Vous pouvez consulter ces sous-champs avec la norme de syntaxe de chemin d’accès OData. Par exemple, le filtre suivant retourne tous les hôtels avec au moins une chambre de luxe et toutes les chambres non-fumeurs :
$filter=Rooms/any(room: room/Type eq 'Deluxe Room') and Rooms/all(room: not room/SmokingAllowed)
Comme avec les champs simples de niveau supérieur, les sous-champs simples de champs complexes ne peuvent être inclus dans des filtres que si leur attribut filtrable est défini sur true
dans la définition d’index. Pour plus d’informations, consultez la référence Créer une API d’index.
Solution de contournement pour la limite de collection complexe
N’oubliez pas que Recherche Azure AI limite les objets complexes d’une collection à 3 000 objets par document. Dépasser cette limite entraîne le message suivant :
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."
Si vous avez besoin de plus de 3 000 éléments, nous pouvons utiliser un séparateur (|
) ou toute autre forme afin de délimiter les valeurs, les concaténer et les stocker sous forme de chaîne délimitée. Il n’existe aucune limitation quant au nombre de chaînes stockées dans un tableau. Le stockage de valeurs complexes en tant que chaînes contourne la limitation de collection complexe.
À titre d’exemple, supposons que vous disposez d’un tableau "searchScope
" avec plus de 3 000 éléments :
"searchScope": [
{
"countryCode": "FRA",
"productCode": 1234,
"categoryCode": "C100"
},
{
"countryCode": "USA",
"productCode": 1235,
"categoryCode": "C200"
}
. . .
]
La solution de contournement pour stocker les valeurs sous forme de chaîne délimitée peut ressembler à ceci :
"searchScope": [
"|FRA|1234|C100|",
"|FRA|*|*|",
"|*|1234|*|",
"|*|*|C100|",
"|FRA|*|C100|",
"|*|1234|C100|"
]
Le stockage de toutes les variantes de recherche dans la chaîne délimitée est utile dans les scénarios de recherche où vous souhaitez rechercher des éléments qui ont simplement « FRA » ou « 1234 » ou une autre combinaison dans le tableau.
Voici un extrait de code de mise en forme de filtre en langage C# qui convertit les entrées en chaînes pouvant faire l’objet d’une recherche :
foreach (var filterItem in filterCombinations)
{
var formattedCondition = $"searchScope/any(s: s eq '{filterItem}')";
combFilter.Append(combFilter.Length > 0 ? " or (" + formattedCondition + ")" : "(" + formattedCondition + ")");
}
La liste suivante fournit des entrées et des chaînes de recherche (sorties) côte à côte :
Pour le code du comté « FRA » et le code de produit « 1234 », la sortie mise en forme est
|FRA|1234|*|
.Pour le code de produit « 1234 », la sortie mise en forme est
|*|1234|*|
.Pour le code de catégorie « C100 », la sortie mise en forme est
|*|*|C100|
.
Fournissez uniquement le caractère générique (*
) si vous implémentez la solution de contournement du tableau de chaînes. Sinon, si vous utilisez un type complexe, votre filtre peut ressembler à cet exemple :
var countryFilter = $"searchScope/any(ss: search.in(countryCode ,'FRA'))";
var catgFilter = $"searchScope/any(ss: search.in(categoryCode ,'C100'))";
var combinedCountryCategoryFilter = "(" + countryFilter + " and " + catgFilter + ")";
Si vous implémentez la solution de contournement, veillez à effectuer des tests de manière étendue.
Étapes suivantes
Essayer le jeu de données des hôtels dans l’Assistant Importer des données. Vous avez besoin des informations de connexion Azure Cosmos DB fournies dans le fichier Lisez-moi pour accéder aux données.
Lorsque vous avez ces informations, votre première étape dans l’Assistant est de créer une nouvelle source de données Azure Cosmos DB. Plus loin dans l’Assistant, lorsque vous accédez à la page d’index cible, vous pouvez voir un index avec des types complexes. Créez et chargez cet index, puis exécutez des requêtes pour comprendre la nouvelle structure.