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 une même référence, 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": "Secret Point Motel",
  "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,
    }
    . . .
  ]
}

Indexation des types 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 membre de cette collection. par conséquent, dans le cas des chambres (la seule collection complexe de l’exemple Hôtel), chaque espace est un élément. Dans l’exemple ci-dessus, si le « Secret Point Motel » 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).

Créer des champs complexes

Comme avec n’importe quelle définition d’index, vous pouvez utiliser le portail, 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.

  1. Connectez-vous au portail Azure.

  2. À partir de la page Vue d’ensemble du service de recherche, sélectionnez l’onglet Index.

  3. Ouvrez un index existant ou créez un nouvel index.

  4. 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.

  5. Donnez un nom au champ et définissez le type sur Edm.ComplexType ou Collection(Edm.ComplexType).

  6. Sélectionnez les ellipses à l’extrême droite, puis sélectionnez Ajouter un champ ou Ajouter un sous- champ, puis affectez des attributs.

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. Dans le cadre du rappel de quelques règles principales, notons que, contrairement à la plupart des modifications, l’ajout d’un champ à un type de complexe ne nécessite pas une reconstruction de l’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

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.

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.

La Recherche cognitive Azure a un seuil limite ne pouvant pas dépasser 3 000 pour les objets complexes dans les collections faites sur un seul document.

Les utilisateurs rencontrent l’erreur ci-dessous lors de l’indexation si les regroupements complexes dépassent la limite de 3 000.

« Une collection dans votre document dépasse la limite maximale d’éléments pour les collections complexes. Le document avec la clé "1052" comporte "4303" objets dans les collections (tableaux JSON). Le nombre maximum d’objets autorisés est de "3000" pour les collections sur l’ensemble du document. Supprimez des objets des collections et réessayez d’indexer le document. »

Dans certains cas d’usage, nous devrons peut-être ajouter plus de 3 000 éléments à une collection. Dans ces cas d’usage, nous pouvons utiliser un séparateur (|) ou tout 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 sur le nombre de chaînes stockées dans un tableau dans Recherche cognitive Azure. Le stockage de ces valeurs complexes en tant que chaînes évite la limitation. Le client doit vérifier que cette solution de contournement répond aux exigences de son scénario.

Par exemple, il ne serait pas possible d’utiliser des types complexes si le tableau « searchScope » ci-dessous comportait plus de 3 000 éléments.


"searchScope": [
  {
     "countryCode": "FRA",
     "productCode": 1234,
     "categoryCode": "C100" 
  },
  {
     "countryCode": "USA",
     "productCode": 1235,
     "categoryCode": "C200" 
  }
]

Le stockage de ces valeurs complexes sous forme de chaînes avec un délimiteur évite la limitation

"searchScope": [
        "|FRA|1234|C100|",
        "|FRA|*|*|",
        "|*|1234|*|",
        "|*|*|C100|",
        "|FRA|*|C100|",
        "|*|1234|C100|"
]

Au lieu de les stocker avec des caractères génériques, nous pouvons également utiliser un analyseur personnalisé qui fractionne le mot en | pour réduire la taille de stockage.

La raison pour laquelle nous avons stocké les valeurs avec des caractères génériques au lieu de les stocker comme ci-dessous

|FRA|1234|C100|

est de répondre aux scénarios de recherche dans lesquels le client peut souhaiter rechercher des éléments qui comportent le pays France, quels que soient les produits et catégories. De même, le client peut avoir besoin de rechercher si l’élément a le produit 1234, quel que soit le pays ou la catégorie.

Si nous n’avions stocké qu’une seule entrée

|FRA|1234|C100|

sans caractères génériques, et que l’utilisateur souhaite filtrer uniquement sur la France, nous ne pourrions pas convertir l’entrée utilisateur en fonction du tableau « searchScope », car nous ne pourrions pas savoir quelle combinaison de France est présente dans notre tableau « searchScope »

Si l’utilisateur souhaite filtrer uniquement par pays, par exemple la France. Nous allons prendre l’entrée de l’utilisateur et la construire sous forme de chaîne comme ci-dessous :

|FRA|*|*|

ainsi nous pouvons ensuite l'utiliser pour filtrer dans la recherche cognitive Azure, car nous recherchons dans un tableau de valeurs d’élément

foreach (var filterItem in filterCombinations)
        {
            var formattedCondition = $"searchScope/any(s: s eq '{filterItem}')";
            combFilter.Append(combFilter.Length > 0 ? " or (" + formattedCondition + ")" : "(" + formattedCondition + ")");
        }

De même, si l’utilisateur recherche la France et le code de produit 1234, nous allons prendre l’entrée de l’utilisateur, la construire sous forme de chaîne délimitée comme ci-dessous et la mettre en correspondance avec notre tableau de recherche.

|FRA|1234|*|

Si l’utilisateur recherche le code de produit 1234, nous allons prendre l’entrée de l’utilisateur, la construire de forme de chaîne délimitée comme ci-dessous et la mettre en correspondance avec notre tableau de recherche.

|*|1234|*|

Si l’utilisateur recherche le code de catégorie C100, nous allons prendre l’entrée de l’utilisateur, la construire sous forme de chaîne délimitée comme ci-dessous et la mettre en correspondance avec notre tableau de recherche.

|*|*|C100|

Si l’utilisateur recherche la France et le code de produit 1234 et le code de catégorie C100, nous allons prendre l’entrée de l’utilisateur, la construire sous forme de chaîne délimitée comme ci-dessous et la mettre en correspondance avec notre tableau de recherche.

|FRA|1234|C100|

Si un utilisateur tente de rechercher des pays non présents dans notre liste, il n'y aura pas de correspondance dans le tableau délimité « searchScope » stocké dans l’index de recherche, et aucun résultat ne sera retourné. Par exemple, un utilisateur recherche le Canada et le code de produit 1234. La recherche de l’utilisateur est convertie en

|CAN|1234|*|

Cela ne correspond à aucune des entrées du tableau délimité dans notre index de recherche.

Seul le choix de conception ci-dessus nécessite cette entrée de caractère générique ; s’il avait été enregistré en tant qu’objet complexe, nous aurions pu simplement effectuer une recherche explicite, comme indiqué ci-dessous.

           var countryFilter = $"searchScope/any(ss: search.in(countryCode ,'FRA'))";
            var catgFilter = $"searchScope/any(ss: search.in(categoryCode ,'C100'))";
            var combinedCountryCategoryFilter = "(" + countryFilter + " and " + catgFilter + ")";

Nous pouvons donc répondre aux exigences pour lesquelles nous devons rechercher une combinaison de valeurs en la stockant en tant que chaîne délimitée au lieu d’une collection complexe si nos collections complexes dépassent la limite de la Recherche cognitive Azure. Il s’agit de l’une des solutions de contournement, et le client doit vérifier qu'il répond aux exigences de ses scénarios.

É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.