# 一起 on FHIR! ## 前言:為什麼要介接FHIR? * ~~公司走向國際~~,可以拿來宣傳 * 衛福部推動[電子病歷](https://emr.mohw.gov.tw/myemr/fhir.html) * 未來與其他FHIR系統介接時,更為方便 ## FHIR的文件精選 > [官方文件](http://hl7.org/fhir/R4B/index.html) * 文件怎麼讀Owen講過了,沒看過的去看:[用FHIR開發App:基礎與實作介紹 ](https://dev-internal.jubo.health/docs/blog/FHIR/) 以下會省略Owen之前講過的部分,進到若是要介接FHIR時的注意事項~~血淚史~~ 在介接之前,還是建議閱讀以下✨✨✨精挑細選✨✨✨出來的的FHIR官方文章 ~~如果你認為你工作上完全不會碰到,或是不想睡著的話,那可以先左轉離開ㄌ~~ ### ✨✨✨精挑細選✨✨✨:了解FHIR API的設計 * [HTTP](http://hl7.org/fhir/R4B/http.html): 非常詳細的講解了 FHIR Restful API的設計 * [Search](http://hl7.org/fhir/R4B/search.html):列出所有可以拿來 search FHIR 資源的 parameters ## 一步一步 toward to FHIR 以下會用實際範例告訴你怎麼去從頭開始介接FHIR ![](https://hackmd.io/_uploads/SkC9pDPp3.png) 以設計稿上的實際案例來看,如上圖,介接SpO2(%)/Flow(L/min) 血氧含量、供氧。 要先判斷這要放到什麼樣的FHIR Resource,到官方看了一下眼花撩亂的首頁 ![](https://hackmd.io/_uploads/SJIkldPTh.png) 看起來似乎是 **Diagnostics** 的類別下比較適合,再一個個點進去看,首先看 [**Observation**](http://hl7.org/fhir/R4B/observation.html) ![](https://hackmd.io/_uploads/S1AXlOwp2.png) > Measurements and simple assertions made about a patient, device or other subject 很幸運一次就找到似乎可以用的Resource了 ![](https://hackmd.io/_uploads/rJzgW_Da3.png) 往下滑,看到下面的Scope and Usage的舉例更加確定他可以使用在Vital signs等量測項目上 但好像沒看到血氧欸,請教Google大神,找到[美國版的IG](https://build.fhir.org/ig/HL7/US-Core/Observation-satO2-fiO2.json.html)也是把他塞到**Observation**裡面,那似乎是可以隨便塞了 ![](https://hackmd.io/_uploads/BJNRbODTh.png) 接下來很簡單,照抄就行了...嗎? 以下是美國IG的列的範例的全貌 ```JSON { "resourceType" : "Observation", "id" : "satO2-fiO2", "meta" : { "extension" : [ { "url" : "http://hl7.org/fhir/StructureDefinition/instance-name", "valueString" : "Observation SatO2 FiO2 Example" }, { "url" : "http://hl7.org/fhir/StructureDefinition/instance-description", "valueMarkdown" : "This is a observation satO2 fiO2 example for the *US Core Pulse Oximetry Profile* representing a spO2 value with a for a patient on 6 l/min of O2 suppplemental oxygen." } ], "profile" : [ "http://hl7.org/fhir/us/core/StructureDefinition/us-core-pulse-oximetry" ] }, "text" : { "status" : "generated", "div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p><b>Generated Narrative: Observation</b><a name=\"satO2-fiO2\"> </a></p><div style=\"display: inline-block; background-color: #d9e0e7; padding: 6px; margin: 4px; border: 1px solid #8da1b4; border-radius: 5px; line-height: 60%\"><p style=\"margin-bottom: 0px\">Resource Observation &quot;satO2-fiO2&quot; </p><p style=\"margin-bottom: 0px\">Profile: <a href=\"StructureDefinition-us-core-pulse-oximetry.html\">US Core Pulse Oximetry Profile</a></p></div><p><b>identifier</b>: id:\u00a0o1223435-10</p><p><b>status</b>: final</p><p><b>category</b>: Vital Signs <span style=\"background: LightGoldenRodYellow; margin: 4px; border: 1px solid khaki\"> (<a href=\"http://terminology.hl7.org/5.0.0/CodeSystem-observation-category.html\">Observation Category Codes</a>#vital-signs)</span></p><p><b>code</b>: Oxygen saturation in Arterial blood <span style=\"background: LightGoldenRodYellow; margin: 4px; border: 1px solid khaki\"> (<a href=\"https://loinc.org/\">LOINC</a>#2708-6; <a href=\"https://loinc.org/\">LOINC</a>#59408-5 &quot;Oxygen saturation in Arterial blood by Pulse oximetry&quot;; <a href=\"http://terminology.hl7.org/5.0.0/CodeSystem-v3-mdc.html\">ISO 11073-10101 Health informatics - Point-of-care</a>#150456 &quot;MDC_PULS_OXIM_SAT_O2&quot;)</span></p><p><b>subject</b>: <a href=\"Patient-example.html\">Patient/example</a> &quot; SHAW&quot;</p><p><b>effective</b>: 2014-12-05 09:30:10+0100</p><p><b>value</b>: 95 %<span style=\"background: LightGoldenRodYellow\"> (Details: UCUM code % = '%')</span></p><p><b>interpretation</b>: Normal (applies to non-numeric results) <span style=\"background: LightGoldenRodYellow; margin: 4px; border: 1px solid khaki\"> (<a href=\"http://terminology.hl7.org/5.0.0/CodeSystem-v3-ObservationInterpretation.html\">ObservationInterpretation</a>#N &quot;Normal&quot;)</span></p><p><b>device</b>: <span>: Acme Pulse Oximeter 2000</span></p><h3>ReferenceRanges</h3><table class=\"grid\"><tr><td style=\"display: none\">-</td><td><b>Low</b></td><td><b>High</b></td></tr><tr><td style=\"display: none\">*</td><td>90 %<span style=\"background: LightGoldenRodYellow\"> (Details: UCUM code % = '%')</span></td><td>99 %<span style=\"background: LightGoldenRodYellow\"> (Details: UCUM code % = '%')</span></td></tr></table><h3>Components</h3><table class=\"grid\"><tr><td style=\"display: none\">-</td><td><b>Code</b></td><td><b>Value[x]</b></td></tr><tr><td style=\"display: none\">*</td><td>Inhaled oxygen flow rate <span style=\"background: LightGoldenRodYellow; margin: 4px; border: 1px solid khaki\"> (<a href=\"https://loinc.org/\">LOINC</a>#3151-8)</span></td><td>6 liters/min<span style=\"background: LightGoldenRodYellow\"> (Details: UCUM code L/min = 'L/min')</span></td></tr></table></div>" }, "identifier" : [ { "system" : "http://goodcare.org/observation/id", "value" : "o1223435-10" } ], "status" : "final", "category" : [ { "coding" : [ { "system" : "http://terminology.hl7.org/CodeSystem/observation-category", "code" : "vital-signs", "display" : "Vital Signs" } ], "text" : "Vital Signs" } ], "code" : { "coding" : [ { "system" : "http://loinc.org", "code" : "2708-6", "display" : "Oxygen saturation in Arterial blood" }, { "system" : "http://loinc.org", "code" : "59408-5", "display" : "Oxygen saturation in Arterial blood by Pulse oximetry" }, { "system" : "urn:iso:std:iso:11073:10101", "code" : "150456", "display" : "MDC_PULS_OXIM_SAT_O2" } ] }, "subject" : { "reference" : "Patient/example" }, "effectiveDateTime" : "2014-12-05T09:30:10+01:00", "valueQuantity" : { "value" : 95, "unit" : "%", "system" : "http://unitsofmeasure.org", "code" : "%" }, "interpretation" : [ { "coding" : [ { "system" : "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation", "code" : "N", "display" : "Normal" } ], "text" : "Normal (applies to non-numeric results)" } ], "device" : { "display" : "Acme Pulse Oximeter 2000" }, "referenceRange" : [ { "low" : { "value" : 90, "unit" : "%", "system" : "http://unitsofmeasure.org", "code" : "%" }, "high" : { "value" : 99, "unit" : "%", "system" : "http://unitsofmeasure.org", "code" : "%" } } ], "component" : [ { "code" : { "coding" : [ { "system" : "http://loinc.org", "code" : "3151-8", "display" : "Inhaled oxygen flow rate" } ], "text" : "Inhaled oxygen flow rate" }, "valueQuantity" : { "value" : 6, "unit" : "liters/min", "system" : "http://unitsofmeasure.org", "code" : "L/min" } } ] } ``` 看完馬上想:啊設計稿不是才兩個的數值,一個字串,怎麼欄位多到靠杯 ![設計稿](https://hackmd.io/_uploads/SkC9pDPp3.png) 這邊為大家整理一下重點: ### 必填有哪些欄位? * status * code ### [status](http://hl7.org/fhir/R4B/observation-definitions.html#Observation.status): 紀錄的狀態 因為紀錄可能不是最終版本,可以看到文件上有規範要使用 [**ObservationStatus**](http://hl7.org/fhir/R4B/valueset-observation-status.html)的值 ![](https://hackmd.io/_uploads/r1--Iuv6n.png) 點**ObservationStatus**進去後可以直接看各個值的定義 ![](https://hackmd.io/_uploads/HkDo8uvT3.png) 思考一下我們的使用情境,似乎是 **final** 就可以滿足了,因為表單輸入值量測後就是完成的狀態了 ### code: 描述被觀察了什麼 ![](https://hackmd.io/_uploads/ry9dDdwTn.png) 是 [**CodeableConcept**](http://hl7.org/fhir/R4B/datatypes.html#CodeableConcept) 的結構,文件上有建議可以使用 [LOINC Codes](http://hl7.org/fhir/R4B/valueset-observation-codes.html) ![CodeableConcept](https://hackmd.io/_uploads/H141_dvan.png) ![Coding](https://hackmd.io/_uploads/HySgtuD62.png) 想到剛剛美國IG上的範例: ```json "code" : { "coding" : [ { "system" : "http://loinc.org", // 定義醫療專業術語的系統 "code" : "2708-6", // 系統上的定義碼 "display" : "Oxygen saturation in Arterial blood" // 系統定義的表示形式 }, { "system" : "http://loinc.org", "code" : "59408-5", "display" : "Oxygen saturation in Arterial blood by Pulse oximetry" }, { "system" : "urn:iso:std:iso:11073:10101", "code" : "150456", "display" : "MDC_PULS_OXIM_SAT_O2" } ] } ``` 這邊目前無法確定是否能照用,所以先填一樣的,畢竟我不是醫療專業人員,怕用錯的話,需要整理所有的欄位對應表格,並與FHIR專家諮詢確認使用情境,錯的話再改就行了 那到底要怎麼讀這些系統上的定義碼詳細的內容呢? 其實只要把網址組起來就好了:http://loinc.org/2708-6 ![](https://hackmd.io/_uploads/ry5ZndPT3.png) 可以看到網站列出了詳細的醫療術語定義,這就是為什麼FHIR可以作為一個公定的醫療資訊交換規範了,因為只需要知道code,與定義的系統網址,就可以直接查詢到結果,而不會有要交換資料的雙方,對於定義不一致的問題。 以下是目前我介接時常用的兩個網站,都可以註冊帳號或直接免費使用: * LONIC * SNOMED #### [LONIC search](https://loinc.org/search/) 註冊免費帳號,可以進 [LONIC search](https://loinc.org/search) 查詢專業術語,這邊查詢的是 [oxygen saturation](https://loinc.org/search/?t=1&s=oxygen+saturation) ![](https://hackmd.io/_uploads/BkTIAOPph.png) 列表上列出了一大堆 ![](https://hackmd.io/_uploads/BJN3CODan.png) LONIC 上只要有列出 `LHC-Forms` 的tag ![](https://hackmd.io/_uploads/BkBWJFD6n.png) 都可以進入[form頁面](https://forms.loinc.org/43146-0)去確認欄位到底可以填什麼 ![](https://hackmd.io/_uploads/rkQYyKPan.png) #### [SNOMED](https://browser.ihtsdotools.org/?perspective=full&conceptId1=74262004&edition=MAIN/2023-07-31&release=&languages=en&latestRedirect=false) 通常在 LONIC 查不到的術語,可以到[SNOMED](http://hl7.org/fhir/R4B/terminologies-systems.html)找 > 如果以上兩個網站都沒有的話,hl7官方也有很好心的[列出其他可以查詢的系統](http://hl7.org/fhir/R4B/terminologies-systems.html) 以上終於把這筆關於血氧、供氧要填入什麼到 Observation 的兩個欄位搞清楚了 接著是對於資料關聯的必要資訊: * subject:誰的量測資料是使用 * performer:誰執行量測 #### subject: 量測的目標 因為我們目前的介面是針對患者的量測,因此我們會關聯到 [Patient](http://hl7.org/fhir/R4B/patient.html) ![](https://hackmd.io/_uploads/r1m1mKvp2.png) JSON範例如下: ```json "subject" : { "reference" : "Patient/id-on-fhir-system" // 格式一定要填 ResourceType/id }, ``` #### performer: 由誰量測 量測者則是使用 [Practitioner(從業者)](http://hl7.org/fhir/R4B/practitioner.html) ![](https://hackmd.io/_uploads/BJjSNtv6h.png) ```json "performer" : [{ "reference" : "Practitioner/id-on-fhir-system" // 格式一定要填 ResourceType/id }], ``` 接著是量測相關的資料 * effectiveDateTime: 量測時間 * valueQuantity:量測數值 * component:組合結果 * method: 量測方式 #### effective[x]: 臨床進行觀察的相關時間/時間週期 ![](https://hackmd.io/_uploads/SJfyBFPpn.png) 根據需求,可以選擇自己需要的格式,我們這邊是使用 effectiveDateTime ```json "effectiveDateTime" : "2014-12-05T09:30:10+01:00", ``` 而針對不同的時間規範,官方文件有列出相對應的[格式驗證準則](http://hl7.org/fhir/R4B/datatypes.html#dateTime) #### value[x]: 實際的結果 ![](https://hackmd.io/_uploads/S1QPUtvph.png) 同樣也有不同格式,為了不同的量測資料的相容性,選擇 value[Quantity](http://hl7.org/fhir/R4B/datatypes.html#Quantity) ```json "valueQuantity" : { "value" : 95, "unit" : "%", "system" : "http://unitsofmeasure.org", "code" : "%" }, ``` ![Quantity](https://hackmd.io/_uploads/BJpoCo6Th.png) 這邊又看到了裡面有 `system`、 `unit`、`code`要填寫,就會知道要去 http://unitsofmeasure.org 瞭解了,查詢的頁面是在 https://ucum.org/ucum ![ucum.org](https://hackmd.io/_uploads/HkqPnsap2.png) #### component: 組合結果 ![](https://hackmd.io/_uploads/HJDxFFvah.png) 可以放多個量測結果的欄位,範例中放了供氧的資訊 ```json "component" : [ { "code" : { "coding" : [ { "system" : "http://loinc.org", "code" : "3151-8", "display" : "Inhaled oxygen flow rate" } ], "text" : "Inhaled oxygen flow rate" }, "valueQuantity" : { "value" : 6, "unit" : "liters/min", "system" : "http://unitsofmeasure.org", "code" : "L/min" } } ] ``` #### method: 量測方式 ![](https://hackmd.io/_uploads/rJn5qtvp2.png) [官方文件](http://hl7.org/fhir/R4B/valueset-observation-methods.html#definition)推薦在 Snomed 查詢 ![snomed room air](https://hackmd.io/_uploads/r1z5ynT63.png) ```json "method": { "coding": [ { "system": "http://snomed.info/sct", "code": "722742002", "display": "Breathing room air" //供氧方式為空氣 } ] }, ``` 講到這邊大家看得快睡著,但真的把這一小個欄位的血氧、供氧的量測資料介接FHIR講完了 ![](https://hackmd.io/_uploads/SkC9pDPp3.png) 但我們設計稿其實長這樣,按照上面講的方法去對就好了 ![](https://hackmd.io/_uploads/Hyhg3Kwph.png) ![](https://hackmd.io/_uploads/ByMfhKwa2.png) 你以為已經會介接FHIR了,會這麼順利嗎?我還是有遇到一些介接上的問題 ## 遇到的問題 以下會ㄧㄧ提出與解決我當初遇到的所有問題 ### 問題1: Google FHIR 的支援度 * 不是所有官方文件上列出來的,都在 Google FHIR store都有支援 * 官方文件 * [search parameter](http://hl7.org/fhir/R4B/search.html) * [Observation serach parameters](http://hl7.org/fhir/R4B/observation.html#search) * Google 文件 * [Searching for FHIR resources](https://cloud.google.com/healthcare-api/docs/how-tos/fhir-search) * [Advanced FHIR search features](https://cloud.google.com/healthcare-api/docs/how-tos/fhir-advanced-search) * 如果要知道Google healthcare FHIR store有支援哪些,最準的還是去用 [FHIR viewer](https://cloud.google.com/healthcare-api/docs/how-tos/fhir-search#using_the_fhir_viewer) 去點點看作查詢 * 因為支援度不是那麼完全,所以在儲存FHIR資源時,都要先思考好要怎麼取資料出來,才不會導致介接好但是沒有可以用來取資料的search parameter ![Googoe FHIR query builder](https://hackmd.io/_uploads/ByapEjS6h.png) 甚至還有search templates可以儲存使用 ![](https://hackmd.io/_uploads/H1UBb1ta3.png) 順帶列出可以用哪些花式操作: #### 範例一 `Observation?_include=%2A&performer=97ba9869-a80b-47f5-bb4d-6b241e0fddc5&code:text=vitalSigns&date=ge2023-06-01T16:00:00Z&date=le2023-06-14T16:00:00Z` #### `_include:%2A`:取得指定的關聯資料 * 如 performer(Practitioner), subject(Patient),看你的資料有根哪些資源關聯 * %2A: ASCII Encoding Reference的`*`,代表在該筆資料內所有的references都會被取出 * 使用時要注意,有可能會因為reference的資料太大,而導致timeout * 因此怕取出資料量太多的話,可以指定要`_include`的欄位,例如:`_include=DocumentReference:author` > 有另一個參數是 `_revinclude`,可以取出所有引用用該筆資源的資料,不建議使用,有限制他可以找到至多100個資源 #### `subject:id-of-subject`: 搜尋資料紀錄的對象 * 直接給定id就好 #### `performer:id-of-performer1`:搜尋紀錄資料執行者 * 同上 #### `code:text` * 搜尋在 `code` 裡面的 `text` #### `date=ge2023-06-01T16:00:00Z&date=le2023-06-14T16:00:00Z`: 時間區間 * 在observation內是`effective[x]` * 可以加上`ge`,`le`去搜尋時間區間內的資料 建議所有操作都先在 Google FHIR viewer上先操作過,確定取得的結果真的是自己需要的資料 ### 問題2: 設計稿上定義的其他部位要放哪!? 看完了上面的FHIR介接,應該會了解到,醫療術語上,只有明確的定義去講每個專有名詞,並沒有所謂的「其他」 但設計稿上,出現了「其他」部位,要怎麼辦? ![](https://hackmd.io/_uploads/rkkQaJYTh.png) ![](https://hackmd.io/_uploads/rJyN61Yah.png) 怎麼找,都在術語定義網站LONIC,SNOMED上找不到相關的code 當然r,因為哪有一個身體部位叫「其他」 因為FHIR的介接規範,只需要列好指定的system以及code,因此我們可以定義好jubo自己的靜態網站(例如[台灣IG](https://twcore.mohw.gov.tw/ig/twcore/CodeSystem-medication-path-tw.html),去放這些定義 ```go BodySite = &fhir.CodeableConcept{ Coding: []fhir.Coding{ { System: fhirenum.System_CustomJuboHealth.String(), Code: "other-body-site", // TODO: not yet defined the code system Display: "Other Position", }, }, Text: pointer.TypePointer(fhirenum.BodyPosition_Other.String()), } ``` ## 結語 * 雖然介接FHIR比起一般API介接較複雜,但了解去哪裡找定義後,就可以駕輕就熟,~~或是由jubo link統一跟FHIR service介接~~ * **蛤!?既然你都說比較複雜,那還用FHIR幹嘛?** * Google 官方有寫 [Importance of FHIR in healthcare information technology](https://cloud.google.com/healthcare-api/docs/concepts/fhir#importance_of_fhir_in_healthcare_information_technology) ![](https://hackmd.io/_uploads/Sy_rPt66h.png) > 簡而言之:FHIR 的目的時期是為了讓醫療系統之間的資訊交換更為容易,而不會有「你說你的我說我的」,因為定義都統一寫好了 * 多寫測試 * 省下未來的自己去修bug的時間 * 省下PM、QA、同事找你的時間 * 省下溝通成本 * 有機會重來的話:或許可以試試看grpc? * 優點 * Service內部溝通不需要另外寫struct去parse JSON * Google 有 package ([googl/fhir](https://github.com/google/fhir)) * 缺點 * proto buff 定義的 Ineterface 跟FHIR文件不太一樣