08 août 2016 Technique

Elasticsearch : vérification de l'intégrité des données lors de l'utilisation d'entrepôts de données externes

Par Chris Earle

Elasticsearch est parfois utilisé en parallèle d'autres bases de données. Il est alors souvent compliqué de mettre en place des solutions de validation en deux phases en raison de l'absence de prise en charge des transactions par tous les systèmes concernés. Selon votre utilisation, vous pourrez donc devoir vérifier que les données existent dans les deux entrepôts de données, dont l'un joue le rôle de référentiel d'intégrité.

En tant qu'ingénieurs chargés du support chez Elastic, on nous demande souvent quelle est la meilleure façon de structurer les données pour en permettre la vérification ou tout simplement s'il existe une méthode permettant une vérification efficace des données. Cet article présente quelques exemples de complexité variable que nous avons observés ou sur lesquels nous avons travaillé. Ils vérifient qu'Elasticsearch contient les données nécessaires issues d'une base de données comme PostgreSQL.

Modélisation des données pour la vérification

La façon dont sont stockées vos données, que ce soit dans ou en dehors d'Elasticsearch, influe considérablement sur la difficulté de la vérification. Le premier point à définir est la portée de la vérification :

  • Suffit-il de vérifier l'existence d'un document ?
  • Un processus interne vous impose-t-il de vérifier l'intégralité du contenu des documents ?

Cette décision n'est pas sans conséquence sur la complexité de l'opération.

Vérification de l'existence

Rassurez-vous, il ne s'agit pas là d'analyser un grand principe philosophique, mais simplement de se poser la question suivante : « Tous les documents existent-ils dans Elasticsearch ? »

Elasticsearch permet de vérifier l'existence d'un document de nombreuses faços, mais vous devez parfaitement comprendre le processus utilisé. En effet, Elasticsearch effectue des recherches en quasi-temps réel, mais la récupération des documents se fait, elle, bien en temps réel. Cela signifie qu'après avoir été indexé, un document peut ne pas être visible dans les recherches, tout en étant accessible par des requêtes directes de type GET.

Un par un

Dans les déploiements de taille réduite, l'approche la plus simple consiste à émettre des requêtes HEAD sur chaque document, puis à vérifier que le code de réponse HTTP n'est pas 404 (page ou document introuvable).

HEAD /my_index/my_type/my_id1

En cas de réussite, la requête renverrait des en-têtes similaires à ceux-ci :

HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
Content-Length: 0

En cas d'absence, la réponse est presque identique, au code de réponse près :

HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=UTF-8
Content-Length: 0

Cette méthode impose d'émettre N requêtes sur un index dont vous pensez qu'il contient N documents. Comme vous vous en doutez, elle n'est pas adaptée lorsque N est un nombre trop important.

Traitement par lots

Vous pouvez également émettre ces requêtes par lot à l'aide de l'API Multi GET : _mget.

GET /my_index/my_type/_mget
{
  "ids": [ "my_id1", "my_id2" ]
}

Une autre approche équivalente consiste à rechercher les ID et à renvoyer le nombre attendu.

GET /my_index/_refresh
GET /my_index/my_type/_search
{
  "size": 2,
  "query": {
    "ids": {
      "values": [ "my_id1", "my_id2" ]
    }
  }
}

Tout d'abord, nous appelons l'API _refresh pour nous assurer que l'ensemble de l'index peut faire l'objet de recherches. Cette action permet de résoudre le problème du « quasi-temps réel ». Dans le cadre d'une recherche classique, vous ne devez pas procéder ainsi, mais cette méthode s'avère tout à fait logique ici ! Il est probable que le traitement de vérification ait lieu en une seule fois. L'appel de _refresh une fois au début de la tâche est donc suffisant tant que vous ne vérifiez pas la présence de données ajoutées après le démarrage du processus.

La gestion des requêtes est ainsi améliorée, mais nécessite encore de la préparation. De plus, chaque document sera renvoyé, ce qui signifie du temps de traitement supplémentaire.

Recherche, puis traitement par lots

Une fois arrivé au traitement par lots, il est fréquent d'essayer de contourner le problème en le divisant. Par exemple, en déterminant quels documents sont absents, puis en recherchant ces documents :

GET /my_index/_refresh
GET /my_index/my_type/_search
{
  "size": 0,
  "query": {
    "ids": {
      "values": [ "my_id1", "my_id2" ]
    }
  }
}

Le nombre renvoyé par la valeur hits.total devrait correspondre au nombre que vous attendez. Dans cet exemple simple, cette valeur devrait être 2. Si ce n'est pas le cas, deux possibilités s'offrent à vous :

  1. Rechercher tous les ID et les renvoyer en spécifiant la taille appropriée, puis rechercher l'aiguille manquante dans la botte de foin.
  2. Revenir à l'exemple _mget ci-dessus et faire la même chose, à savoir déterminer quel ID est manquant.

Il doit y avoir une meilleure méthode

Ce traitement long ou en deux étapes peut ne pas être très efficace avec les grands volumes. Vous posez ainsi une question coûteuse en ressources, qui n'est peut-être pas la bonne.

Et si vous renversiez la question pour ne pas rechercher tout ce qui existe, mais uniquement ce qui est manquant ? Il est difficile de rechercher ce qui est absent. C'est même impossible, car les données n'existent pas. Pour autant, nous pouvons utiliser les agrégations pour répondre à cette question si vous structurez vos données de la manière appropriée.

Dans la plupart des cas, la vérification est liée au SQL, car les clés primaires et étrangères basées sur des entiers sont très utilisées dans ce langage. Même si vous n'utilisez pas ces ID numériques pour la valeur _id d'Elasticsearch, indexez-les sous forme d'entiers en activant les Doc values (elles sont activées par défaut dans les versions 2.x et supérieures d'ES). Par exemple :

POST /my_index/my_type/my_id1
{
  "id": 1,
  …
}

Le nom du champ n'a aucune importance, mais notez toutefois qu'il ne s'agit pas de _id, qui est un champ de métadonnées réservé dans Elasticsearch. Une fois l'indexation de cette valeur commencée, il devient très simple de trouver les données manquantes avec des histogrammes :

GET /my_index/my_type/_search
{
  "size": 0,
  "aggs": {
    "find_missing_ids": {
      "histogram": {
        "field": "id",
        "interval": 1,
        "min_doc_count": 0
      },
      "aggs": {
        "remove_existing_bucket_selector": {
          "bucket_selector": {
            "buckets_path": {
              "count": "_count"
            },
            "script": {
              "inline": "count == 0",
              "lang": "expression"
            }
          }
        }
      }
    }
  }
}

Cette action a deux conséquences :

  1. Elle réalise un histogramme à partir du champ numérique id avec un pas de 1 entre chaque valeur. Vous allez ainsi déterminer tous les entiers compris entre chaque id (par ex., 1 - 5 donnerait un histogramme de 1, 2, 3, 4, 5).
  2. Elle supprime toute catégorie d'histogramme qui contient des données à l'aide d'un sélecteur de catégorie (bucket selector), disponible uniquement dans Elasticsearch 2.0 et versions ultérieures.

En supprimant ce qui existe, vous gardez uniquement ce qui n'existe pas. Ce résultat peut se présenter sous la forme suivante :

{
  "took": 4,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 5,
    "max_score": 0,
    "hits": []
  },
  "aggregations": {
    "find_missing_ids": {
      "buckets": [
        {
          "key": 4,
          "doc_count": 0
        }
      ]
    }
  }
}

Pour un petit index de 5 documents, dont nous connaissons la taille grâce à hits.total, nous voyons que le document dont la valeur id est 4 est manquant.

Et c'est tout : si vous pouvez créer un histogramme à partir de la clé unique du document, vous pouvez en créer un qui ne présente que les données manquantes. Cette méthode ne fonctionne pas avec les chaînes, ce qui signifie que les valeurs comme "my_id" et les UUID (par ex. un GUID) ne sont pas compatibles avec cette approche via des agrégations. Vous devez donc structurer vos données en fonction de ce type de vérification.

Mind the gap!

Pour ceux d'entre vous qui ne connaissent pas bien les identifiants séquentiels, sachez qu'il n'est pas rare que les systèmes (bases de données SQL par exemple) récupèrent en amont des lots d'identifiants. Au sein de ces lots, il est possible que certaines valeurs soient ignorées pour diverses raisons : échec d'une transaction ou suppression explicite de l'enregistrement par la suite.

Dans ce type de situation, vous devez garder à l'esprit que ces valeurs apparaîtront comme étant des documents manquants dans l'histogramme. Pour les éviter, en tant que client, vous pouvez demander à Elasticsearch de les ignorer explicitement en ajoutant un sélecteur de catégorie secondaire qui élimine explicitement ces id ou les ignorer côté client.

Vérification de documents entiers

La vérification de documents entiers est bien différente de la simple vérification de l'existence d'un document. Dans ce type de scénario, vous devez vérifier des documents entiers pour vous assurer que les données qu'ils contiennent sont bien celles attendues. Si pour Elasticsearch, cette tâche est en fait plus simple, elle demande davantage de travail de votre côté.

Parcours des données

La manière la plus simple d'effectuer cette vérification consiste à parcourir les données. Heureusement, cela ne signifie pas que vous devez placer toutes vos données dans un tableau HTML et vérifier tout à la souris, mais que vous devez utiliser l'API _scroll pour parcourir les données (_doc, utilisé ci-dessous, est décrit dans le lien précédent). Comme toute API de recherche, cette API suit les principes du quasi-temps réel évoqués précédemment.

GET /my_index/my_type/_search?scroll=1m
{
  "sort": [
    "_doc"
  ]
}

La durée de défilement est le temps dont vous, le logiciel client, avez besoin pour traiter ce lot avant de demander le suivant. Assurez-vous que cette durée est suffisamment longue pour vous permettre d'analyser la réponse obtenue et prenez le temps de lire le lien de documentation ci-dessus !

En parcourant les données, vous pouvez passer d'un document à l'autre et vérifier que toutes les données répondent à vos exigences.

Contrôle de la gestion de versions

Elasticsearch prend en charge le contrôle de concurrence optimiste, autrement dit la gestion de versions.

Pour contrôler la gestion de versions de bout en bout, fournissez votre propre numéro de version. Ainsi, vous pouvez contourner la vérification des documents complets en vérifiant simplement les numéros de version. Pour activer les numéros de version dans les réponses aux recherches, vous devez spécifier l'indicateur de version :

GET /my_index/my_type/_search
{
  "version": true
}

Contournement de la vérification

Il est possible de contourner entièrement la vérification, si vous savez que les données ont été fournies par un utilisateur de confiance et que tout échec d'ingestion a été géré comme il se doit (par ex. si Elasticsearch était indisponible au moment de l'indexation du document 15123, un autre système a dû l'ajouter). Dans ce cas, la vérification peut être redondante.

X-Pack Security dans la version 5.0 et Shield dans les versions précédentes peuvent offrir le contexte de sécurité permettant d'accorder un accès aux utilisateurs de confiance et de bloquer les utilisateurs non fiables. La gestion appropriée des échecs d'ingestion reste pour autant de votre ressort, car elle dépend entièrement du système chargé de l'ingestion.

The end

Et voilà ! Vous êtes arrivés au bout de cet interminable billet.

J'espère que vous avez apprécié ces précisions sur la vérification des données et sur la manière dont la structure de vos données peut considérablement simplifier leur vérification. Vous réalisez probablement que d'autres problèmes peuvent être résolus en approchant la requête ou les données selon un autre angle et en s'appuyant sur des fonctions riches comme les agrégations ou X-Pack Security.

Nous étudions sans relâche des manières créatives de résoudre des problèmes intéressants comme celui-ci. Nous vous encourageons à débattre de ces sujets sur notre forum et à ouvrir des tickets sur GitHub lorsque vous rencontrez un problème. Sachez que nous sommes également disponibles sur Twitter (@pickypg en ce qui me concerne) et sur IRC pour vous donner un coup de main !