Un serveur FHIR accumule des dossiers médicaux complexes et profondément structurés provenant de chaque système qui l'alimente, et il est facile de supposer qu'ils sont tous corrects. Faire confiance aux données est une chose ; pouvoir les vérifier — de façon robuste, à grande échelle — en est une autre. Avec des millions de ressources, valider chacune manuellement est fastidieux, et probablement déraisonnable. Et on ne valide que rarement une seule fois, car les profils ne sont pas statiques. Les guides d'implémentation comme US Core et les guides HL7 Da Vinci publient régulièrement de nouvelles versions, chacune ajoutant des éléments, resserrant les cardinalités ou modifiant les ensembles de valeurs auxquels elles sont liées. Chaque fois que vous adoptez une nouvelle version de profil — ou que vous en publiez une vous-même — la même préoccupation revient : quelle proportion de ce que vous avez déjà est encore conforme. Alors, comment vérifier l'ensemble des données, à répétition, sans que cela devienne un projet à part entière?
L'opération $validate de FHIR pourrait théoriquement être automatisée, mais il vous faudrait récupérer chaque ressource et la soumettre à nouveau par POST, en collectant et filtrant les résultats.
Cette approche comporte de nombreux problèmes et est difficile à mettre à l'échelle.
Idéalement, vous voudriez demander au serveur de valider un type de ressource précis et de vous indiquer ce qui ne va pas en un seul appel, avec une capacité de mise à l'échelle horizontale et verticale adéquate.
C'est précisément ce que fait $batch-validate.
Il est livré avec Aidbox 2607 et remplace entièrement l'ancienne API de validation par lots.
Pourquoi nous avons refondu la validation par lots
Aidbox dispose depuis des années d'une validation asynchrone par lots, exposée par un ensemble de RPC (aidbox.validation/batch-validation et ses variantes).
Cela fonctionnait, mais présentait plusieurs problèmes.
D'abord, chaque erreur de validation était stockée comme sa propre ressource BatchValidationError.
Le processus était également assez lent, rendant la validation d'un grand nombre de ressources inutilement longue.
Enfin, la validation d'un jeu de données volumineux pouvait produire tellement de résultats qu'ils rivalisaient en taille avec les données elles-mêmes. Cent gigaoctets de ressources non conformes pouvaient engendrer quelque chose d'approchant cent gigaoctets de ressources d'erreurs. Le mécanisme auquel vous recouriez pour comprendre un problème de qualité des données aggravait votre problème de stockage.
L'approche était aussi uniquement asynchrone, de forme RPC plutôt que d'opération FHIR, et elle vous remettait un tas de ressources d'erreurs à interroger plutôt qu'une réponse directe.
$batch-validate conserve l'aspect positif — valider ce qui est déjà stocké, en parallèle — et corrige le reste.
| Ancienne validation par lots | $batch-validate | |
|---|---|---|
| Interface | RPC propriétaires | Opération FHIR (Parameters en entrée et en sortie) |
| Modes | Asynchrone uniquement | Synchrone ou asynchrone |
| Stockage des résultats | Une ressource BatchValidationError par erreur | Une ligne par problème distinct plus une petite table d'identifiants de ressources |
| Coût de stockage | Croît avec le nombre d'erreurs | Borné — les corps des ressources ne sont jamais copiés |
| Sortie | Un ensemble de ressources d'erreurs à interroger | Résumé des problèmes les plus graves en premier, avec exploration à la demande |
| Mise à l'échelle | Fixe | Blocs partitionnés par hachage, en flux continu, parallèles sur les nœuds, indexables |
Les anciens RPC aidbox.validation/* ainsi que les ressources BatchValidationRun / BatchValidationError n'existent plus. Il s'agit d'un changement incompatible; si vous les utilisiez, migrez vers l'opération décrite ci-dessous.
Comment utiliser
$batch-validate s'exécute sur un seul type de ressource.
Le seul paramètre obligatoire est _since, une borne inférieure sur meta.lastUpdated.
Il oblige chaque exécution à déclarer une fenêtre temporelle plutôt que d'analyser accidentellement l'ensemble du jeu de données — pour tout valider, passez l'époque.
Ainsi, pour valider toutes les Observations mises à jour en avril 2026, nous pouvons appeler $batch-validate comme suit :
POST /fhir/Observation/$batch-validate
Content-Type: application/json
resourceType: Parameters
parameter:
- {name: _since, valueInstant: "2026-04-01T00:00:00Z"}
- {name: _until, valueInstant: "2026-05-01T00:00:00Z"}
Par défaut, l'appel est synchrone : il bloque et renvoie un résumé Parameters avec les comptages principaux et une entrée par problème distinct, les plus graves en premier.
resourceType: Parameters
parameter:
- {name: task-id, valueString: "b1f9..."}
- {name: validated, valueUnsignedInt: 1804646} # resources checked
- {name: valid, valueUnsignedInt: 1317494} # no issues
- {name: invalid, valueUnsignedInt: 487152} # total invalid resources
- {name: invalid-resources, valueUrl: "/fhir/$batch-validate/b1f9.../invalid-resources"}
- name: issue
part:
- {name: code, valueCode: invalid-slice-cardinality}
- {name: expression, valueString: category}
- {name: profile, valueString: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-lab"}
- {name: count, valueUnsignedInt: 486018} # resources hitting this exact issue
- {name: diagnostics, valueString: "Observation.category: element count is outside the allowed range"}
count indique le nombre de ressources distinctes touchées par ce problème précis — le moyen le plus rapide de savoir si un problème concerne six ressources ou six cent mille.
Les appels synchrones conviennent bien à un petit ensemble de ressources, lorsque vous souhaitez obtenir la réponse immédiatement.
Le travail s'exécute tout de même en parallèle : number-of-chunks (défini par appel) divise les ressources en autant de blocs, et le paramètre scheduler-executors contrôle le nombre de blocs exécutés simultanément sur le nœud.
Ensemble, ils permettent de moduler la granularité des blocs par rapport à la charge d'une seule machine — c'est votre levier de mise à l'échelle verticale.
Cependant, lorsque vous souhaitez valider un jeu de données nettement plus important, vous pourriez préférer le faire de façon asynchrone.
Asynchrone pour les grands jeux de données
Pour rendre n'importe quel appel $batch-validate asynchrone, il suffit d'ajouter un en-tête Prefer: respond-async à l'appel.
Aidbox planifie alors le travail sur son moteur de tâches, le répartissant entre les nœuds et permettant une mise à l'échelle horizontale et verticale.
POST /fhir/Observation/$batch-validate
Prefer: respond-async
resourceType: Parameters
parameter:
- {name: _since, valueInstant: "1970-01-01T00:00:00Z"} # basically will validate every Observation in the database
Vous obtenez un 202 avec un en-tête Content-Location que vous pouvez interroger pour suivre la progression :
GET /fhir/$batch-validate/b1f9...
Pendant l'exécution, vous obtenez un 202 avec un en-tête X-Progress: 45%.
À la fin, vous obtenez le même résumé Parameters qu'un appel synchrone renverrait.
Les appels synchrones et asynchrones conservent leurs résultats sous un task-id, de sorte qu'il n'y a aucune différence dans la façon d'analyser les résultats.
Explorer les ressources invalides
Le résumé vous indique quels problèmes existent et combien de ressources chacun touche.
Pour voir les ressources réelles, suivez le lien invalid-resources — filtrez-le à un seul problème avec _issue, et paginez avec _count / _page :
GET /fhir/$batch-validate/b1f9.../invalid-resources?_count=50&_page=1
Chaque ressource invalide est renvoyée avec une fullUrl spécifique à la version pointant vers la version exacte qui a été validée, le corps de la ressource, et un OperationOutcome énumérant tous les problèmes de cette ressource :
- name: resource
part:
- {name: fullUrl, valueUrl: "/Observation/obs-42/_history/7"}
- name: resource
resource: {resourceType: Observation}
- name: outcome
resource:
resourceType: OperationOutcome
issue:
- {severity: fatal, code: invalid, expression: [Observation.category], diagnostics: "..."}
Le résultat liste l'ensemble complet des problèmes d'une ressource, même lorsque _issue restreint les ressources retournées — de sorte que vous ne corrigez jamais un problème pour en découvrir un second lors du passage suivant.
Tester un profil avant de l'appliquer
La raison la plus courante d'exécuter cette opération est l'adoption d'un nouveau profil : vous voulez savoir ce qui est brisé avant d'en faire une exigence.
Passez un ou plusieurs canoniques profile, et chaque ressource est validée par rapport à ceux-ci en plus de son schéma de base.
resourceType: Parameters
parameter:
- {name: _since, valueInstant: "1970-01-01T00:00:00Z"}
- {name: profile, valueCanonical: "http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-lab"}
Les profils multiples sont conjonctifs (ET) : une ressource est conforme uniquement si elle satisfait chacun d'eux, et les problèmes constituent l'union de tous. Vous pouvez donc activer US Core en sachant exactement ce qui échouerait, plutôt que de le découvrir en production.
Ajuster le validateur par exécution
Parfois vous souhaitez un balayage structurel rapide sans vous soucier de la terminologie pour l'instant; parfois vous voulez être plus strict que les paramètres par défaut du serveur.
Chaque paramètre disable-* / strict-* remplace un paramètre du validateur pour cette exécution seulement — omettez-le pour conserver le comportement configuré du serveur.
| Paramètre | Effet |
|---|---|
disable-terminology-validation | Ignorer les vérifications de liaison codée et de terminologie |
disable-primitive-validation | Ignorer les vérifications de type primitif et de format |
disable-slicing-validation | Ignorer la validation des tranches |
disable-constraint-validation | Ignorer tous les invariants FHIRPath (ou tous les vérifier si false) |
disable-constraint | Ignorer des invariants spécifiques par clé (p. ex. us-core-8) |
strict-profile-resolution | Traiter un profil non résolu comme une erreur plutôt que de l'ignorer |
strict-extension-resolution | Traiter une extension non résolue comme une erreur |
strict-profile-resolution mérite d'être souligné.
Sans lui, une URL de profil qui ne se résout pas est ignorée, de sorte qu'une faute de frappe se transforme en rapport « conforme » discrètement erroné.
Activez-le lorsque vous voulez que l'exécution échoue bruyamment plutôt que silencieusement.
Conçu pour la mise à l'échelle
En coulisses, $batch-validate partitionne les ressources par hachage en un nombre fixe de blocs (number-of-chunks, par défaut 12).
Chaque bloc valide sa tranche mod(hash(id), N), et les blocs s'agrègent en un seul résultat :
Chaque bloc est traité en flux continu, de sorte que la mémoire vive reste bornée quelle que soit la taille du jeu de données.
Une exécution synchrone traite ses blocs sur un groupe de travailleurs dédié — un groupe de fils d'exécution fixe qu'Aidbox crée pour cette exécution et détruit à sa fin, dimensionné par le paramètre scheduler-executors et distinct des fils servant votre trafic API habituel.
Au plus ce nombre de blocs s'exécutent simultanément, de sorte qu'il n'y a pas de limite supérieure à number-of-chunks — un nombre plus élevé se met simplement en file d'attente plutôt que de faire croître la mémoire vive.
Une exécution asynchrone n'utilise pas ce groupe local ; elle s'exécute plutôt sur le groupe propre au moteur de tâches, enregistrant une ligne de planificateur par bloc pour que n'importe quel nœud puisse la prendre en charge.
Puisqu'elle n'emprunte ni les fils de requête ni les emplacements du groupe de connexions, elle est sans risque pour un serveur en production ; c'est le processeur que vous utilisez, alors privilégiez le mode asynchrone pour un premier balayage important.
Le chemin asynchrone est également ce qui permet à une exécution de se mettre à l'échelle sur plusieurs machines. Chaque bloc est une tâche sur un planificateur qui réside dans la base Postgres partagée, de sorte que vous pouvez faire tourner plusieurs instances Aidbox sur différentes machines contre une seule base de données et elles se répartissent les blocs entre elles — une exécution s'accélère à mesure que vous ajoutez des nœuds. Un bloc est pris en charge par exactement une instance, de sorte qu'il ne s'exécute jamais deux fois ; et si une instance tombe en plein milieu d'une exécution, le planificateur récupère son bloc et le réessaie sur une autre, avec des écritures idempotentes pour qu'un bloc récupéré ne compte jamais en double.
Comme N est fixe pour une exécution, le prédicat de partition est une expression constante que vous pouvez indexer.
Pour un très grand jeu de données validé avec un nombre de blocs élevé, un index d'expression correspondant transforme chaque bloc d'un balayage complet en un balayage d'index sélectif :
CREATE INDEX CONCURRENTLY observation_batch_validate_10000
ON observation (mod(abs(hashtextextended(id, 0)), 10000));
Puis exécutez avec number-of-chunks: 10000.
Le modulo de l'index doit correspondre au nombre de blocs, sinon PostgreSQL ne l'utilisera pas — vérifiez avec EXPLAIN.
Compact par conception
Voici pourquoi la validation de 100 Go de mauvaises données ne vous coûte plus 100 Go supplémentaires.
Aidbox stocke les résultats sous une forme agrégée et indexée par problème dans un schéma dédié aidbox_batch_validation :
| Table | Contenu |
|---|---|
issue | une ligne par erreur distincte |
invalid_resource | une petite ligne (issue, resource_id, version) par ressource — identifiants seulement |
chunk_stat | une ligne par bloc avec ses métriques |
Toutes les occurrences partageant un profil, un type de ressource, un chemin normalisé, un code et une contrainte se regroupent en un seul problème dont le comptage est le nombre de ressources distinctes touchées.
Aidbox ne copie jamais les corps des ressources invalides ni leurs OperationOutcome : l'exploration relit chaque corps depuis l'historique à la version validée et reconstruit le résultat à partir des champs stockés.
Ce sont les problèmes distincts, et non le volume brut, qui déterminent le coût de stockage.
Essayez-le
$batch-validate est disponible dans Aidbox 2607.
Pointez-le vers un type de ressource, passez un _since d'époque, et vous obtenez une cartographie de vos problèmes de qualité des données, les plus graves en premier, en un seul appel — puis explorez les ressources exactes derrière chacun.
Vous voulez le voir fonctionner sans rien écrire? Nous avons publié un carnet de notes interactif qui exécute l'ensemble du flux de bout en bout : il charge un ensemble de Patients échantillons, crée un profil modélisé sur US Core Patient, les valide en un seul appel et présente les résultats sous forme de graphiques — la répartition valide/invalide, les Patients invalides par problème et où les problèmes se concentrent. Ouvrez le carnet Batch validation depuis la section Notebooks dans Aidbox et exécutez-le du début à la fin.
Référence complète : Validation de ressources par lots.



