Nous faisons tourner la gestion des données de référence (Master Data Management) dans MDMbox au sein de déploiements en production depuis deux ans — dédoublonnage de patients, fusion d'organisations, réconciliation de professionnels de santé entre établissements. Au fil du temps, nous avons constaté que chaque organisation fusionne différemment, et qu'aucun algorithme côté serveur ne peut couvrir la variété des politiques de fusion du monde réel.
FHIR R5 a introduit Patient/$merge — une opération très attendue qui permet de fusionner des dossiers patients en double. Elle définit des paramètres d'entrée (source-patient, target-patient, result-patient), attend du serveur qu'il 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 connu de développement supplémentaire 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 avec 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 énormément selon les organisations :
- 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
- Le réseau d'échange d'informations de santé D doit fusionner des Organizations et des Practitioners, pas seulement des Patients
La spécification ne couvre rien de tout cela — elle ne s'applique qu'aux Patients et l'étape « mettre à jour toutes les références » est traitée de façon expéditive.
Ce dont nous avions besoin, c'était une opération de fusion où :
- 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 compatible FHIR qui accepte un Bundle de transaction standard comme 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 comme une seule transaction FHIR. Soit tout réussit — y compris la piste d'audit — soit rien ne se produit.
Remarquez les en-têtes ifMatch sur chaque entrée — c'est du verrouillage optimiste. Si une ressource a été modifiée entre le moment où le client a construit le plan et celui où il est exécuté, la transaction échoue. Aucun écrasement silencieux, aucune mise à jour perdue.
Un sur-ensemble, pas un dérivé
Vous remarquerez que nous utilisons source, target et result plutôt que les paramètres source-patient, target-patient et result-patient de la spécification. C'est intentionnel — l'opération est indépendante du type de ressource, et une dénomination propre aux Patients serait trompeuse. Fusion d'Organizations en double ? De 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. Le paramètre plan est ce qui le rend strictement plus puissant : 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 il n'existe pas d'opération générique qui dise « donne-moi toutes les ressources de la base de données qui pointent vers cette référence ». Nous en avons conçu une.
Sous le capot, il s'agit d'une requête PostgreSQL utilisant JSONPath pour effectuer une recherche 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 demeure : $.** parcourt récursivement l'ensemble de la structure de la ressource, trouvant tout objet imbriqué où reference est égal à la cible — indépendamment de l'endroit où il 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 par balayage complet de la table. Pour une fusion typique, l'opération se termine en quelques millisecondes. Pour les types de ressources comportant des millions de lignes, nous parallélisons la recherche entre les tables et transmettons les résultats au client en flux continu.
Nous avons exposé cela comme une opération $referencing autonome dans MDMbox — un bloc de construction que tout flux de fusion (ou d'analyse d'impact) peut utiliser.
Task + Provenance : bien plus que de l'audit
Chaque fusion crée une Task et une Provenance dans la même transaction. C'est bien plus qu'une exigence de conformité :
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. Des 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 la 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 elle s'est produite, et les liens vers la source et la cible. La requête « montrez-moi toutes les fusions qui ont touché ce patient » se résume à une seule recherche FHIR sur Task.
La prochaine étape : $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 un 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 redirigés, des Observations réassignées, le Patient source supprimé.
Avec la Task qui suit le cycle de vie de la fusion, la Provenance qui capture chaque ressource affectée avec sa version d'avant la fusion, et l'API History qui préserve tous les états antérieurs — nous disposons de tout ce qu'il faut pour inverser une fusion de façon fiable. L'opération $unmerge lit la Provenance, récupère les versions d'avant la fusion depuis l'historique, et construit un Bundle de transaction inverse qui restaure chaque ressource à son état antérieur.
Nous couvrirons $unmerge dans le prochain article — maintenant publié : Concevoir $unmerge : inverser la fusion de patients FHIR. Le deuxième article de la série, Les fournisseurs MPI n'ont pas 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 les bases du Master Patient Index sous-jacent, consultez Master Patient Index (MPI) : fonctionnement et exemples.
Vous souhaitez essayer la fusion pilotée par le client dans votre projet ? MDMbox est disponible dès aujourd'hui.



