FHIR Programming using Java and HAPI FHIR Server - Searching FHIR Resources

Introduction

Welcome back to my series on FHIR Programming using Java and HAPI FHIR Server. In this article, we will explore how to search for FHIR resources using Java. Searching for resources is a fundamental operation when working with healthcare data, allowing you to retrieve specific pieces of information based on various criteria.

This tutorial builds upon our previous discussions on deleting FHIR resources. If you're unfamiliar with the previous operations in this series, I recommend reviewing those articles before proceeding. By the end of this article, you'll be equipped to search and retrieve FHIR resources efficiently, enabling you to access the data you need for your healthcare applications.

FHIR search is one of the most powerful features of the standard. Before diving into code, let's understand the comprehensive search capabilities FHIR provides.

Search Parameter Types

FHIR defines several types of search parameters, each designed for specific data types:

  • string - For text searches (names, addresses). By default, performs case-insensitive, accent-insensitive matching from the start of the string.
    • Example: Patient?name=john matches "John", "Johnny", "Johnston"
  • token - For coded values and identifiers. Matches exact code values, optionally scoped by system.
    • Example: Patient?identifier=http://hospital.org|12345
    • Example: Patient?gender=male
  • reference - For references to other resources. Can search by resource type and ID.
    • Example: Observation?subject=Patient/123
  • date - For date/time values. Supports precision levels and ranges.
    • Example: Patient?birthdate=1980-01-01
  • quantity - For numerical quantities with units. Matches value, system, and code.
    • Example: Observation?value-quantity=5.4|http://unitsofmeasure.org|mg
  • number - For numerical values without units.
    • Example: RiskAssessment?probability=0.5
  • uri - For URI values. Matches the full URI exactly.
    • Example: ValueSet?url=http://hl7.org/fhir/ValueSet/example
  • composite - Combines multiple parameters into a single search. Useful for searching multi-component values.
    • Example: Observation?component-code-value-quantity=http://loinc.org|8480-6$gt100

Search Modifiers

Modifiers change the behavior of search parameters:

  • :exact - For string parameters: matches the entire string exactly (case-sensitive)
    • Example: Patient?name:exact=John matches only "John", not "john" or "Johnny"
  • :contains - For string parameters: matches if the value appears anywhere in the target
    • Example: Patient?name:contains=onn matches "John", "Donna", "Connor"
  • :missing - Tests whether the element is present or absent
    • Example: Patient?birthdate:missing=true finds patients without birth dates
  • :not - For token parameters: negates the search (finds resources that don't match)
    • Example: Patient?gender:not=male finds non-male patients
  • :text - For token parameters: searches the display text rather than the code
    • Example: Condition?code:text=headache
  • :above/:below - For token parameters on hierarchical code systems: matches codes in the hierarchy
    • Example: Condition?code:below=http://snomed.info/sct|73211009 (diabetes and all subtypes)

Search Prefixes for Comparisons

For date, number, and quantity parameters, prefixes specify comparison operators:

  • eq - Equal (default if no prefix specified)
  • ne - Not equal
  • gt - Greater than
  • lt - Less than
  • ge - Greater than or equal
  • le - Less than or equal
  • sa - Starts after (for periods)
  • eb - Ends before (for periods)
  • ap - Approximately equal (the acceptable range is determined by the server implementation)
// Find patients born after 1980
client.search()
    .forResource(Patient.class)
    .where(Patient.BIRTHDATE.afterOrEquals().day("1980-01-01"))
    .returnBundle(Bundle.class)
    .execute();

// Find observations with values greater than 100
client.search()
    .forResource(Observation.class)
    .where(Observation.VALUE_QUANTITY.quantity().greaterThan().number(100))
    .returnBundle(Bundle.class)
    .execute();

Search Result Parameters

These parameters control what's included in search results:

  • _include - Include referenced resources in the result bundle
    • Example: MedicationRequest?_include=MedicationRequest:patient
    • Returns MedicationRequests AND their referenced Patients
  • _revinclude - Include resources that reference the matched resources
    • Example: Patient?_revinclude=Observation:subject
    • Returns Patients AND Observations that reference those Patients
  • _summary - Return only a subset of elements (true, false, text, count, data)
    • _summary=true returns only elements marked as "summary"
    • _summary=count returns only the total count, no actual resources
  • _elements - Specify exactly which elements to return
    • Example: Patient?_elements=name,birthDate,gender
  • _sort - Sort results by a search parameter
    • Example: Patient?_sort=birthdate (ascending)
    • Example: Patient?_sort=-birthdate (descending)
  • _count - Limit the number of results per page
  • _total - Control whether total count is returned (none, estimate, accurate)

Compartment Searches

Compartments define logical groupings of resources related to a specific resource. The most common is the Patient compartment, which groups all resources related to a patient:

// Search all observations in a patient's compartment
// This finds all Observations where subject=Patient/123
Bundle results = client.search()
    .forResource(Observation.class)
    .withAdditionalHeader("Content-Type", "application/fhir+json")
    .byUrl("Patient/123/Observation")
    .returnBundle(Bundle.class)
    .execute();

Common compartments include:

  • Patient - All resources related to a patient
  • Practitioner - All resources related to a practitioner
  • Encounter - All resources related to an encounter
  • RelatedPerson - All resources related to a related person
  • Device - All resources related to a device

Compartment searches are efficient ways to retrieve all data related to a specific entity, which is common in clinical workflows.

Chained and Reverse Chained Searches

Chained searches allow you to filter based on properties of referenced resources:

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

Reverse chained searches (using _has) find resources that are referenced by other resources matching criteria:

// Find patients who have observations with a specific code
// Patient?_has:Observation:subject:code=http://loinc.org|8867-4
client.search()
    .forResource(Patient.class)
    .revInclude(Observation.class, "subject")
    .where(/* complex _has criteria */)
    .returnBundle(Bundle.class)
    .execute();

Prerequisites

Before you start, ensure your environment is ready:

  • Java Development Kit (JDK) is installed and configured.
  • Apache Maven is installed and set up for your project.
  • HAPI FHIR Server is running and accessible.
  • You can find all the code demonstrated in this tutorial on GitHub here

“Knowledge is power. Information is liberating. Education is the premise of progress, in every society, in every family.” ~ Kofi Annan

Step 1 of 4: Import Required Classes

To search for FHIR resources, we need to import specific classes from the HAPI FHIR library. These imports will allow us to interact with the FHIR server and perform search operations. Open your `App.java` file and include the following imports:

package com.saravanansubramanian.fhir;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.gclient.ReferenceClientParam;
import ca.uhn.fhir.rest.gclient.StringClientParam;
import ca.uhn.fhir.rest.gclient.TokenClientParam;
import ca.uhn.fhir.rest.gclient.DateClientParam;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import org.hl7.fhir.r4.model.*;

import java.util.List;

These imports provide access to the FHIR context and client classes required for communication with the FHIR server, as well as various parameter types and models that facilitate searching for FHIR resources.

Step 2 of 4: Basic Search Operations

Now that we have our imports set up, let's start with basic search operations. The following example demonstrates how to search for `Patient` resources by name and other simple criteria:

public class BasicSearchExample {
    public static void main(String[] args) {
        // Initialize FHIR context
        FhirContext ctx = FhirContext.forR4();

        // Create a client to interact with the FHIR server
        IGenericClient client = ctx.newRestfulGenericClient("http://localhost:8080/fhir");

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

            // Print the search results
            System.out.println("Search Results by Family Name 'Smith':");
            System.out.println("Found " + results.getTotal() + " patient(s)");
            
            printPatientResults(ctx, results);
            
            // Search for patients by given name
            Bundle givenNameResults = client.search()
                .forResource(Patient.class)
                .where(Patient.GIVEN.matches().value("John"))
                .returnBundle(Bundle.class)
                .execute();
                
            System.out.println("\nSearch Results by Given Name 'John':");
            System.out.println("Found " + givenNameResults.getTotal() + " patient(s)");
            
            printPatientResults(ctx, givenNameResults);
            
            // Search for patients by identifier
            Bundle identifierResults = client.search()
                .forResource(Patient.class)
                .where(Patient.IDENTIFIER.exactly().systemAndCode(
                    "http://hospital.org/mrns", "12345"))
                .returnBundle(Bundle.class)
                .execute();
                
            System.out.println("\nSearch Results by Medical Record Number '12345':");
            System.out.println("Found " + identifierResults.getTotal() + " patient(s)");
            
            printPatientResults(ctx, identifierResults);
            
        } catch (Exception e) {
            System.err.println("Error performing search: " + e.getMessage());
            e.printStackTrace();
        }
    }
    
    private static void printPatientResults(FhirContext ctx, Bundle results) {
        int count = 0;
        for (Bundle.BundleEntryComponent entry : results.getEntry()) {
            Resource resource = entry.getResource();

            // Safe type check using instanceof with pattern matching
            if (resource instanceof Patient patient) {
                count++;
                System.out.println("\nPatient " + count + ":");
                System.out.println("ID: " + patient.getIdElement().getIdPart());

                if (patient.hasName() && !patient.getName().isEmpty()) {
                    HumanName name = patient.getNameFirstRep();
                    String givenNames = name.hasGiven()
                        ? String.join(" ", name.getGiven().stream().map(StringType::getValue).collect(java.util.stream.Collectors.toList()))
                        : "";
                    System.out.println("Name: " + name.getFamily() + ", " + givenNames);
                }

                if (patient.hasIdentifier()) {
                    Identifier identifier = patient.getIdentifierFirstRep();
                    System.out.println("Identifier: " +
                        (identifier.hasSystem() ? identifier.getSystem() + "|" : "") +
                        identifier.getValue());
                }

                if (patient.hasGender()) {
                    System.out.println("Gender: " + patient.getGender().getDisplay());
                }

                if (patient.hasBirthDate()) {
                    System.out.println("Birth Date: " + patient.getBirthDate());
                }
            }
        }
    }
}

This code demonstrates three basic search operations: searching for patients by family name, given name, and identifier. The `printPatientResults` helper method formats the search results for better readability. These basic search capabilities allow you to quickly retrieve patient information based on common criteria.

Step 3 of 4: Advanced Search Operations

Beyond basic searches, FHIR supports more advanced search techniques. The following example shows how to search for resources using multiple criteria, date ranges, and chained searches:

public class AdvancedSearchExample {
    public static void main(String[] args) {
        // Initialize FHIR context
        FhirContext ctx = FhirContext.forR4();

        // Create a client to interact with the FHIR server
        IGenericClient client = ctx.newRestfulGenericClient("http://localhost:8080/fhir");

        try {
            // Search for patients with multiple criteria (AND logic)
            Bundle multiCriteriaResults = client.search()
                .forResource(Patient.class)
                .where(Patient.FAMILY.matches().value("Smith"))
                .and(Patient.GENDER.exactly().code("male"))
                .returnBundle(Bundle.class)
                .execute();
                
            System.out.println("Search Results - Male Patients with Family Name 'Smith':");
            System.out.println("Found " + multiCriteriaResults.getTotal() + " patient(s)");
            printPatientResults(ctx, multiCriteriaResults);
            
            // Search with date range
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            Date startDate = sdf.parse("1980-01-01");
            Date endDate = sdf.parse("1990-12-31");
            
            DateRangeParam dateRange = new DateRangeParam()
                .setLowerBoundInclusive(startDate)
                .setUpperBoundInclusive(endDate);
                
            Bundle dateRangeResults = client.search()
                .forResource(Patient.class)
                .where(Patient.BIRTHDATE.afterOrEquals().day("1980-01-01"))
                .and(Patient.BIRTHDATE.beforeOrEquals().day("1990-12-31"))
                .returnBundle(Bundle.class)
                .execute();
                
            System.out.println("\nSearch Results - Patients Born Between 1980 and 1990:");
            System.out.println("Found " + dateRangeResults.getTotal() + " patient(s)");
            printPatientResults(ctx, dateRangeResults);
            
            // Chained search - find Observations for a specific Patient
            Bundle chainedResults = client.search()
                .forResource(Observation.class)
                .where(Observation.SUBJECT.hasChainedProperty(
                    Patient.IDENTIFIER.exactly().systemAndCode("http://hospital.org/mrns", "12345")))
                .returnBundle(Bundle.class)
                .execute();
                
            System.out.println("\nSearch Results - Observations for Patient with MRN '12345':");
            System.out.println("Found " + chainedResults.getTotal() + " observation(s)");
            
            printObservationResults(ctx, chainedResults);
            
        } catch (Exception e) {
            System.err.println("Error performing advanced search: " + e.getMessage());
            e.printStackTrace();
        }
    }
    
    private static void printPatientResults(FhirContext ctx, Bundle results) {
        // Same implementation as in the previous example
        // ...
    }
    
    private static void printObservationResults(FhirContext ctx, Bundle results) {
        int count = 0;
        for (Bundle.BundleEntryComponent entry : results.getEntry()) {
            Resource resource = entry.getResource();

            // Safe type check using instanceof with pattern matching
            if (resource instanceof Observation obs) {
                count++;
                System.out.println("\nObservation " + count + ":");
                System.out.println("ID: " + obs.getIdElement().getIdPart());

                if (obs.hasCode() && obs.getCode().hasText()) {
                    System.out.println("Code: " + obs.getCode().getText());
                } else if (obs.hasCode() && obs.getCode().hasCoding()) {
                    Coding coding = obs.getCode().getCodingFirstRep();
                    System.out.println("Code: " +
                        (coding.hasDisplay() ? coding.getDisplay() : coding.getCode()));
                }

                if (obs.hasEffectiveDateTimeType()) {
                    System.out.println("Effective Date: " + obs.getEffectiveDateTimeType().getValue());
                }

                if (obs.hasValueQuantity()) {
                    Quantity quantity = obs.getValueQuantity();
                    System.out.println("Value: " + quantity.getValue() + " " + quantity.getUnit());
                } else if (obs.hasValueCodeableConcept()) {
                    System.out.println("Value: " + obs.getValueCodeableConcept().getText());
                }

                if (obs.hasSubject()) {
                    System.out.println("Subject: " + obs.getSubject().getReference());
                }
            }
        }
    }
}

This example demonstrates advanced search capabilities, including combining multiple criteria with AND logic, searching within date ranges, and performing chained searches across related resources. These techniques allow for more precise and complex queries, enabling you to retrieve exactly the data you need.

Step 4 of 4: Paginated Search and Search Parameters

When working with large datasets, pagination becomes essential. Additionally, FHIR provides many search parameters for fine-tuning your queries. The following example demonstrates these capabilities:

public class PaginatedSearchExample {
    public static void main(String[] args) {
        // Initialize FHIR context
        FhirContext ctx = FhirContext.forR4();

        // Create a client to interact with the FHIR server
        IGenericClient client = ctx.newRestfulGenericClient("http://localhost:8080/fhir");

        try {
            // Search with pagination - limit to 5 results per page
            Bundle firstPageResults = client.search()
                .forResource(Patient.class)
                .count(5)  // Set page size
                .returnBundle(Bundle.class)
                .execute();
                
            System.out.println("First Page of Results:");
            System.out.println("Total available: " + firstPageResults.getTotal() + " patient(s)");
            System.out.println("Results on this page: " + firstPageResults.getEntry().size());
            
            // Process first page results
            printPatientResults(ctx, firstPageResults);
            
            // Check if there are more pages
            if (firstPageResults.getLink(Bundle.LINK_NEXT) != null) {
                System.out.println("\nLoading next page...");
                
                // Load the next page of results
                Bundle secondPageResults = client.loadPage()
                    .next(firstPageResults)
                    .execute();
                    
                System.out.println("\nSecond Page of Results:");
                System.out.println("Results on this page: " + secondPageResults.getEntry().size());
                
                // Process second page results
                printPatientResults(ctx, secondPageResults);
            }
            
            // Search with sorting
            Bundle sortedResults = client.search()
                .forResource(Patient.class)
                .sort().ascending(Patient.FAMILY)
                .returnBundle(Bundle.class)
                .execute();
                
            System.out.println("\nPatients Sorted by Family Name (Ascending):");
            System.out.println("Found " + sortedResults.getTotal() + " patient(s)");
            
            printPatientResults(ctx, sortedResults);
            
            // Search with includes - include Patient's managing organization
            Bundle includedResults = client.search()
                .forResource(Patient.class)
                .include(Patient.INCLUDE_ORGANIZATION.asNonRecursive())
                .returnBundle(Bundle.class)
                .execute();
                
            System.out.println("\nPatients with Included Organizations:");
            System.out.println("Found " + includedResults.getEntry().size() + " resources");
            
            // Process included results
            int patientCount = 0;
            int orgCount = 0;
            
            for (Bundle.BundleEntryComponent entry : includedResults.getEntry()) {
                if (entry.getResource() instanceof Patient) {
                    patientCount++;
                } else if (entry.getResource() instanceof Organization) {
                    orgCount++;
                }
            }
            
            System.out.println("Patients: " + patientCount);
            System.out.println("Organizations: " + orgCount);
            
        } catch (Exception e) {
            System.err.println("Error performing paginated search: " + e.getMessage());
            e.printStackTrace();
        }
    }
    
    private static void printPatientResults(FhirContext ctx, Bundle results) {
        // Same implementation as in the previous examples
        // ...
    }
}

This example demonstrates pagination through large result sets, sorting search results, and including related resources in the search results. These features are crucial for handling large amounts of data efficiently and retrieving complex related data in a single request.

Conclusion

In this article, we've explored the process of searching for FHIR resources using Java and the HAPI FHIR library. We covered basic searches using simple criteria, advanced searches with multiple parameters and chained relationships, and pagination techniques for handling large datasets. These search capabilities are vital for accessing and managing healthcare data within your applications.

With the knowledge gained from this tutorial, you can now efficiently retrieve the data you need from your FHIR server. Stay tuned for the next article in this series, where we will explore validating FHIR resources to ensure data quality and conformance.