Search and Query Operations

Section 8 of 18
44% complete

Search operations retrieve multiple resources matching specified criteria. FHIR search uses a powerful query syntax supporting various parameter types, modifiers, and chaining. Search results return in a Bundle with pagination links for large result sets. Understanding search is essential because it is how most applications discover and retrieve data from FHIR servers. Build queries incrementally using the fluent API for readability.

import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.gclient.StringClientParam;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Patient;

public class BasicSearchExample {

    private static final FhirContext ctx = FhirContext.forR4();
    private static final String serverBase = "http://hapi.fhir.org/baseR4";

    public static void searchByName() {
        IGenericClient client = ctx.newRestfulGenericClient(serverBase);

        // Search for patients by name
        Bundle results = client.search()
            .forResource(Patient.class)
            .where(Patient.FAMILY.matches().value("Smith"))
            .returnBundle(Bundle.class)
            .execute();

        System.out.println("Found " + results.getTotal() + " patients");

        // Iterate through results
        for (Bundle.BundleEntryComponent entry : results.getEntry()) {
            Patient patient = (Patient) entry.getResource();
            System.out.println("Patient: " +
                patient.getNameFirstRep().getNameAsSingleString());
        }
    }

    public static void searchWithMultipleParameters() {
        IGenericClient client = ctx.newRestfulGenericClient(serverBase);

        // Search with multiple criteria
        Bundle results = client.search()
            .forResource(Patient.class)
            .where(Patient.FAMILY.matches().value("Smith"))
            .and(Patient.GENDER.exactly().code("male"))
            .and(Patient.BIRTHDATE.after().day("1980-01-01"))
            .returnBundle(Bundle.class)
            .execute();

        System.out.println("Found " + results.getTotal() + " matching patients");
    }
}

Pagination

Handle large result sets with pagination. FHIR servers typically limit results per page to prevent memory issues and improve response times. The Bundle contains next and previous links for navigation. When processing all results, iterate through pages until no next link exists. Consider using offset-based pagination for random access or the count parameter to control page size based on your use case.

public class PaginationExample {

    private static final FhirContext ctx = FhirContext.forR4();
    private static final String serverBase = "http://hapi.fhir.org/baseR4";

    public static void searchWithPagination() {
        IGenericClient client = ctx.newRestfulGenericClient(serverBase);

        // First page
        Bundle results = client.search()
            .forResource(Patient.class)
            .count(10)  // Page size
            .returnBundle(Bundle.class)
            .execute();

        System.out.println("Page 1: " + results.getEntry().size() + " results");

        // Get next page
        if (results.getLink(Bundle.LINK_NEXT) != null) {
            Bundle nextPage = client.loadPage()
                .next(results)
                .execute();

            System.out.println("Page 2: " + nextPage.getEntry().size() + " results");
        }
    }

    public static void iterateAllPages() {
        IGenericClient client = ctx.newRestfulGenericClient(serverBase);

        Bundle results = client.search()
            .forResource(Patient.class)
            .count(20)
            .returnBundle(Bundle.class)
            .execute();

        int totalProcessed = 0;
        int pageNumber = 1;

        do {
            System.out.println("Processing page " + pageNumber);

            for (Bundle.BundleEntryComponent entry : results.getEntry()) {
                Patient patient = (Patient) entry.getResource();
                // Process patient
                totalProcessed++;
            }

            // Load next page if available
            if (results.getLink(Bundle.LINK_NEXT) != null) {
                results = client.loadPage().next(results).execute();
                pageNumber++;
            } else {
                results = null;
            }

        } while (results != null);

        System.out.println("Total processed: " + totalProcessed);
    }
}

Includes and Reverse Includes

Include related resources in search results to reduce round trips. The _include parameter retrieves resources that the search results reference (forward direction), while _revinclude retrieves resources that reference the search results (reverse direction). For example, when searching patients, use _revinclude to fetch their observations in the same request. This dramatically improves performance compared to making separate requests for each reference.

public class IncludesExample {

    private static final FhirContext ctx = FhirContext.forR4();
    private static final String serverBase = "http://hapi.fhir.org/baseR4";

    public static void searchWithIncludes() {
        IGenericClient client = ctx.newRestfulGenericClient(serverBase);

        // Include related resources
        Bundle results = client.search()
            .forResource(Observation.class)
            .where(Observation.PATIENT.hasId("example"))
            .include(Observation.INCLUDE_PATIENT)  // Include the patient
            .include(Observation.INCLUDE_PERFORMER)  // Include performer
            .returnBundle(Bundle.class)
            .execute();

        for (Bundle.BundleEntryComponent entry : results.getEntry()) {
            if (entry.getResource() instanceof Observation) {
                System.out.println("Observation: " + entry.getResource().getId());
            } else if (entry.getResource() instanceof Patient) {
                System.out.println("Included Patient: " + entry.getResource().getId());
            }
        }
    }

    public static void searchWithRevIncludes() {
        IGenericClient client = ctx.newRestfulGenericClient(serverBase);

        // Get patient and all related observations
        Bundle results = client.search()
            .forResource(Patient.class)
            .where(Patient.RES_ID.exactly().code("example"))
            .revInclude(Observation.INCLUDE_PATIENT)
            .returnBundle(Bundle.class)
            .execute();

        System.out.println("Found patient with " +
            (results.getEntry().size() - 1) + " related observations");
    }
}

Sorting

Sort search results by specific fields. Sorting ensures predictable, reproducible result ordering which is important for paginated displays and synchronization workflows. You can sort by multiple fields with ascending or descending order. Not all search parameters support sorting, so check server capability statements when designing queries that depend on specific ordering.

public class SortingExample {

    private static final FhirContext ctx = FhirContext.forR4();
    private static final String serverBase = "http://hapi.fhir.org/baseR4";

    public static void searchWithSort() {
        IGenericClient client = ctx.newRestfulGenericClient(serverBase);

        // Sort by birth date descending
        Bundle results = client.search()
            .forResource(Patient.class)
            .sort().descending(Patient.BIRTHDATE)
            .returnBundle(Bundle.class)
            .execute();

        for (Bundle.BundleEntryComponent entry : results.getEntry()) {
            Patient patient = (Patient) entry.getResource();
            System.out.println("Patient: " +
                patient.getNameFirstRep().getNameAsSingleString() +
                " - DOB: " + patient.getBirthDate());
        }
    }

    public static void multipleSort() {
        IGenericClient client = ctx.newRestfulGenericClient(serverBase);

        // Sort by family name ascending, then birth date descending
        Bundle results = client.search()
            .forResource(Patient.class)
            .sort().ascending(Patient.FAMILY)
            .sort().descending(Patient.BIRTHDATE)
            .returnBundle(Bundle.class)
            .execute();
    }
}

Date and Quantity Searches

Search using date ranges and quantity comparisons. Date parameters support comparison operators (greater than, less than, equals) and can match partial dates. Quantity searches include unit-aware comparisons essential for clinical data like lab values. Combine multiple date criteria to create date ranges, which is common for filtering clinical events within a time period.

import ca.uhn.fhir.rest.gclient.DateClientParam;
import ca.uhn.fhir.rest.gclient.NumberClientParam;

public class DateQuantitySearchExample {

    private static final FhirContext ctx = FhirContext.forR4();
    private static final String serverBase = "http://hapi.fhir.org/baseR4";

    public static void searchByDateRange() {
        IGenericClient client = ctx.newRestfulGenericClient(serverBase);

        // Search for observations in a date range
        Bundle results = client.search()
            .forResource(Observation.class)
            .where(Observation.DATE.afterOrEquals().day("2023-01-01"))
            .and(Observation.DATE.beforeOrEquals().day("2023-12-31"))
            .returnBundle(Bundle.class)
            .execute();

        System.out.println("Found " + results.getTotal() + " observations in 2023");
    }

    public static void searchByQuantity() {
        IGenericClient client = ctx.newRestfulGenericClient(serverBase);

        // Search for observations with value > 100
        Bundle results = client.search()
            .forResource(Observation.class)
            .where(Observation.VALUE_QUANTITY.greaterThan()
                .number(100)
                .andUnits("http://unitsofmeasure.org", "mg/dL"))
            .returnBundle(Bundle.class)
            .execute();

        System.out.println("Found high values: " + results.getTotal());
    }

    public static void searchByCodeSystem() {
        IGenericClient client = ctx.newRestfulGenericClient(serverBase);

        // Search using specific coding system
        Bundle results = client.search()
            .forResource(Observation.class)
            .where(Observation.CODE.exactly()
                .systemAndCode("http://loinc.org", "2339-0"))  // Glucose
            .returnBundle(Bundle.class)
            .execute();

        System.out.println("Found glucose observations: " + results.getTotal());
    }
}

Token and Reference Searches

Search by identifiers and references. Token searches find resources by coded values and identifiers using system|value notation. Reference searches find resources that reference specific entities. Chained searches (patient.name) traverse references to search on properties of linked resources, enabling powerful queries like “find observations where the patient’s name is Smith” without knowing the patient ID.

public class TokenReferenceSearchExample {

    private static final FhirContext ctx = FhirContext.forR4();
    private static final String serverBase = "http://hapi.fhir.org/baseR4";

    public static void searchByIdentifier() {
        IGenericClient client = ctx.newRestfulGenericClient(serverBase);

        // Search by identifier
        Bundle results = client.search()
            .forResource(Patient.class)
            .where(Patient.IDENTIFIER.exactly()
                .systemAndIdentifier("http://hospital.org/mrn", "MRN-123456"))
            .returnBundle(Bundle.class)
            .execute();

        if (results.getEntry().size() > 0) {
            Patient patient = (Patient) results.getEntry().get(0).getResource();
            System.out.println("Found patient: " +
                patient.getNameFirstRep().getNameAsSingleString());
        }
    }

    public static void searchByReference() {
        IGenericClient client = ctx.newRestfulGenericClient(serverBase);

        // Search for all observations for a specific patient
        Bundle results = client.search()
            .forResource(Observation.class)
            .where(Observation.PATIENT.hasId("Patient/example"))
            .returnBundle(Bundle.class)
            .execute();

        System.out.println("Found " + results.getTotal() + " observations");
    }

    public static void chainedSearch() {
        IGenericClient client = ctx.newRestfulGenericClient(serverBase);

        // Search for observations where the patient's name is "Smith"
        Bundle results = client.search()
            .forResource(Observation.class)
            .where(Observation.PATIENT.hasChainedProperty(
                Patient.FAMILY.matches().value("Smith")))
            .returnBundle(Bundle.class)
            .execute();

        System.out.println("Found observations for patients named Smith: " +
            results.getTotal());
    }
}

Complex Query Example

Combine multiple search features for comprehensive queries. Real-world applications typically need multiple filters, includes, sorting, and pagination in a single query. Build these queries incrementally, testing each addition. The elementsSubset feature can further optimize by returning only needed fields. Always implement proper pagination handling for production code to process all matching results.

public class ComplexQueryExample {

    private static final FhirContext ctx = FhirContext.forR4();
    private static final String serverBase = "http://hapi.fhir.org/baseR4";

    public static void comprehensiveSearch() {
        IGenericClient client = ctx.newRestfulGenericClient(serverBase);

        // Complex multi-criteria search
        Bundle results = client.search()
            .forResource(Observation.class)
            // Search parameters
            .where(Observation.CATEGORY.exactly().code("vital-signs"))
            .and(Observation.CODE.exactly()
                .systemAndCode("http://loinc.org", "85354-9"))  // Blood pressure
            .and(Observation.DATE.afterOrEquals().day("2023-01-01"))
            .and(Observation.PATIENT.hasChainedProperty(
                Patient.ACTIVE.exactly().code("true")))
            // Include related resources
            .include(Observation.INCLUDE_PATIENT)
            .include(Observation.INCLUDE_PERFORMER)
            // Sorting
            .sort().descending(Observation.DATE)
            // Pagination
            .count(20)
            // Return elements
            .elementsSubset("code", "effectiveDateTime", "component", "subject")
            // Execute
            .returnBundle(Bundle.class)
            .execute();

        System.out.println("Complex search found: " + results.getTotal() + " results");

        // Process results
        for (Bundle.BundleEntryComponent entry : results.getEntry()) {
            if (entry.getResource() instanceof Observation) {
                Observation obs = (Observation) entry.getResource();
                System.out.println("Observation " + obs.getId() +
                    " - Date: " + obs.getEffectiveDateTimeType().getValueAsString());
            }
        }

        // Handle pagination
        while (results.getLink(Bundle.LINK_NEXT) != null) {
            results = client.loadPage().next(results).execute();
            System.out.println("Next page: " + results.getEntry().size() + " entries");
        }
    }
}

Search Parameter Types

TypeDescriptionExample
StringText matchingname=Smith
TokenCode/identifier matchingidentifier=MRN-123
ReferenceResource referencepatient=Patient/123
DateDate/time matchingdate=gt2023-01-01
QuantityNumeric with unitsvalue-quantity=gt100
NumberSimple numericlength=gt10
URIURI matchingurl=http://example.com
CompositeCombined parameterscode-value-quantity

Search Modifiers

ModifierDescription
:exactExact string match
:containsSubstring match
:missingCheck for missing values
:notNegation
:textText search on CodeableConcept
:aboveHierarchical code search (above)
:belowHierarchical code search (below)

Explore FHIR search capabilities with these practical tutorials:

Quiz: Search and Query Operations

Question 1 of 5

What method is used to retrieve the next page of search results?