|
8 min de lectura
|

Más allá de Patient/$merge: FHIR Merge independiente del recurso

Resumen del artículo

Hemos extendido el $merge de FHIR para aceptar un Bundle de transacción proporcionado por el cliente como parámetro plan. El cliente decide qué actualizar, crear o eliminar; el servidor garantiza la atomicidad y crea recursos de auditoría Task + Provenance en la misma transacción. Combinado con una nueva operación $referencing para reverse-include genérico, esto proporciona un merge completamente auditable e independiente del recurso que funciona para Patient, Organization, Practitioner o cualquier otro tipo.

Resumir este artículo con:
ChatGPTPerplexityClaudeGrok
How many duplicates are in your patient data?

Most teams can't answer this. A 45-minute call with our engineers will get you close.

Tell us how patient records get created and move between your systems. We'll point out where duplicates usually come from in setups like yours, give a rough estimate of your duplicate rate, and say honestly whether you need an MPI. Sometimes the answer is "not yet" — we'll tell you that too.

Free · 45 minutes · you talk to an engineer, not sales

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.

Merge flow

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.

Before and after merge

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:

Audit trail

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.

Compartir este artículo
Comments
Comments
Sign in
Loading comments...
Subscribe to our blog

Get the latest articles on FHIR, interoperability, and healthcare IT.