|
6 min de lecture
|

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

Résumer cet article avec :
ChatGPTPerplexityClaudeGrok

Qu'est-ce qu'une ViewDefinition et comment fonctionne-t-elle?

Le groupe de travail « SQL on FHIR v2 » est sur le point de publier sa premiÚre version, prévue pour la fin de l'été 2024. La spécification vise à bùtir 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 méthode normalisée pour aplatir les ressources FHIR en tables relationnelles. Nous croyons qu'une représentation aplatie des données de santé permettra aux ingénieurs de données et aux outils analytiques de travailler de façon plus efficace.

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. GrĂące Ă  des requĂȘtes SQL ANSI standard, elles peuvent constituer la base d'une analytique et d'une production de rapports interopĂ©rables sur FHIR. Cet article vous aidera Ă  comprendre le fonctionnement de ViewDefinition.

Une 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 tension 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, et est principalement utilisĂ©e pour 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 cas d'utilisation principal 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 et gratuit pour convertir des données FHIR stockées en représentation JSON dans un format tabulaire et aplati, idéal pour l'analyse de données. Accéder au générateur de ViewDefinition

Une ViewDefinition est représentée sous la forme d'une ressource FHIR (document JSON) dont les éléments (mots-clés) correspondent à des 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, oĂč chaque ligne reprĂ©sente 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 cette 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 rendre l'implémentation aussi simple que possible. 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

Examinons 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 afin d'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 naïve 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 fonction « where » conserve uniquement 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 de base en JavaScript :

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 à l'aplatissement de 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 ce 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 les éléments parents (comme Patient.id) et les é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 provenant 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, en 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 n'est qu'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 ordre de prioritĂ©, les fonctions de prioritĂ© supĂ©rieure remontant vers le haut :

  • 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

Une ViewDefinition peut ĂȘtre exĂ©cutĂ©e par un moteur afin de 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 exportent les rĂ©sultats dans un flux, un fichier ou une table. On peut imaginer un pipeline ETL transformant des fichiers NDJSON issus d'une exportation Bulk FHIR en fichiers Parquet.

  • Moteurs en base de donnĂ©es : Ces moteurs traduisent une 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 beaucoup plus efficaces que les moteurs en mĂ©moire en ce qui concerne la vitesse et les ressources de stockage, mais ils sont plus complexes Ă  implĂ©menter.

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 et une base de donnĂ©es FHIR pour les systĂšmes natifs FHIR, avec une prise en charge intĂ©grĂ©e de SQL on FHIR. Aidbox transpile une 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, cette ViewDefinition

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

sera transpilée 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 une ViewDefinition avec la saisie semi-automatique FHIRPath grùce à notre générateur de ViewDefinition. 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 de l'information pour répondre aux exigences spécifiques d'un utilisateur ou d'un systÚme. En utilisant ViewDefinition, vous pouvez simplifier et accélérer considérablement le processus d'intégration des données tout en garantissant l'intégrité et l'accessibilité de celles-ci.

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écouvrez la démonstration de l'implémentation ELT pour PostgreSQL avec Aidbox, le générateur de ViewDefinition à source ouverte, et Grafana.

Rejoindre le groupe de travail

Si vous souhaitez poser des questions ou contribuer à SQL on FHIR, rejoignez-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.