Nous déployons la gestion des données de référence (MDM) dans MDMbox en production depuis deux ans — dédupliquant des patients, fusionnant des organisations, réconciliant des professionnels de santé entre établissements. Au fil du temps, nous avons appris que chaque organisation fusionne différemment, et qu'aucun algorithme côté serveur ne peut couvrir la diversité des politiques de fusion rencontrées en conditions réelles.
FHIR R5 a introduit Patient/$merge — une opération très attendue qui permet de fusionner des dossiers de patients en double. Elle définit des paramètres d'entrée (source-patient, target-patient, result-patient), s'attend à ce que le serveur gère les mises à jour de références, et retourne le résultat fusionné. L'opération est actuellement au niveau de maturité 0 et n'a pas évolué depuis son introduction.
C'est un bon début. Mais après avoir mis en œuvre la fusion en production, nous avons constaté que la spécification ne va pas assez loin.
Le problème de la fusion pilotée par le serveur
La spécification FHIR $merge suppose que le serveur sait comment fusionner. Le serveur désactive la source, copie les identifiants, met à jour les références. Mais la logique de fusion varie considérablement d'une organisation à l'autre :
- L'hôpital A supprime entièrement le patient source et réécrit toutes les références
- L'hôpital B conserve la source inactive et crée une ressource Linkage pour la traçabilité
- L'hôpital C fusionne les Encounters et les Observations dans la cible, mais conserve des enregistrements AllergyIntolerance séparés pour révision manuelle
- L'organisme d'échange d'information sur la santé D doit fusionner des Organizations et des Practitioners, pas seulement des Patients
La spécification ne couvre aucun de ces cas — elle se limite aux Patients et l'étape de « mise à jour de toutes les références » est traitée de façon superficielle.
Ce dont nous avions besoin, c'est d'une opération de fusion dans laquelle :
- Le client décide de ce qui se passe (quelles ressources mettre à jour, créer ou supprimer)
- Le serveur garantit l'atomicité, crée une piste d'audit et applique les vérifications de sécurité
- Elle fonctionne pour tout type de ressource, pas seulement Patient
Notre approche : le paramètre plan
Nous avons conçu un $merge conforme à FHIR qui accepte un Bundle de transaction standard en tant que paramètre plan supplémentaire :
POST $merge
{
"resourceType": "Parameters",
"parameter": [
{"name": "source", "valueReference": {"reference": "Patient/123"}},
{"name": "target", "valueReference": {"reference": "Patient/456"}},
{"name": "preview", "valueBoolean": false},
{
"name": "plan",
"resource": {
"resourceType": "Bundle",
"type": "transaction",
"entry": [
{
"resource": {"resourceType": "Patient", "id": "456", "...": "merged state"},
"request": {"method": "PUT", "url": "Patient/456", "ifMatch": "W/\"3\""}
},
{
"resource": {"resourceType": "Encounter", "id": "789",
"subject": {"reference": "Patient/456"}},
"request": {"method": "PUT", "url": "Encounter/789", "ifMatch": "W/\"1\""}
},
{
"request": {"method": "DELETE", "url": "Patient/123", "ifMatch": "W/\"2\""}
}
]
}
}
]
}
Le client envoie explicitement chaque PUT, POST et DELETE. Le serveur enveloppe le plan avec des ressources d'audit (Task + Provenance) et exécute l'ensemble en une seule transaction FHIR. Soit tout réussit — y compris la piste d'audit — soit rien n'est exécuté.
Remarquez les en-têtes ifMatch sur chaque entrée — il s'agit du verrouillage optimiste. Si une ressource a été modifiée entre le moment où le client a construit le plan et celui de son exécution, la transaction échoue. Aucune réécriture silencieuse, aucune mise à jour perdue.
Un surensemble, pas un dérivé
Vous remarquerez que nous utilisons source, target et result plutôt que source-patient, target-patient et result-patient de la spécification. C'est intentionnel — l'opération est agnostique aux ressources, et une dénomination spécifique à Patient serait trompeuse. Vous fusionnez des Organizations en double? Des Practitioners? Même opération, même piste d'audit.
Omettez le paramètre plan et passez result à la place — vous obtenez le comportement standard de FHIR $merge. La recherche par identifiant pour la source et la cible est prise en charge. C'est le paramètre plan qui confère à l'opération une puissance accrue : lorsqu'il est présent, le client prend le contrôle total de la logique de fusion.
La pièce manquante : $referencing
Pour construire un bon plan de fusion, le client doit répondre à une question : « Quelles ressources référencent Patient/123 ? »
FHIR dispose de _revinclude pour les résultats de recherche et de $everything pour Patient, mais aucune opération générique ne permet de demander « donne-moi toutes les ressources dans la base de données qui pointent vers cette référence ». Nous en avons créé une.
En coulisses, il s'agit d'une requête PostgreSQL utilisant JSONPath pour effectuer des recherches dans les tables de ressources :
SELECT id, resource_type, resource
FROM <resource_table>
WHERE jsonb_path_query_array(resource, '$.** ? (@.reference == "Patient/123")')
!= '[]'::jsonb
La requête ci-dessus est simplifiée pour la clarté, mais l'idée centrale reste valable : $.** parcourt récursivement toute la structure de la ressource, trouvant tout objet imbriqué où reference est égale à la cible — peu importe où elle apparaît dans l'arbre de la ressource. Aucun chemin codé en dur, aucune configuration par type de ressource. Une seule requête trouve les références dans Encounter.subject, Observation.performer, Claim.provider, ou tout autre champ de référence.
Sur les performances : cette requête utilise un index GIN sur la colonne JSONB, de sorte que le parcours récursif s'effectue sur l'index — et non sur un balayage complet de la table. Pour une fusion typique, l'opération se complète en quelques millisecondes. Pour les types de ressources comportant des millions de lignes, nous parallélisons la recherche entre les tables et diffusons les résultats au client en continu.
Nous avons exposé cette fonctionnalité comme une opération $referencing autonome dans MDMbox — un bloc de construction qu'un flux de fusion (ou d'analyse d'impact) peut utiliser.
Task + Provenance : au-delà de la simple conformité
Chaque fusion crée une Task et une Provenance dans la même transaction. Cela va bien au-delà de la conformité réglementaire :
Abonnements. Les systèmes externes peuvent s'abonner à la création de Task de fusion et réagir immédiatement — mettre à jour leurs caches, déclencher des flux de travail en aval, notifier les utilisateurs. Abonnements FHIR standards, sans plomberie personnalisée.
Instantanés versionnés. La Provenance enregistre une référence versionnée (p. ex. Patient/456/_history/3) pour chaque ressource modifiée ou supprimée par la fusion. Combiné à l'API History de FHIR, cela signifie que l'état complet avant fusion est toujours récupérable.
Suivi du cycle de vie. La Task enregistre le statut de la fusion (requested → in-progress → completed ou failed), qui l'a initiée, quand cela s'est produit, et contient des liens vers la source et la cible. Interroger « montrez-moi toutes les fusions qui ont touché ce patient » se résume à une seule recherche FHIR sur Task.
La suite : $unmerge
FHIR ne définit pas encore d'opération de défusion. Mais considérez ce scénario : une fusion est exécutée le lundi, et le mercredi, quelqu'un réalise que les deux patients étaient en réalité des personnes différentes. Entre-temps, 50 ressources ont été modifiées — des Encounters réassignés, des Observations réattribuées, le Patient source supprimé.
Avec Task qui assure le suivi du cycle de vie de la fusion, Provenance qui capture chaque ressource affectée avec sa version antérieure à la fusion, et l'API History qui préserve tous les états précédents — nous disposons de tout le nécessaire pour inverser une fusion de façon fiable. L'opération $unmerge lit la Provenance, récupère les versions antérieures à la fusion depuis l'historique, et construit un Bundle de transaction inverse qui restaure chaque ressource à son état précédent.
Nous couvrirons $unmerge dans le prochain article — maintenant publié : Conception de $unmerge : inverser la fusion de patients FHIR. Le deuxième article de la série, Les fournisseurs MPI n'ont pu s'entendre. Nous avons résolu la fusion de patients, explique pourquoi un seul $merge piloté par le serveur ne peut pas répondre à toutes les politiques de production. Pour une mise en contexte sur les fondements de l'index patient maître sous-jacent, consultez Index patient maître (MPI) : fonctionnement et exemples.
Vous souhaitez essayer la fusion pilotée par le client dans votre projet ? MDMbox est disponible dès aujourd'hui.




