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.
Understanding FHIR Search
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=johnmatches "John", "Johnny", "Johnston"
- Example:
- 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
- Example:
- reference - For references to other resources. Can search by resource type and ID.
- Example:
Observation?subject=Patient/123
- Example:
- date - For date/time values. Supports precision levels and ranges.
- Example:
Patient?birthdate=1980-01-01
- Example:
- quantity - For numerical quantities with units. Matches value, system, and code.
- Example:
Observation?value-quantity=5.4|http://unitsofmeasure.org|mg
- Example:
- number - For numerical values without units.
- Example:
RiskAssessment?probability=0.5
- Example:
- uri - For URI values. Matches the full URI exactly.
- Example:
ValueSet?url=http://hl7.org/fhir/ValueSet/example
- 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
- Example:
Search Modifiers
Modifiers change the behavior of search parameters:
- :exact - For string parameters: matches the entire string exactly (case-sensitive)
- Example:
Patient?name:exact=Johnmatches only "John", not "john" or "Johnny"
- Example:
- :contains - For string parameters: matches if the value appears anywhere in the target
- Example:
Patient?name:contains=onnmatches "John", "Donna", "Connor"
- Example:
- :missing - Tests whether the element is present or absent
- Example:
Patient?birthdate:missing=truefinds patients without birth dates
- Example:
- :not - For token parameters: negates the search (finds resources that don't match)
- Example:
Patient?gender:not=malefinds non-male patients
- Example:
- :text - For token parameters: searches the display text rather than the code
- Example:
Condition?code:text=headache
- Example:
- :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)
- Example:
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
- Example:
- _revinclude - Include resources that reference the matched resources
- Example:
Patient?_revinclude=Observation:subject - Returns Patients AND Observations that reference those Patients
- Example:
- _summary - Return only a subset of elements (true, false, text, count, data)
_summary=truereturns only elements marked as "summary"_summary=countreturns only the total count, no actual resources
- _elements - Specify exactly which elements to return
- Example:
Patient?_elements=name,birthDate,gender
- Example:
- _sort - Sort results by a search parameter
- Example:
Patient?_sort=birthdate(ascending) - Example:
Patient?_sort=-birthdate(descending)
- Example:
- _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.