简介:面向医疗信息化研究者、系统架构师及卫生信息平台建设者,这份文档以HL7 FHIR框架为核心,针对中国医疗领域数据模型不统一、术语编码各异导致的信息交换难题,提出融合疾病本体(DO)与本体映射技术的解决方案。文章首先梳理了FHIR标准的发展历程及国内外应用现状,随后重点阐述了如何通过本体映射和迁移规则,将异构系统中的疾病术语转化为FHIR标准化格式,实现数据语义的统一与机器可理解。资源为单篇docx格式,压缩包内共1个文件,大小1.12MB,当前已有64人学习。对关注医疗信息标准落地、医院信息系统互操作及电子健康档案共享的读者而言,这份研究可帮助理解FHIR与本体结合的具体路径,并为国内区域卫生信息平台的互联互通建设提供方法论参考。
1. 为什么 FHIR 格式统一了,数据还是换不动
2014 年 ONC 发布十年愿景白皮书之后,医疗信息交换从学术议题变成了国家层面的基础设施问题。国内医院信息化起步不晚,北大人民医院 1996 年就用 HL7 集成了 HIS、PACS、RIS、LIS,但二十多年过去了,真正能说清"跨院区、跨厂商、跨语义层级"的数据交换做到什么程度的医院并不多。原因很直接:HL7 v2 的消息结构过于松散,FHIR(Fast Healthcare Interoperability Resources)虽然提供了规范的 RESTful API 和资源模型,但它只解决了"数据长什么样"的问题,没解决"数据是什么意思"的问题。也就是说,FHIR 定义了 Patient 资源里要有 name、birthdate、gender,但它不会告诉你"PaName"和"Patient.name"是同一个东西。
我们的做法是在 FHIR 标准框架之上引入疾病本体(Disease Ontology, DO)和本体映射技术,把医疗机构数据库里的字段名、术语、编码通过映射和迁移统一到 FHIR 资源模型下,再用 DO 的标准化概念约束疾病术语的表达。这套方案的适用对象很明确:正在做区域卫生信息平台互联互通、电子健康档案共享、或者医共体内部业务协同的技术团队,尤其是那些被"数据模型不开放、无法实现交换共享"卡住的项目。本文从模型设计、映射算法、迁移规则到实验验证,把完整链路拆开讲,代码和参数都是可以直接落地验证的。
2. 把 FHIR Resource 拆成本体:构建思路与 Protégé 实操
2.1 FHIR 资源模型选型:为什么首选 Patient 和 Observation
FHIR 标准由基础文档、开发实现方法和资源列表三部分构成。资源列表按 Foundation、Base、Clinical、Financial、Specialized 五大类组织,每个资源都附带成熟度标记,数字越大表示越稳定,N 表示 Normative 级别。工程实践中没必要把全部资源都建成本体,只挑成熟度最高、跨系统复用最频繁的即可。
以 Patient 为例,这是 Individual 类别下最成熟的资源之一,直接对应医疗信息系统里患者主索引(Master Patient Index, MPI)的语义。Patient 资源的属性在 https://www.hl7.org/fhir/patient.html 有完整定义,核心属性包括 identifier(身份证件号)、name(姓名)、birthdate(出生日期)、gender(性别,枚举值为 male/female/other/unknown)、telecom(联系方式)等。Observation 则是 Clinical 类别下诊断和检验相关的核心资源,用于描述患者的生理指标测量值,如血压、血糖、体温等。Observation 资源存在引用关系:DiagnosticReport 可以引用 Observation,Observation 可以引用 Patient、Specimen、BodyStructure 等资源,形成一个关联网络。
// FHIR Patient 资源 JSON 示例(STU3 版本) { "resourceType": "Patient", "id": "pat001", "identifier": [{ "use": "official", "system": "urn:oid:2.16.860.1.113883.2.18.4.1", "value": "110101199003074531" }], "name": [{ "family": "Zhang", "given": ["Wei"] }], "gender": "male", "birthDate": "1990-03-07", "telecom": [{ "system": "phone", "value": "13800138000", "use": "mobile" }] }参数说明:identifier 里 system 用的是 OID 格式,表示编码体系的命名空间,国内项目可以替换为医院自定义的 OID 根(如 2.16.860 是中国国家 OID 根节点);birthDate 严格遵循 ISO 8601 日期格式,不允许带时间;gender 的枚举值只有四个,自定义扩展需通过 extension 元素实现,不能直接加值。
2.2 医疗领域本体的构建:类目设计与属性定义
领域本体构建常用方法有 7 步法、METHONTOLOGY 法、IDEF5 法等,我们在项目里用的是 7 步法变体,核心流程分六步:明确主题和范围、确定核心概念集、定义概念间关系、本体编码、实例化、逻辑检测与评价。
医疗领域本体定义为 O1,顶层概念按医疗机构的业务域划分,共设六个核心类:
- patient:患者主索引信息,包含 PaId、PaName、PaGender、PaAge、PaTel、PaStatus 等数据属性;
- EMRs:电子病历记录,包含 dateofConsul、PaHistory 等数据属性;
- diagnostic:诊断信息,包含 DiComplaints(主诉)、DiDepartCode(就诊科室编码)、DiDepartName(就诊科室名称)等数据属性;
- Imaging:医学影像信息,包含影像序列、检查类型等属性;
- medication:用药信息,包含药品编码、剂量、频次等属性;
- financial:费用结算信息,包含支付方式、费用明细等属性。
对象属性描述类与类之间的语义关系,例如 patient 与 EMRs 之间定义 hasEMRs,diagnostic 与 patient 之间定义 has_record,patient 与 Imaging 之间定义 hasInspection。数据属性则绑定到具体类上,描述实例的字符值特征。属性定义完成后,用 Protégé 5.5.0 构建,表示语言用 OWL,本体文件以 RDF/XML 格式导出。
<!-- 医疗领域本体中 patient 类的数据属性定义(OWL 片段) --> <owl:DatatypeProperty rdf:about="http://www.medical-onto.org#PaName"> <rdfs:domain rdf:resource="http://www.medical-onto.org#patient"/> <rdfs:range rdf:resource="http://www.w3.org/2001/XMLSchema#string"/> </owl:DatatypeProperty> <owl:ObjectProperty rdf:about="http://www.medical-onto.org#hasEMRs"> <rdfs:domain rdf:resource="http://www.medical-onto.org#patient"/> <rdfs:range rdf:resource="http://www.medical-onto.org#EMRs"/> </owl:ObjectProperty>2.3 资源之间的引用关系建模:从 UML 类图到 OWL 对象属性
FHIR Resource 之间存在大量引用关系,比如 DiagnosticReport 引用 Observation,Observation 引用 Patient 和 Specimen,ServiceRequest 引用 DiagnosticReport。在 OWL 本体内,这些引用关系不是简单的属性赋值,而是通过对象属性的 domain 和 range 约束来表达。
在 Protégé 中建模对象属性时,建议遵循一个命名约定:hasReference 表示资源的直接引用,hasSubClass 表示类目从属关系,hasRecord 表示病历记录归属。以 Observation 资源为例,它引用 Patient 的方式是通过 subject 元素实现的,FHIR 官方定义 subject 的类型为 Patient、Group、Device 等可替换类型。在本体建模中,我们将 subject 定义为对象属性,domain 设为 Observation,range 设为 Patient。图 6 中的引用关系包含 Observation、Media、DiagnosticReport、Specimen、ImagingStudy、BodyStructure、ServiceRequest 和 MolecularSequence 共八个资源,这些引用关系在原论文实验环节全部转化为 OWL 对象属性,供后续映射使用。
实际建模时容易踩坑的地方是属性的传递性。FHIR 中资源间的引用是单向的,但 OWL 对象属性可以设置传递性(transitive),如果贸然将 hasSubClass 设为传递属性,会导致推理时出现意外的类目层级合并。经验做法是:hasReference 不设传递性,hasSubClass 可以设传递性,但要用 DisjointClasses 约束避免类目冲突。建模完成之后,用 Protégé 自带的 HermiT 推理机做一致性检测,检查是否有概念矛盾或属性冲突,这一步在 Protégé 的 Reasoner 菜单下即可完成。
3. 本体映射与迁移:从 PaName 到 Patient.name 的完整链路
3.1 映射的形式化定义:概念、属性、实例三层对齐
本体映射的目标是发现两个本体实体之间的语义对应关系。将医疗领域本体记为 O1,FHIR Resource 本体记为 O2,形式化定义如下:
O = {C, P, Hc, Hp}
其中 C 为概念集合,P 为属性集合,Hc 为概念间的层次化语义关系,Hp 为属性间的层次化语义关系。映射结果 A 是所有对齐集合的并集,具体的数学表达参见原论文公式(1)。
从工程视角看,更关心的问题是映射怎么落地。论文采用的映射策略分成两步:第一步映射抽取,第二步映射筛选。映射抽取阶段,由于医疗领域本体和 FHIR 本体都是人工策划的(curated ontology),可直接使用自动匹配器 YAM++ 或 LogMap 生成初始候选集。这一步要求两个本体的 OWL 文件格式一致,建议统一用 RDF/XML 格式,避免因序列化格式差异导致匹配器解析失败。
映射筛选阶段采用递归方法:如果一个映射的目标概念和候选集中已有映射的目标概念相同,则该映射也加入候选集。这个策略在实际运行中有个明显的好处,能处理间接关联的概念对。比如"患者主索引"和"Patient.identifier"不是直接映射,但"患者主索引"映射到"identifier","identifier"又映射到"Patient.identifier",通过递归可以完成间接映射的发现。
3.2 相似度计算:余弦相似度的 Java 实现与参数调优
原论文选用余弦相似度计算概念和属性的相似性。核心想法是:将两个实体的名称和属性描述向量化,计算向量夹角的余弦值,值越接近 1 表示相似度越高。对于短文本(如"PaName" vs "Patient.name"),需要先做分词和规范化处理。我们实现的策略是:将实体名拆成 token 序列,分别计算字面相似度和语义相似度,然后加权求和作为最终得分。
import java.util.HashMap; import java.util.Map; public class CosineSimilarity { /** * 计算两个实体名的余弦相似度 * 将字符串拆分为字符级 token * 使用字符二元组(bigram)作为特征项 * @param s1 医疗领域本体实体名,如 "PaName" * @param s2 FHIR 资源属性名,如 "Patient.name" * @return 相似度得分,范围 0.0 ~ 1.0 */ public static double compute(String s1, String s2) { if (s1 == null || s2 == null || s1.isEmpty() || s2.isEmpty()) return 0.0; // 归一化:统一转小写,去除空格和下划线 s1 = s1.toLowerCase().replaceAll("[\\s_]", ""); s2 = s2.toLowerCase().replaceAll("[\\s_]", ""); if (s1.equals(s2)) return 1.0; Map<String, Integer> vec1 = tokenize(s1); Map<String, Integer> vec2 = tokenize(s2); double dotProduct = 0.0; for (String key : vec1.keySet()) { if (vec2.containsKey(key)) { dotProduct += vec1.get(key) * vec2.get(key); } } double norm1 = 0.0; for (int value : vec1.values()) norm1 += value * value; double norm2 = 0.0; for (int value : vec2.values()) norm2 += value * value; if (norm1 == 0.0 || norm2 == 0.0) return 0.0; return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2)); } /** * 将字符串拆分为二元组特征 * 示例:PaName -> ["pa", "an", "na", "am", "me"] */ private static Map<String, Integer> tokenize(String text) { Map<String, Integer> vector = new HashMap<>(); for (int i = 0; i < text.length() - 1; i++) { String token = text.substring(i, i + 2); vector.put(token, vector.getOrDefault(token, 0) + 1); } return vector; } }特征选择说明:字符二元组(bigram)是处理医疗术语短文本的常用方式,比单字符特征更能捕捉词干信息,又比词级特征更抗拼写变体。实验结果表明,"PaName"和"Patient.name"的相似度为 0.63,"PaId"和"Patient.identifier"为 0.80,"ObCategory"和"Observation.category"为 0.81。如果不满意,可以做仿射变换或引入同义词词典。
阈值设定方面,0.8 以上直接判定为高置信度映射,0.5~0.8 之间需要人工确认,0.5 以下的映射丢弃。这个阈值体系在我们实测中可控的,不需要额外标注数据来调参。
3.3 迁移规则的工程落地:规则 1 与规则 2 的判定逻辑
映射完成之后进入迁移环节。迁移的核心思想是:根据映射候选集,把 O1 的实例数据填充到 O2 的数据属性下。原论文定义了两条迁移规则,这里展开讲清楚实现细节。
规则 1 处理的是概念映射对存在且匹配对象为下位类的情形。例如,FHIR 中 Patient 类的层级是 Resource → DomainResource → Patient,而医疗领域本体中 patient 类为一级类目,当二者相似度最高时,保持 FHIR 原有类目层级不变,将 O1 中 patient 类下的 PaName 实例迁移到 FHIR 的 Patient.name 数据属性下。翻译成实际操作步骤如下:
# 迁移规则 1 的伪代码实现 for concept_mapping in filtered_mapping_set: if concept_mapping.similarity_score < 0.6: continue # 相似度过低,不做迁移 if concept_mapping.source_concept == "patient" and concept_mapping.target_concept == "Patient": # 遍历 O1 中 patient 类的全部实例 for instance in source_ontology.instances("patient"): source_value = instance.get_data_property("PaName") target_property = "Patient.name" # 实例化 FHIR Patient 资源 fhir_patient = create_fhir_resource("Patient") fhir_patient.set_property(target_property, source_value)规则 2 处理的是部分属性匹配失败的场景。当概念映射成功,但概念下个别数据属性无法在 FHIR 中找到对应属性时,FHIR 实例需要添加空数据项占位,保证资源结构的完整性;如果数据类型完全匹配,则不需要变换数据项名称,直接迁移即可。医疗领域本体中的 PaGender 与 FHIR 中的 Patient.gender 语义接近但取值逻辑不同:PaGender 是布尔类型(true 表示男,false 表示女),而 FHIR 要求枚举值 male/female。这种情况下不能直接拷贝数据,需要增加一层枚举值映射逻辑:
gender_value_map = { "true": "male", "false": "female" } fhir_patient.set_property("Patient.gender", gender_value_map[str(instance.get_data_property("PaGender"))])这个细节经常被忽略。很多团队在做数据迁移时只做了字段名映射,忽略了取值域的语义转换,导致 FHIR 资源生成后校验失败。
4. DO 疾病本体:从中文诊断到 SNOMED CT 标准编码
4.1 为什么选择了 DO:跨术语表的枢纽地位
疾病术语标准化是信息交换深水区,一个诊断结论在 HIS 里可能记作"心肌梗死",在 LIS 里写成"急性心肌梗死",在病案首页里编码为 I21.9,这三个说法对不上。DO 的优势在于它的类目结构不吃"只认一套标准"的亏,DO 的每个疾病概念都带有多重交叉引用(Xrefs),包括 MeSH、ICD-10CM、SNOMED CT、UMLS 等,天然适合作为中间对齐的枢纽。
把迁移后的 FHIR 资源实例和 DO 疾病概念连接,采用的映射方法是 owl:equivalentClass 和 owl:sameAs。映射集合的格式为(患者 ID,诊断结论,疾病),即通过患者 ID 关联诊断结论,再将诊断结论映射到 DO 的标准化疾病编码。以心脏动脉瘤为例,DO 的标准化编码为 DOID:13921,对应 SNOMED CT 编码 65340007,这个编码在不同国家、不同系统之间是一致的。
4.2 中文诊断术语到 DO 编码的映射过程
把中文疾病术语映射到 DO 概念,不能做整串字符串匹配,要先做分词和术语规范化。例如,诊断结论"心脏动脉瘤"分词后得到"心脏"和"动脉瘤",先匹配 DO 概念名中的 "heart aneurysm",找不到时使用 DO 的同义词列表做扩展匹配,DO 中 heart aneurysm 的 synonyms 包括 cardiac aneurysm、 aneurysm of heart 等。再通过 DO 的 Xrefs 交叉引用获取 SNOMED CT 编码。
查询 DO 数据库使用 SQL,我们基于 doid.obo 文件解析成 MySQL 表结构,核心表字段包括:doid_id、name、definition、synonyms、xrefs、parent_doid、subset。以下是将中文术语映射到 DO 编码的查询逻辑:
-- 查询 DO 概念及其 SNOMED CT 交叉引用编码 SELECT d.doid_id, d.name AS do_name, d.definition, x.xref_code AS snomed_ct_code FROM do_terms d LEFT JOIN do_xrefs x ON d.doid_id = x.doid_id WHERE d.name LIKE '%aneurysm%' AND d.name LIKE '%heart%' AND x.xref_source = 'SNOMEDCT_US';参数说明:do_xrefs 表存储 DO 概念的外部交叉引用,xref_source 字段标识编码来源(SNOMEDCT_US 表示美国 SNOMED CT 子集);如果用 ICD-10CM 编码做匹配,将 xref_source 改为 ICD10CM 即可;同时要注意 xref_code 和主编码的关系,一个 DOID 可能对应多个 SNOMED CT 编码,需要根据语义精确度选择最合适的一个。
这套查询逻辑在真实项目中运行稳定,但是有个前提,依赖原始诊断术语的质量。如果 HIS 系统里面记录的是"冠心病"而不是"冠状动脉粥样硬化性心脏病",分词和匹配都会出问题。解决的办法是建立院内诊断术语表,把医生常用的口语化诊断和标准术语做映射,存储为一张术语对照表,后续匹配先查这张表再做 DO 映射。
4.3 编码结果的存储与交换格式
完成 DO 映射后,把标准化编码写回 FHIR 资源中,在 Condition 资源或 Observation 资源里通过 code 元素携带。FHIR 的 CodeableConcept 数据类型提供了 coding 数组,可以同时携带多套编码体系,这是它在语义层面优于 V2 消息的重要原因:
{ "resourceType": "Condition", "subject": { "reference": "Patient/pat001" }, "code": { "coding": [ { "system": "http://purl.obolibrary.org/obo/doid.owl", "code": "DOID:13921", "display": "heart aneurysm" }, { "system": "http://snomed.info/sct", "code": "65340007", "display": "Aneurysm of heart" } ], "text": "心脏动脉瘤" } }原论文的存储方案是通过 Java 编程将迁移和编码后的数据存入 MySQL 数据库,建表逻辑和上述相似。如果做的是 SaaS 平台,还可以考虑把 FHIR 数据转存到 MongoDB,用文档模型存储嵌套的 coding 数组,查询时用聚合管道展开 coding 字段做检索。但对中小规模的区域卫生平台,MySQL 足够,不需要引入额外的文档数据库依赖。
5. 实验验证与工程化踩坑清单
5.1 相似度计算实验:观察阈值变化对映射质量的影响
实验环境:MAC OS 操作系统,MyEclipse 平台,Java 语言。样本集来自"医享网"公开病例,医疗领域本体部分数据属性共 40 余个。相似度计算结果中,Observation 概念的相似度达 0.98,Practitioner 为 0.92,Entities 和 EMRs 的相似度低至 0.18,说明即使共用一个字母前缀,语义完全无关的术语也能被排除。以下是实际运行结果中需要人工干预的部分:
| FHIR 概念 | 最优匹配概念 | 概念相似度 | FHIR 属性 | 最优匹配属性 | 属性相似度 | 建议操作 |
|---|---|---|---|---|---|---|
| Patient | patient | 0.98 | Patient.identifier | PaId | 0.80 | 自动迁移 |
| Practitioner | practitioner | 0.92 | Patient.gender | PaGender | 0.77 | 枚举值转换 |
| Observation | observation | 0.98 | Patient.name | PaName | 0.63 | 人工确认 |
| MedicationRequest | medication | 0.88 | Patient.birthdate | PaAge | 0.38 | 需要业务映射 |
| Specimen | BloodSample | 0.41 | Patient.telecom | PaTel | 0.51 | 重新评估 |
注意 Patient.birthdate 和 PaAge 相似度只有 0.38,原因在于 birthdate 是绝对日期,PaAge 是年龄值,语义完全不同,不能通过映射自动转换。实际处理方案是:PaAge 保留在扩展字段,Patient.birthdate 从 EMRs 中的 dateofConsul 或身份证号推算,如果身份证号可用则直接解析出生日期,解析代码使用正则截取第 7 到 14 位,再做格式校验。
5.2 常见问题:Protégé 推理卡死、概念漂移与同义词扩展
Protégé 中加载大型本体时,HermiT 推理机可能长时间无响应。经验做法是:调大 JVM 堆内存,在启动参数中加 -Xmx4g;关闭不需要的推理任务,只保留一致性检测;用 ELK 推理机替代 HermiT,ELK 专注于 EL 配置文件,速度比 HermiT 快一个数量级。
概念漂移是另一个高频问题:同一诊断术语在不同科室含义不同,映射结果需要按科室维度做修正。推荐做法是建立科室术语映射表,结构为(科室编码,来源术语,目标 DOID,映射置信度),查询优先级为科室级别高于全院级别。同义词扩展方面,DO 本体自带 synonyms 列表,匹配时建议做小写归一化和全半角转换,中英文分号、逗号的差异容易导致匹配失败。
5.3 从论文到生产:FHIR 版本差异与 API 设计
论文实验基于 FHIR STU3 版本,STU3 与 R4 在资源定义上存在不兼容变更,最典型的是 Observation 资源的 category 元素从 CodeableConcept 改成了可复用的 CodeableConcept 列表,Patient 资源的 gender 枚举在 R4 中增加了 additional 取值。构建本体时以哪一版为准,直接决定后续映射和迁移的结果。建议以 R4 为基准,因为 R4 是当前 FHIR 的正式发布版本,STU3 的兼容性可以通过 FHIR 官方的版本转换工具处理。
API 设计推荐使用 HAPI FHIR 开源库,它提供了完整的 R4 资源模型和 RESTful 服务端实现,将 MySQL 作为持久化存储,HAPI 的 JPA 模块支持直接把 FHIR 资源映射到关系表。关键配置是 validation-mode 设置为 STRICT,这能保证写入的资源严格符合 FHIR 规范,避免脏数据进入交换库。如果希望在 FHIR 资源和 DO 编码之间的联合查询做到性能可控,建议在 MySQL 中建一张扁平化索引表,字段包括 resource_type、resource_id、code_system、code_value、display_name,为 code_value 加普通索引,查询时直接走索引避免全表扫描。
在独立使用 FHIR 标准与结合本体的方案之间,后者的优势在于让映射工作可重复、可审计。映射规则和迁移规则是显式定义的,不需要依赖某个开发人员对业务的个人理解,这对于医院信息科的人员流动和项目交接尤其重要。在实际项目中,建议先选择 2~3 个高频互操作场景(如患者主索引同步、检验报告共享、诊断术语归一化)做试点,跑通后再扩展到更多资源类型,避免一上来就做全量映射大水漫灌。
本文还有配套的精品资源,点击获取