---
{
  "title": "$batch-validate : valider chaque ressource stockée par rapport à ses profils, à grande échelle",
  "description": "Aidbox 2607 remplace l'ancienne API de validation par lots par l'opération $batch-validate — validez un type de ressource complet déjà dans votre base de données, de façon synchrone ou asynchrone, et déterminez exactement quelles ressources ne sont pas conformes et pourquoi.",
  "date": "2026-07-13",
  "author": "Andrew Listopadov",
  "reading-time": "9 min read",
  "tags": ["Aidbox", "FHIR Profiling", "Compliance", "Database"],
  "utm-campaign": "feature",
  "utm-content": "batch-validate",
  "tldr": "$batch-validate vérifie chaque ressource d'un type déjà stocké dans Aidbox par rapport à son schéma FHIR et aux profils que vous indiquez, de façon synchrone ou asynchrone. Les résultats sont agrégés sous une forme compacte et indexée par problème, de sorte que la validation de 100 Go de données non conformes n'ajoute pas 100 Go à votre base de données. Disponible à partir d'Aidbox 2607.",
  "seo-tags": ["FHIR validation", "batch validation", "FHIR profile validation", "Aidbox", "FHIR conformance", "healthcare data quality"]
}
---

> For the complete documentation index, see [llms.txt](https://www.health-samurai.io/llms.txt).
> Use it to discover all available pages before guessing URLs.

---

Un serveur FHIR accumule des dossiers médicaux complexes et profondément structurés provenant de tous les systèmes qui l'alimentent, et il est facile de supposer qu'ils sont tous corrects.
Faire confiance aux données, c'est une chose ; pouvoir les vérifier — de façon robuste, à grande échelle — c'en est une autre.
Avec des millions de ressources, valider manuellement chacune d'elles 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 ils sont liés.
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 détenez déjà est toujours conforme.
Alors, comment tout vérifier, à répétition, sans que cela devienne un projet en soi ?

L'opération `$validate` de FHIR pourrait, en théorie, être automatisée, mais vous devriez récupérer chaque ressource et la renvoyer par POST, en collectant et filtrant les résultats.
Cette approche pose 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 la capacité de mise à l'échelle horizontale et verticale appropriée.

C'est précisément ce que fait `$batch-validate`.
Cette fonctionnalité est intégrée dans Aidbox 2607 et remplace entièrement l'ancienne API de validation par lots.

## Pourquoi nous avons refait la validation par lots

Aidbox disposait depuis des années d'une validation par lots asynchrone, exposée via un ensemble de RPC (`aidbox.validation/batch-validation` et ses variantes).
Cela fonctionnait, mais posait plusieurs problèmes.
D'abord, **chaque erreur de validation était stockée comme sa propre ressource `BatchValidationError`.**
Le processus était également assez lent, ce qui rendait 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 avec les données elles-mêmes en taille.
Cent gigaoctets de ressources non conformes pouvaient produire près de cent gigaoctets de ressources d'erreurs.
Le mécanisme utilisé pour *comprendre* un problème de qualité des données aggravait votre problème de stockage.

Il était également asynchrone uniquement, de forme RPC plutôt que d'opération FHIR, et il vous remettait un ensemble de ressources d'erreurs à interroger plutôt qu'une réponse directe.

`$batch-validate` conserve la bonne partie — 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 seulement                          | 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                                        | Fragments partitionnés par hachage, diffusés en 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 non rétrocompatible ; si vous les utilisiez, migrez vers l'opération décrite ci-dessous.

## Comment l'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 de balayer 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 :

```yaml
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 retourne un résumé `Parameters` avec les totaux principaux et une entrée par problème distinct, les plus graves en premier.

```yaml
resourceType: Parameters
parameter:
  - {name: task-id,   valueString: "b1f9..."}
  - {name: validated, valueUnsignedInt: 1804646}   # ressources vérifiées
  - {name: valid,     valueUnsignedInt: 1317494}   # sans problème
  - {name: invalid,   valueUnsignedInt: 487152}    # total des ressources invalides
  - {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}   # ressources touchées par ce problème exact
      - {name: diagnostics, valueString: "Observation.category: element count is outside the allowed range"}
```

`count` est le nombre de ressources distinctes touchées par ce problème précis — le moyen le plus rapide de voir si un problème affecte six ressources ou six cent mille.

Les appels synchrones conviennent bien à un petit ensemble de ressources, lorsque vous voulez 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 fragments, et le paramètre [`scheduler-executors`](https://www.health-samurai.io/docs/aidbox/reference/all-settings#scheduler-executors) contrôle combien s'exécutent simultanément sur le nœud.
Ensemble, ils permettent d'équilibrer la granularité des fragments par rapport à la charge imposée à une seule machine — c'est votre levier de mise à l'échelle verticale.

Cependant, lorsque vous souhaitez valider un jeu de données considérablement plus grand, il peut être préférable de procéder de façon asynchrone.

## Asynchrone pour les grands jeux de données

Pour rendre un 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, ce qui permet une mise à l'échelle horizontale et verticale.

```yaml
POST /fhir/Observation/$batch-validate
Prefer: respond-async

resourceType: Parameters
parameter:
  - {name: _since, valueInstant: "1970-01-01T00:00:00Z"} # va essentiellement valider toutes les Observations dans la base de données
```

Vous obtenez un `202` avec un en-tête `Content-Location` que vous pouvez interroger pour suivre la progression :

```http
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 retourne.
Les appels synchrones et asynchrones conservent leurs résultats sous un `task-id`, donc il n'y a aucune différence dans la façon dont vous analysez 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 sur un problème précis avec `_issue`, et paginez avec `_count` / `_page` :

```http
GET /fhir/$batch-validate/b1f9.../invalid-resources?_count=50&_page=1
```

Chaque ressource invalide est retourné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` listant tous les problèmes de cette ressource :

```yaml
- 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 — ainsi, vous ne corrigez jamais un problème pour en découvrir un second à la prochaine itération.

## 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 ne fonctionnera pas avant d'en faire une exigence.
Passez un ou plusieurs canoniques `profile`, et chaque ressource est validée par rapport à eux en plus de son schéma de base.

```yaml
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 sont 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 voulez un balayage structurel rapide sans vous soucier de la terminologie pour l'instant ; d'autres fois, vous souhaitez être plus strict que les paramètres par défaut.
Chaque paramètre `disable-*` / `strict-*` remplace un paramètre du validateur pour cette exécution uniquement — omettez-le pour conserver le comportement configuré.

| Paramètre                        | Effet                                                                       |
|----------------------------------|-----------------------------------------------------------------------------|
| `disable-terminology-validation` | Ignorer les vérifications de liaisons codées et de terminologie             |
| `disable-primitive-validation`   | Ignorer les vérifications de types primitifs 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 le ignorer      |
| `strict-extension-resolution`    | Traiter une extension non résolue comme une erreur                          |

`strict-profile-resolution` mérite d'être souligné.
Sans ce paramètre, une URL de profil qui ne se résout pas est ignorée silencieusement, de sorte qu'une faute de frappe produit un rapport « conforme » qui est discrètement erroné.
Activez-le lorsque vous voulez que l'exécution échoue bruyamment à la place.

## Conçu pour évoluer à grande échelle

Sous le capot, `$batch-validate` partitionne les ressources par hachage en un nombre fixe de fragments (`number-of-chunks`, 12 par défaut).
Chaque fragment valide sa tranche `mod(hash(id), N)`, et les fragments s'agrègent en un seul résultat :

```mermaid
flowchart LR
    A["POST /fhir/Observation/$batch-validate"] --> B{Partitionnement par hachage selon l'id}
    B --> C[fragment 0]
    B --> D[fragment 1]
    B --> E[fragment ...]
    C --> F[(Résultats agrégés)]
    D --> F
    E --> F
```

Chaque fragment est diffusé en 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 fragments sur un pool de travail dédié — un pool de fils d'exécution fixe qu'Aidbox lance pour cette exécution et détruit à la fin, dimensionné par le paramètre [`scheduler-executors`](https://www.health-samurai.io/docs/aidbox/reference/all-settings#scheduler-executors) et distinct des fils qui traitent le trafic normal de votre API.
Au plus ce nombre de fragments s'exécutent simultanément, donc il n'y a pas de limite supérieure à `number-of-chunks` — un nombre plus élevé met simplement les fragments en file d'attente plutôt que d'augmenter la mémoire vive.
Une exécution asynchrone n'utilise pas ce pool local ; elle s'exécute sur le pool propre du moteur de tâches, en écrivant une ligne de planificateur par fragment pour que n'importe quel nœud puisse le réclamer.
Puisqu'elle n'emprunte ni les fils de requête ni les emplacements du pool de connexions, elle est sans danger pour un serveur en production ; c'est le CPU que vous consommez, donc privilégiez l'asynchrone pour un grand premier balayage.

Le chemin asynchrone est également ce qui permet à une exécution de se mettre à l'échelle sur plusieurs machines.
Chaque fragment est une tâche sur un planificateur qui réside dans le Postgres partagé, de sorte que vous pouvez exécuter plusieurs instances Aidbox sur différentes machines contre une seule base de données et elles se partagent les fragments — une exécution s'accélère à mesure que vous ajoutez des nœuds.
Un fragment est réclamé par exactement une instance, donc il ne s'exécute jamais deux fois ; et si une instance tombe en cours d'exécution, le planificateur récupère son fragment et le réessaie sur une autre, avec des écritures idempotentes pour qu'un fragment récupéré ne soit jamais comptabilisé en double.

```mermaid
flowchart LR
    A["POST + Prefer: respond-async"] --> Q[(File de fragments Postgres partagée)]
    Q -->|réclamer| N1[Nœud Aidbox 1]
    Q -->|réclamer| N2[Nœud Aidbox 2]
    Q -->|réclamer| N3[Nœud Aidbox ...]
    N1 --> R[(Résultats agrégés)]
    N2 --> R
    N3 --> R
```

Puisque `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 élevé de fragments, un index d'expression correspondant transforme chaque fragment d'un balayage complet en un balayage d'index sélectif :

```sql
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 fragments, sinon PostgreSQL ne l'utilisera pas — confirmez 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              | Contient                                                                    |
|--------------------|-----------------------------------------------------------------------------|
| `issue`            | une ligne par erreur **distincte**                                          |
| `invalid_resource` | une petite ligne `(issue, resource_id, version)` par ressource — ids seulement |
| `chunk_stat`       | une ligne par fragment avec ses métriques                                   |

Toutes les occurrences qui partagent un profil, un type de ressource, un chemin normalisé, un code et une contrainte sont regroupées en un seul problème dont le nombre est le nombre de ressources distinctes qui le rencontrent.
Aidbox ne copie jamais les corps des ressources invalides ni leurs `OperationOutcome`s : 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 une époque `_since`, et vous obtenez en un seul appel une cartographie de vos problèmes de qualité des données, les plus graves en premier — puis explorez les ressources exactes à l'origine de chacun.

Vous voulez le voir fonctionner sans rien écrire ?
Nous avons publié un carnet interactif qui exécute l'ensemble du flux de bout en bout : il charge un ensemble d'exemples de Patients, crée un profil inspiré de 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.
Lisez le carnet [**Batch validation**](/docs/aidbox/notebooks/64c853e6-f13a-4246-bb0b-044020b3b01a) ici dans la documentation, puis ouvrez-le dans votre propre Aidbox et exécutez-le de haut en bas.

Référence complète : [Validation de ressources par lots](https://www.health-samurai.io/docs/aidbox/modules/profiling-and-validation/batch-resource-validation).