Custom Search

Intro

FHIR search has a lot of capabilities, but in some cases, there aren't enough. For example, we want to get a patient resource with an additional field 'encounters' that contains all encounters of that patient, but it's not possible to implement it with FHIR search API. Aidbox has a solution for such complex tasks, and this tutorial is about how to solve this kind of problem.

Prepare Data

We need some sample data to see results of our queries. Let's create it using Batch/Transaction. Copy the following snippet to the Aidbox.Cloud REST Console.

POST /
Accept: text/yaml
Content-Type: text/yaml

type: transaction
entry:
- resource:
    id: patient1
    name:
    - given: [Max]
      family: Turikov
  request:
    method: POST
    url: "/Patient"

- resource:
    id: patient2
    name:
    - given: [Alex]
      family: Antonov
  request:
    method: POST
    url: "/Patient"

- resource:
    id: enc1
    status: draft
    subject:
      resourceType: Patient
      id: patient1
    class:
      code: enc1
  request:
    method: POST
    url: "/Encounter"

- resource:
    id: enc2
    status: draft
    subject:
      resourceType: Patient
      id: patient1
    class:
      code: enc2
  request:
    method: POST
    url: "/Encounter"

- resource:
    id: enc3
    status: draft
    subject:
      resourceType: Patient
      id: patient2
    class:
      code: enc3
  request:
    method: POST
    url: "/Encounter"

We created 2 patients and 3 encounters that are linked to those patients.

SQL API

Aidbox uses PostgreSQL (open-source DBMS), which allows expressing very complex queries. Let's try to implement our task in SQL queries.

Let's get a list of patients. Access the DB Console of our box and run the following code snippets:

patients.sql
SELECT
id, resource_type, resource
FROM 
patient;
idresource_typeresource

patient1

Patient

{"name":[{"given":["Max"],"family":"Turikov"}]}

patient2

Patient

{"name":[{"given":["Alex"],"family":"Antonov"}]}

Next, we want to add the patient id and resource_type into resource.

patients.sql
SELECT
id, resource_type, 
    jsonb_set( 
      jsonb_set(resource, '{id}', to_jsonb(id)),
      '{resourceType}', 
      to_jsonb(resource_type))
FROM 
patient;
idresource_typeresource

patient1

Patient

{"id":"patient1","name":[{"given":["Max"],"family":"Turikov"}],"resource_type":"Patient"}

patient2

Patient

{"id":"patient2","name":[{"given":["Alex"],"family":"Antonov"}],"resource_type":"Patient"}

Curious how to work with JSON in PostgreSQL? Join our community chat.

Also, we can obtain a list of encounters for each patient:

patients-encounters.sql
SELECT
p.id AS patient_id,
e.id AS encounter_id,
e.resource AS encounter    
FROM 
patient AS p
JOIN encounter AS e
ON p.id = e.resource->'subject'->>'id';
patient_idencounter_idencounter

patient1

enc1

{"status":"draft","subject":{"id":"patient1","resourceType":"Patient"}}

patient1

enc2

{"status":"draft","subject":{"id":"patient1","resourceType":"Patient"}}

patient2

enc3

{"status":"draft","subject":{"id":"patient2","resourceType":"Patient"}}

Our next step is obtaining aggregated data by patients.

patient-encounters.sql
SELECT
p.id AS patient_id,
json_agg(e.resource::jsonb) AS encounters 
FROM 
patient AS p
JOIN encounter AS e
ON p.id = e.resource->'subject'->>'id'
GROUP BY p.id;
patient_idencounters

patient1

[{"status":"draft","subject":{"id":"patient1","resourceType":"Patient"}}, {"status":"draft","subject":{"id":"patient1","resourceType":"Patient"}}]

patient2

[{"status":"draft","subject":{"id":"patient2","resourceType":"Patient"}}]

Looks good but there's no information about the encounter id.

patients-encounters-with-ids.sql
SELECT
p.id AS patient_id,
json_agg(e.resource::jsonb) AS encounters
FROM (SELECT id, 
      resource_type,
      jsonb_set(
        jsonb_set(resource, '{id}', to_jsonb(id)),
        '{resourceType}', 
        to_jsonb(resource_type)) 
      AS resource 
      FROM patient) AS p
JOIN (SELECT id, 
      resource_type, 
      jsonb_set(
        jsonb_set(resource, '{id}', to_jsonb(id)), 
        '{resourceType}', 
        to_jsonb(resource_type)) 
      AS resource 
      FROM encounter) AS e
ON p.id = e.resource->'subject'->>'id'
GROUP BY p.id;
patient_idencounters

patient1

[{"id":"enc1","status":"draft","subject":{"id":"patient1","resourceType":"Patient"},"resource_type":"Encounter"},{"id":"enc2","status":"draft","subject":{"id":"patient1","resourceType":"Patient"},"resource_type":"Encounter"}]

patient2

[{"id":"enc3","status":"draft","subject":{"id":"patient2","resourceType":"Patient"},"resource_type":"Encounter"}]

Additionally, we added resourceType and id to the patient resource but didn't use it yet. Let's put encounters to the patient resource and take only one patient by the specified id.

patients-with-encounters-and-ids.sql
SELECT
p.id AS id,
jsonb_set(p.resource, '{encounters}', json_agg(e.resource::jsonb)::jsonb) AS resource
FROM (SELECT id, 
      resource_type,
      jsonb_set(
        jsonb_set(resource, '{id}', to_jsonb(id)),
        '{resourceType}', 
        to_jsonb(resource_type)) 
      AS resource 
      FROM patient) AS p
JOIN (SELECT id, 
      resource_type, 
      jsonb_set(
        jsonb_set(resource, '{id}', to_jsonb(id)), 
        '{resourceType}', 
        to_jsonb(resource_type)) 
      AS resource 
      FROM encounter) AS e
ON p.id = e.resource->'subject'->>'id'
GROUP BY p.id, p.resource
HAVING p.id = 'patient1';

The result should look like the following table (but without pretty printing):

idresource

patient1

{"id":"patient1",

"name":[{"given":["Max"],"family":"Turikov"}],

"encounters":[

{"id":"enc1", "status":"draft", "subject":{"id":"patient1","resourceType":"Patient"}, "resourceType":"Encounter"}, {"id":"enc2", "status":"draft", "subject":{"id":"patient1","resourceType":"Patient"}, "resourceType":"Encounter"}], "resourceType":"Patient"}

Now let's make the results of this query accessible via REST API. To do that, we need to create the AidboxQuery resource:

POST /AidboxQuery
Accept: text/yaml
Content-Type: text/yaml

id: patient-with-encounters
params:
  patient-id: {isRequired: true}
query: |
  SELECT
  jsonb_set(p.resource, '{encounters}', json_agg(e.resource::jsonb)::jsonb) AS resource
  FROM (SELECT id, 
        resource_type,
        jsonb_set(
          jsonb_set(resource, '{id}', to_jsonb(id)),
          '{resourceType}', 
          to_jsonb(resource_type)) 
        AS resource 
        FROM patient) AS p
  JOIN (SELECT id, 
        resource_type, 
        jsonb_set(
          jsonb_set(resource, '{id}', to_jsonb(id)), 
          '{resourceType}', 
          to_jsonb(resource_type)) 
        AS resource 
        FROM encounter) AS e
  ON p.id = e.resource->'subject'->>'id'
  GROUP BY p.id, p.resource
  HAVING p.id = {{params.patient-id}};

Pay attention to the end of the query: we used {{params.patient-id}} which takes the value from the request and passes it to the query securely (using PostgreSQL PREPARE statement). This means that the user of our custom search can change some parameters of the query and get different results.

Let's try it in action!

GET /$query/patient-with-encounters?patient-id=patient1
GET /$query/patient-with-encounters?patient-id=patient2

We got all the needed data in the exact shape we wanted. Additional information about custom queries can be found in REST API $query documentation.

Want to know more about Aidbox, FHIR, and custom search? Join our community chat.

Last updated