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.

TypeExample valueUsed for
tokensystem|code, code, |codecoded fields, identifiers, booleans
stringSmithnames, addresses, free text fields
date2024-01-15, 2024-01date and dateTime elements
number21.5numeric elements
quantity6.8||kg/m2Quantity elements with unit
referencePatient/123, 123Reference elements
urihttp://example.org/profile/1uri, url, canonical elements
composite8480-6$gt110pairs of parameters on the same element
specialvaries_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.

ModifierApplies toMeaning
:exactstringCase-sensitive exact match
:containsstringSubstring match anywhere in string
:missinganytrue = element is absent; false = element is present
:nottokenExclude resources where element has this value
:texttokenSearch the display text, not the code
:intokenCode must be in the referenced ValueSet
:not-intokenCode must not be in the referenced ValueSet
:belowtoken, uriSubsumption — code is this code or a descendant
:abovetoken, uriSubsumption — 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):

PrefixMeaning
eqequals (default if omitted)
nenot equal
ltless than
gtgreater than
leless than or equal
gegreater than or equal
sastarts after (for Period: period starts after this date)
ebends before (for Period: period ends before this date)
apapproximately (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 — matches Resource.id directly. Equivalent to a read, but returns a Bundle. Useful for fetching multiple resources in one request: _id=a,b,c
  • _lastUpdated — matches Resource.meta.lastUpdated. Accepts date prefixes. Essential for incremental sync.
  • _profile — matches Resource.meta.profile. Finds resources conforming to a specific profile.
  • _tag — matches Resource.meta.tag. Token search.
  • _security — matches Resource.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 criteria
  • include — this entry was added by _include or _revinclude
  • outcome — 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.

Section: fhir Content Type: reference Audience: technical
FHIR Versions:
R4 R5
Published: 28/09/2022 Modified: 17/01/2026 15 min read
Keywords: FHIR search FHIR query search parameters searchset Bundle _include _revinclude FHIR pagination search modifiers
Sources: