文章 John Pan · 3 小时 前 14m read

利用 IRIS for Health 实施 openEHR

一听到“openEHR”,你是否就感到不寒而栗?“原型”这个词是否让你起鸡皮疙瘩?

通过本文克服你的恐惧,利用 InterSystems IRIS for Health 的强大功能,轻松掌握 OpenEHR!

什么是 openEHR?

openEHR 是一项开放且与供应商无关的规范,旨在以语义丰富且长期可持续的方式表示、存储和交换临床信息。与许多互操作性标准定义固定消息结构的做法不同,openEHR 通过多层建模方法将临床知识与技术实现分离。

从本质上讲,openEHR 基于三个基本概念:

  • 参考模型(RM)
    该模型定义了健康记录中使用的核心结构,例如组合(Compositions)、条目(Entries)、观察结果(Observations)、评估(Evaluations)和操作(Actions)。参考模型(RM)刻意设计为通用且与技术无关。
  • 原型
    可机读的模型(以 ADL 表示),用于定义特定概念(如血压测量值或出院小结)的详细临床语义。原型对参考模型施加约束,并提供可重用的临床词汇。
  • 模板(OPT文件)
    基于原型构建的特化形式。模板将原型定制为适用于特定用例、系统或表单的形式(例如,生命体征模板或区域出院摘要)。模板消除了可选性,并生成系统可安全实施的操作性定义。

下面来看一个基于组合(Composition)的 RAW 文件示例(JSON 格式):

 

剧透

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

这种分层建模方法使 openEHR 系统能够保持多年——甚至数十年——的稳定性,同时允许临床模型独立于底层软件平台进行演进。

openEHR 生态系统的一个关键组成部分是AQL(原型查询语言),这是用于检索存储在 openEHR 存储库中的临床数据的标准查询语言。

原型查询语言(AQL)

AQL 受 SQL 启发,但针对 openEHR 信息模型进行了适配。AQL 并非查询关系型表,而是允许开发人员利用原型路径,查询openEHR 组合(Compositions)中的结构化临床内容。

AQL 的主要特征

  • 基于路径的查询:AQL使用原型路径(类似于 XPath)来导航组合的内部结构,例如:

    /content[openEHR-EHR-OBSERVATION.blood_pressure.v1]/data/events/time
  • 临床语义感知:查询引用的是临床概念(原型、条目类型、数据点),而非数据库列名。
  • 灵活的 WHERE 子句:AQL支持对 Composition 内的值进行过滤,例如收缩压 > 140 或与特定代码匹配的诊断。
  • 多组合查询:可跨多个组合检索数据,例如某位患者随时间推移的所有观察结果。
  • 供应商中立:原则上,任何支持 AQL 的 openEHR 实现都应接受相同的查询。

下面来看一个 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'

OpenEHR 与 FHIR 的比较

从技术角度来看,OpenEHR 和 FHIR 非常非常相似。信息基于文档格式、REST API 等。让我们来看看 FHIR 和 OpenEHR 的主要概念及其关联:

要在 IRIS for Health 中实现 openEHR,我们需要什么?

原始组合存储

  • 将接收到的组合数据以原始 JSON 或 XML格式原样存储。
  • 任何转换操作均不得修改语义内容。
  • 在本示例中,我们将使用 JSON 格式,但还有多种其他选项。

REST API 服务

我们的实现必须公开以下 REST API:

电子健康记录(EHR)服务

用于创建和查询患者的“病历”。

  • POST /ehr:创建新的电子健康记录(EHR)。
  • GET /ehr/{ehr_id}:检索EHR 元数据。
  • GET /ehr?subject_id=?:根据外部标识符定位电子健康记录(EHR)。

组合服务

用于存储患者的临床信息。

  • POST /ehr/{id}/composition:提交一个 RAW 格式的新组合。在可能的情况下,根据 OPT 进行验证
  • GET /composition/{version_uid}:检索特定版本。
  • GET /ehr/{id}/compositions:列出某份电子健康记录(EHR)的组合。
  • DELETE /composition/{uid}:将组合标记为已删除(逻辑删除)。

AQL 查询端点

  • POST /query/aql:接收 AQL 查询,将其转换为 IRIS SQL 或基于 JSON-path 的查找,并以 openEHR 规范 JSON 格式返回结果。

原始组合验证

我们必须强制基于 OPT2 文件对 RAW 组合进行验证,不能将收到的任何 JSON 数据直接保存到我们的存储库中。

在 IRIS for Health 中实现 openEHR

要实现 openEHR 服务器中所有可用功能需要一定时间,因此我将重点关注核心功能:

使用 Web 应用程序部署 REST API 服务

发布 REST API 非常简单,我们只需要两个组件:一个继承自 %CSP.REST 的类,以及 Web 应用程序列表中的一条新记录。让我们来看一下我们继承的 %CSP.REST 类的声明:

如您所见,我们已为存储库定义了所有必需的最小路由。其中包括组合管理、用于 RAW 验证的 OPT2 文件,以及 AQL 查询的执行。

在本示例中,我们不会定义任何安全配置,但建议使用 JWT 身份验证。

好,我们已经有了自己的类和 %CSP.REST 类,还有什么需要做的吗?

RAW 验证

openEHR 绝非新生事物,因此不难想象,已有多种库可用于支持 RAW 验证等功能。 在本示例中,我们使用了Arche库并对其进行了定制。Arche是一个用 Java 开发的开源库,用于根据 OPT2 文件验证 rawcompositions。该验证器是一个 JAR 文件,通过外部语言服务器(在 Docker 镜像部署中默认启用)进行配置,并在使用 JavaGateway 功能保存 raw 之前被调用:

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

如果 raw 通过验证,JSON 文档将被保存到数据库中。

JSON 原始数据存储

虽然可以使用 DocDB,但我们希望利用 SQL 数据库的性能优势。与 openEHR 相关的一个最大问题是文档查询性能不佳,因此我们将对组合进行预处理,提取所有组合类型共有的信息,以提升查询效率。

我们的 Composition 类定义了以下属性:

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:患者的电子健康记录标识符。
  • compositionUid:组合标识符。
  • compositionType:保存的组合类型。
  • startTime:组合的创建时间。
  • archetypes:组合中包含的原型列表。
  • doc:JSON 格式的文档。
  • deleted:表示软删除的布尔值。

这些索引将用于优化查询。

AQL 支持

正如前文所述,AQL 是一种基于路径的查询语言。我们该如何在 IRIS for Health 中模拟这种行为呢?欢迎了解 JSON_TABLE!

什么是 JSON_TABLE?

JSON_TABLE 函数返回一个表,该表通过将JSON值映射到列中,可在 SQL 查询中使用。从 JSON 值到列的映射以SQL/JSON 路径语言表达式形式编写。

作为表值函数,JSON_TABLE 返回的表可在SELECT语句的FROM子句中使用,以访问存储在 JSON 值中的数据;该表不会在不同查询之间保留。 在单个 FROM 子句中可以多次调用 JSON_TABLE,并且可以与其他表值函数并列使用。

我们已用 Python 实现了一个 ClassMethod 来将 AQL 转换为 SQL,但存在一个问题:AQL 基于原型的相对路径,而非绝对路径,因此我们需要确定每个原型的绝对路径,并将其与 AQL 的相对路径进行关联。

如何获取绝对路径?非常简单!当用户将组合的 OPT2 文件保存到 IRIS 时,我们就能获取到它!一旦获得绝对路径,我们会将其保存到该组合专用的 CSV 文件中(可保存在全局变量中或通过其他方式保存),因此, 我们只需从该特定组合文件中获取绝对路径;或者,如果 AQL 未定义该组合,则可在现有的 CSV 文件中搜索该 AQL 中原型的绝对路径。

让我们看看它是如何工作的。以下是一个通过特定 ICD-10 代码获取所有诊断的 AQL 示例:

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 e
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

OPENEHR.Utils.AuxiliaryFunctions 类中的 Transform函数将其转换为:

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

让我们尝试使用 AQL 调用我们的 REST API:

成功!

现在再试一个数值比较:

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

转换为:

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

又一次响亮的成功!

结论

正如您从本示例中所见,InterSystems IRIS for Health 为您提供了实施和部署 openEHR 存储库所需的所有工具。如果您有任何疑问,请随时留言!

免责声明

相关代码仅作为概念验证,其目的并非实现具备全部功能的存储库,而是为了证明使用 InterSystems IRIS for Health 完全可以实现这一目标。祝您使用愉快!