在上一篇文章中,我們認識了FHIR Patient的搜尋方式。
當Client知道Patient的Resource id時,可以使用read:
GET /Patient/patient-001
成功時通常會直接收到一筆Patient Resource。
但是,如果Client使用姓名、生日或病歷號搜尋:
GET /Patient?name=王小明
Server回傳的最外層通常不是Patient,而是Bundle:
{
"resourceType": "Bundle",
"type": "searchset"
}
為什麼搜尋Patient,結果卻變成Bundle?
因為一次搜尋可能找到零筆、一筆或多筆Resource,FHIR需要一個共同的容器包裝搜尋結果、分頁連結及其他相關資訊。這個容器就是Bundle。
今天不進行實際API操作,而是透過範例認識FHIR Bundle的結構與用途。
本文所有姓名、網址、id及醫療資料均為虛構教學範例。
Bundle本身也是一種FHIR Resource。
它的主要用途是將一組Resource及相關資訊放在一起。
一份Bundle可能包含:
不過,Bundle並不只有搜尋結果一種用途。
FHIR還會在文件、訊息、批次處理、交易及歷史紀錄等情境中使用Bundle。
因此,看到:
"resourceType": "Bundle"
之後,還要繼續查看:
"type"
才能知道這份Bundle的用途。
以下是一份簡化的Patient搜尋結果:
{
"resourceType": "Bundle",
"id": "bundle-example",
"type": "searchset",
"total": 2,
"link": [
{
"relation": "self",
"url": "https://hospital.example.org/fhir/Patient?name=王小明"
}
],
"entry": [
{
"fullUrl": "https://hospital.example.org/fhir/Patient/patient-001",
"search": {
"mode": "match"
},
"resource": {
"resourceType": "Patient",
"id": "patient-001",
"name": [
{
"text": "王小明"
}
]
}
},
{
"fullUrl": "https://hospital.example.org/fhir/Patient/patient-002",
"search": {
"mode": "match"
},
"resource": {
"resourceType": "Patient",
"id": "patient-002",
"name": [
{
"text": "王小明"
}
]
}
}
]
}
這份Bundle表示搜尋到兩筆姓名符合王小明的Patient。
接下來逐一拆解主要欄位。
"resourceType": "Bundle"
resourceType表示最外層Resource的類型。
雖然Bundle裡面可能包含Patient,但最外層仍然是Bundle。
可以想成一個包裹:
如果Client想取得實際的Patient,需要查看:
entry → resource
"id": "bundle-example"
這是Bundle Resource自己的id。
它不是Bundle裡某位病人的Patient id。
可以比較:
| 位置 | id | 代表的內容 |
|---|---|---|
| Bundle最外層 | bundle-example |
Bundle的id |
第一筆entry.resource |
patient-001 |
第一筆Patient的id |
第二筆entry.resource |
patient-002 |
第二筆Patient的id |
Resource位於不同層級時,即使欄位名稱都叫id,也要先確認目前正在閱讀哪一個Resource。
"type": "searchset"
type表示Bundle的用途。
因為這份Bundle來自FHIR搜尋,所以使用:
searchset
搜尋Bundle應使用searchset,不能只因為Bundle中裝著Patient,就把type寫成Patient。
FHIR R4常見Bundle類型包括:
| Bundle type | 主要用途 |
|---|---|
document |
FHIR文件 |
message |
FHIR訊息 |
transaction |
將多個操作視為一組交易 |
transaction-response |
交易處理結果 |
batch |
一次傳送多個獨立操作 |
batch-response |
批次操作結果 |
history |
Resource或Server的歷史紀錄 |
searchset |
搜尋結果 |
collection |
一組Resource的集合 |
不同類型Bundle對欄位及內容可能有不同規則。
一般JSON可以直接使用Array裝入多筆資料:
[
{
"resourceType": "Patient",
"id": "patient-001"
},
{
"resourceType": "Patient",
"id": "patient-002"
}
]
但這只是一個JSON Array,不是FHIR Bundle。
FHIR Bundle除了包裝Resource,也能提供:
因此,Bundle是一種具有明確FHIR結構及語意的Resource,不是隨意把幾筆JSON放進Array。
"total": 2
在搜尋Bundle中,total表示符合搜尋條件的Resource總數。
如果Request概念是:
GET /Patient?name=王小明
而Server找到兩筆符合條件的Patient,就可能回傳:
"total": 2
不過,total不一定等於目前這一頁entry的數量。
假設總共有100筆結果,但每頁只顯示10筆:
"total": 100
目前這一頁的entry可能只有10筆。
可以整理成:
| 欄位 | 意義 |
|---|---|
total |
符合條件的總數 |
entry數量 |
目前Bundle頁面實際包含的項目數 |
另外,FHIR搜尋可以透過_total參數影響Server是否計算總數。Server也可能因效能或自身能力而採取不同處理方式,因此不能永遠假設total一定存在。
"entry": [
{
...
},
{
...
}
]
entry是一個Array,裡面可以放入多個Bundle Entry。
每個Entry可能包含:
fullUrl
resource
search
request
response
link
不同Bundle類型會使用不同欄位。
在搜尋Bundle中,最常關注的是:
entry.fullUrl
entry.resource
entry.search
每個Entry中的resource會放入實際Resource。
例如:
"resource": {
"resourceType": "Patient",
"id": "patient-001",
"name": [
{
"text": "王小明"
}
]
}
這裡才是真正的Patient Resource。
閱讀搜尋結果時,可以依照以下層級理解:
Bundle
└── entry
└── resource
└── Patient
如果有兩筆搜尋結果:
Bundle
├── entry[0]
│ └── resource:Patient/patient-001
└── entry[1]
└── resource:Patient/patient-002
entry使用Array,所以程式處理時通常需要依序查看每一筆Entry。
"fullUrl": "https://hospital.example.org/fhir/Patient/patient-001"
fullUrl表示Entry中Resource的完整識別位置。
可以拆成:
FHIR Server Base URL
+Resource類型
+Resource id
也就是:
https://hospital.example.org/fhir
+/Patient
+/patient-001
形成:
https://hospital.example.org/fhir/Patient/patient-001
fullUrl可以協助Bundle中的Reference解析及Resource識別。
它不一定永遠是一般HTTP網址,在某些Bundle情境中也可能使用其他形式的URI,例如UUID。
假設Entry中包含:
{
"fullUrl": "https://hospital.example.org/fhir/Patient/patient-001",
"resource": {
"resourceType": "Patient",
"id": "patient-001"
}
}
兩者可以比較如下:
| 欄位 | 內容 | 用途 |
|---|---|---|
fullUrl |
完整URI | 識別Entry中Resource的完整位置 |
resource.id |
patient-001 |
Resource本身的邏輯id |
fullUrl包含更多位置資訊,而resource.id只保存Resource的id。
搜尋Bundle的Entry可能包含:
"search": {
"mode": "match"
}
search.mode表示這筆Resource為什麼被放進搜尋結果。
FHIR R4常見的Search Entry Mode包括:
| mode | 意義 |
|---|---|
match |
Resource符合原本搜尋條件 |
include |
Resource因_include或_revinclude被一併帶入 |
outcome |
與搜尋處理結果相關的OperationOutcome |
"search": {
"mode": "match"
}
表示這筆Resource直接符合Client提出的搜尋條件。
例如,搜尋:
GET /Patient?name=王小明
符合姓名條件的Patient,其search.mode可能是:
match
FHIR搜尋可以透過_include要求Server將搜尋結果所參照的其他Resource一起放入Bundle。
例如,搜尋Observation時,希望同時取得Observation所參照的Patient。
概念上可能是:
GET /Observation?patient=patient-001&_include=Observation:patient
搜尋到的Observation可能是:
"search": {
"mode": "match"
}
被一併帶入的Patient可能是:
"search": {
"mode": "include"
}
這表示Patient本身不是原始搜尋條件直接比對出的主要結果,而是因為Observation參照它,所以被一起放入Bundle。
某些搜尋Bundle可能包含OperationOutcome,用來提供:
這類Entry可能使用:
"search": {
"mode": "outcome"
}
因此,不能假設搜尋Bundle中的所有entry.resource都是Patient或原本搜尋的Resource類型。
應先查看每一筆Entry的:
resource.resourceType
search.mode
搜尋Entry也可能包含:
"search": {
"mode": "match",
"score": 0.95
}
score可以表示搜尋結果的相關程度,數值範圍通常介於0到1之間。
它可能出現在文字搜尋或需要計算相關性的情境中。
不是所有FHIR搜尋都會提供score,也不能假設不同Server的分數可以直接比較。
當搜尋結果很多時,Server不一定會一次回傳全部資料。
Bundle可以使用link提供分頁資訊。
例如:
"link": [
{
"relation": "self",
"url": "https://hospital.example.org/fhir/Patient?name=王小明&_count=10"
},
{
"relation": "next",
"url": "https://hospital.example.org/fhir/Patient?...server-page-token..."
}
]
每一筆link包含:
relation
url
| relation | 意義 |
|---|---|
self |
目前這一頁 |
first |
第一頁 |
previous |
上一頁 |
next |
下一頁 |
last |
最後一頁 |
不是每份Bundle都一定包含所有relation。
例如:
previous
next
last
self
Server提供的next URL可能包含:
例如:
https://hospital.example.org/fhir/Patient?_getpages=abc123&_pageId=2
Client不應自行猜測這些參數的意義,或只將頁碼加1。
概念上應使用Server提供的完整next URL,因為分頁方式由Server決定。
Request可能包含:
GET /Patient?name=王小明&_count=10
_count=10表示Client希望每頁最多回傳10筆搜尋結果。
假設符合條件的Patient共有35筆:
"total": 35
但目前頁面的entry可能只有10筆。
因此:
| 項目 | 數量 |
|---|---|
| 符合條件的總數 | 35 |
| Client希望的每頁數量 | 10 |
| 目前Entry數量 | 最多約10筆 |
| 是否可能有下一頁 | 是 |
_count不是「只搜尋前10筆」,而是與搜尋結果分頁大小有關。
Server仍可能依照自身限制調整實際回傳數量。
如果沒有任何Patient符合條件,Server仍可能回傳成功:
200 OK
Response Body:
{
"resourceType": "Bundle",
"type": "searchset",
"total": 0
}
這是一份有效的搜尋結果,表示:
搜尋成功執行,但沒有找到符合條件的Resource。
它和read找不到id的情況不同。
GET /Patient/not-exist
可能回傳:
404 Not Found
GET /Patient?name=不存在的姓名
通常回傳:
200 OK
以及結果為0的Bundle。
可以。
以搜尋Bundle來說,如果使用_include或_revinclude,可能同時包含不同Resource。
例如:
Bundle
├── Observation
├── Observation
└── Patient
其中:
search.mode為match
search.mode為include
所以不能看到Bundle來自Observation搜尋,就假設所有Entry一定都是Observation。
程式處理每筆Entry時,仍然需要查看:
"resourceType"
"type": "searchset"
用於FHIR搜尋結果。
常見欄位:
total
link
entry.resource
entry.search
"type": "history"
用於表示Resource、某一Resource類型或Server的歷史紀錄。
Entry可能包含:
"type": "transaction"
用於將多個操作作為一組交易處理。
交易中的操作通常具有「全部成功或全部失敗」的概念,適合需要維持資料一致性的情境。
"type": "batch"
用於一次提交多個彼此獨立的操作。
某一個Entry失敗時,其他Entry仍可能成功,這與transaction不同。
"type": "document"
用於FHIR臨床文件。
第一筆Entry通常需要是Composition Resource,再連結文件中的其他Resource。
"type": "collection"
表示一組Resource集合,但不具有searchset、transaction或document等特定處理語意。
| 類型 | 是否為FHIR Resource | 是否提供搜尋語意 | 是否可包含額外資訊 |
|---|---|---|---|
| JSON Array | 否 | 否 | 只有一般JSON結構 |
| Bundle collection | 是 | 否 | 可以 |
| Bundle searchset | 是 | 是 | 可以包含total、link及search |
| Bundle transaction | 是 | 否 | 可以描述多個交易操作 |
因此,不能只看它們都能裝入多筆Resource,就認為用途完全相同。
Bundle中的Resource可能互相Reference。
例如,一筆Observation包含:
"subject": {
"reference": "Patient/patient-001"
}
同一份Bundle中又包含:
{
"fullUrl": "https://hospital.example.org/fhir/Patient/patient-001",
"resource": {
"resourceType": "Patient",
"id": "patient-001"
}
}
Client可以透過Reference、Resource類型、id及fullUrl找出相關Resource。
不過,並不是所有Reference目標都一定會被放進Bundle。除非搜尋條件、_include或其他規則要求Server一併提供,Client可能需要另外讀取相關Resource。
一份搜尋Bundle可能包含多位病人的資料。
因此,Server需要控制:
即使Client有權查看其中一位病人,也不代表可以查看搜尋Bundle中的所有病人。
FHIR定義Bundle結構,但實際資料存取仍需符合身分驗證、授權、病人同意、法規及組織政策。
閱讀FHIR搜尋Bundle時,可以按照以下思考順序:
"resourceType": "Bundle"
"type": "searchset"
"total": 2
"link": [...]
"entry": [...]
"resourceType": "Patient"
"mode": "match"
確認它是主要搜尋結果、被include的Resource,還是OperationOutcome。
這是一種閱讀資料的思考順序,不需要進行實際操作。
Bundle
├── resourceType:Bundle
├── id:Bundle識別碼
├── type:searchset
├── total:符合條件的總數
├── link
│ ├── self
│ ├── previous
│ └── next
└── entry
├── fullUrl
├── search
│ ├── mode
│ └── score
└── resource
├── Patient
├── Observation
└── 其他FHIR Resource
今天認識了FHIR Bundle,以及搜尋結果為什麼需要使用Bundle包裝。
搜尋可能找到零筆、一筆或多筆Resource,所以FHIR使用type為searchset的Bundle,同時提供:
今天的重要欄位包括:
resourceType
type
total
link
entry
fullUrl
resource
search.mode
search.score
其中最重要的觀念是:
total代表符合條件的總數,而entry只代表目前這一頁實際包含的項目。
另外,Bundle不只用於搜尋,也能用於文件、訊息、批次、交易及歷史紀錄。
完成Patient與搜尋Bundle的介紹後,下一篇將進入臨床資料Resource,認識Observation如何表示檢驗結果與生命徵象。
Day 20|Observation:用FHIR表示檢驗與生命徵象
HL7 FHIR R4:Bundle
https://hl7.org/fhir/R4/bundle.html
HL7 FHIR R4:Search
https://hl7.org/fhir/R4/search.html
HL7 FHIR R4:Search Result Parameters
https://hl7.org/fhir/R4/search.html#return
HL7 FHIR R4:Bundle Type ValueSet
https://hl7.org/fhir/R4/valueset-bundle-type.html
HL7 FHIR R4:Search Entry Mode ValueSet
https://hl7.org/fhir/R4/valueset-search-entry-mode.html