|
8 min de lectura
|

Más allá de Patient/$merge: una fusión agnóstica de recursos impulsada por el cliente para FHIR

Resumen del artículo

Hemos extendido FHIR $merge para aceptar un Bundle de transacción suministrado 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 dentro de la misma transacción. Combinado con una nueva operación $referencing para la inclusión inversa genérica, esto ofrece una fusión completamente auditable y agnóstica de recursos 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 la Gestión de Datos Maestros en MDMbox en despliegues en producción: deduplicando pacientes, fusionando organizaciones y conciliando profesionales sanitarios entre centros. En el proceso, aprendimos que cada organización fusiona de manera diferente, y ningún algoritmo de servidor puede abarcar la variedad de políticas de fusión del mundo real.

FHIR R5 introdujo Patient/$merge — una operación largamente esperada que permite fusionar registros de pacientes duplicados. 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 el nivel de madurez 0 y no ha recibido mayor desarrollo desde su introducción.

Es un buen comienzo. Sin embargo, tras implementar la fusión en producción, comprobamos que la especificación no llega suficientemente lejos.

El problema con la fusión impulsada por el servidor

La especificación FHIR $merge 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 fusión varía enormemente entre organizaciones:

  • El hospital A elimina completamente el paciente origen y reescribe todas las referencias
  • El hospital B mantiene el origen inactivo y crea un recurso Linkage para la procedencia
  • El hospital C fusiona Encounters y Observations en el destino, pero conserva registros AllergyIntolerance separados para revisión manual
  • El intercambio de información sanitaria D necesita fusionar Organizations y Practitioners, no solo Patients

La especificación no cubre ninguno de esos casos: es exclusiva para Patient y el paso de «actualizar todas las referencias» se resuelve de manera vaga.

Merge flow

Lo que necesitábamos era una operación de fusión 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 comprobaciones de seguridad
  • Funcione para cualquier tipo de recurso, no solo para Patient

Nuestro enfoque: el parámetro plan

Hemos construido un $merge similar a 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 bien todo tiene éxito — incluida la traza de auditoría — o nada lo tiene.

Observe las cabeceras ifMatch en cada entrada: se trata de bloqueo optimista. Si cualquier 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 sobrescrituras silenciosas ni actualizaciones perdidas.

Es un superconjunto, no una bifurcación

Observará que usamos source, target y result en lugar de los source-patient, target-patient y result-patient de la especificación. Esto es intencionado: la operación es agnóstica de recursos, por lo que una nomenclatura específica para Patient resultaría confusa. ¿Fusionando Organizations duplicadas? ¿Practitioners? La misma operación, la misma traza de auditoría.

Elimine el parámetro plan y pase result en su lugar para obtener el comportamiento estándar de FHIR $merge. Se admite la búsqueda por identificador para el origen y el destino. El plan es lo que lo hace estrictamente más potente: cuando está presente, el cliente asume el control total de la lógica de fusión.

La pieza que faltaba: $referencing

Para construir un buen plan de fusión, el cliente necesita responder a una pregunta: «¿Qué recursos referencian a Patient/123?»

FHIR dispone de _revinclude para resultados de búsqueda y $everything para Patient, pero no existe ninguna operación genérica que indique «dame todos los recursos de la base de datos que apunten a esta referencia». Nosotros hemos construido una.

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 para mayor claridad, pero la idea central se mantiene: $.** recorre recursivamente toda la estructura del recurso y encuentra cualquier objeto anidado en el que reference sea igual al destino, independientemente del lugar del árbol de recursos en que aparezca. Sin rutas codificadas de forma fija, 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, de modo que el recorrido recursivo se realiza contra el índice, no mediante un escaneo completo de la tabla. Para una fusión típica, la operación se completa en milisegundos. Para tipos de recursos con millones de filas, paralelizamos la búsqueda entre tablas y transmitimos los resultados al cliente en streaming.

Hemos expuesto esto como una operación $referencing independiente en MDMbox: un bloque de construcción que cualquier flujo de trabajo de fusión (o análisis de impacto) puede utilizar.

Task + Provenance: no es solo auditoría

Cada fusión 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 Task de fusión y reaccionar de inmediato: actualizar sus cachés, desencadenar flujos de trabajo descendentes o notificar a los usuarios. FHIR Subscriptions estándar, sin tuberías personalizadas.

Instantáneas versionadas. El Provenance registra una referencia versionada (p. ej., Patient/456/_history/3) para cada recurso modificado o eliminado por la fusión. Combinado con la History API de FHIR, el estado previo a la fusión siempre es recuperable.

Seguimiento del ciclo de vida. El Task registra el estado de la fusión (requestedin-progresscompleted o failed), quién la inició, cuándo ocurrió y los enlaces tanto al origen como al destino. La consulta «muéstrame todas las fusiones que afectaron a este paciente» se resuelve con una única búsqueda FHIR sobre Task.

Lo que sigue: $unmerge

FHIR aún no define una operación de desfusión. Pero considere este escenario: se ejecuta una fusión el lunes, y el miércoles alguien se da cuenta de que los dos pacientes eran personas distintas. Para entonces, 50 recursos han sido modificados: Encounters reapuntados, Observations reasignadas y el Patient origen eliminado.

Con Task haciendo seguimiento del ciclo de vida de la fusión, Provenance capturando cada recurso afectado con su versión previa a la fusión y la History API preservando todos los estados anteriores, disponemos de todo lo necesario para revertir una fusión de forma fiable. La operación $unmerge lee el Provenance, recupera las versiones previas a la fusión desde el historial y construye un Bundle de transacción inversa que restaura cada recurso a su estado anterior.

Cubriremos $unmerge en la siguiente entrada — ya publicada: Diseñando $unmerge: reversión de la fusión de pacientes en FHIR. La segunda entrada de la serie, Los proveedores de MPI no lograron ponerse de acuerdo. Nosotros resolvimos la fusión de pacientes, explica por qué un único $merge impulsado por el servidor no puede servir a todas las políticas en producción. Para conocer los fundamentos del Índice Maestro de Pacientes subyacente, véase Índice Maestro de Pacientes (MPI): cómo funciona + ejemplos.


¿Desea probar la fusión impulsada 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.