|
6 min de lectura
|

FHIR ViewDefinition explicado (especificación SQL on FHIR)

Resumir este artículo con:
ChatGPTPerplexityClaudeGrok

El grupo de trabajo «SQL on FHIR v2» está próximo a su primera publicación, prevista para finales del verano de 2024. La especificación tiene como objetivo construir un puente entre los datos FHIR y los ecosistemas modernos de bases de datos y analítica. La idea central es introducir una forma estandarizada de aplanar los recursos FHIR en tablas relacionales. Creemos que una representación plana de los datos sanitarios hará más eficientes a los ingenieros de datos y a las herramientas analíticas.

Esta transformación de aplanamiento está definida por un tipo de recurso especial: ViewDefinition. Aunque no existen vistas planas universales para la mayoría de los recursos FHIR, creemos que podrían existir muchas vistas útiles y específicas para cada caso de uso. Las ViewDefinitions son CanonicalResources y pueden publicarse como parte de Implementation Guides. Mediante consultas SQL ANSI estándar, pueden constituir la base para la analítica interoperable y la generación de informes sobre FHIR. Esta entrada le ayudará a comprender cómo funciona ViewDefinition.

Una ViewDefinition es un algoritmo que describe la transformación de aplanamiento de recursos FHIR, compuesto por combinaciones de unas pocas funciones.

  • column({name:column_name,path: fhirpath},...) - el motor principal de la transformación; esta función extrae elementos mediante expresiones FHIRPath y coloca el resultado en columnas

  • where(fhirpath) - esta función filtra recursos mediante una expresión FHIRPath. Por ejemplo, es posible que desee transformar únicamente perfiles específicos, como la tensión arterial, en una tabla sencilla

  • forEach(expr, transform) - esta función desanida elementos de una colección en filas separadas

  • select(rows1, rows2) - esta función realiza una combinación cruzada de rows1 y rows2, y se utiliza principalmente para unir los resultados de forEach con columnas de nivel superior

  • union(rows, rows) - esta función concatena conjuntos de filas. El principal caso de uso es combinar filas de distintas ramas de un recurso (por ejemplo, telecom y contact.telecom)

Utilice nuestro ViewDefinition Builder gratuito en línea para convertir datos FHIR almacenados en formato JSON en un formato tabular y plano que facilite el análisis de datos. Ir al ViewDefinition Builder

Una ViewDefinition se representa como un recurso FHIR (documento JSON) donde los elementos (palabras clave) corresponden a funciones:

{
    "resourceType": "ViewDefinition",
    "resource": "Patient",
    // (0)
    "where": [{filter: "active = true"}],
    // (5)
    "select": [
        {
          // (4)
          "column": [
             {"path": "getResourceKey()", "name": "id"},
             {"path": "identifier.where(system='ssn')", "name": "ssn"},
          ]
        },
        { 
          // (3)
          "unionAll": [
            {
              // (1)
              "forEach": "telecom.where(system='phone')",
              "column": [{"path": "value", "name": "phone"}] 
            },
            { 
              // (2)
              "forEach": "contact.telecom.where(system='phone')",
              "column": [{"path": "value",  "name": "phone"}] 
            }
       ]}       
    ]
}

Esta vista produce una tabla de contactos del paciente, donde cada fila representa una entrada de telecom.

  • «where» filtra únicamente los pacientes activos

  • «forEach» desanida Patient.telecom y selecciona números de teléfono

  • «forEach» desanida Patient.contact.telecom y selecciona números de teléfono

  • «unionAll» concatena los resultados de las dos operaciones «forEach»

  • La instrucción «column» extrae el id y el ssn

  • La instrucción «select» realiza una combinación cruzada de id y ssn con los números de teléfono de telecom

A continuación se muestra un ejemplo de la entrada y la salida para esta ViewDefinition.

1[
2 {
3    "resourceType": "Patient", 
4    "id": "pt1", 
5    "identifier": [{"system": "ssn", "value": "s1"}],
6    "telecom":   [{"system": "phone", "value": "tt1"}],
7    "contact": [
8         {"telecom": [{"system": "phone", "value": "t12"}]},
9         {"telecom": [{"system": "phone", "value": "t13"}]}
10     ]
11 },
12 {
13    "resourceType": "Patient", 
14    "id": "pt2", 
15    "identifier": [{"system": "ssn", "value": "s2"}],
16    "telecom":   [{"system": "phone", "value": "t21"}],
17    "contact": [
18         {"telecom": [{"system": "phone", "value": "t22"}]},
19         {"telecom": [{"system": "phone", "value": "t23"}]}
20     ]
21 }
22]

Resultado

idssnphone
pt1s1t11
pt1s1t12
pt1s1t13
pt2s1t21
pt2s1t22
pt2s1t23

Subconjunto de FHIRPath

Las ViewDefinitions utilizan un subconjunto mínimo de FHIRPath para hacer la implementación lo más sencilla posible. Además, la especificación introduce algunas funciones especiales:

  • getResourceKey - obtiene indirectamente el ID del recurso. En ocasiones puede ser complejo, razón por la cual se utiliza esta capa de indirección

  • getReferenceKey(resourceType) - una función similar que obtiene el ID a partir de una referencia

Funciones / Palabras clave

Analicemos cada función en detalle.

column

La función «column» extrae elementos en columnas mediante expresiones FHIRPath. El algoritmo comienza recibiendo una lista de pares {name, path}. Para cada registro en el contexto dado, evalúa la expresión de ruta para extraer los elementos deseados. Los valores resultantes se añaden entonces como columnas a la fila de salida.

{
    "column": [
        {"name": "id",          "path": "getResourceKey()"},
        {"name": "bod",         "path": "birthDate"},
        {"name": "first_name",  "path": "name.first().given.join(' ')"},
        {"name": "last_name",   "path": "name.first().family"},
        {"name": "ssn",         "path": "identifier.where(system='ssn').value.first()"},
        {"name": "phone",       "path": "telecom.where(system='phone').value.first()"},
    ]
}

A continuación se muestra la implementación básica en JavaScript:

function column(cols, rows) {
    return rows.map((row)=> {
        return cols.reduce((res, col ) => {
            res[col.name] = fhirpath(col.path, row)
            return res
        }, {})
    })
}

where

La función «where» conserva únicamente los registros para los cuales su expresión FHIRPath devuelve verdadero.

{
  "resourceType": "ViewDefinition",
  "resource": "Patient",
  "where": [
      {"filter": "meta.profile.where($this = 'myprofile').exists()"},
      {"filter": "active = 'true'"}
  ]
}

Implementación básica en JavaScript:

function where(exprs, rows) {
    return rows.filter((row)=> {
        return exprs.every((expr)=>{
            return fhirpath(expr, row) == true;
        })
    })
}

forEach & forEachOrNull

La función forEach está diseñada para aplanar colecciones anidadas aplicando una transformación a cada elemento. Consta de una expresión FHIRPath para la colección sobre la que se itera y una transformación que se aplica a cada elemento. Esta función es análoga a flatMap o mapcat en otros lenguajes de programación.

{
    "resourceType": "ViewDefinition",
    "resource": "Patient",
    "select": [{
      "forEach": "name",
      "column": [
        {"path": "given.join(' ')", "name": "first_name"},
        {"path": "family", "name": "last_name"}
      ]
    }]
}

Existen dos versiones de esta función: forEach y forEachOrNull. La diferencia principal es que forEach elimina los registros en los que la expresión FHIRPath no devuelve resultados, mientras que forEachOrNull mantiene un registro vacío en dichos casos.

function forEach(path, expr, rows) {
    return rows.flatMap((row)=> {
        return fhirpath(expr, row).map((item)=>{
            // evalKeyword will call column, select or other functions
            return evalKeyword(expr, item)
        })
    })
}

select

La función select se utiliza en combinación con forEach para realizar una combinación cruzada de elementos padre (como Patient.id) con elementos de colección desanidados (como Patient.name). Esta función combina columnas de cada conjunto de filas, generando una combinación completa de datos procedentes de las colecciones de entrada.

{
    "resourceType": "ViewDefinition",
    "resource": "Patient",
    "select": [
     {
         "column": [
            {"path": "getResourceKey()", "name": "id"}
         ]
     },
     {
        "forEach": "name",
        "column": [
          {"path": "given.join(' ')", "name": "first_name"},
          {"path": "family", "name": "last_name"}
        ]
     }
    ]
}

La implementación básica es:

function select(rows1, rows2){
    return rows1.flatMap((r1)=> {
        return rows2.map((r2)=>{
            // merge r1 and r2
            return { ...r1, ...r2 }
        })
    })
}

select([{a: 1}, {a: 2}], [{b: 1}, {b: 2}])
//=>
[{a: 1, b: 1}, 
 {a: 1, b: 2},
 {a: 2, b: 1},
 {a: 2, b: 2}]

unionAll

La función unionAll combina filas de distintas ramas de un árbol de recursos concatenando múltiples conjuntos de registros. Esta función esencialmente concatena varias colecciones de registros en una única colección unificada, preservando todas las filas de los conjuntos de entrada.

{
    "resourceType": "ViewDefinition",
    "resource": "Patient",
    "select": [
        {
          "column": [
             {"path": "getResourceKey()", "name": "id"}
          ]
        },
        { 
          "unionAll": [
            {
              "forEach": "telecom.where(system='phone')",
              "column": [{"path": "value", "name": "phone"}] 
            },
            { 
              "forEach": "contact.telecom.where(system='phone')",
              "column": [{"path": "value",  "name": "phone"}] 
            }
       ]}       
    ]
}

La implementación es una simple concatenación:

function unionAll(rowSets){
    return rowSet.flatMap((rows)=> { return rows})
}

unionAll([1,2,3], [3,4,5])
//=>
[1,2,3,3,4,5]

En un recurso, distintas palabras clave pueden aparecer al mismo nivel. Por ejemplo, select, forEach y unionAll pueden estar presentes en el mismo nodo JSON. Para interpretar dichos nodos, es necesario reordenar las palabras clave (funciones) según su precedencia, haciendo que las funciones de mayor precedencia «suban»:

  • forEach(OrNull)
  • select
  • unionAll
  • column
{
    "forEach":   FOREACH,
    "column":    [COLUMNS], // got into select
    "unionALL":  [UNIONS],  // got into select
    "select":    [SELECTS]
}
//=>
{     
    "forEach": FOREACH
    "select": [
       {"column":   [COLUMNS]},
       {"unionAll": [UNIONS]},
       SELECTS...
    ]
}

Consulte la implementación de referencia.

Motores de ViewDefinition

La ViewDefinition puede ser ejecutada por un motor para producir vistas planas a partir de recursos FHIR. Existen dos categorías de motores:

  • Motores en memoria: Estos motores consumen recursos, los aplanan y envían los resultados a un flujo de datos, un archivo o una tabla. Puede imaginarse un pipeline ETL que transforma archivos NDJSON de exportación Bulk FHIR en archivos Parquet.

  • Motores en base de datos: Estos motores traducen la ViewDefinition en una consulta SQL sobre una base de datos nativa de FHIR. En este caso, la vista puede ser una vista de base de datos real. Los motores en base de datos pueden ser mucho más eficientes que los motores en memoria en términos de velocidad y recursos de almacenamiento, aunque resultan más complejos para los implementadores.

Image 1 Existe una lista oficial de implementaciones disponible en https://fhir.github.io/sql-on-fhir-v2/#impls. La mayoría de las implementaciones son motores en memoria. Aidbox (PostgreSQL) y Pathling (Spark SQL) son motores en base de datos.

Aidbox (motor en base de datos)

Aidbox es un servidor y base de datos FHIR para sistemas nativos de FHIR con soporte integrado para SQL on FHIR. Aidbox transpila una ViewDefinition en una consulta SQL de PostgreSQL, que puede ejecutarse «tal cual» o utilizarse para crear una vista de base de datos.

Por ejemplo, esta ViewDefinition

{
  "resource": "Patient",
  "select": [
    {
      "column": [
        {
          "name": "id",
          "path": "getResourceKey()"
        }
      ]
    },
    {
      "forEach": "name",
      "select": [
        {
          "column": [
            {
              "name": "family",
              "path": "family"
            },
            {
              "name": "given",
              "path": "given.join(' ')"
            }
          ]
        }
      ]
    }
  ]
}

será transpilada en:

SELECT
  cast(id AS text) as "id",
  cast(
    jsonb_path_query_first(q1_1, '$  . family') #>> '{}' AS text
  ) as "family",
  coalesce(
    array_to_string(
      (
        SELECT
          array_agg(x)
        FROM     jsonb_array_elements_text(jsonb_path_query_array(q1_1, '$  . given  [*]')) as x
      ),
      ' '
    ),
    ''
  ) as "given"
FROM
  "patient" as r
  JOIN LATERAL jsonb_path_query(r.resource, '$  . name  [*]') q1_1 
  ON true
LIMIT 100

Puede ejecutar Aidbox localmente o en el Sandbox en la nube en minutos: https://www.health-samurai.io/aidbox#run.

Puede construir y depurar ViewDefinition visualmente con autocompletado de FHIRPath utilizando nuestro ViewDefinition Builder. Image 2 ViewDefinition en FHIR es una herramienta potente que permite gestionar de forma flexible la representación de datos mediante la creación de vistas personalizables basadas en diversas condiciones y parámetros. Esto resulta especialmente útil cuando se necesita adaptar la presentación de la información para satisfacer los requisitos específicos de un usuario o sistema. Mediante el uso de ViewDefinition, puede simplificar y agilizar considerablemente el proceso de integración de datos, garantizando al mismo tiempo la integridad y la accesibilidad de los datos.

Para explorar de forma práctica las capacidades de ViewDefinition, puede instalar la versión gratuita de Aidbox. Esta le permite probar todas las funcionalidades sin limitaciones, proporcionando un entorno ideal para el desarrollo y la experimentación.

Demostración de la implementación ELT para PostgreSQL utilizando Aidbox, el ViewDefinition Builder de código abierto y Grafana.

Únase al grupo de trabajo

Si desea formular preguntas o contribuir a SQL on FHIR, únase a nosotros en el chat de chat.fhir.org. Para preguntas personales, no dude en contactarme en LinkedIn.

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

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