FHIR Search
Overview
FHIR search is the primary mechanism for querying resources from a server. Every search response — regardless of what you’re querying or how many parameters you combine — comes back as a searchset Bundle. That uniformity is intentional: clients always parse a Bundle, extract entries, follow pagination links, and handle includes in the same way.
This article covers the mechanics. If you’re building an integration that retrieves resources at any meaningful scale, you need to understand parameter types, modifiers, prefixes, result shaping, and how servers behave differently from each other.
How Search Works
A FHIR search request is an HTTP GET (or POST for long queries) against a resource endpoint:
GET [base]/Observation?patient=Patient/123&category=laboratory&date=ge2024-01-01
The server evaluates the parameters against its dataset, applies any includes or result-shaping flags, and returns a Bundle with type: "searchset". The client’s job is to parse that Bundle, process entries, and — if the result is paginated — follow the next link.
POST-based search uses application/x-www-form-urlencoded body instead of query string, which avoids URL-length limits on large parameter sets:
POST [base]/Observation/_search
Content-Type: application/x-www-form-urlencoded
patient=Patient%2F123&category=laboratory&date=ge2024-01-01
Both forms are semantically equivalent. Prefer POST when you have many parameters or long value lists.
Parameter Types
Each search parameter has a type. The type determines what values are valid and which operators and modifiers apply.
| Type | Example value | Used for |
|---|---|---|
token | system|code, code, |code | coded fields, identifiers, booleans |
string | Smith | names, addresses, free text fields |
date | 2024-01-15, 2024-01 | date and dateTime elements |
number | 21.5 | numeric elements |
quantity | 6.8||kg/m2 | Quantity elements with unit |
reference | Patient/123, 123 | Reference elements |
uri | http://example.org/profile/1 | uri, url, canonical elements |
composite | 8480-6$gt110 | pairs of parameters on the same element |
special | varies | _filter, _text, _content |
Token
Token searches against coded fields, identifiers, and booleans. The general form is system|code. You can omit either side:
code=http://loinc.org|1975-2— code with system (most precise)code=1975-2— code without system (matches any system — use cautiously on crowded datasets)code=http://loinc.org|— any code in this system
Token is the right type for Observation.code, Patient.identifier, Condition.clinicalStatus, and boolean-like flags.
String
String searches name fields and addresses. By default, string search is a prefix match — name=Smi matches Smith and Smithson. Case and accent insensitive unless you use :exact.
Date
Date parameters match date, dateTime, and Period elements. Date values follow the ISO 8601 partial-date format: 2024, 2024-01, 2024-01-15, 2024-01-15T10:30:00. Servers match partial dates against the full range they imply — date=2024 matches any date in 2024.
Number and Quantity
Number is for integer or decimal fields (RiskAssessment.probability, etc.). Quantity adds a unit dimension: value-quantity=6.8||kg/m2 matches any Quantity with value 6.8 and unit kg/m2. The triple-pipe syntax is value|system|unit.
Reference
Reference searches match resource references. You can search by relative reference (patient=Patient/123), by logical ID alone (patient=123), or by chaining into the referenced resource (see Chained Parameters below).
Composite
Composite parameters pair two parameter values that must apply to the same repeating element. The canonical example is component-code-value-quantity on Observation, which ensures a systolic code matches the same component as a systolic value:
GET [base]/Observation?component-code-value-quantity=8480-6$gt110
Without composite, you’d get Observations where any component has the code and any component has the value — those two don’t have to be the same component.
Modifiers
Modifiers alter how a parameter is interpreted. They append to the parameter name with a colon.
| Modifier | Applies to | Meaning |
|---|---|---|
:exact | string | Case-sensitive exact match |
:contains | string | Substring match anywhere in string |
:missing | any | true = element is absent; false = element is present |
:not | token | Exclude resources where element has this value |
:text | token | Search the display text, not the code |
:in | token | Code must be in the referenced ValueSet |
:not-in | token | Code must not be in the referenced ValueSet |
:below | token, uri | Subsumption — code is this code or a descendant |
:above | token, uri | Subsumption — code is this code or an ancestor |
Modifier notes that matter
:exact vs default string: The default string search is prefix + case-insensitive. If you search name=John, you get John, Johnathan, and JOHN. Use :exact when you need literal matching. Never use default string on a field where your query value might be a common prefix.
:contains has a cost: Full substring search requires the server to scan more index entries than prefix. On large datasets, :contains on name or address can be orders of magnitude slower. Avoid it in high-frequency queries.
:missing: Useful for data quality — birthDate:missing=true finds patients with no recorded DOB. Note that servers vary in whether they index missing-ness.
:below for hierarchies: When your CodeSystem has a hierarchy (SNOMED, ICD-10), :below lets you search by parent code and get all descendants. This requires the server to have a terminology service backing the search.
Prefixes for Date and Number
Date and number parameters accept comparison prefixes. These apply before the value, attached directly (no separator):
| Prefix | Meaning |
|---|---|
eq | equals (default if omitted) |
ne | not equal |
lt | less than |
gt | greater than |
le | less than or equal |
ge | greater than or equal |
sa | starts after (for Period: period starts after this date) |
eb | ends before (for Period: period ends before this date) |
ap | approximately (within ±10% for numbers, implementation-defined for dates) |
Examples:
date=ge2024-01-01 # on or after Jan 1 2024
date=ge2024-01-01&date=lt2025-01-01 # during 2024
value-quantity=gt5.5 # quantity greater than 5.5
For date ranges, use two parameters with the same name. The server ANDs them.
Standard Parameters
These parameters apply to all resources:
_id— matchesResource.iddirectly. Equivalent to a read, but returns a Bundle. Useful for fetching multiple resources in one request:_id=a,b,c_lastUpdated— matchesResource.meta.lastUpdated. Accepts date prefixes. Essential for incremental sync._profile— matchesResource.meta.profile. Finds resources conforming to a specific profile._tag— matchesResource.meta.tag. Token search._security— matchesResource.meta.security. Token search._text— full-text search against the narrative (div element). Server support varies widely._content— full-text search against all resource content. Server support varies widely._filter— a structured query language for complex conditions a single query string cannot express. R4 trial-use; not universally supported._type— on whole-system search (GET [base]/?...), restricts which resource types to search.
Sorting
Use _sort to control result order. Provide a comma-separated list of parameter names; prefix with - for descending:
GET [base]/Observation?patient=Patient/123&_sort=-date,code
This sorts by date descending, then by code ascending. Servers are only required to support sorting on a limited set of parameters and may silently ignore unsupported sort fields. Check the server’s CapabilityStatement for searchParam with sort: true.
Counting and Paging
_count sets the maximum number of entries per page. The default varies by server — some default to 20, some to 50, some to 100. Never assume a default. Set _count explicitly.
GET [base]/Patient?family=Smith&_count=50
When a search has more results than fit in one page, the Bundle contains a link array with a next relation. Follow it to retrieve the next page. Previous, first, and last relations may also be present.
Pagination tokens vs offset: FHIR leaves the pagination mechanism implementation-defined. Most servers use an opaque cursor in the next URL. Some use _offset. Do not construct next URLs manually — always use the URL from Bundle.link.
_include and _revinclude
_include adds resources referenced by the primary result set to the response Bundle. _revinclude adds resources that reference the primary result set.
# Forward include: get Observations and the Patients they reference
GET [base]/Observation?category=laboratory&_include=Observation:patient
# Reverse include: get Patients and Observations that reference them
GET [base]/Patient?name=Smith&_revinclude=Observation:patient
Syntax: _include=[SourceResourceType]:[searchParameter]:[targetResourceType]
The third segment (target type) is optional if the search parameter only points to one type, required when it’s polymorphic.
:iterate makes includes recursive. Use with caution — it can pull large subgraphs:
GET [base]/MedicationRequest?patient=Patient/123
&_include=MedicationRequest:medication
&_include:iterate=Medication:ingredient
Included resources appear in the Bundle with entry.search.mode = "include" rather than "match".
_summary and _elements
When you don’t need the full resource, use _summary or _elements to reduce payload size.
_summary has defined values:
_summary=true— a server-defined subset of elements (typically mandatory + must-support elements)_summary=text— only narrative (text element) + mandatory fields_summary=data— everything except narrative_summary=count— no entries at all, just the total count in Bundle.total
_elements is a comma-separated list of element names you want returned:
GET [base]/Patient?_elements=id,name,birthDate,identifier
Servers must include mandatory and modifier elements even if not listed in _elements, so the response may contain more than you asked for.
The Search Result Bundle
A searchset Bundle has this structure:
{
"resourceType": "Bundle",
"id": "search-results-001",
"type": "searchset",
"total": 847,
"link": [
{
"relation": "self",
"url": "https://server.example.org/fhir/Observation?patient=Patient%2F123&category=laboratory&_count=20"
},
{
"relation": "next",
"url": "https://server.example.org/fhir/Observation?patient=Patient%2F123&category=laboratory&_count=20&_cursor=eyJsYXN0SWQiOiJvYnMtMTA4NyJ9"
}
],
"entry": [
{
"fullUrl": "https://server.example.org/fhir/Observation/obs-1001",
"resource": {
"resourceType": "Observation",
"id": "obs-1001",
"status": "final",
"category": [
{
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/observation-category",
"code": "laboratory"
}
]
}
],
"code": {
"coding": [
{
"system": "http://loinc.org",
"code": "1975-2",
"display": "Bilirubin.total [Mass/volume] in Serum or Plasma"
}
]
},
"subject": { "reference": "Patient/123" },
"effectiveDateTime": "2024-03-15T09:22:00Z",
"valueQuantity": {
"value": 0.8,
"unit": "mg/dL",
"system": "http://unitsofmeasure.org",
"code": "mg/dL"
}
},
"search": {
"mode": "match",
"score": 1
}
}
]
}
Key Bundle fields
Bundle.total: The total number of matching resources on the server, not the number returned in this page. Servers may omit this (it can be expensive to compute) or return an estimate. _summary=count returns only the total with no entries.
entry.search.mode: Values are:
match— this entry matched your search criteriainclude— this entry was added by_includeor_revincludeoutcome— an OperationOutcome warning about this entry
Always check search.mode when processing Bundles that used includes. You do not want to process included resources as if they were search matches.
Chained Parameters
Chain into a referenced resource using dot notation:
GET [base]/Observation?patient.name=Smith
GET [base]/Observation?patient.birthdate=1978-04-12
GET [base]/DiagnosticReport?result.code=1975-2
Each . traverses one reference hop. Servers must support chaining on references they index; check the CapabilityStatement.
Reverse chaining (_has) asks: “give me resources that are referenced by a resource that meets this criterion”:
# Patients who have an Observation with this LOINC code
GET [base]/Patient?_has:Observation:patient:code=1975-2
Performance Patterns
Narrow early
Always include the most selective parameters first in your mental model (the query planner may reorder, but good selectivity helps). Searching by patient + category + date range is far cheaper than searching by date range alone on a populated server.
Avoid :contains on large collections
:contains substring search cannot use a prefix index. On a table with millions of names, name:contains=ohn is a full scan. Use it only when the dataset is small or the search is low-frequency.
Set _count explicitly
Never rely on server defaults. If you expect large result sets, use a count that balances network round trips against payload size. 50–100 is a reasonable starting point; tune based on your use case.
Cache search results where appropriate
Search results are cacheable via standard HTTP cache headers. Servers that support ETags allow conditional GET to avoid retransmitting unchanged results. Use this for reference data queries (ValueSets, CodeSystems) that change infrequently.
Bulk Data for large extracts
If you need more than a few thousand resources, FHIR search is the wrong tool. Use the FHIR Bulk Data Access ($export) mechanism instead. It produces newline-delimited JSON files asynchronously and is orders of magnitude more efficient for population-scale extracts.
Common Mistakes
Assuming _count has a standard default. It does not. A server returning 10 results when you expected 500 is probably paginating at its default, not filtering your data. Set _count explicitly and follow next links.
Using :exact when you mean :contains, or vice versa. For patient name lookup in production, default string (prefix, case-insensitive) is usually what end-users expect. :exact for machine-generated identifiers; :contains only when you genuinely need substring anywhere.
Chained parameter syntax errors. patient.name is correct; patient:name is not (that’s a modifier, not a chain). Observation?patient.name=Smith chains through the patient reference to the Patient’s name parameter.
Not validating entry.search.mode. If you use _include and process every entry as a match, you’ll double-process included resources.
Constructing pagination URLs by hand. The next URL from the server may contain opaque state. Reconstruct it by appending _offset and you may get duplicate or missing results. Always use the URL exactly as provided in Bundle.link.
Assuming Bundle.total equals Bundle.entry.count. On a paged response, total reflects the server-side count; entry contains only the current page. They will usually be different.
Token search without system. code=1975-2 matches the code value in any system. If multiple systems share a code value (they do), you get false positives. Always include the system: code=http://loinc.org|1975-2.