Llevamos dos años ejecutando Master Data Management en MDMbox en despliegues en producción: deduplicando pacientes, fusionando organizaciones y reconciliando profesionales sanitarios entre centros. En el proceso, aprendimos que cada organización fusiona de manera diferente, y ningún algoritmo del lado del servidor puede cubrir la variedad de políticas de merge del mundo real.
FHIR R5 introdujo Patient/$merge — una operación largamente esperada que permite fusionar registros duplicados de pacientes. Define parámetros de entrada (source-patient, target-patient, result-patient), espera que el servidor gestione las actualizaciones de referencias y devuelve el resultado fusionado. La operación se encuentra actualmente en nivel de madurez 0 y no ha recibido mayor desarrollo desde su introducción.
Es un buen comienzo. Pero tras implementar el merge en producción, comprobamos que la especificación no va suficientemente lejos.
El problema del merge dirigido por el servidor
La especificación $merge de FHIR asume que el servidor sabe cómo fusionar. El servidor desactiva el origen, copia los identificadores y actualiza las referencias. Pero la lógica de merge varía enormemente entre organizaciones:
- Hospital A elimina completamente el paciente origen y reescribe todas las referencias
- Hospital B mantiene el origen inactivo y crea un recurso Linkage para la procedencia
- Hospital C fusiona Encounters y Observations en el destino, pero conserva registros separados de AllergyIntolerance para revisión manual
- La red de intercambio de información sanitaria D necesita fusionar Organizations y Practitioners, no solo Patients
La especificación no cubre nada de eso — es exclusiva para Patient y el paso de «actualizar todas las referencias» se trata de forma muy superficial.
Lo que necesitábamos era una operación de merge en la que:
- El cliente decida qué ocurre (qué recursos actualizar, crear o eliminar)
- El servidor garantice la atomicidad, cree una traza de auditoría y aplique las comprobaciones de seguridad
- Funcione para cualquier tipo de recurso, no solo Patient
Nuestro enfoque: el parámetro plan
Hemos desarrollado un $merge compatible con FHIR que acepta un Bundle de transacción estándar como parámetro adicional plan:
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\""}
}
]
}
}
]
}
El cliente envía cada PUT, POST y DELETE de forma explícita. El servidor envuelve el plan con recursos de auditoría (Task + Provenance) y ejecuta todo como una única transacción FHIR. O todo tiene éxito — incluida la traza de auditoría — o nada lo tiene.
Observe las cabeceras ifMatch en cada entrada — esto es bloqueo optimista. Si algún recurso fue modificado entre el momento en que el cliente construyó el plan y el momento en que se ejecuta, la transacción falla. Sin sobreescrituras silenciosas, sin pérdida de actualizaciones.
Es un superconjunto, no una bifurcación
Observará que usamos source, target y result en lugar de source-patient, target-patient y result-patient de la especificación. Esto es intencionado — la operación es independiente del recurso, por lo que una nomenclatura específica de Patient resultaría confusa. ¿Fusionando Organizations duplicadas? ¿Practitioners? La misma operación, la misma traza de auditoría.
Omita el parámetro plan y pase result en su lugar — obtendrá el comportamiento estándar de $merge de FHIR. La búsqueda basada en identificadores para source y target está soportada. El plan es lo que lo hace estrictamente más potente: cuando está presente, el cliente toma el control total de la lógica de merge.
La pieza que faltaba: $referencing
Para construir un buen plan de merge, el cliente necesita responder a una pregunta: «¿Qué recursos hacen referencia a Patient/123?»
FHIR dispone de _revinclude para resultados de búsqueda y $everything para Patient, pero no existe una operación genérica que diga «dame todos los recursos de la base de datos que apuntan a esta referencia». Nosotros la hemos desarrollado.
Internamente, es una consulta PostgreSQL que utiliza JSONPath para buscar en las tablas de recursos:
SELECT id, resource_type, resource
FROM <resource_table>
WHERE jsonb_path_query_array(resource, '$.** ? (@.reference == "Patient/123")')
!= '[]'::jsonb
La consulta anterior está simplificada por claridad, pero la idea central es válida: $.** recorre recursivamente toda la estructura del recurso, encontrando cualquier objeto anidado donde reference sea igual al destino — independientemente de dónde aparezca en el árbol del recurso. Sin rutas predefinidas, sin configuración por tipo de recurso. Una sola consulta encuentra referencias en Encounter.subject, Observation.performer, Claim.provider o cualquier otro campo de referencia.
Sobre el rendimiento: esta consulta utiliza un índice GIN sobre la columna JSONB, por lo que el recorrido recursivo se realiza sobre el índice — no mediante un escaneo completo de la tabla. Para un merge típico, la operación se completa en milisegundos. Para tipos de recursos con millones de filas, paralelizamos la búsqueda entre tablas y devolvemos los resultados al cliente en streaming.
Hemos expuesto esto como una operación $referencing independiente en MDMbox — un componente reutilizable que cualquier flujo de trabajo de merge (o análisis de impacto) puede utilizar.
Task + Provenance: algo más que auditoría
Cada merge crea un Task y un Provenance dentro de la misma transacción. Esto va más allá del cumplimiento normativo:
Subscriptions. Los sistemas externos pueden suscribirse a la creación de Tasks de merge y reaccionar de inmediato: actualizar sus cachés, desencadenar flujos de trabajo posteriores, notificar a los usuarios. FHIR Subscriptions estándar, sin tuberías personalizadas.
Instantáneas versionadas. El Provenance registra una referencia versionada (por ejemplo, Patient/456/_history/3) para cada recurso modificado o eliminado por el merge. Combinado con la History API de FHIR, esto significa que el estado completo previo al merge siempre es recuperable.
Seguimiento del ciclo de vida. El Task registra el estado del merge (requested → in-progress → completed o failed), quién lo inició, cuándo ocurrió y enlaces tanto al origen como al destino. Consultar «muéstrame todos los merges que afectaron a este paciente» es una única búsqueda FHIR sobre Task.
Qué viene a continuación: $unmerge
FHIR no define todavía una operación de unmerge. Pero considere este escenario: se ejecuta un merge el lunes y el miércoles alguien se da cuenta de que los dos pacientes eran en realidad personas diferentes. Para entonces, 50 recursos han sido modificados — Encounters reasignados, Observations reapuntadas, el Patient origen eliminado.
Con el Task haciendo seguimiento del ciclo de vida del merge, el Provenance capturando cada recurso afectado con su versión previa al merge, y la History API preservando todos los estados anteriores — disponemos de todo lo necesario para revertir un merge de forma fiable. La operación $unmerge lee el Provenance, recupera las versiones previas al merge del historial y construye un Bundle de transacción inverso que restaura cada recurso a su estado anterior.
Cubriremos $unmerge en la próxima entrada — ya publicada: Diseñando $unmerge: Revirtiendo el merge de pacientes en FHIR. La segunda entrada de la serie, Los proveedores de MPI no llegaron a un acuerdo. Nosotros resolvimos el merge de pacientes, explica por qué un único $merge dirigido por el servidor no puede servir a todas las políticas de producción. Para conocer los fundamentos del Master Patient Index subyacente, consulte Master Patient Index (MPI): Cómo funciona + Ejemplos.
¿Desea probar el merge dirigido por el cliente en su proyecto? MDMbox está disponible hoy mismo.





