Chapitre 8. Relations entre documents

Ce chapitre couvre

  • Objets et tableaux d’objets
  • Mapping, query et filtres imbriqués
  • Mapping des objets ayant une relation de type parent-child, query et filtres has_parent et has_child
  • Techniques de dénormalisation

Certaines données sont intrinsèquement relationnelles. Par exemple, avec le site de rencontre que nous avons utilisé tout au long du livre, il existe des groupes de personnes partageant les mêmes intérêts et des événements organisés par ces même groupes. Comment rechercheriez vous des groupes qui organisent des événements sur un sujet donné? Et bien via les relations qui existent entre les différentes entités de notre jeux de données.

Si vos données ont une structure plate, c’est à dire aucune relation entre les différents documents, alors vous pouvez sauter ce chapitre et passer au suivant sur le Scaling. C’est généralement le cas des logs applicatifs dans lesquels vous disposez de champs indépendants, tels que l’horodatage, la sévérité(level), le message…etc. Si, par contre, vous avez des entités apparentées dans vos données, telles que des articles de blog, et des commentaires, ou encore des utilisateurs et les produits qu’ils achètent, etc., vous vous demandez peut-être comment mieux représenter ces relations dans vos documents afin de pouvoir exécuter vos requêtes et agrégations habituelles.

Avec Elasticsearch, vous n’avez pas de jointure comme dans une base de données SQL. Comme nous le verrons à la section 8.4 sur la dénormalisation (duplication de données), cela tient du fait que les jointures lors des requêtes dans un système distribué sont généralement lentes et qu’Elasticsearch s’efforce d’être en temps réel en renvoyant des résultats en quelques millisecondes. Nous allons explorer toutes les possibilités de définition des relations entre les documents dans Elasticsearch – types Objets, documents imbriqués, relations parent-enfant et dénormalisation – et nous explorerons les avantages et inconvénients de chacun d’entre dans ce chapitre.

8.1. VUE D’ENSEMBLE DES DIFFERENTES RELATIONS POSSIBLE ENTRE DOCUMENTS

Premièrement, définissons rapidement chacune de ces approches:

  • Type objet – Cela vous permet d’avoir un objet (avec ses propres champs et valeurs) comme valeur d’un champ de votre document. Par exemple, votre champ d’adresse pour un événement peut être un objet avec ses propres champs: ville, code postal, nom de rue, etc. Vous pouvez même avoir un tableau d’adresses si le même événement se produit dans plusieurs villes.
  • Documents imbriqués — Le problème que vous pouvez avoir avec le type d’objet est que toutes les données sont stockées dans le même document. Par conséquent, les correspondances pour une recherche peuvent concerner des sous-documents. Par exemple, city = Paris et street_name = Broadway peut renvoyer un événement organisé simultanément à New York et à Paris, même s’il n’ya pas de rue Broadway à Paris. Les documents imbriqués vous permettent d’indexer le même document JSON mais conservent vos adresses dans des documents Lucene distincts. Du coup les recherches ayant des critères telles que city = New York AND street_name = Broadway renveront le résultat attendu.
  • Relations parent-enfant entre les documents— Cette méthode vous permet d’utiliser des documents Elasticsearch complètement séparés pour différents types de données, tels que des événements et des groupes, tout en définissant une relation entre eux. Par exemple, vous pouvez avoir des groupes en tant que parents d’événements pour indiquer quels événements est organisé par quels groupes. Cela vous permettra de rechercher des événements hébergés par des groupes de votre région ou des groupes hébergeant des événements concernant Elasticsearch.
  • Dénormalisation— Il s’agit d’une technique générale permettant de dupliquer des données afin de représenter des relations. Dans Elasticsearch, vous l’utiliserez probablement pour représenter des relations plusieurs à plusieurs, car les autres options ne fonctionnent que dans des relations un à plusieurs. Par exemple, tous les groupes ont des membres et les membres peuvent appartenir à plusieurs groupes. Vous pouvez dupliquer un côté de la relation en incluant tous les membres d’un groupe dans le document de ce groupe.
  • Jointures au niveau de l’application — Il s’agit d’une autre technique générale dans laquelle vous traitez des relations à partir de votre application. Cela fonctionne bien lorsque vous avez moins de données et que vous pouvez vous permettre de les garder normalisées. Par exemple, au lieu de dupliquer les membres de tous les groupes dont ils font partie, vous pouvez les stocker séparément et inclure uniquement leurs identifiants dans les groupes. Ensuite, vous allez exécuter deux requêtes: d’abord, sur les membres pour filtrer les critères de membres correspondants. Ensuite, vous prendrez leurs identifiants et les incluez dans les critères de recherche des groupes.

Avant de plonger dans les détails de chaque possibilité, nous vous en donnerons une vue d’ensemble, ainsi que leurs cas d’utilisation typiques.

8.1.1. Type Objet

Le moyen le plus simple de représenter un groupe d’intérêt commun et les événements correspondants consiste à utiliser le type d’objet. Cela vous permet de définir un objet JSON ou un tableau d’objets JSON comme valeur de votre champ, comme dans l’exemple suivant:

Si vous souhaitez rechercher un groupe avec des événements concernant Elasticsearch, vous pouvez effectuer une recherche dans le champ events.title.

Sous le capot, Elasticsearch (ou plutôt Lucene) n’a pas conscience de la structure de chaque objet; il ne connaît que les champs et les valeurs. Le document finit par être indexé comme s’il ressemblait à ceci:

En raison de la façon dont ils sont indexés, les objets fonctionnent à merveille lorsqu’il est nécessaire d’interroger un seul champ à la fois (généralement des relations un à un), mais lors de l’interrogation de plusieurs champs – de nombreuses relations), vous pourriez obtenir des résultats inattendus. Par exemple, supposons que vous souhaitiez filtrer les groupes hébergeant des réunions Hadoop en décembre 2014. Votre requête peut ressembler à ceci:

Cela correspond à l’exemple de document car son titre correspond à hadoop et sa date correspond à la plage spécifiée. Mais ce n’est pas ce que vous voulez: c’est l’événement Elasticsearch de décembre; l’événement Hadoop a lieu en juin. S’en tenir au type Objet par défaut est l’approche la plus rapide et la plus simple, mais Elasticsearch ignore les limites entre les documents, comme l’illustre la figure 8.1.

8.1.2. Type imbriqué

Si vous devez vous assurer que de telles correspondances entre objets ne se produisent pas, vous pouvez utiliser le type imbriqué, qui indexera vos événements dans des documents Lucene distincts. Dans les deux cas, le document JSON du groupe aura exactement le même aspect et les applications indexeront chacune de la même manière. La différence réside dans le mapping, ce qui amène Elasticsearch à indexer des objets internes imbriqués dans des documents Lucene adjacents mais distincts, comme illustré à la figure 8.2. Lors de la recherche, vous devrez utiliser des filtres et des requêtes imbriqués, qui seront explorés à la section 8.2; ceux-ci vont chercher dans tous ces documents Lucene.

Dans certains cas d’utilisation, il n’est pas judicieux d’écraser toutes les données d’un même document comme le font les objets et les types imbriqués. Prenons le cas des groupes et des événements: si un nouvel événement est organisé par un groupe et que toutes les données de ce groupe sont dans le même document, vous devrez réindexer l’ensemble du document pour cet événement. Cela peut nuire aux performances et à la concurrence, en fonction de la taille de ces documents et de la fréquence à laquelle ces opérations sont effectuées.

8.1.3. Relations parent-enfant

Remarque: Notez que tout ce qui suit est vrai pour les versions anciennes d’Elasticsearch, jusqu’à la suppression de TYPE. En effet dans les versions récentes (5.xx), l’attribut join type  fait son apparition dans le mapping.

Avec les relations parent-enfant, vous pouvez utiliser des documents Elasticsearch complètement différents en les plaçant dans différents types et en définissant leur relation dans le mapping de chaque type. Par exemple, vous pouvez avoir des événements dans un type de mapping et des groupes dans un autre et vous pouvez spécifier dans le mapping que les groupes sont les parents d’événements. De même, lorsque vous indexez un événement, vous pouvez le diriger vers le groupe auquel il appartient, comme dans la figure 8.3. Au moment de la recherche, vous pouvez utiliser des requêtes et des filtres has_parent ou has_child pour prendre en compte l’autre partie de la relation. Nous en discuterons plus tard dans ce chapitre.

8.1.4. Dénormalisation

Pour tout travail relationnel, vous avez des objets, des documents imbriqués et des relations parent-enfant. Celles-ci fonctionnent pour des relations un à un et un à plusieurs, celles qui ont un parent avec un ou plusieurs enfants. Il existe également des techniques qui ne sont pas spécifiques à Elasticsearch, mais qui sont souvent utilisées par les data stores NoSQL pour pallier le manque de jointures: l’une est la dénormalisation, ce qui signifie qu’un document comprendra des données qui lui sont associées, même si ces mêmes données doivent être dupliqué dans un autre document. Une autre méthode consiste à faire des jointures dans votre application.

Par exemple, prenons les groupes et leurs membres. Un groupe peut avoir plusieurs membres et un utilisateur peut être membre de plusieurs groupes. Les deux ont leurs propres propriétés. Pour représenter cette relation, vous pouvez avoir des groupes en tant que parents des membres. Pour les utilisateurs membres de plusieurs groupes, vous pouvez dénormaliser leurs données: une fois pour chaque groupe auquel ils appartiennent, comme dans la figure 8.4.

Vous pouvez également séparer les groupes et les membres et inclure uniquement les ID de membre dans les documents de groupe. Vous rejoindrez les groupes et leurs membres en utilisant les identifiants de membre dans votre application, ce qui fonctionne bien si vous avez un petit nombre d’identifiants à interroger, comme le montre la figure 8.5.

Dans la suite de ce chapitre, nous examinerons plus en détail chacune de ces techniques: objets et tableaux, jointures imbriquées, parent-enfant, dénormalisantes et jointure côté application. Vous apprendrez comment ils fonctionnent en interne, comment les définir dans le mapping, comment les indexer et comment rechercher ces documents.

8.2. AVOIR DES OBJETS COMME VALEURS DE CHAMPS

Comme vous l’avez vu au chapitre 2, les documents dans Elasticsearch peuvent être hiérarchisés. Par exemple, dans les exemples de code, un événement du site de rencontre a son champ location en tant qu’objet avec deux champs: name et geolocalisation:

Si vous connaissez Lucene, vous pouvez vous demander: « Comment les documents Elasticsearch peuvent-ils être hiérarchisés lorsque Lucene ne prend en charge que des structures plates? » . Vous pouvez voir le processus à la figure 8.6.

En règle générale, lorsque vous souhaitez rechercher le nom de la localisation d’un événement, vous vous référez à celui-ci sous le nom location.name. Nous examinerons cela à la section 8.2.2, mais avant de lancer la recherche, définissons le mapping et voyons comment indexer certains documents.

8.2.1. Mapping et indexation d’objets

Par défaut, les mappings d’objets internes sont automatiquement détectés. Dans la figure 8.1, vous indexerez un document hiérarchique et verrez à quoi ressemble le mapping détecté. Si ces documents d’événements vous semblent familiers, c’est que les exemples de code stockent également l’emplacement d’un événement dans un objet. Vous pouvez vous rendre sur https://github.com/ftounga/elasticsearch pour obtenir les exemples de code maintenant si vous ne l’avez pas encore fait.

Vous pouvez voir que l’objet interne possède une liste de propriétés, tout comme l’objet JSON racine. Vous configurez les types de champ à partir d’objets internes de la même manière que vous le faites pour les champs de l’objet racine. Par exemple, vous pouvez mettre à jour location.address pour qu’il ait plusieurs champs, comme vous l’avez vu au chapitre 3. Cela vous permettra d’indexer l’adresse de différentes manières, par exemple, une version non analysée pour les correspondances exactes en plus de la version analysée par défaut.



Si vous devez examiner les types de base ou vous rappeler comment utiliser les champs multiples, vous pouvez revenir au chapitre 3. Pour plus de détails sur l’analyse, retournez au chapitre 5.



Le mapping pour un seul objet interne fonctionnera également si vous avez plusieurs objets de ce type dans un tableau. Par exemple, si vous indexez le document suivant, le mapping de la figure 8.1 restera le même:

Pour résumer, utiliser des objets et des tableaux d’objets dans le mapping revient à travailler avec les champs et les tableaux que vous avez vus au chapitre 3. Nous allons maintenant examiner les recherches, qui fonctionnent également comme celles que vous avez vues dans les chapitres 4 et 6.

8.2.2. Recherche d’objets

Par défaut, Elasticsearch reconnaîtra et indexera les documents JSON hiérarchiques avec des objets internes sans rien définir à l’avance. Comme vous pouvez le voir à la figure 8.7, il en va de même pour la recherche. Par défaut, vous devez faire référence aux objets internes en spécifiant le chemin d’accès au champ que vous consultez, tel que location.name.

En parcourant les chapitres 2 et 4, vous avez indexé des documents à partir des exemples de code. Vous pouvez maintenant effectuer une recherche dans les événements se déroulant dans les bureaux, comme dans la figure 8.2, où vous spécifierez le chemin complet location.name comme champ dans lequel effectuer la recherche.

Lors de la recherche, traitez les champs d’objet tels que location.name de la même manière que tout autre champ. Cela fonctionne également avec les agrégations que vous avez vues au chapitre 7. Par exemple, l’agrégation de termes suivante obtient les mots les plus utilisés dans le champ location.name pour vous aider à créer un nuage de mots:

Les relations un à un constituent le cas d’utilisation idéal pour les objets: vous pouvez rechercher dans les champs de l’objet interne comme s’il s’agissait de champs dans le document racine. C’est parce qu’ils le sont! Au niveau de Lucene, location.name est un autre champ de la même structure plate.

Vous pouvez également avoir des relations un à plusieurs avec des objets en les plaçant dans des tableaux. Par exemple, prenons un groupe avec plusieurs membres. Si chaque membre a son propre objet, vous les représentez comme suit:

Vous pouvez toujours rechercher members.first_name: lee et il correspondra à «Lee» comme prévu. Mais gardez à l’esprit que, dans Lucene, la structure du document ressemble davantage à ceci:

Cela ne fonctionne bien que si vous effectuez une recherche dans un champ, même si vous avez plusieurs critères. Si vous recherchez members.first_name: lee AND members.last_name: gheorghe, le document correspondra car il correspond à chacun de ces deux critères. Cela se produit même s’il n’ya pas de membre nommé Lee Gheorghe parce qu’Elasticsearch jette tout dans le même document et qu’il n’a pas conscience des limites entre les objets. Pour qu’Elasticsearch comprenne ces limites, vous pouvez utiliser le type imbriqué, qu’on verra par la suite.



Utilisation d’objets pour définir les relations entre documents: avantages et inconvénients

Avant de poursuivre, voici un bref résumé des raisons pour lesquelles vous devriez (ou ne devriez pas) utiliser des objets. Les points positifs:

  • Ils sont faciles à utiliser. Elasticsearch les détecte par défaut; dans la plupart des cas, vous n’avez rien de spécial à définir pour indexer les objets.
  • Vous pouvez exécuter des requêtes et des agrégations sur des objets comme vous le feriez avec des documents plats. C’est parce qu’au niveau de Lucene, ce sont effectivement des documents plats.
  • Aucune jointure n’est impliquée. Comme tout est dans le même document, l’utilisation des objets vous donnera les meilleures performances parmi les options présentées dans ce chapitre.

Les inconvénients:

  • Il n’y a pas de frontière entre les objets. Si vous avez besoin d’une telle fonctionnalité, vous devez examiner d’autres options (imbriqué, parent-enfant et dénormalisation) et éventuellement les combiner avec des objets si cela convient à votre cas d’utilisation.
  • La mise à jour d’un seul objet réindexera l’ensemble du document.


8.3. TYPE IMBRIQUES: CONNEXION DE DOCUMENTS IMBRIQUES

Le type imbriqué est défini dans le mapping de la même manière que le type d’objet, ce dont nous avons déjà parlé. En interne, les documents imbriqués sont indexés en tant que différents documents Lucene. Pour indiquer que vous souhaitez utiliser le type imbriqué au lieu du type d’objet, vous devez définir le type sur nested, comme vous le verrez à la section 8.3.1.

Du point de vue d’une application, l’indexation des documents imbriqués est identique à l’indexation des objets, car le document JSON indexé en tant que document Elasticsearch a la même apparence. Par exemple:

Au niveau Lucene, Elasticsearch indexera le document racine et tous les objets membres dans des documents distincts. Mais cela les mettra dans un seul bloc, comme le montre la figure 8.8.

Les documents d’un bloc resteront toujours ensemble, garantissant qu’ils seront récupérés et interrogés avec un nombre minimum d’opérations.

Maintenant que vous savez comment fonctionnent les documents imbriqués, voyons comment les faire utiliser par Elasticsearch. Vous devez spécifier que vous souhaitez les imbriquer au moment de l’index et au moment de la recherche:

Les objets internes doivent avoir un mapping imbriqué pour pouvoir être indexés en tant que documents distincts dans le même bloc.
Les requêtes et les filtres imbriqués doivent être utilisés pour utiliser ces blocs lors de la recherche.
Nous verrons comment les utiliser dans les deux prochaines sections.

8.3.1. Mapping et indexation de documents imbriqués

Le mapping imbriqué ressemble au mapping d’objet, à la différence que, au lieu du type objet, vous devez utiliser nested. Dans la figure suivante, vous allez définir un mapping avec un champ de type nested et indexer un document contenant un tableau d’objets imbriqués.

Les objets JSON avec le mapping imbriqué, comme ceux que vous avez indexés dans cette figure, vous permettent de les rechercher avec des requêtes et des filtres imbriqués. Nous allons explorer ces recherches plus bas, mais il ne faut pas oublier que les requêtes et les filtres imbriqués vous permettent d’effectuer une recherche dans les limites de ces documents. Par exemple, vous pourrez rechercher des groupes dont les membres portent les prénoms «Lee» et «Hinman». Les requêtes imbriquées ne permettent pas les correspondances entre objets, évitant ainsi les correspondances inattendues telles que «Lee» avec le nom de famille « Gheorghe. »

Activation des correspondances entre objets

Dans certaines situations, vous pouvez également avoir besoin de correspondances entre objets. Par exemple, si vous recherchez un groupe comprenant à la fois Lee et Radu, une requête comme celle-ci fonctionnerait pour les objets JSON normaux décrits dans la section sur le type d’objet:

Cette requête fonctionnerait car, lorsque tout est dans le même document, les deux critères seront identiques.

Avec les documents imbriqués, une requête structurée de cette manière ne fonctionnera pas car les objets membres seraient stockés dans des documents Lucene distincts. Et aucun objet de membre ne correspond aux deux critères: il existe un pour Lee et un autre pour Radu, mais aucun document contenant les deux.

Dans de telles situations, vous souhaiterez peut-être disposer des deux objets: objets pour quand vous voulez des correspondances entre objets et documents imbriqués pour quand vous voulez les éviter. Elasticsearch vous permet de le faire via plusieurs options de mapping: include_in_root et include_in_parent.

include_in_root

L’ajout de include_in_root à votre mapping imbriqué indexera les objets membres internes deux fois: une fois en tant que document imbriqué et une fois en tant qu’objet dans le document racine, comme illustré à la figure 8.9.

Le mapping suivant vous permettra d’utiliser des requêtes imbriquées pour les documents imbriqués et des requêtes standard lorsque vous avez besoin de correspondances inter-objets:

include_in_parent

Elasticsearch vous permet d’avoir plusieurs niveaux de documents imbriqués. Par exemple, si votre groupe peut avoir des membres comme enfants imbriqués, les membres peuvent avoir leurs propres enfants, tels que les commentaires qu’ils ont publiés sur ce groupe. La figure 8.10 illustre cette hiérarchie.

Avec l’option include_in_root que vous venez de voir, vous pouvez ajouter des champs de n’importe quel niveau au document racine (dans ce cas, le grand-parent). Il existe également une option include_in _parent, qui vous permet d’indexer les champs d’un document imbriqué dans le document parent immédiat. Par exemple, la figure suivante inclura les commentaires dans les documents des membres.

A présent, vous vous demandez probablement comment interroger ces structures imbriquées. C’est exactement ce que nous verrons ensuite.

8.3.2. Recherches et agrégations sur des documents imbriqués

Comme pour les mappings, lorsque vous effectuez des recherches et des agrégations sur des documents imbriqués, vous devez spécifier que les objets que vous examinez sont imbriqués. Des requêtes, des filtres et des agrégations imbriquées vous aident à atteindre cet objectif. L’exécution de ces requêtes et agrégations spéciales obligera Elasticsearch à joindre les différents documents Lucene au sein du même bloc et à traiter les données résultantes comme le même document Elasticsearch.

La méthode de recherche dans les documents imbriqués consiste à utiliser la query imbriquée ou le filtre imbriqué. Comme on pouvait s’y attendre après le chapitre 4, elles sont équivalentes, avec les différences traditionnelles entre les query et les filtres:

  • Les query calculent le score; ils peuvent donc renvoyer les résultats triés par pertinence.
  • Les filtres ne calculent pas le score, ce qui les rend plus rapides et plus faciles à mettre en cache.


En particulier, le filtre imbriqué n’est pas mis en cache par défaut. Vous pouvez changer cela en définissant _cache sur true, comme vous pouvez le faire dans tous les filtres.



Si vous souhaitez exécuter des agrégations sur des champs imbriqués (par exemple, pour obtenir les membres du groupe les plus fréquents), vous devrez les envelopper dans une agrégation imbriquée. Si les sous-agrégations doivent faire référence au document Lucene parent (par exemple, afficher les balises de groupe supérieures pour chaque membre), vous pouvez remonter dans la hiérarchie avec l’agrégation reverse_nested.

Query et filtre imbriqué

Lorsque vous exécutez une requête ou un filtre imbriqué, vous devez spécifier en argument le chemin d’accès pour indiquer à Elasticsearch l’emplacement dans lequel se trouvent ces objets imbriqués dans le bloc Lucene. De plus, votre requête ou filtre imbriqué encapsulera une requête ou un filtre standard, respectivement. Dans la figure suivante, vous rechercherez des membres portant le prénom «Lee» et le nom «Gheorghe», et vous verrez que le document indexé dans la figure 8.3 ne correspondra pas, car vous n’avez que Lee Hinman et Radu. Gheorghe et aucun membre appelé Lee Gheorghe.

Un filtre imbriqué aurait exactement la même apparence que la queryimbriquée que vous venez de voir. Vous devrez remplacer le mot query par filter.

Recherche dans plusieurs niveaux d’imbrication

Elasticsearch vous permet également d’avoir plusieurs niveaux d’imbrication. Par exemple, dans la figure 8.4, vous avez ajouté un mapping qui s’ deux niveaux: les membres et leurs commentaires. Pour effectuer une recherche dans les documents imbriqués dans les commentaires, vous devez spécifier members.comments en tant que chemin, comme indiqué dans la figure suivante:

Agrégation de scores d’objets imbriqués

La requête imbriquée calcule le score, mais nous n’indiquerons pas comment. Disons que vous avez trois membres dans un groupe: Lee Hinman, Radu Gheorghe et un autre gars appelé Lee Smith. Si vous avez une requête imbriquée pour «Lee», elle correspondra à deux membres. Chaque document de membre interne obtient son propre score, en fonction de sa conformité aux critères. Mais la requête provenant de l’application concerne les documents de type groupe. Elasticsearch devra donc attribuer un score à l’ensemble du document de type groupe. À ce stade, quatre options peuvent être spécifiées à l’aide de l’option score_mode:

  • avg: il s’agit de l’option par défaut qui prend les scores des documents internes correspondants et renvoie leur score moyen.
  • total— Celui ci fait la somme des scores des documents internes correspondants et les renvoie, ce qui est utile lorsque le nombre de correspondances compte.
  • max— Le score maximum du document interne est renvoyé.
  • none – Aucun score n’est conservée, pris en compte ou comptée dans le score total du document.

Si vous pensez qu’il existe trop d’options pour inclure le type imbriqué dans la racine ou le parent, reportez-vous au tableau 8.1 pour obtenir des références rapides sur toutes ces options et sur leur pertinence.

Savoir quel document interne correspond

Lorsque vous indexez de gros documents contenant de nombreux sous-documents imbriqués, vous pouvez vous demander lequel des documents imbriqués correspondrait à une requête imbriquée spécifique; dans ce cas, lequel des membres du groupe correspondrait à une requête recherchant le mot clé lee dans first_name. À partir de Elasticsearch 1.5, vous pouvez ajouter un objet inner_hits dans votre query ou filtre imbriqué pour afficher les documents imbriqués correspondants. Comme votre requête de recherche principale, elle prend en charge des options telles que from et size:

La réponse contiendra un objet inner_hits pour chaque document correspondant, ressemblant beaucoup à une réponse de requête classique, sauf que chaque document est un sous-document imbriqué:

Afin d’identifier le sous-document, vous pouvez regarder l’objet _nested. Field est le chemin de l’objet imbriqué et offset indique l’emplacement de ce document imbriqué dans le tableau. Dans ce cas, Lee est le premier membre.

Tri imbriqué

Dans la plupart des cas d’utilisation, vous triez les documents racine par partition, mais vous pouvez également les trier en fonction des valeurs numériques des documents imbriqués internes. Cela se ferait de la même manière que pour le tri sur d’autres champs, comme vous l’avez vu au chapitre 6. Par exemple, si vous avez un site agrégateur de prix avec des produits comme documents racine et des offres de différents magasins comme documents imbriqués, vous pouvez trier les données par ordre croissant. prix minimum de chaque offre. Semblable à l’option score_mode que vous avez vue auparavant, vous pouvez spécifier un mode et utiliser les valeurs min, max, sum ou avg des documents imbriqués comme valeur de tri pour le document racine:

Elasticsearch sera intelligent et déterminera que offers.price se situe dans l’objet offers(si c’est ce que vous avez défini dans le mapping) et accédera au champ price sous ces documents imbriqués pour le tri.

Agrégations imbriquées et inversées

Pour effectuer des agrégations sur des objets de type imbriqués, vous devez utiliser l’agrégation imbriquée. Il s’agit d’une agrégation à un seul compartiment, dans laquelle vous indiquez le chemin d’accès à l’objet imbriqué contenant votre champ. Comme le montre la figure 8.11, l’agrégation imbriquée force Elasticsearch à effectuer les jointures nécessaires afin que les autres agrégations fonctionnent correctement sur le chemin indiqué.

Par exemple, vous devez normalement exécuter une agrégation de termes sur un nom de membre afin d’obtenir les meilleurs utilisateurs en fonction du nombre de groupes dont ils font partie. Si ce champ name est stocké dans l’objet de type imbriqué members, vous regrouperez cette agrégation de termes dans une agrégation imbriquée dont le chemin d’accès est défini sur members:

Vous pouvez placer plus d’agrégations sous l’agrégation de membres imbriquée et Elasticsearch saura rechercher dans le type membre pour toutes.

Il existe des cas d’utilisation dans lesquels vous devez revenir au document parent ou racine. Par exemple, vous souhaitez que chacun des membres fréquents obtenus affiche les balises de groupe supérieures. Pour ce faire, vous utiliserez l’agrégation reverse_nested, qui indiquera à Elasticsearch de remonter la hiérarchie imbriquée:

Les agrégations imbriquées et inversées peuvent effectivement être utilisées pour indiquer à Elasticsearch dans quel document Lucene rechercher les champs de la prochaine agrégation. Cela vous donne la possibilité d’utiliser tous les types d’agrégation que vous avez vus au chapitre 7 pour les documents imbriqués, tout comme vous pouvez les utiliser pour des objets. Le seul inconvénient de cette flexibilité est la performance.

Considérations de performance

Nous traiterons des performances plus en détail au chapitre 10, mais en général, vous pouvez vous attendre à ce que les requêtes et les agrégations imbriquées soient plus lentes que leurs homologues d’objet. C’est parce que Elasticsearch doit faire un travail supplémentaire pour joindre plusieurs documents au sein d’un bloc. Mais en raison de l’implémentation sous-jacente utilisant des blocs, ces requêtes et ces agrégations sont beaucoup plus rapides que si vous deviez joindre des documents Elasticsearch complètement séparés.

Cette implémentation en bloc présente également des inconvénients. Comme les documents imbriqués sont collés ensemble, la mise à jour ou l’ajout d’un document interne nécessite la réindexation de l’ensemble. Les applications fonctionnent également avec des documents imbriqués dans un seul JSON.

Si vos documents imbriqués deviennent volumineux, comme dans un site de rencontre, si vous aviez un document par groupe et tous ses événements imbriqués, une meilleure option pourrait être d’utiliser des documents Elasticsearch séparés et de définir des relations parent-enfant entre eux.



Utilisation de type imbriqué pour définir les relations entre documents: avantages et inconvénients

Avant de poursuivre, voici un bref résumé des raisons pour lesquelles vous devriez (ou ne devriez pas) utiliser des documents imbriqués. Les points positifs:

  • Les types imbriqués sont conscients des limites des objets: plus aucune correspondance pour «Radu Hinman»!
  • Vous pouvez indexer le document entier en une fois, comme avec les objets, après avoir défini votre mapping imbriqué.
  • Les requêtes et les agrégations imbriquées joignent les parties parent et enfant et vous pouvez exécuter n’importe quelle requête sur l’union. Aucune autre option décrite dans ce chapitre n’offre cette fonctionnalité.
  • Les jointures au moment des requêtes sont rapides car tous les documents Lucene constituant le document Elasticsearch se trouvent ensemble dans le même bloc, dans le même segment.
  • Vous pouvez inclure des documents enfants dans les parents pour obtenir toutes les fonctionnalités d’objets si vous en avez besoin. Cette fonctionnalité est transparente pour votre application.

Les inconvénients:

  • Les requêtes seront plus lentes que leurs équivalents d’objet. Si les objets vous fournissent toutes les fonctionnalités nécessaires, ils constituent la meilleure option car ils sont plus rapides.
  • La mise à jour d’un enfant réindexera l’ensemble du document.

8.4. RELATIONS PARENTS-ENFANTS: CONNEXION DE DOCUMENTS DISTINCTS

NB: Comme spécifié plus haut, cette partie concerne les anciènnes version d’Elasticsearch. En effet l’attribut _parent n’est plus utilisé dans les nouvels. On verra la nouvelle façon de gerer les relations parents enfant en Annexe.

Une autre option pour définir les relations entre les données dans Elasticsearch consiste à définir un type dans un index en tant qu’enfant d’un autre type du même index. Ceci est utile lorsque des documents ou des relations doivent souvent être mis à jour. Vous définissez la relation dans le mapping via le champ _parent. Par exemple, vous pouvez voir dans le fichier mapping.json des exemples de code du livre que les événements sont des enfants de groupes, comme illustré dans la figure 8.12.

Une fois cette relation définie dans le mapping, vous pouvez commencer à indexer des documents. Les parents (les documents de groupe dans ce cas) sont indexés normalement. Pour les enfants (les événements dans cet exemple), vous devez spécifier l’ID du parent dans le champ _parent. En gros, cela liera l’événement àson groupe et vous permettra de rechercher des groupes qui incluent certains critères de l’événement ou inversement, comme dans la figure 8.13.

Par rapport à l’approche imbriquée, les recherches sont plus lentes. Avec les documents imbriqués, le fait que tous les objets internes soient des documents Lucene dans le même bloc est avantageux, car ils peuvent facilement être joints au document racine. Les documents parents et enfants sont des documents Elasticsearch complètement différents. Ils doivent donc être recherchés séparément.

L’approche parent-enfant se démarque lorsqu’il s’agit d’indexer, de mettre à jour et de supprimer des documents. Les documents parents et enfants étant des documents Elasticsearch différents, ils peuvent être gérés séparément. Par exemple, si un groupe contient plusieurs événements et que vous devez en ajouter un nouveau, vous pouvez le faire sans problême. Alors que en utilisant l’approche de type imbriqué, Elasticsearch devra réindexer les documents du groupe avec le nouvel événement et tous les événements existants, ce qui est beaucoup plus lent.

Un document parent peut déjà être indexé ou non lorsque vous indexez son enfant. Ceci est utile lorsque vous avez beaucoup de nouveaux documents et que vous souhaitez les indexer de manière asynchrone. Par exemple, vous pouvez indexer des événements sur votre site Web générés par des utilisateurs et également les indexer. Les événements peuvent provenir de votre système de logs et les utilisateurs peuvent être synchronisés à partir d’une base de données. Vous n’avez pas à vous soucier de la présence d’un utilisateur avant de pouvoir indexer un événement qui aura cet utilisateur comme parent. Si l’utilisateur n’existe pas, l’événement est quand même indexé.

Mais comment indexeriez-vous les documents parents et enfants en premier lieu? C’est ce que nous explorerons ensuite.

8.4.1. Indexation, mise à jour et suppression de documents enfants

Nous ne nous préoccuperons que des documents enfants car les parents sont indexés comme tout autre document que vous avez indexé jusqu’à présent. Ce sont les documents enfants qui doivent pointer vers leurs parents via le champ _parent.



Remarque
Les parents d’un type de document peuvent être des enfants d’un autre type. Vous pouvez avoir plusieurs niveaux de telles relations, tout comme avec le type imbriqué. Vous pouvez même les combiner. Par exemple, un groupe peut avoir ses membres stockés en tant que type imbriqué et des événements séparément stockés en tant que leurs enfants.



En ce qui concerne les documents enfants, vous devez définir le champ _parent dans le mapping, et lors de l’indexation, vous devez spécifier l’ID du parent dans le champ _parent. L’identifiant et le type du parent serviront également de valeur de routage pour l’enfant.



Routage et valeurs de routage

Vous vous souvenez peut-être du chapitre 2 comment les opérations d’indexation sont distribuées par défaut en fragments: chaque document que vous indexez possède un ID et cet ID est haché. En même temps, chaque fragment de l’index a une tranche égale de la plage totale des hachages. Le document que vous indexez est dirigé vers la partition dont l’ID haché de ce document se trouve dans sa plage.

L’ID haché est appelé la valeur de routage et le processus d’attribution d’un document à un fragment s’appelle le routage. Parce que chaque ID est différent et que vous les hachez tous, le mécanisme de routage par défaut équilibrera les documents de manière égale entre les fragments.

Vous pouvez également spécifier une valeur de routage personnalisée. Nous verrons plus en détail l’utilisation du routage personnalisé au chapitre 9, mais l’idée de base est que Elasticsearch hache cette valeur de routage et non l’ID du document pour déterminer le fragment. Vous utiliserez un routage personnalisé lorsque vous voulez vous assurer que plusieurs documents se trouvent dans le même fragment, car le hachage de la même valeur de routage vous donnera toujours le même résultat.

Le routage personnalisé devient utile lorsque vous lancez la recherche car vous pouvez fournir une valeur de routage à votre requête. Lorsque vous le faites, Elasticsearch ne recherche que la partition qui correspond à cette valeur de routage, au lieu de rechercher toutes les partitions. Cela réduit beaucoup la charge de votre cluster et est généralement utilisé pour conserver ensemble les documents de chaque utilisateur.

Le champ _parent fournit à Elasticsearch l’identifiant et le type du document parent, ce qui lui permet d’acheminer les documents enfants vers le même hachage que le document parent. _parent est essentiellement une valeur de routage, et vous en tirez profit lors de la recherche. Elasticsearch utilisera automatiquement cette valeur de routage pour interroger uniquement le fragment du parent afin d’obtenir ses enfants ou le fragment de l’enfant pour obtenir son parent.



La valeur de routage commune fait que tous les enfants du même parent atterrissent dans le même fragment que le parent lui-même. Lors de la recherche, toutes les corrélations que doit avoir Elasticsearch entre un parent et ses enfants se produisent sur le même noeud. Ceci est beaucoup plus rapide que la diffusion de tous les documents enfants sur le réseau à la recherche d’un parent. Une autre implication du routage est que lorsque vous mettez à jour ou supprimez un document enfant, vous devez spécifier le champ _parent.

Nous verrons ensuite comment vous faire toutes ces choses:

  • Définir le champ _parent dans le mapping.
  • Indexer, mettre à jour et supprimer des documents enfants en spécifiant le champ _parent.

Mapping

La figure suivante montre la partie pertinente du mapping des événements à partir des exemples de code. Le champ _parent doit pointer vers le type parent – dans ce cas, groupe.

Indexation et récupération

Avec le mapping en place, vous pouvez commencer à indexer des documents. Ces documents doivent contenir la valeur parent dans l’URI en tant que paramètre. Pour vos événements, cette valeur est l’ID de document des groupes auxquels ils appartiennent, par exemple,  2 pour le groupe Elasticsearch Denver:

Le champ _parent est stocké afin que vous puissiez le récupérer ultérieurement. Il est également indexé afin que vous puissiez rechercher sa valeur. Si vous examinez le contenu de _parent pour un groupe, vous verrez le type que vous avez défini dans le mapping ainsi que l’ID de groupe que vous avez spécifié lors de l’indexation.

Pour récupérer un document d’événement, vous exécutez une requête normale et vous devez également spécifier la valeur _parent:

La valeur _parent est requise car vous pouvez avoir plusieurs événements avec le même ID pointant vers différents groupes. Mais la combinaison _parent et _id est unique. Si vous essayez d’obtenir le document enfant sans spécifier son parent, vous obtiendrez une erreur indiquant qu’une valeur de routage est requise. La valeur _parent est la valeur de routage attendue par Elasticsearch:

Mise à jour

Vous mettrez à jour un document enfant via l’API de mise à jour, à l’instar de ce que vous avez fait au chapitre 3, section 3.5. La seule différence ici est que vous devez fournir à nouveau le parent. Comme dans le cas de la récupération d’un document d’événement, le parent est nécessaire pour obtenir la valeur de routage du document d’événement que vous essayez de modifier. Sinon, vous obtiendrez la même exception RoutingMissingException que celle que vous aviez précédemment lorsque vous tentez de récupérer le document sans spécifier de parent.

L’extrait de code suivant ajoute une description au document que vous venez d’indexer:

Suppression

Pour supprimer un seul document d’événement, exécutez une demande de suppression comme dans le chapitre 3, section 3.6.1, et ajoutez le paramètre parent:

La suppression par requête fonctionne comme avant: les documents correspondants sont supprimés. Cette API n’a pas besoin de valeurs parent et elle ne les prend pas en compte:

En parlant de requêtes, voyons comment rechercher dans les relations parent-enfant.

8.4.2. Recherche dans les documents parents et enfants

Avec les relations parent-enfant, comme celles que vous avez avec les groupes et leurs événements, vous pouvez rechercher des groupes et ajouter des critères d’événement ou l’inverse. Voyons quelles sont les requêtes et les filtres que vous utiliserez:

  • Les query et filtres has_child sont utiles pour rechercher des parents avec des critères sur leurs enfants, par exemple, si vous avez besoin de groupes organisant des événements sur Elasticsearch.
  • Les query et filtres has_parent sont utiles lors de la recherche d’enfants avec des critères sur leurs parents, par exemple, des événements qui se produisent à Denver, car l’attribut location est une propriété du groupe.

Query et filtre has_child

Si vous souhaitez rechercher dans des groupes hébergeant des événements sur Elasticsearch, vous pouvez utiliser la query ou le filtre has_child. La différence classique ici est que les filtres ne s’intéressent pas au score.

Un filtre has_child peut envelopper un autre filtre ou une query. Il exécute ce filtre ou interroge le type enfant spécifié et collecte les correspondances. Les enfants correspondants contiennent les identifiants de leurs parents dans le champ _parent. Elasticsearch collecte ces ID parents et supprime les doublons (car un même ID parent peut apparaître plusieurs fois, une fois pour chaque enfant), et renvoie la liste des documents parents. L’ensemble du processus est illustré à la figure 8.14.

Dans la phase 1 de la figure, les actions suivantes ont lieu:

  • L’application exécute un filtre has_child, demandant des documents de groupe avec des enfants de type événement ayant «Elasticsearch» dans leur titre.
  • Le filtre est exécuté sur le type d’événement pour les documents correspondant à «Elasticsearch».
  • Les documents d’événement qui en résultent indiquent leurs parents respectifs. Plusieurs événements peuvent pointer vers le même groupe.

En phase 2, Elasticsearch rassemble tous les documents de groupe uniques et les renvoie à l’application.

Le filtre de la figure 8.14 ressemblerait à ceci:

La requête has_child s’exécute de la même manière que le filtre, à la différence qu’elle peut attribuer un score à chaque parent en agrégeant les scores de documents enfants. Pour ce faire, définissez score_mode sur max, sum, avg ou no, comme vous pouvez le faire avec des requêtes imbriquées.



Si le filtre has_child peut encapsuler un filtre ou une query, la query has_child ne peut en encapsuler qu’une autre query.



Par exemple, vous pouvez définir score_mode sur max et obtenir la requête suivante pour renvoyer les groupes par lesquels on héberge l’événement le plus pertinent concernant Elasticsearch:



Attention

Pour permettre aux query et aux filtres has_child de supprimer rapidement les doublons parents, il met en cache leurs identifiants dans le cache de champs présenté au chapitre 6. Cela peut prendre beaucoup de mémoire JVM si vous avez beaucoup de correspondances parent pour vos requêtes. Ce sera moins un problème une fois que vous pourrez avoir des doc_values pour le champ _parent, comme décrit pour ce problème: https://github.com/elastic/elasticsearch/issues/6107.



Obtenir les documents enfants dans les résultats

Par défaut, seuls les documents parents sont renvoyés par la requête has_child, pas les enfants correspondants. Vous pouvez également obtenir les enfants en ajoutant l’option inner_hits que vous avez vue précédemment pour les documents imbriqués:

Comme pour les documents imbriqués, la réponse de chaque groupe correspondant contiendra également des événements correspondants, sauf que les événements sont désormais des documents distincts et ont leur propre ID au lieu d’un décalage:

Query et filtre has_parent

has_parent est, comme on pouvait s’y attendre, l’opposé de has_child. Vous l’utilisez lorsque vous souhaitez rechercher des événements tout en incluant des critères des groupes auxquels ils appartiennent.

Le filtre has_parent peut encapsuler une query ou un filtre. Il s’exécute sur le « type » que vous indiquez, prend les résultats parent et renvoie les enfants, en indiquant leur ID depuis leur champ _parent.

La figure suivante montre comment rechercher des événements sur Elasticsearch, mais uniquement s’ils se produisent à Denver.

Comme un enfant n’a qu’un parent, il n’y a pas de scores à agréger, comme ce serait le cas avec has_child. Par défaut, has_parent n’a aucune influence sur le score de l’enfant (« score_mode« : « none« ). Vous pouvez remplacer « score_mode » par « score » pour que les événements héritent du score de leurs groupes parents.

A l’instar des query et des filtres has_child, les query et les filtres has_parent doivent charger des identifiants parents dans les données de champ(fielddata) afin de prendre en charge les recherches rapides. Cela étant dit, vous pouvez vous attendre à ce que toutes les requêtes parent / enfant soient plus lentes que les requêtes imbriquées équivalentes. C’est le prix à payez pour pouvoir indexer et rechercher tous les documents indépendamment.

Une autre similitude avec les query et les filtres has_child est le fait que has_parent ne renvoie, par défaut, qu’un seul côté de la relation, à savoir les documents enfants. À partir de Elasticsearch 1.5, vous pouvez également extraire les parents en ajoutant l’objet inner_hits à la requête.

Agrégation d’enfants

Avec la version 1.4, une agrégation d’enfants a été introduite, ce qui vous permet d’imbriquer des agrégations sur des documents enfants sous celles que vous créez sur des documents parents. Supposons que vous obteniez déjà les balises les plus populaires pour vos groupes de rencontre grâce à l’agrégation de termes. Pour chacun de ces tags, vous avez également besoin des participants les plus fréquents aux événements appartenant aux groupes de chaque tag. En d’autres termes, vous souhaitez voir les personnes ayant de fortes préférences pour des catégories d’événements spécifiques.

Vous obtiendrez ces personnes dans la figure suivante en imbriquant une agrégation d’enfants sous votre agrégation de termes top-tags. Sous l’agrégation enfants, vous allez imbriquer une autre agrégation de termes qui comptera le nombre de participants pour chaque balise.



Remarque

Vous avez peut-être remarqué que l’agrégation des enfants est similaire à l’agrégation imbriquée: elle transmet les documents enfants aux agrégations qu’elle contient. Malheureusement, au moins jusqu’à la version 1.4, Elasticsearch ne fournit pas d’équivalent parent-enfant de l’agrégation imbriquée inverse pour vous permettre de faire le contraire: passez les documents parents aux agrégations qu’elle contient.



Vous pouvez considérer les documents imbriqués comme des jointures au moment de l’index et des relations parent-enfant comme des jointures au moment des requêtes. Avec imbriqué, un parent et tous ses enfants sont réunis dans un seul bloc Lucene lors de l’indexation. En revanche, le champ _parent permet de corréler différents types de documents au moment de la requête.

Les structures imbriquées et parent-enfant sont bonnes pour les relations un à plusieurs. Pour les relations plusieurs à plusieurs, vous devrez employer une technique courante dans l’espace NoSQL: la dénormalisation.



Utilisation de la relation parent-enfant entre documents: avantages et inconvénients

Avant de poursuivre, voici un bref récapitulatif des raisons pour lesquelles vous devriez ou non utiliser les relations parent-enfant. Les points positifs:

  • Les enfants et les parents peuvent être mis à jour séparément.
  • Les performances de jointure au moment de la requête sont meilleures que si vous le faisiez dans votre application, car tous les documents associés sont routés vers le même fragment et les jointures sont effectuées au niveau du fragment sans ajouter de sauts de réseau.

Les inconvénients:

  • Les requêtes coûtent plus cher que leurs équivalents imbriqués et nécessitent plus de mémoire que les données de champ.
  • Les agrégations peuvent uniquement joindre des documents enfants à leurs parents et non l’inverse, du moins jusqu’à la version 1.4.

8.5 DENORMALISATION: UTILISATION DE CONNEXIONS DE DONNEES REDONDANTES

La dénormalisation consiste à multiplier les données afin d’éviter des jointures coûteuses. Prenons un exemple dont nous avons déjà discuté: les groupes et les événements. Il s’agit d’une relation un à plusieurs car un événement ne peut être hébergé que par un seul groupe et un groupe peut accueillir de nombreux événements.

Avec les structures parent-enfant ou imbriquées, les groupes et les événements sont stockés dans différents documents Lucene, comme illustré à la figure 8.15.

Cette relation peut être dénormalisée en ajoutant les informations de groupe à tous les événements, comme illustré à la figure 8.16.

Nous verrons ensuite comment et quand la dénormalisation vous aide et comment indexer et interroger de manière concrète des données dénormalisées.

8.5.1. Cas d’utilisation pour la dénormalisation

Commençons par les inconvénients: les données dénormalisées prennent plus de place et sont plus difficiles à gérer que les données normalisées. Dans l’exemple de la figure 8.16, si vous modifiez les détails du groupe, vous devez mettre à jour trois documents, car ces détails apparaissent trois fois.

Sur le plan positif, vous n’avez pas à faire des jointures sur différents documents lorsque vous interrogez. Ceci est particulièrement important dans les systèmes distribués, car le fait de faire des documents sur le réseau introduit de grandes latences, comme le montre la figure 8.17.

Les documents imbriqués et parent-enfant résolvent ce problème en s’assurant qu’un parent et tous ses enfants sont stockés dans le même nœud, comme illustré à la figure 8.18:

  • Les documents imbriqués sont indexés dans des blocs Lucene, qui sont toujours ensemble dans le même segment du même fragment.
  • Les documents enfants sont indexés avec la même valeur de routage que leurs parents, ce qui les rend appartenant au même fragment.

Dénormaliser les relations one-to-many

Les jointures locales effectuées avec des structures imbriquées et parent-enfant sont beaucoup, beaucoup plus rapides que les jointures distantes. Néanmoins, ils coûtent plus cher que de ne pas avoir de jointure du tout. C’est ici que la dénormalisation peut aider, mais cela implique qu’il y a plus de données. Vos opérations d’indexation entraîneront plus de charge, car vous indexerez plus de données et les requêtes seront exécutées sur des index plus volumineux, ce qui les ralentira.

Vous pouvez constater qu’il ya un compromis entre choisir entre imbriqué, parent-enfant et dénormalisation. En règle générale, vous allez dénormaliser les relations un à plusieurs si vos données sont relativement petites et statiques et que vous avez beaucoup de requêtes. De cette façon, les inconvénients sont moins pénalisants – la taille de l’index est acceptable et le nombre d’opérations d’indexation n’est pas trop important – et éviter les jointures devrait accélérer les requêtes.



Si les performances sont importantes pour vous, reportez-vous au chapitre 10, consacré à l’indexation et à la recherche rapide.



Dénormaliser les relations many-to-many

Les relations many-to-many sont traitées différemment des relations un à plusieurs dans Elasticsearch. Par exemple, un groupe peut contenir plusieurs membres et une personne peut être membre de plusieurs groupes.

Dénormaliser est une proposition bien meilleure car contrairement aux implémentations un-à-plusieurs d’imbrication et parent-enfant, Elasticsearch ne peut pas promettre de contenir des relations plusieurs-à-plusieurs dans un seul nœud. Comme le montre la figure 8.19, une seule relation peut s’étendre à l’ensemble de votre jeu de données. Cela rendrait inévitable les jointures inter-réseaux et coûteuses.

En raison de la lenteur des jointures réseau, à partir de la version 1.5, la dénormalisation est le seul moyen de représenter des relations plusieurs à plusieurs dans Elasticsearch. La figure 8.20 montre comment se présente la structure de la figure 8.19 lorsque les membres sont dénormalisés en tant qu’enfants de chaque groupe auquel ils appartiennent. Nous dénormalisons un côté de la relation plusieurs-à-plusieurs en plusieurs relations un à plusieurs.

Nous verrons ensuite comment indexer, mettre à jour et interroger une structure telle que celle de la figure 8.20.

8.5.2. Indexation, mise à jour et suppression de données dénormalisées

Avant de commencer l’indexation, vous devez décider de la manière dont vous voulez dénormaliser votre relation many-to-many en one-to-many.

Quel côté sera dénormalisé?

Les membres seront-ils multipliés en tant qu’enfants de groupes ou inversement? Pour en choisir un, vous devez comprendre comment les données sont indexées, mises à jour, supprimées et interrogées. La partie dénormalisée – l’enfant – sera plus difficile à gérer dans tous les aspects:

  • Vous indexez ces documents plusieurs fois, une fois pour chacun de ses parents.
  • Lorsque vous mettez à jour, vous devez mettre à jour toutes les instances de ce document.
  • Lorsque vous supprimez, vous devez supprimer toutes les instances.
  • Lorsque vous interrogez des enfants séparément, vous obtiendrez plus de résultats avec le même contenu. Vous devez donc supprimer les doublons du côté de l’application.

Sur la base de ces hypothèses, il semble plus logique de faire des membres des enfants de groupes. Les documents des membres sont de taille plus petite, changent moins souvent et sont interrogés moins souvent que les groupes avec leurs événements. En conséquence, la gestion des documents de membre clonés devrait être plus facile.

Comment voulez-vous représenter la relation un-à-plusieurs?

Aurez-vous des documents parent-enfant ou imbriqués? Vous choisirez ici en fonction de la fréquence à laquelle les groupes et les membres sont recherchés et extraits ensemble. Les query imbriquées fonctionnent mieux que les query has_parent ou has_child.

Un autre aspect important est la fréquence à laquelle les membres changent. Les structures parent-enfant fonctionnent mieux ici car elles peuvent être mises à jour séparément.

Pour cet exemple, supposons que la recherche et la récupération de groupes et de membres ensemble soient rares et que les membres rejoignent et quittent souvent des groupes. Nous choisirons donc parent-enfant.

Indexation

Les groupes et leurs événements seraient indexés comme auparavant, mais les membres doivent être indexés une fois pour chaque groupe auquel ils appartiennent. La figure suivante définit d’abord un mapping pour le nouveau type de membre, puis indexe M. Hinman en tant que membre des groupes Denver Clojure et Denver Elasticsearch à partir des exemples de code.

Plusieurs opérations d’indexation peuvent être effectuées dans une seule requête HTTP à l’aide de l’API bulk. Nous discuterons de l’API bulk au chapitre 10, qui concerne les performances.



Mise à jour

Une fois encore, les groupes ont de la chance et vous les mettez à jour comme vous l’avez vu au chapitre 3, section 3.5. Mais si un membre change ses détails parce qu’il est dénormalisé, vous devrez d’abord rechercher tous ses doublons, puis les mettre à jour. Dans la figure 8.11, vous allez rechercher tous les documents qui portent un id de «10001» et mettre à jour leur prénom à Lee.

Vous recherchez des ID au lieu de noms, car ils ont tendance à être plus fiables que d’autres champs, tels que les noms. Vous vous souviendrez peut-être de la section parent-enfant que, lorsque vous utilisez le champ _parent, plusieurs documents du même type au sein du même index peuvent avoir la même valeur _id. Seules les combinaisons _id et _parent sont uniques. Lors de la dénormalisation, vous pouvez utiliser cette fonctionnalité et utiliser intentionnellement le même _id pour la même personne, une fois pour chaque groupe auquel ils appartiennent. Cela vous permet de récupérer rapidement et de manière fiable toutes les instances de la même personne en recherchant leur ID.

Remarque

Plusieurs mises à jour peuvent également être effectuées dans une seule requête HTTP via l’API bulk. Comme pour l’indexation en masse, nous aborderons les mises à jour en masse au chapitre 10.



Suppression

La suppression d’un membre dénormalisé nécessite que vous identifiiez à nouveau toutes les copies. Dans la section parent-enfant, rappelez-vous que pour supprimer un document spécifique, vous devez spécifier à la fois le _id et le _parent; c’est parce que la combinaison des deux est unique dans le même index et le même type. Vous devez d’abord identifier les membres via un filtre de termes, comme celui de la figure 8.11. Ensuite, vous supprimerez chaque instance de membre:

Maintenant que vous savez indexer, mettre à jour et supprimer des membres dénormalisés, voyons comment vous pouvez exécuter des requêtes sur eux.

8.5.3. Interrogation des données dénormalisées

Si vous devez interroger des groupes, rien de spécifique à la dénormalisation, car les groupes ne sont pas dénormalisés. Si vous avez besoin des critères de recherche de leurs membres, utilisez la query has_child comme vous l’avez fait à la section 8.4.2.

Les membres ont reçu la paille la plus courte avec les query, car ils sont dénormalisés. Vous pouvez les rechercher, même en incluant des critères des groupes auxquels ils appartiennent, avec la requête has_parent. Mais il y a un problème: vous allez récupérer des membres identiques. Dans la figure suivante, vous indexerez deux autres membres et, lorsque vous effectuerez une recherche, vous les retrouverez tous les deux.

À partir de la version 1.5, vous ne pouvez supprimer les membres en double que de votre application. Encore une fois, si la même personne a toujours le même identifiant, vous pouvez utiliser cet identifiant pour faciliter cette tâche: deux résultats portant le même identifiant sont identiques.

Le même problème se produit avec les agrégations: si vous souhaitez compter certaines propriétés des membres, ces comptes seront inexacts, car le même membre apparaît à plusieurs endroits.

La solution de contournement pour la plupart des recherches et des agrégations consiste à conserver une copie de tous les membres dans un index séparé. Appelons-le «membres». L’interrogation de cet index ne renverra qu’une copie de chaque membre. Le problème avec cette solution de contournement est qu’elle n’est utile que lorsque vous interrogez les membres seuls, à moins que vous ne fassiez des jointures côté application, ce dont nous parlerons ensuite.



Utilisation de la dénormalisation pour définir des relations: avantages et inconvénients

Comme nous l’avons fait avec les autres méthodes, nous fournissons un aperçu rapide des forces et des faiblesses de la dénormalisation. Les points positifs:

  • Il vous permet de travailler avec plusieurs relations.
  • Aucune jointure n’est impliquée, ce qui accélère les requêtes si votre cluster peut gérer les données supplémentaires générées par la duplication.

Les inconvénients:

  • Votre application doit gérer les doublons lors de l’indexation, de la mise à jour et de la suppression.
  • Certaines recherches et agrégations ne fonctionneront pas comme prévu car les données sont dupliquées.


8.6. JOINTURE CÔTÉ APPLICATIION

Au lieu de dénormaliser, une autre option pour la relation entre les groupes et les membres consiste à les conserver dans des index distincts et à effectuer les jointures à partir de votre application. Comme Elasticsearch le fait avec parent-enfant, cela nécessite de stocker des ID pour indiquer quel membre appartient à quel groupe et vous devez interroger les deux.

Par exemple, si vous avez une requête pour des groupes avec le nom «Denver», où «Lee» ou «Radu» est un membre, vous pouvez exécuter une query bool sur les membres en premier pour trouver Lee et Radu. Une fois que vous avez obtenu leurs ID, vous pouvez exécuter une deuxième requête sur les groupes, en ajoutant les ID de membre dans un filtre de termes en regard de la requête pour Denver. L’ensemble du processus est illustré à la figure 8.21.

Cela fonctionne bien quand il n’y a pas beaucoup de membres correspondants. Mais si vous souhaitez inclure tous les membres d’une ville, par exemple, la deuxième requête devra exécuter un filtre de termes avec éventuellement des milliers de membres, ce qui la rendra coûteuse. Cependant, vous pouvez faire certaines choses:

  • Lorsque vous exécutez la première requête, si vous avez besoin uniquement d’IDs de membre, vous pouvez désactiver la récupération du champ _source pour réduire le trafic:

  • Dans la deuxième requête, si vous avez beaucoup d’ID, il serait peut-être plus rapide d’exécuter le filtre terms sur les données de champ:

Nous aborderons plus en détail la performance dans le chapitre 10, mais lorsque vous modélisez des relations entre documents, vous décidez en dernier ressort de choisir vos batailles.

8.7. RÉSUMÉ

De nombreux cas d’utilisation doivent traiter des données relationnelles. Dans ce chapitre, vous avez vu comment vous pouvez gérer ces données:

  • Mapping d’objet, surtout utile pour les relations un à un
  • Documents imbriqués et structures parent-enfant, qui traitent des relations un-à-plusieurs
  • Dénormalisation et jointures côté application, qui sont surtout utiles pour les relations plusieurs à plusieurs.

La jonction nuit aux performances, même au niveau local. Il est donc judicieux de regrouper autant de propriétés que possible dans un seul document. Le mapping d’objets facilite cette tâche car il permet la hiérarchisation de vos documents. Les recherches et les agrégations fonctionnent ici comme avec un document à structure plate; vous devez faire référence aux champs en utilisant leur chemin complet, comme location.name.

Lorsque vous devez éviter les correspondances entre objets, des documents imbriqués et parent / enfant sont disponibles pour vous aider:

  • Les documents imbriqués sont essentiellement des jointures au moment de l’indexation, ce qui place plusieurs documents Lucene dans un seul bloc. Pour l’application, le bloc ressemble à un document Elasticsearch unique.
  • Le champ _parent vous permet de pointer un document vers un autre document d’un autre type dans le même index pour qu’il soit son parent. Elasticsearch utilisera le routage pour s’assurer qu’un parent et tous ses enfants atterrissent dans le même fragment afin qu’il puisse effectuer une jointure locale au moment de la requête.

Vous pouvez rechercher des documents imbriqués et parent-enfant avec les query et filtres suivants:

  • Query et filtre nested
  • Query et filtre has_child
  • Query et filtre has_parent 

Les agrégations ne fonctionnent à travers les relations que avec des documents imbriqués via les agrégation nested et reverse_nested.

Les objets, les documents imbriqués et parent-enfant, ainsi que la technique générique de dénormalisation peuvent être combinés de quelque manière que ce soit pour vous permettre d’obtenir un bon mélange de performances et de fonctionnalités.