---
{
  "title": "$batch-validate : Valider l'ensemble des ressources stockées par rapport à leurs 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 entier déjà présent dans votre base de données, de façon synchrone ou asynchrone, et déterminez précisément quelles ressources sont non 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ée dans Aidbox par rapport à son schéma FHIR et à tout profil que vous désignez, 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"]
}
---

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 :

```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 renvoie un résumé `Parameters` avec les comptages 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}   # 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`](https://www.health-samurai.io/docs/aidbox/reference/all-settings#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.

```yaml
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 :

```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 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` :

```http
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 :

```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 — 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.

```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 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 :

```mermaid
flowchart LR
    A["POST /fhir/Observation/$batch-validate"] --> B{Hash-partition by id}
    B --> C[chunk 0]
    B --> D[chunk 1]
    B --> E[chunk ...]
    C --> F[(Aggregated results)]
    D --> F
    E --> F
```

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`](https://www.health-samurai.io/docs/aidbox/reference/all-settings#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.

```mermaid
flowchart LR
    A["POST + Prefer: respond-async"] --> Q[(Shared Postgres chunk queue)]
    Q -->|claim| N1[Aidbox node 1]
    Q -->|claim| N2[Aidbox node 2]
    Q -->|claim| N3[Aidbox node ...]
    N1 --> R[(Aggregated results)]
    N2 --> R
    N3 --> R
```

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 :

```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 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](https://www.health-samurai.io/docs/aidbox/modules/profiling-and-validation/batch-resource-validation).