iT邦幫忙

2026 iThome 鐵人賽

DAY 2
0

前言

在上一篇文章中,我們認識了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是什麼?

Bundle本身也是一種FHIR Resource。

它的主要用途是將一組Resource及相關資訊放在一起。

一份Bundle可能包含:

  • Patient
  • Observation
  • Encounter
  • Condition
  • MedicationRequest
  • OperationOutcome
  • 其他Bundle允許的內容

不過,Bundle並不只有搜尋結果一種用途。

FHIR還會在文件、訊息、批次處理、交易及歷史紀錄等情境中使用Bundle。

因此,看到:

"resourceType": "Bundle"

之後,還要繼續查看:

"type"

才能知道這份Bundle的用途。


搜尋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": "Bundle"

resourceType表示最外層Resource的類型。

雖然Bundle裡面可能包含Patient,但最外層仍然是Bundle。

可以想成一個包裹:

  • 包裹本身是Bundle。
  • 包裹裡面的物品是Patient。
  • 不能因為包裹裡面有Patient,就把外層當成Patient。

如果Client想取得實際的Patient,需要查看:

entry → resource

id:Bundle自己的識別碼

"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:Bundle的用途

"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對欄位及內容可能有不同規則。


Bundle不是普通的Resource陣列

一般JSON可以直接使用Array裝入多筆資料:

[
  {
    "resourceType": "Patient",
    "id": "patient-001"
  },
  {
    "resourceType": "Patient",
    "id": "patient-002"
  }
]

但這只是一個JSON Array,不是FHIR Bundle。

FHIR Bundle除了包裝Resource,也能提供:

  • Bundle類型
  • 符合條件的總數
  • 分頁連結
  • Resource完整網址
  • 搜尋結果類型
  • 搜尋分數
  • Request資訊
  • Response資訊
  • 其他與交換情境相關的資料

因此,Bundle是一種具有明確FHIR結構及語意的Resource,不是隨意把幾筆JSON放進Array。


total:符合條件的結果數量

"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:Bundle中的項目

"entry": [
  {
    ...
  },
  {
    ...
  }
]

entry是一個Array,裡面可以放入多個Bundle Entry。

每個Entry可能包含:

  • fullUrl
  • resource
  • search
  • request
  • response
  • link

不同Bundle類型會使用不同欄位。

在搜尋Bundle中,最常關注的是:

entry.fullUrl
entry.resource
entry.search

resource:實際的FHIR Resource

每個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:Resource的完整位置

"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。


fullUrl和Resource id有什麼不同?

假設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。


search.mode:這筆Entry為什麼出現在結果中?

搜尋Bundle的Entry可能包含:

"search": {
  "mode": "match"
}

search.mode表示這筆Resource為什麼被放進搜尋結果。

FHIR R4常見的Search Entry Mode包括:

mode 意義
match Resource符合原本搜尋條件
include Resource因_include_revinclude被一併帶入
outcome 與搜尋處理結果相關的OperationOutcome

match:符合搜尋條件

"search": {
  "mode": "match"
}

表示這筆Resource直接符合Client提出的搜尋條件。

例如,搜尋:

GET /Patient?name=王小明

符合姓名條件的Patient,其search.mode可能是:

match

include:因關聯而被帶入

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。


outcome:與搜尋結果相關的訊息

某些搜尋Bundle可能包含OperationOutcome,用來提供:

  • 警告
  • 資訊
  • 搜尋參數處理結果
  • Server想回報的其他問題

這類Entry可能使用:

"search": {
  "mode": "outcome"
}

因此,不能假設搜尋Bundle中的所有entry.resource都是Patient或原本搜尋的Resource類型。

應先查看每一筆Entry的:

  • resource.resourceType
  • search.mode

search.score:搜尋分數

搜尋Entry也可能包含:

"search": {
  "mode": "match",
  "score": 0.95
}

score可以表示搜尋結果的相關程度,數值範圍通常介於0到1之間。

它可能出現在文字搜尋或需要計算相關性的情境中。

不是所有FHIR搜尋都會提供score,也不能假設不同Server的分數可以直接比較。


link:搜尋結果的分頁連結

當搜尋結果很多時,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

relation 意義
self 目前這一頁
first 第一頁
previous 上一頁
next 下一頁
last 最後一頁

不是每份Bundle都一定包含所有relation。

例如:

  • 第一頁可能沒有previous
  • 最後一頁可能沒有next
  • Server可能無法提供last
  • 單頁結果可能只有self

為什麼要使用Server提供的next連結?

Server提供的next URL可能包含:

  • 分頁Token
  • 游標
  • Server內部搜尋狀態
  • 搜尋結果識別資料
  • Client不需要理解的參數

例如:

https://hospital.example.org/fhir/Patient?_getpages=abc123&_pageId=2

Client不應自行猜測這些參數的意義,或只將頁碼加1。

概念上應使用Server提供的完整next URL,因為分頁方式由Server決定。


_count和total有什麼不同?

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的情況不同。

Read找不到

GET /Patient/not-exist

可能回傳:

404 Not Found

Search找不到

GET /Patient?name=不存在的姓名

通常回傳:

200 OK

以及結果為0的Bundle。


Bundle中可以放入不同Resource嗎?

可以。

以搜尋Bundle來說,如果使用_include_revinclude,可能同時包含不同Resource。

例如:

Bundle
├── Observation
├── Observation
└── Patient

其中:

  • Observation是主要搜尋結果,search.modematch
  • Patient是被一併帶入的相關Resource,search.modeinclude

所以不能看到Bundle來自Observation搜尋,就假設所有Entry一定都是Observation。

程式處理每筆Entry時,仍然需要查看:

"resourceType"

不同Bundle類型的差異

searchset

"type": "searchset"

用於FHIR搜尋結果。

常見欄位:

  • total
  • link
  • entry.resource
  • entry.search

history

"type": "history"

用於表示Resource、某一Resource類型或Server的歷史紀錄。

Entry可能包含:

  • Resource版本
  • Request資訊
  • Response資訊
  • 操作方法
  • 操作時間

transaction

"type": "transaction"

用於將多個操作作為一組交易處理。

交易中的操作通常具有「全部成功或全部失敗」的概念,適合需要維持資料一致性的情境。


batch

"type": "batch"

用於一次提交多個彼此獨立的操作。

某一個Entry失敗時,其他Entry仍可能成功,這與transaction不同。


document

"type": "document"

用於FHIR臨床文件。

第一筆Entry通常需要是Composition Resource,再連結文件中的其他Resource。


collection

"type": "collection"

表示一組Resource集合,但不具有searchset、transaction或document等特定處理語意。


searchset、collection與JSON Array比較

類型 是否為FHIR Resource 是否提供搜尋語意 是否可包含額外資訊
JSON Array 只有一般JSON結構
Bundle collection 可以
Bundle searchset 可以包含total、link及search
Bundle transaction 可以描述多個交易操作

因此,不能只看它們都能裝入多筆Resource,就認為用途完全相同。


搜尋Bundle中的Reference

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需要注意的隱私問題

一份搜尋Bundle可能包含多位病人的資料。

因此,Server需要控制:

  • 使用者可以搜尋哪些Patient
  • 一次可以看到多少結果
  • 是否允許模糊搜尋
  • 是否遮蔽敏感欄位
  • 是否記錄搜尋行為
  • 是否允許使用姓名或電話大量查詢
  • 是否只回傳與使用者有照護關係的病人
  • 被include的Resource是否也符合權限

即使Client有權查看其中一位病人,也不代表可以查看搜尋Bundle中的所有病人。

FHIR定義Bundle結構,但實際資料存取仍需符合身分驗證、授權、病人同意、法規及組織政策。


如何閱讀一份搜尋Bundle?

閱讀FHIR搜尋Bundle時,可以按照以下思考順序:

1. 確認最外層Resource

"resourceType": "Bundle"

2. 確認Bundle用途

"type": "searchset"

3. 查看符合條件的總數

"total": 2

4. 查看分頁連結

"link": [...]

5. 查看entry

"entry": [...]

6. 查看每筆Resource類型

"resourceType": "Patient"

7. 查看search.mode

"mode": "match"

確認它是主要搜尋結果、被include的Resource,還是OperationOutcome。

這是一種閱讀資料的思考順序,不需要進行實際操作。


搜尋Bundle結構整理

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使用typesearchset的Bundle,同時提供:

  • 搜尋結果總數
  • 分頁連結
  • Resource完整位置
  • 實際Resource
  • 搜尋結果類型
  • 其他相關資訊

今天的重要欄位包括:

  • resourceType
  • type
  • total
  • link
  • entry
  • fullUrl
  • resource
  • search.mode
  • search.score

其中最重要的觀念是:

total代表符合條件的總數,而entry只代表目前這一頁實際包含的項目。

另外,Bundle不只用於搜尋,也能用於文件、訊息、批次、交易及歷史紀錄。

完成Patient與搜尋Bundle的介紹後,下一篇將進入臨床資料Resource,認識Observation如何表示檢驗結果與生命徵象。

明日預告

Day 20|Observation:用FHIR表示檢驗與生命徵象

參考資料

  1. HL7 FHIR R4:Bundle
    https://hl7.org/fhir/R4/bundle.html

  2. HL7 FHIR R4:Search
    https://hl7.org/fhir/R4/search.html

  3. HL7 FHIR R4:Search Result Parameters
    https://hl7.org/fhir/R4/search.html#return

  4. HL7 FHIR R4:Bundle Type ValueSet
    https://hl7.org/fhir/R4/valueset-bundle-type.html

  5. HL7 FHIR R4:Search Entry Mode ValueSet
    https://hl7.org/fhir/R4/valueset-search-entry-mode.html


上一篇
Day 18|如何搜尋FHIR Patient?
下一篇
Day 20|Observation:用FHIR表示檢驗與生命徵象
系列文
《醫資生的 FHIR 30日入門:用 Postman 讀懂醫療資料交換》30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言