Écrit par

Solution Architect at Zorgi
MOD
Article Lorenzo Scalese · Avr 9 17m read

Mise en œuvre d'openEHR avec IRIS for Health

Le simple fait d'entendre parler d'OpenEHR vous glace-t-il le sang ? Les archétypes vous font-ils froid dans le dos ?

Surmontez vos craintes grâce à cet article et maîtrisez OpenEHR grâce aux fonctionnalités d'InterSystems IRIS for Health !

Qu'est-ce qu'openEHR ?

openEHR est une spécification ouverte et indépendante des fournisseurs, conçue pour représenter, stocker et échanger des informations cliniques d'une manière sémantiquement riche et durable à long terme. Au lieu de définir des structures de messages fixes (comme le font de nombreuses normes d'interopérabilité), openEHR sépare les connaissances cliniques de la mise en œuvre technique grâce à une approche de modélisation multicouche.

Essentiellement, openEHR repose sur trois concepts fondamentaux :

  • Modèle de référence (RM) Modèle définissant les structures fondamentales utilisées dans les dossiers médicaux, telles que les compositions, les entrées, les observations, les évaluations et les actions. Le modèle de dossier est délibérément générique et indépendant de toute technologie.
  • Archétypes Modèles de données lisibles par machine (exprimés en ADL ou Archetype Definition Language) qui définissent la sémantique clinique détaillée d'un concept spécifique, tel que la mesure de la pression artérielle ou un résumé de sortie. Les archétypes restreignent le RM et fournissent un vocabulaire clinique réutilisable.
  • Modèles (fichiers OPT) Spécialisations basées sur des archétypes. Les modèles adaptent les archétypes à un cas d'utilisation, un système ou un formulaire spécifique (par exemple, un modèle de signes vitaux ou une note de sortie régionale). Les modèles suppriment les options et génèrent des définitions opérationnelles que les systèmes peuvent mettre en œuvre en toute sécurité.

Prenons l'exemple d'un fichier RAW basé sur une composition (en JSON) :

 

Spoiler

{
  "_type": "COMPOSITION",
  "archetype_node_id": "openEHR-EHR-COMPOSITION.diagnostic_summary.v1",
  "archetype_details": {
    "_type": "ARCHETYPED",
    "archetype_id": {
      "_type": "ARCHETYPE_ID",
      "value": "openEHR-EHR-COMPOSITION.diagnostic_summary.v1"
    },
    "template_id": {
      "_type": "TEMPLATE_ID",
      "value": "ES - AP - Diagnoses and treatment"
    },
    "rm_version": "1.0.4"
  },
  "uid": {
    "_type": "OBJECT_VERSION_ID",
    "value": "diag-1003::ehr.local::1"
  },
  "name": {
    "_type": "DV_TEXT",
    "value": "Summary of diagnoses and treatment – Moderate COPD"
  },
  "language": {
    "_type": "CODE_PHRASE",
    "terminology_id": {
      "_type": "TERMINOLOGY_ID",
      "value": "ISO_639-1"
    },
    "code_string": "es"
  },
  "territory": {
    "_type": "CODE_PHRASE",
    "terminology_id": {
      "_type": "TERMINOLOGY_ID",
      "value": "ISO_3166-1"
    },
    "code_string": "ES"
  },
  "category": {
    "_type": "DV_CODED_TEXT",
    "value": "event",
    "defining_code": {
      "_type": "CODE_PHRASE",
      "terminology_id": {
        "_type": "TERMINOLOGY_ID",
        "value": "openehr"
      },
      "code_string": "433"
    }
  },
  "composer": {
    "_type": "PARTY_IDENTIFIED",
    "name": "Dr. García"
  },
  "context": {
    "_type": "EVENT_CONTEXT",
    "start_time": {
      "_type": "DV_DATE_TIME",
      "value": "2025-11-22T08:45:00Z"
    },
    "setting": {
      "_type": "DV_CODED_TEXT",
      "value": "primary medical care",
      "defining_code": {
        "_type": "CODE_PHRASE",
        "terminology_id": {
          "_type": "TERMINOLOGY_ID",
          "value": "openehr"
        },
        "code_string": "228"
      }
    }
  },
  "content": [
    {
      "_type": "SECTION",
      "archetype_node_id": "openEHR-EHR-SECTION.diagnoses_and_treatments.v1",
      "name": {
        "_type": "DV_TEXT",
        "value": "Respiratory diagnosis and treatment"
      },
      "items": [
        {
          "_type": "EVALUATION",
          "archetype_node_id": "openEHR-EHR-EVALUATION.problem_diagnosis.v1",
          "name": {
            "_type": "DV_TEXT",
            "value": "Moderate COPD"
          },
          "archetype_details": {
            "_type": "ARCHETYPED",
            "archetype_id": {
              "_type": "ARCHETYPE_ID",
              "value": "openEHR-EHR-EVALUATION.problem_diagnosis.v1"
            },
            "template_id": {
              "_type": "TEMPLATE_ID",
              "value": "ES - AP - Diagnoses and treatment"
            },
            "rm_version": "1.0.4"
          },
          "language": {
            "_type": "CODE_PHRASE",
            "terminology_id": {
              "_type": "TERMINOLOGY_ID",
              "value": "ISO_639-1"
            },
            "code_string": "es"
          },
          "encoding": {
            "_type": "CODE_PHRASE",
            "terminology_id": {
              "_type": "TERMINOLOGY_ID",
              "value": "IANA_character-sets"
            },
            "code_string": "UTF-8"
          },
          "subject": {
            "_type": "PARTY_SELF"
          },
          "data": {
            "_type": "ITEM_TREE",
            "archetype_node_id": "at0001",
            "name": {
              "_type": "DV_TEXT",
              "value": "Diagnostic data"
            },
            "items": [
              {
                "_type": "ELEMENT",
                "archetype_node_id": "at0002",
                "name": {
                  "_type": "DV_TEXT",
                  "value": "Diagnosis description"
                },
                "value": {
                  "_type": "DV_TEXT",
                  "value": "Moderate COPD with exertional dyspnoea."
                }
              },
              {
                "_type": "ELEMENT",
                "archetype_node_id": "at0003",
                "name": {
                  "_type": "DV_TEXT",
                  "value": "Diagnosis code"
                },
                "value": {
                  "_type": "DV_CODED_TEXT",
                  "value": "J44.1",
                  "defining_code": {
                    "_type": "CODE_PHRASE",
                    "terminology_id": {
                      "_type": "TERMINOLOGY_ID",
                      "value": "ICD-10"
                    },
                    "code_string": "J44.1"
                  }
                }
              },
              {
                "_type": "ELEMENT",
                "archetype_node_id": "at0004",
                "name": {
                  "_type": "DV_TEXT",
                  "value": "Onset date"
                },
                "value": {
                  "_type": "DV_DATE_TIME",
                  "value": "2020-09-15T00:00:00Z"
                }
              }
            ]
          }
        },
        {
          "_type": "EVALUATION",
          "archetype_node_id": "openEHR-EHR-EVALUATION.medication_summary.v1",
          "name": {
            "_type": "DV_TEXT",
            "value": "Inhaled treatment for COPD"
          },
          "archetype_details": {
            "_type": "ARCHETYPED",
            "archetype_id": {
              "_type": "ARCHETYPE_ID",
              "value": "openEHR-EHR-EVALUATION.medication_summary.v1"
            },
            "template_id": {
              "_type": "TEMPLATE_ID",
              "value": "ES - AP - Diagnoses and treatment"
            },
            "rm_version": "1.0.4"
          },
          "language": {
            "_type": "CODE_PHRASE",
            "terminology_id": {
              "_type": "TERMINOLOGY_ID",
              "value": "ISO_639-1"
            },
            "code_string": "es"
          },
          "encoding": {
            "_type": "CODE_PHRASE",
            "terminology_id": {
              "_type": "TERMINOLOGY_ID",
              "value": "IANA_character-sets"
            },
            "code_string": "UTF-8"
          },
          "subject": {
            "_type": "PARTY_SELF"
          },
          "data": {
            "_type": "ITEM_TREE",
            "archetype_node_id": "at0001",
            "name": {
              "_type": "DV_TEXT",
              "value": "Associated medication tree"
            },
            "items": [
              {
                "_type": "ELEMENT",
                "archetype_node_id": "at0002",
                "name": {
                  "_type": "DV_TEXT",
                  "value": "Medication associated with the diagnosis"
                },
                "value": {
                  "_type": "DV_TEXT",
                  "value": "Inhaled salbutamol 100 mcg as needed; inhaled tiotropium 18 mcg every 24 hours."
                }
              }
            ]
          }
        }
      ]
    }
  ]
}

Cette approche de modélisation par couches permet aux systèmes openEHR de rester stables pendant des années, voire des décennies, tout en permettant aux modèles cliniques d'évoluer indépendamment de la plateforme logicielle sous-jacente.

'un des éléments clés de l'écosystème openEHR est l'AQL (Archetype Query Language), le langage de requête standard utilisé pour extraire les données cliniques stockées dans les référentiels openEHR.

Archetype Query Language (AQL)

L'AQL s'inspire du SQL, mais est adapté au modèle d'information d'openEHR. Au lieu d'interroger des tables relationnelles, l'AQL permet aux développeurs d'interroger du contenu clinique structuré au sein des compositions openEHR, en tirant parti des chemins d'archétypes.

Caractéristiques clés de l'AQL

  • Requêtes basées sur des chemins : L'AQL utilise des chemins d'archétypes (similaires à XPath) pour naviguer dans la structure interne d'une composition, par exemple :

    /content[openEHR-EHR-OBSERVATION.blood_pressure.v1]/data/events/time
    
  • Conscience sémantique clinique : Les requêtes portent sur des concepts cliniques (archétypes, types d'entrée, points de données) plutôt que sur des noms de colonnes de bases de données.

  • Clauses WHERE flexibles : l'AQL prend en charge le filtrage des valeurs au sein des compositions, par exemple, pression artérielle systolique > 140 ou diagnostics correspondant à un code spécifique.

  • Requêtes multi-compositions : Vous pouvez extraire des données de plusieurs compositions, par exemple toutes les observations d'un patient particulier au fil du temps

  • Indépendant du fournisseur technologique: Toute implémentation d'openEHR qui prend en charge l'AQL devrait, en principe, accepter les mêmes requêtes.

Prenons un exemple de l'AQL :

SELECT
    c/uid/value AS composition_id,
    o/data[at0001]/events[at0006]/data[at0003]/value/magnitude AS systolic,
    o/data[at0001]/events[at0006]/data[at0004]/value/magnitude AS diastolic
FROM
    EHR e
    CONTAINS COMPOSITION c
    CONTAINS OBSERVATION o[openEHR-EHR-OBSERVATION.blood_pressure.v1]
WHERE
    e/ehr_id/value = '12345'
AND
    o/data[at0001]/events[at0006]/time/value > '2023-01-01T00:00:00Z'

Comparaison entre OpenEHR et FHIR

D'un point de vue technique, OpenEHR et FHIR sont très similaires. Informations basées sur un format de document, API REST, etc. Examinons les principaux concepts de FHIR et d'OpenEHR ainsi que leurs correspondances :

De quoi avons-nous besoin pour mettre en œuvre openEHR dans IRIS for Health ?

Stockage des données brutes

  • Le système doit stocker les compositions entrantes au format RAW JSON ou XML brut, telles qu’elles sont reçues.
  • Aucune transformation ne doit modifier le contenu sémantique.
  • Dans notre exemple, nous travaillerons avec le format JSON, mais plusieurs options sont disponibles.

Services API REST

Notre implémentation doit exposer l'API REST suivante :

Services EHR

Pour créer et localiser le « dossier médical » d'un patient.

  • POST /ehr: Création d'un nouveau EHR.
  • GET /ehr/{ehr_id}: Récupération des métadonnées d'un EHR.
  • GET /ehr?subject_id=?: Localisation d'un EHR à partir d'identifiants externes.

Services relatifs aux compositions

Pour stocker les informations cliniques des patients.

  • POST /ehr/{id}/composition: Pour confirmer une nouvelle composition au format RAW. Validation avec OPT lorsque cela est possible
  • GET /composition/{version_uid}: Récupération d'une version spécifique.
  • GET /ehr/{id}/compositions: Liste des compositions pour un EHR.
  • DELETE /composition/{uid}: Composition marquée comme supprimée (suppression logique).

Points de terminaison pour les requêtes AQL

  • POST /query/aql: Accepte une requête AQL, la convertit en SQL (en utilisant les chemins JSON si nécessaire) et renvoie les informations demandées.

Validation des compositions brutes

Nous devons imposer la validation des compositions brutes basées sur des fichiers OPT2 ; nous ne pouvons enregistrer dans notre référentiel aucun fichier JSON que nous recevons.

Mise en œuvre d'openEHR dans IRIS for Health

Bien sûr, la mise en œuvre de toutes les fonctionnalités disponibles sur un serveur openEHR prendra un certain temps ; je me concentrerai donc sur les fonctionnalités principales :

Utilisation d'une application web pour la mise en œuvre d'un service API REST

La publication d'une API REST est très simple ; nous n'avons besoin que de deux éléments : une classe qui étend %CSP.REST et une nouvelle entrée dans la liste des applications web. Examinons l'en-tête de notre classe %CSP.REST étendue :

Comme vous pouvez le constater, nous avons défini tous les chemins d'accès minimaux requis pour notre référentiel. Nous gérons les compositions, les fichiers OPT2 pour les validations RAW et, enfin, l'exécution des requêtes AQL.

Pour notre exemple, nous ne définirons aucune configuration de sécurité, mais l'authentification JWT est recommandée.

Bien, nous avons désormais notre définition d'application web et notre classe %CSP.REST, que reste-t-il ?

Validation des compositions brutes

openEHR n'est pas une nouveauté, on peut donc supposer qu'il existe plusieurs bibliothèques permettant de prendre en charge certaines fonctionnalités, telles que la validation des fichiers RAW. Pour cet exemple, nous avons utilisé et personnalisé la bibliothèque Arche, une bibliothèque open source développée en Java pour valider les compositions RAW avec des fichiers OPT2. Le validateur est un fichier JAR configuré à partir du serveur de langage externe (par défaut dans l'implémentation des images Docker) et qui est invoqué avant l'enregistrement du fichier RAW via la fonctionnalité JavaGateway :

set javaGate = $system.external.getJavaGateway()
set result = javaGate.invoke("org.validator.openehr.Cli", "validate", filePath, optPath)

Si le fichier brut est validé, le document JSON sera enregistré dans la base de données.

Stockage brut des données JSON

Nous pourrions utiliser DocDB, mais nous souhaitons tirer pleinement parti des performances des bases de données SQL. L'un des principaux problèmes d'openEHR réside dans les faibles performances lors de la consultation des documents ; c'est pourquoi nous allons pré-traiter les compositions afin d'extraire les informations communes à tous les types de compositions et d'optimiser les requêtes.

Notre classe Composition définit les propriétés suivantes :

Class OPENEHR.Object.Composition Extends (%Persistent, %XML.Adaptor) [ DdlAllowed ]
{
/// Description
Property ehrId As %Integer;
Property compositionUid As %String(MAXLEN = 50);
Property compositionType As %String;
Property startTime As %DateTime;
Property endTime As %Date;
Property archetypes As list Of %String(MAXLEN = 50000);
Property doc As %String(MAXLEN = 50000);
Property deleted As %Boolean [ InitialExpression = 0 ];
Index compositionUidIndex On compositionUid;
Index ehrIdIndex On ehrId;
Index ExampleIndex On archetypes(ELEMENTS);
}
  • ehrId: identifiant du dossier médical électronique du patient.
  • compositionUid: identifiant de la composition.
  • compositionType: type de composition enregistrée.
  • startTime: la date et l'heure de création de la composition.
  • archetypes: liste des archétypes présents dans la composition.
  • doc: format JSON du document.
  • deleted: valeurs booléennes pour les suppressions « temporaires ».

Nous utiliserons des index pour améliorer les performances des requêtes.

Prise en charge de l'AQL

Comme nous l'avons mentionné précédemment, AQL est un langage de requête basé sur les chemins. Comment pourrions-nous reproduire ce même comportement dans IRIS for Health ? Bienvenue dans JSON_TABLE !

Qu'est-ce que JSON_TABLE ?

La fonction JSON_TABLE renvoie un tableau pouvant être utilisé dans une requête SQL en attribuant des valeurs JSON à des colonnes. Les attributions d'une valeur JSON à une colonne s'écrivent sous forme d'expressions du langage SQL/JSON.

En tant que fonction de table, JSON_TABLE renvoie une table pouvant être utilisée dans la clause FROM d'une instruction SELECT pour accéder aux données stockées dans une valeur JSON ; cette table ne persiste pas d'une requête à l'autre. Plusieurs appels à JSON_TABLE peuvent être effectués au sein d'une même clause FROM et peuvent apparaître conjointement avec d'autres fonctions table.

Nous avons eu l'idée d'implémenter une méthode de classe en Python pour traduire l'AQL en SQL, mais il y a un problème : l'AQL repose sur des chemins relatifs pour les archétypes, et non sur des chemins absolus. Nous devons donc déterminer le chemin absolu de chaque archétype et le combiner avec le chemin relatif de l'AQL.

Comment déterminer le chemin absolu ? C'est très simple ! Nous pouvons le trouver lorsque l'utilisateur enregistre le fichier OPT2 de la composition dans IRIS ! Dès que nous obtenons le chemin absolu, nous l'enregistrons dans un fichier CSV spécifique à la composition (il serait enregistré dans un répertoire global ou de toute autre manière) ; ainsi, il nous suffit d'avoir le chemin absolu à partir du fichier de composition spécifique ou, si l'AQL ne définit pas la composition, de rechercher dans les fichiers CSV disponibles le chemin absolu des archétypes de l'AQL.

Voyons comment cela fonctionne. Voici un exemple d'AQL au moyen duquel on peut obtenir tous les diagnostics avec un code CIM-10 spécifique :

SELECT
  c/uid/value AS comp_uid,
  c/context/start_time/value AS comp_start_time,
  dx/data[at0001]/items[at0002]/value/value AS diagnosis_text,
  dx/data[at0001]/items[at0003]/value/defining_code/code_string AS diagnosis_code
FROM EHR evaleur booléenne pour les suppressions « temporaires » qui n'impliquent pas une suppression totale
CONTAINS COMPOSITION c[openEHR-EHR-COMPOSITION.diagnostic_summary.v1]
CONTAINS SECTION s[openEHR-EHR-SECTION.diagnoses_and_treatments.v1]
CONTAINS EVALUATION dx[openEHR-EHR-EVALUATION.problem_diagnosis.v1]
WHERE dx/data[at0001]/items[at0003]/value/defining_code/code_string
      MATCHES {'E11', 'I48.0'}
ORDER BY c/context/start_time/value DESC

La fonction Transform de la classe OPENEHR.Utils.AuxiliaryFunctions transformera le code AQL comme suit :

SELECT comp_uid, comp_start_time, diagnosis_text, diagnosis_code 
FROM ( 
    SELECT c.compositionUid AS comp_uid, 
        jt_root.comp_start_time AS comp_start_time, 
        jt_n1.diagnosis_text AS diagnosis_text, 
        jt_n1.diagnosis_code AS diagnosis_code 
    FROM OPENEHR_Object.Composition AS c, 
        JSON_TABLE( c.doc, '$' COLUMNS ( comp_start_time VARCHAR(4000) PATH
            '$.context.start_time.value' ) ) AS jt_root, 
        JSON_TABLE( c.doc, '$.content[*]?(@._type=="SECTION" && @.archetype_node_id==
            "openEHR-EHR-SECTION.diagnoses_and_treatments.v1").items[*]?
            (@._type=="EVALUATION" && @.archetype_node_id=="openEHR-EHR-EVALUATION.problem_diagnosis.v1")'
            COLUMNS ( 
                diagnosis_text VARCHAR(4000) PATH '$.data[*]?(@.archetype_node_id=="at0001").items[*]?
                    (@.archetype_node_id=="at0002").value.value', 
                diagnosis_code VARCHAR(255) PATH '$.data[*]?(@.archetype_node_id=="at0001").items[*]?
                    (@.archetype_node_id=="at0003").value.defining_code.code_string' ) ) AS jt_n1 
    WHERE ('openEHR-EHR-COMPOSITION.diagnostic_summary.v1' %INLIST (c.archetypes) 
        AND 'openEHR-EHR-EVALUATION.problem_diagnosis.v1' %INLIST (c.archetypes)) 
        AND (jt_n1.diagnosis_code LIKE '%E11%' OR jt_n1.diagnosis_code LIKE '%I48.0%') ) U 
ORDER BY comp_start_time DESC

Testons notre API REST avec une requête AQL :

Bravo !

Et voici maintenant une AQL incluant des conditions numériques :

SELECT
  c/uid/value AS comp_uid,
  c/context/start_time/value AS comp_start_time,
  a/items[at0024]/value/magnitude AS creatinine_value,
  a/items[at0024]/value/units AS creatinine_units
FROM EHR e
CONTAINS COMPOSITION c[openEHR-EHR-COMPOSITION.lab_results_and_medications.v1]
CONTAINS OBSERVATION o[openEHR-EHR-OBSERVATION.laboratory_test_result.v1]
CONTAINS CLUSTER a[openEHR-EHR-CLUSTER.laboratory_test_analyte.v1]
WHERE a/items[at0001]/value/value = 'Creatinina (mg/dL)'
  AND a/items[at0024]/value/magnitude BETWEEN 1.2 AND 1.8
ORDER BY c/context/start_time/value DESC

Cela donne :

SELECT comp_uid, comp_start_time, ldl_value, ldl_units 
FROM ( 
    SELECT c.compositionUid AS comp_uid, jt_root.comp_start_time AS comp_start_time,
        jt_n1.ldl_value AS ldl_value, jt_n1.ldl_units AS ldl_units 
    FROM OPENEHR_Object.Composition AS c, 
        JSON_TABLE( c.doc, '$' COLUMNS ( comp_start_time VARCHAR(4000) 
            PATH '$.context.start_time.value' ) ) AS jt_root, 
        JSON_TABLE( c.doc, '$.content[*]?(@._type=="OBSERVATION" && 
            @.archetype_node_id=="openEHR-EHR-OBSERVATION.laboratory_test_result.v1")
                .data.events[*]?(@._type=="POINT_EVENT").data.items[*]?(@._type=="CLUSTER" &&
                @.archetype_node_id=="openEHR-EHR-CLUSTER.laboratory_test_analyte.v1")'
                COLUMNS ( 
                    ldl_value NUMERIC PATH '$.items[*]?
                        (@.archetype_node_id=="at0024").value.magnitude', 
                    ldl_units VARCHAR(64) PATH '$.items[*]?
                        (@.archetype_node_id=="at0024").value.units', 
                    _w1 VARCHAR(4000) PATH '$.items[*]?
                        (@.archetype_node_id=="at0001").value.value' ) ) AS jt_n1 
    WHERE ('openEHR-EHR-COMPOSITION.lab_results_and_medications.v1' %INLIST (c.archetypes) 
        AND 'openEHR-EHR-OBSERVATION.laboratory_test_result.v1' %INLIST (c.archetypes) 
        AND 'openEHR-EHR-CLUSTER.laboratory_test_analyte.v1' %INLIST (c.archetypes)) 
        AND jt_n1._w1 = 'LDL (mg/dL)' AND jt_n1.ldl_value <= 130 ) 
    U ORDER BY comp_start_time DESC

Encore un succès retentissant !

Conclusions

Comme vous pouvez le constater dans cet exemple, InterSystems IRIS for Health vous fournit tous les outils nécessaires pour mettre en place un référentiel openEHR. Si vous avez des questions, n'hésitez pas à laisser un commentaire !

Avertissement

Le code associé n'est qu'une démonstration de faisabilité. Il ne vise pas à mettre en place un référentiel doté de toutes les fonctionnalités, mais à démontrer que cela est parfaitement possible avec InterSystems IRIS for Health. Profitez-en bien !