The SANER Project has been using FHIRPath to define situational awareness measures. It has need to query at least FHIR Server to evaluate data for the measure, and the server to query is something that can be defined by the implementer. Although perhaps I should say "servers" to query, because it's possible that data may be available from more than one location, as a recent commentator on our measures suggested.
For example, in some placees, laboratory data may be available from multiple sources, both the hospital lab, and a health information exchange for example. The existence of any lab data from either would be sufficient to rule a patient into (or out of) a measurement. We are using the resolve() method to resolve the content of these queries (and yes, I have taken into account pagination in my own implementation of resolve), which leads to measures that have somewhat unreadable measures for those not deeply versed in FHIR search.
Here's a couple of examples:
( %Base + 'Encounter?' +
'_include=Encounter:subject&_include=Encounter:condition&' +
'_include=Encounter:reasonReference' +
'&status=in-progress,finished' +
'&date=ge' + %ReportingPeriod.start.toString() +
'&date=lt' + %ReportingPeriod.end.toString()
).resolve().select(entry.resource)
Patient.distinct().where(
%Base + 'Observation?_count=1' +
'&status=registered,preliminary,final,amended,corrected' +
'&patient=' + $this.id +
'&date=gt' + (%ReportingPeriod.start - 14 'days').toString() +
'&code:in=' + %Covid19Labs.url +
'&value-concept:in=' + %PositiveResults.url
).resolve().select(entry.resource as Observation)
Sure, you can read these. Of course you can, but what about your staff, leadership, or customers. You'd probably have to explain that the first looks for Encounters, and their referenced subject, condition and reasonReference resources where Encounter is either in-progress or finished and the date is within the reporting period. The second is an existence test that succeeds if there is at least Observation resource for a patient where the date of the observation is up to two weeks before the reporting period, showing a positive result on a COVID lab test.
But wouldn't something like below be MUCH easier for not just you, but also for your analysts, leadership and customers to read?
findAll('Encounter',
including('subject','condition','reasonReference'),
with('status').equalTo('in-progress'|'finished'),
with('date').within(%ReportingPeriod)
).onServers(%Base)
Patient.distinct()
.whereExists('Observation',
for('patient', $this),
with('status').equalTo(
'registered' | 'preliminary' | 'final' | 'amended' | 'corrected'),
with('date').greaterThan(%ReportingPeriod.start - 14 'days'),
with('code').in(%CovidLabs),
with('value-concept).in(%PositiveResults)
).onServers(%Base)
Of course it probably would. The Reference Implementation of FHIRPath that Grahame developed provides support for custom functions, but unfortunately, custom functions don't have access (yet) to the left has side (the focus) of the expression. I'm in the process of fixing that on my fork of his FHIRPathEngine code.
Once they do, here's how I see this working. A query builder expression returns a URL that is a query being built, taking as input any part of the previously constructed query. For the most part, these are simply specialized concatenations.
There are two functions: findAll() and whereExists() that start a query builder expression. These functions return a string containing the currently built URL. findAll('resourceType') would simply return the string 'resourceType?', while whereExists(resourceType) would return 'resourceType?_count=1 (supporting an existence test).
The returned URL is evaluated by an onServers() function which is like resolve() except that onServers takes a list of base urls and the search is executed on each combination of search url and server (with pagination resolved). In the case of "whereExists()", resolution is allowed to stop after finding the first matching resource on any server (although the queries might be executed in parallel).
onServers() is effectively equivalent to resolve().select(entry.resource), but there's one little bit of extra juice, because it can do this for more than one server at a time. OnServers is the principle reason that my function set needs access to the focus, so that it can produce the cartesian product of of the URLs and the server base addresses. And if there are multiple queries, onServers could be smart enough to send a batch query or use FHIR bulk data to get it's results.
The work above is really out of scope for SANER, although we might consider including it as an appendix for others to consider (that's probably the easiest way to address a couple of issues in the ballot wrt to scope, making sure we've got it documented, but not requiring it to be used for successful implementation).
One of the values of FluentQuery is that it provides a means for someone to write a query without it being completely depedendent on how the query URL is constructed. The lightweight implementation can construct a URL as it goes (and that will be the first implementation that I write). But other implementations could do some smart things like:
- Limiting queries to what a FHIR Server supports, and handling some of the filter parameters differently.
- :in and :not-in
Not every server supports code:in queries on Observation for example. But it's a really valuable way to simplify the writing of the query. - _has
Rewriting has queries for servers that don't support them.
Resource1?_has:Resource2:name=value can be handled as first querying for Resource2?name=value&_elements=name with a post filter that collects all the references and then performs Resource2?_id=Reference1,Reference2, et cetera. - Return lists that defer pagination of results until needed.
Search Functions
findAll(ResourceType [, QueryFunctionExpression]*)
The findAll function constructs a relative search URL for the specified resource type, appending the queries specified by any of the QueryFunctionExpression values separated by an &, and returns this in a string. When executed, this query will return all results including those returned via pagination.whereExists(ResourceType [, QueryFunctionExpression]*)
The whereExists function constructs a relative search URL for the specified resource type, adds _count=1 to it, and appends the queries specified by any of the QueryFunctionExpression values separated by an &, and returns this in a string. This query is used for existence tests.
Query Execution Functions
onServers([reliability, ] Servers*)
Query Parameter Name Functions
with(Name)
The with function constructs the first half of a named query parameter. It should be followed by a Query Parameter Value Function to construct the second half (the part containing the equals sign).
for(Name, Reference|Resource|Identifier)
The for function constructs a complete query parameter that matches a resource or identifier.
If the first parameter is a reference or resource id, the query parameter is written as Name=Reference.
If the second parameter is a resource, this is converted to a reference to that resource, and treated as above.
If the second parameter is an identifier, this is converted to a reference by identifier search, and written as Name:identifer=Identifier
including(Names)
The including function specifies what other resources should be included. If any value in Names doesn't start with a resource type, it is prepended with the type of resource specified in _
0 comments:
Post a Comment