|
6 min de lecture
|

ViewDefinition FHIR expliqué (spécification SQL on FHIR)

Résumer cet article avec :
ChatGPTPerplexityClaudeGrok

Le groupe de travail « SQL on FHIR v2 » approche de sa premiÚre publication, prévue pour la fin de l'été 2024. La spécification vise à établir un pont entre les données FHIR et les écosystÚmes modernes de bases de données et d'analytique. L'idée centrale est d'introduire une façon normalisée d'aplatir les ressources FHIR en tables relationnelles. Nous croyons qu'une représentation aplatie des données de santé rendra les ingénieurs de données et les outils analytiques plus efficaces.

Cette transformation d'aplatissement est dĂ©finie par un type de ressource spĂ©cial : ViewDefinition. Bien qu'il n'existe pas de vues aplaties universelles pour la plupart des ressources FHIR, nous croyons que de nombreuses vues utiles et propres Ă  des cas d'utilisation prĂ©cis pourraient exister. Les ViewDefinitions sont des CanonicalResources et peuvent ĂȘtre publiĂ©es dans le cadre de guides d'implĂ©mentation (IG). GrĂące Ă  des requĂȘtes SQL ANSI standard, elles peuvent constituer la base d'une analytique interopĂ©rable et de rapports sur FHIR. Cet article vous aidera Ă  comprendre le fonctionnement du ViewDefinition.

Un ViewDefinition est un algorithme qui décrit la transformation d'aplatissement des ressources FHIR, composé de combinaisons de quelques fonctions.

  • column({name:column_name,path: fhirpath},...) - le principal moteur de la transformation ; cette fonction extrait des Ă©lĂ©ments Ă  l'aide d'expressions FHIRPath et place le rĂ©sultat dans des colonnes

  • where(fhirpath) - cette fonction filtre les ressources par expression FHIRPath. Par exemple, vous pourriez vouloir transformer uniquement des profils spĂ©cifiques, comme la pression artĂ©rielle, en une table simple

  • forEach(expr, transform) - cette fonction dĂ©plie les Ă©lĂ©ments d'une collection en lignes distinctes

  • select(rows1, rows2) - cette fonction effectue une jointure croisĂ©e entre rows1 et rows2 ; elle sert principalement Ă  joindre les rĂ©sultats de forEach avec les colonnes de niveau supĂ©rieur

  • union(rows, rows) - cette fonction concatĂšne des ensembles de lignes. Le principal cas d'utilisation est la combinaison de lignes provenant de diffĂ©rentes branches d'une ressource (par exemple, telecom et contact.telecom)

Utilisez notre Générateur de ViewDefinition en ligne gratuit pour convertir des données FHIR stockées en représentation JSON en un format tabulaire et aplati, pratique pour l'analyse de données. Accéder au Générateur de ViewDefinition

Un ViewDefinition est représenté comme une ressource FHIR (document JSON) dont les éléments (mots-clés) correspondent aux fonctions :

{
    "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"}] 
            }
       ]}       
    ]
}

Cette vue produit une table des contacts des patients, chaque ligne représentant une entrée de télécommunication.

  • « where » filtre uniquement les patients actifs

  • « forEach » dĂ©plie Patient.telecom et sĂ©lectionne les numĂ©ros de tĂ©lĂ©phone

  • « forEach » dĂ©plie Patient.contact.telecom et sĂ©lectionne les numĂ©ros de tĂ©lĂ©phone

  • « unionAll » concatĂšne les rĂ©sultats des deux opĂ©rations « forEach »

  • L'instruction « column » extrait l'identifiant et le NAS

  • L'instruction « select » effectue une jointure croisĂ©e entre l'identifiant et le NAS avec les numĂ©ros de tĂ©lĂ©phone des tĂ©lĂ©communications

Voici un exemple d'entrée et de sortie pour ce 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]

Résultat

idssnphone
pt1s1t11
pt1s1t12
pt1s1t13
pt2s1t21
pt2s1t22
pt2s1t23

Sous-ensemble FHIRPath

Les ViewDefinitions utilisent un sous-ensemble minimal de FHIRPath afin de simplifier au maximum l'implémentation. De plus, la spécification introduit quelques fonctions spéciales :

  • getResourceKey - obtient indirectement l'identifiant de la ressource. Cela peut parfois ĂȘtre complexe, d'oĂč l'utilisation de cette couche d'indirection

  • getReferenceKey(resourceType) - une fonction similaire qui obtient l'identifiant Ă  partir d'une rĂ©fĂ©rence

Fonctions / Mots-clés

Passons en revue chaque fonction en détail.

column

La fonction « column » extrait des éléments dans des colonnes à l'aide d'expressions FHIRPath. L'algorithme commence par recevoir une liste de paires {name, path}. Pour chaque enregistrement dans le contexte donné, il évalue l'expression de chemin pour extraire les éléments souhaités. Les valeurs résultantes sont ensuite ajoutées sous forme de colonnes à la ligne de sortie.

{
    "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()"},
    ]
}

Voici l'implémentation JavaScript naïve :

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

where

La fonction « where » ne conserve que les enregistrements pour lesquels son expression FHIRPath retourne vrai.

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

Implémentation JavaScript de base :

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

forEach & forEachOrNull

La fonction forEach est destinée à aplatir les collections imbriquées en appliquant une transformation à chaque élément. Elle se compose d'une expression FHIRPath pour la collection à itérer et d'une transformation à appliquer à chaque élément. Cette fonction est analogue à flatMap ou mapcat dans d'autres langages de programmation.

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

Il existe deux versions de cette fonction : forEach et forEachOrNull. La principale différence est que forEach supprime les enregistrements pour lesquels l'expression FHIRPath ne retourne aucun résultat, tandis que forEachOrNull conserve un enregistrement vide dans ces cas.

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 fonction select est utilisée en combinaison avec forEach pour effectuer une jointure croisée entre des éléments parents (comme Patient.id) et des éléments de collection dépliés (comme Patient.name). Cette fonction fusionne les colonnes de chaque ensemble de lignes, produisant une combinaison complÚte des données des collections d'entrée.

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

L'implémentation naïve est :

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 fonction unionAll combine des lignes provenant de différentes branches d'un arbre de ressources en concaténant plusieurs ensembles d'enregistrements. Cette fonction concatÚne essentiellement plusieurs collections d'enregistrements en une seule collection unifiée, préservant toutes les lignes des ensembles d'entrée.

{
    "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"}] 
            }
       ]}       
    ]
}

L'implémentation est une simple concaténation :

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

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

Dans une ressource, diffĂ©rents mots-clĂ©s peuvent apparaĂźtre au mĂȘme niveau. Par exemple, select, forEach et unionAll peuvent tous ĂȘtre prĂ©sents dans le mĂȘme nƓud JSON. Pour interprĂ©ter ces nƓuds, vous devez rĂ©ordonner les mots-clĂ©s (fonctions) selon leur prioritĂ©, les fonctions de prioritĂ© supĂ©rieure remontant en tĂȘte :

  • 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...
    ]
}

Consultez l'implémentation de référence.

Moteurs ViewDefinition

Le ViewDefinition peut ĂȘtre exĂ©cutĂ© par un moteur pour produire des vues aplaties Ă  partir de ressources FHIR. Il existe deux catĂ©gories de moteurs :

  • Moteurs en mĂ©moire : Ces moteurs consomment des ressources, les aplatissent et produisent les rĂ©sultats dans un flux, un fichier ou une table. On peut imaginer un pipeline ETL transformant des fichiers NDJSON exportĂ©s en vrac depuis FHIR (Bulk Data) en fichiers Parquet.

  • Moteurs en base de donnĂ©es : Ces moteurs traduisent le ViewDefinition en une requĂȘte SQL sur une base de donnĂ©es native FHIR. Dans ce cas, la vue peut ĂȘtre une vraie vue de base de donnĂ©es. Les moteurs en base de donnĂ©es peuvent ĂȘtre bien plus efficaces que les moteurs en mĂ©moire en termes de vitesse et de ressources de stockage, mais ils sont plus complexes Ă  implĂ©menter.

Image 1 Une liste officielle des implémentations est disponible à l'adresse https://fhir.github.io/sql-on-fhir-v2/#impls. La plupart des implémentations sont des moteurs en mémoire. Aidbox (PostgreSQL) et Pathling (Spark SQL) sont des moteurs en base de données.

Aidbox (moteur en base de données)

Aidbox est un serveur FHIR et une base de donnĂ©es pour les systĂšmes natifs FHIR, avec une prise en charge intĂ©grĂ©e de SQL on FHIR. Aidbox transpile un ViewDefinition en une requĂȘte SQL PostgreSQL, qui peut ĂȘtre exĂ©cutĂ©e « telle quelle » ou utilisĂ©e pour crĂ©er une vue de base de donnĂ©es.

Par exemple, ce ViewDefinition

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

sera transpilé 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

Vous pouvez exécuter Aidbox localement ou dans un bac à sable infonuagique en quelques minutes - https://www.health-samurai.io/aidbox#run.

Vous pouvez créer et déboguer visuellement un ViewDefinition avec la complétion automatique FHIRPath grùce à notre Générateur de ViewDefinition. Image 2 Le ViewDefinition dans FHIR est un outil puissant qui permet une gestion flexible de la représentation des données en créant des vues personnalisables basées sur diverses conditions et paramÚtres. Cela est particuliÚrement utile lorsque vous devez adapter l'affichage des informations pour répondre à des exigences précises d'utilisateurs ou de systÚmes. En utilisant ViewDefinition, vous pouvez simplifier et accélérer considérablement le processus d'intégration des données tout en assurant l'intégrité et l'accessibilité des données.

Pour explorer concrÚtement les capacités de ViewDefinition, vous pouvez installer la version gratuite d'Aidbox. Elle vous permet de tester toutes les fonctionnalités sans limitations, offrant un environnement idéal pour le développement et l'expérimentation.

Démonstration de l'implémentation ELT pour PostgreSQL avec Aidbox, le Générateur de ViewDefinition libre et ouvert, et Grafana.

Rejoindre le groupe de travail

Si vous souhaitez poser des questions ou contribuer à SQL on FHIR, joignez-vous à nous dans la discussion sur chat.fhir.org. Pour des questions personnelles, n'hésitez pas à me contacter sur LinkedIn.

Partager cet article
Comments
Comments
Sign in
Loading comments...
Subscribe to our blog

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