Showing posts with label XQuery. Show all posts
Showing posts with label XQuery. Show all posts

Tuesday, November 15, 2011

Implementing QueryHealth on a Collection of HL7 CCD Documents using XQuery

As promised, my next implementation of Query Health using the HL7 EQMF standard is over a collect of CCD documents.  This implementation isn't quite as cooked as the hQuery implementation, primarily because it doesn't include a "working infrastructure".  Instead, it relies on a conceptual infrastructure that assumes that you have a collection of CCD documents, and that you want to product results from those.  I haven't yet generated a collection of CCD documents against which this work has been tested, so what you see in this post might not quite work (yet).

As I mentioned last night, I discovered last week that my favorite XML Editor has an XQuery debugger built in.  Since XQuery is designed to support querying of collections of XML Documents, I thought it would be an excellent implementation platform for querying a collection of CDA document (which are of course in XML).

I reused the basic structure of the hQuery implementation XSLT, because I figured a lot of the features would be the same:  declarations for parameters, predicates, and query functions (numerator, denominator, et cetera).

The very first part of my XSLT prepares an XML element that collects up all the value sets used in the EQMF content:


        <result>
            <xsl:variable name="valueSets" select="set:distinct(//*/@valueSet)"/>
            <collection xml:id="terms">
                <xsl:for-each select="$valueSets">
                    <doc href="https://example.com/RetrieveValueSet?id={.}"/>
                </xsl:for-each>    
            </collection>

As you can see, it just iterates over the distinct value sets appearing in any valueSet attribute, and produces the following chunk of XML:
   <collection xml:id="terms">
      <doc href="https://example.com/RetrieveValueSet?id=2.16.840.1.113883.3.464.1.42"/>
      <doc href="https://example.com/RetrieveValueSet?id=2.16.840.1.113883.3.464.1.1142"/>
      <doc href="https://example.com/RetrieveValueSet?id=2.16.840.1.113883.3.464.1.37"/>
      <doc href="https://example.com/RetrieveValueSet?id=2.16.840.1.113883.3.464.1.67"/>
      <doc href="https://example.com/RetrieveValueSet?id=2.16.840.1.113883.3.464.1.98"/>
      <doc href="https://example.com/RetrieveValueSet?id=2.16.840.1.113883.3.464.1.113"/>
      <doc href="https://example.com/RetrieveValueSet?id=2.16.840.1.113883.3.464.1.72"/>
      <doc href="https://example.com/RetrieveValueSet?id=2.16.840.1.113883.3.464.1.94"/>
   </collection>

This is just a set of references (links) to value sets being served up using the IHE SVS (pdf) profile as I mentioned in previous posts.  Later, in the XQuery code, I reference this collection using the following variable declaration:
let $terms := fn:collection("#terms")

Since #terms is a fragment identifier in a local URI, it means: the XML element with the ID of terms.  The XML generated above uses xml:id to add that identifier schemalessly to the XML content.  I don't know if my XQuery processor supports that sort of referencing, but other XML processors do.  I used this trick with Self Displaying CDA previously to point to a CSS stylesheet inside a CDA document. 

The next chunk of XSLT  that I wrote created variables for the query parameters:
    <xsl:for-each
      select="$DataDeclarationSection//emf:entry[
        emf:localVariableName != '']/emf:observation[@moodCode='EVN' 
        and not(@isCriterionInd='true')]">
      <xsl:text>
        let $</xsl:text><xsl:value-of select="../emf:localVariableName"/>
        <xsl:text>:=</xsl:text>            
        <xsl:copy-of select="emf:value"/>            
    </xsl:for-each>
 
All this did was declare local variables defined in the EQMF document in the XQuery code, using the XML that was already present in the EQMF document.  This is a result of xml inside an XQuery expression being essentially a "constant" expression having the value of that XML.  Here is what the output looked like:

  let $StartDate:=<value xmlns="urn:hl7-org:v3" xsi:type="TS" value="20100101"/>
  let $EndDate:=<value xmlns="urn:hl7-org:v3" xsi:type="TS" value="20101231"/>

Then I had to deal with data element declarations:
    <xsl:for-each 
      select="$DataDeclarationSection//emf:entry/emf:*[@isCriterionInd='true']">
      <xsl:text>
    declare function local:</xsl:text>
      <xsl:value-of select="../emf:localVariableName"/>
      <!-- Generate a name if one isn't given -->
      <xsl:if test="not(../emf:localVariableName)">
        <xsl:value-of select="generate-id(.)"/>
      </xsl:if>
      <xsl:text>($ids, $ccds) {
      for $d in local:</xsl:text>
      <xsl:value-of 
        select="emf:sourceOf[@typeCode='INST']/*/emf:id/@extension"/>
      <xsl:text>($ids, $ccd)
                where local:matches($d, </xsl:text>
      <xsl:copy-of select="."/>
      <xsl:text>)
                return $e//cda:recordTarget/cda:id
    }
      </xsl:text>
    </xsl:for-each>

What this does is loop through each data declaration and create a function of the form:
declare function local:functionName($ids, $ccds) {
    for $e in local:modelElementName($ids, $ccds)
    where local:matches($e, DateElementCriterion)
    return $e//cda:recordTarget/cda:id
}

The functionName is supplied by the localVariableName for the criterion.  The modelElementName comes from the identifiers I used in the definitions for the criterion model elements. And the DataElementCritierion comes from the XML for the criteria.  A completed one looks like something this:

    declare function local:ageBetween17and64($ids, $ccds) {
      for $d in local:Demographics($ids, $ccd)
      where local:matches($e, 
  <observation xmlns="urn:hl7-org:v3" 
    classCode="OBS" moodCode="EVN" isCriterionInd="true">
      <id root="0" extension="ageBetween17and64"/>
      <code code="424144002" 
        codeSystem="2.16.840.1.113883.6.96" displayName="Age"/>
      <value xsi:type="IVL_PQ">
         <low value="17" unit="a"/>
         <high value="64" unit="a"/>
      </value>
   </observation>)
      return $e//cda:recordTarget/cda:id
    }
 
The critical component here is the "matches" function, which I haven't yet defined.  That function would look at the components of the input element and compare them to the criteria in the criterion element, and return true if they matched.  It's a complex function, but mostly filled with if then else logic to check matches on dates, codes and values.

The next template declares functions for the population, numerator, denominator, and exclusion functions:
 
    <xsl:template name="declareFunction">
        <xsl:param name="name"/>
        <xsl:param name="entries"/>
        <xsl:text>
    declare function local:</xsl:text><xsl:value-of select="$name"/>
        <xsl:text>($ccds, $ids) {
        return 
        </xsl:text>
        <xsl:call-template name="processEntries">
            <xsl:with-param name="entries" select="$entries"/>
        </xsl:call-template>
        <xsl:text>
    }
        </xsl:text>
    </xsl:template>

The general structure of this function is:
  declare function local:population($ccds, $ids) {
    return logicalExpression 
  }
    
The logical expression is computed by the process entries template by walking the precondition tree in the EQMF.

    <xsl:template name="processEntries">
      <xsl:param name="entries"/>
      <xsl:if test="$entries[
                emf:conjunctionCode/@code='AND' or not(emf:conjunctionCode)]">
        <xsl:text>(</xsl:text>
        <xsl:call-template name="and">
          <xsl:with-param name='args' select="$entries[emf:conjunctionCode/@code='AND' or not(emf:conjunctionCode)]"/>
        </xsl:call-template>
        <xsl:text>)</xsl:text>
        <xsl:if test="$entries[emf:conjunctionCode/@code != 'AND']">
          <xsl:text> and </xsl:text>
        </xsl:if>
      </xsl:if>
          ... similar code for OR and XOR ...
    </xsl:template>

It groups the preconditions by their conjunctionCode (AND, OR and XOR), calls a template that generates the appropriate logical expression, and inserts, if necessary, the an "and" between each of the conjunctionCode groups.  The "and" template shown builds the logical expression from the list of preconditions.  It dumps out the first expression, and if there are any remaining, inserts the "and" operator, and dumps out the remainder using a classical XSLT tail recursive function.

    <xsl:template name="and">
      <xsl:param name="args"/>
      <xsl:variable name="first" select="$args[1]"/>
      <xsl:variable name="rest" select="$args[position() != 1]"/>
      <xsl:apply-templates select="$first"/>
      <xsl:if test='$rest'>
        <xsl:text> and </xsl:text>
          <xsl:call-template name="and">
            <xsl:with-param name="args" select="$rest"/>
        </xsl:call-template>
      </xsl:if>
    </xsl:template>

The "precondition" processing template, is based on the same code used for the hQuery implementation, with just a small bit of reconstruction to use XQuery expressions instead of JavaScript.
    
    <xsl:template match="emf:sourceOf[@typeCode='PRCN']">
      <xsl:variable name="act"
        select="emf:act|emf:observation|emf:encounter|emf:procedure|
            emf:substanceAdministration|emf:supply"/>
      <xsl:variable name="declaration"
        select="$DataDeclarationSection/emf:entry/*[
          $act/emf:id/@root = emf:id/@root and 
          $act/emf:id/@extension = emf:id/@extension]"/>
      <xsl:if test="$declaration">
        <xsl:choose>
          <xsl:when test="$act/../@negationInd = 'true' and
                          $act/@actionNegationInd = 'true'"></xsl:when>
          <xsl:when test="$act/@actionNegationInd = 'true'">not(</xsl:when>
          <xsl:when test="$act/../@negationInd = 'true'">not(</xsl:when>
          <xsl:otherwise></xsl:otherwise>
        </xsl:choose>
        <xsl:text>local:</xsl:text>
        <xsl:value-of select="$declaration/../emf:localVariableName"/>
        <xsl:if test="not($declaration/../emf:localVariableName)">
          <xsl:value-of select="generate-id($declaration)"/>
        </xsl:if>
        <xsl:text>($ccds, $ids)</xsl:text>
        <xsl:choose>
          <xsl:when test="$act/../@negationInd = 'true' and
                          $act/@actionNegationInd = 'true'"></xsl:when>
          <xsl:when test="$act/@actionNegationInd = 'true'">)</xsl:when>
          <xsl:when test="$act/../@negationInd = 'true'">)</xsl:when>
          <xsl:otherwise></xsl:otherwise>
        </xsl:choose>
      </xsl:if>
      <xsl:if test="$act/emf:sourceOf[@typeCode=&quot;PRCN&quot;]">
        <xsl:call-template name="processEntries">
          <xsl:with-param name="entries" 
            select="$act/emf:sourceOf[@typeCode='PRCN']"/>
        </xsl:call-template>
      </xsl:if>
    </xsl:template>

The output of this code looks something like this in the XQuery result (without all the pretty indenting yet...):

    declare function local:population($ccds, $ids) {
        return local:ageBetween17and64($ccds, $ids)
    }
        
    declare function local:numerator($ccds, $ids) {
        return local:HbA1C($ccds, $ids)
    }
        
    declare function local:denominator($ccds, $ids) {
        return 
          ( 
            local:HasDiabetes($ccds, $ids)
          )
            and 
          ( 
            local:EDorInpatientEncounter($ccds, $ids) or
            local:AmbulatoryEncounter($ccds, $ids)) or
            local:DiabetesMedAdministered($ccds, $ids) or
            local:DiabetesMedIntended($ccds, $ids) or 
            local:DiabetesMedSupplied($ccds, $ids) or 
            local:DiabetesMedOrdered($ccds, $ids)
          )
    }
        
    declare function local:exclusion($ccds, $ids) {
        return 
        
          ( 
            local:HasPolycysticOvaries($ccds, $ids) and 
            not(local:HasDiabetes($ccds, $ids))
          ) or 
          local:HasSteroidInducedDiabetes($ccds, $ids) or 
          local:HasGestationalDiabetes($ccds, $ids)
        )
    }

The last piece of this is how the result is computed.  Here it is:
    let $ccds := fn:collection("file:///.?select=*.xml")
    let $ids := 
      $ccds/cda:ClinicalDocument/cda:recordTarget/cda:id[
        preceding::cda:id/@root != current()/@root and
        string(preceding::cda:id/@extension) != string(current()/@extension)
    ]
    <population>
    { local:population($ccds, $ids) }
    </population>
    <numerator>
    { local:numerator($ccds, $ids) }
    </numerator>
    <dednominator>
    { local:denominator($ccds, $ids) }
    </dednominator>
    <exclusion>
    { local:exclusion($ccds, $ids) }
    </exclusion>

This simply returns the list of <id> elements for which there are a match in the different categories.  From this, you can compute the count of patients that meet the criteria.  In a real world implementation, you wouldn't actually return the patient identifiers, you'd just return the counts.

The first two variables are what is processed.  I've show initializing the collection of the ccd documents using the XPath 2.0 collection function.  The second variable collects up the unique ids of all patients.  It is patients, and not ccds that we are counting.

So, there you have a description of how my XQuery implementation is created from the HL7 EQMF declaration.  More code is actually needed to make this one work.  And when I have some tested and working coded, I'll post a link to it just like I did for the hQuery implementation.

Next up is a SQL Table implementation.  For that, I'll start tomorrow with a post on the Clinical Data modeling needed in the database.  It turns out, as I said yesterday, that the models needed for all three implementations are very simple.

A Short Primer on XQuery

I was first introduced to the XQuery standard over 10 years ago when it was first in development as a draft W3C Standard.  Back then, it was a mess and I swore I would never use it for anything.  It's ten years later, and what finally emerged looks nothing like those early drafts.  I discovered last week that my favorite XML Editor has an XQuery debugger built in, so I decided that I would use it as the basis for my next demonstration of implementing Query Health over the HL7 Quality Measure Format standard.  I'm still in the process of building that implementation (as you might recall, I plan on querying a collection of CCD documents in that implementation).

So, I figured that it was probably time to learn XQuery, and while I'm learning, you can learn with me.  The first query is very simple:

<result xmlns:cda="urn:org-hl7:v3">
{  /cda:ClinicalDocument/cda:recordTarget/cda:patientRole/cda:id
}
</result>

The query results an XML <result> element that is comprised of a copy of all XML elements matching the XPath expression in between the { and } signs.  I could have use any element name to wrap the results, because XQuery is like XSLT in that it enables you to construct XML entities as results using declarative expressions.  What I learned here is that any XPath expression could be an XQuery expression.

The next thing I learned about was let statements.  The syntax of the let statement is also pretty straight-forward:

let $variableName := expression


So, if you want to declare variables, you can do that to.  The expression can be just about any XQuery expression.  You can put a bunch of let statements together using commas.

So far, so good.  Now let's do something with the if statement:
if (expression) then expression else expression


So, now we can make some simple decisions.  Next is dealing with complex selections.  XQuery was intended to be to XML as SQL-based report writing languages were to SQL Databases.  So, you need something like the SQL "SELECT WHERE" clauses.  This uses the "FLWOR" structure.  FLWOR stands for "for, let, where, order by, return".  Actually the order is let, then for, but LFWOR is harder to say than FLWOR.

let $v = expression
for $v2 in expression2
where expression3
order by expression4
return expression5

The for clause iterates over each element returned by expression2, assigning it to $v2.  The optional where clause selects those items for which expression3 evaluates to true.  The optional order by clause provides an expression which can be use to sort the returned results.  The return clause indicates what elements are returned.

Let's look at a simple example.
let $v := fn:doc("myccd.xml"), 
    $o := "2.16.840.1.113883.19.5.1091"
for $i in $v//cda:id
where $i/@root = $o
order by $i/@extension
return $i/..

This creates two variables, one named $v for a CCD document, and another one named $o containing a particular OID.  Then for each cda:id element in the document, it finds those where the OID of the id  (the root attribute of the id element) matches the particular OID I mentioned.  Then, it returns the element that contained that id.  Then entire set of items returned is ordered by the extension attribute.
 
One cool thing about XQuery is that expressions can be simple XPath expressions,  static XML, or XQuery statements, or various combinations of the three.  Another way I could have set $o is as follows:

let $o := <cda:id root='2.16.840.1.113883.19.5.1091'>/@root

It makes for complicated reading at times (especially when writing XSLT to produce an XQuery program).  One thing that can make your XQuery program simpler are functions.  XQuery and XPath 2.0 define over 100 functions already that you can use.  But you can also create your own functions using the following syntax:

declare function local:functionName($arg1, $arg2) {
    expression
}

Believe it or not, this is almost all the XQuery that you need to know to build an implementation of a Query created from an EQMF input.

There are two important XPath (2.0) functions that you will also need: fn:doc() and fn:collection()
The first works like the document() function described in XSLT 1.0. It takes a URI as an argument and returns a node set containing the XML document at that URI.  That's really usefull for accessing value sets using the SVS RESTful API.

The collection() function also takes a URI, but it can return a node set containing multiple XML documents.  That's the key to processing a collection of CCD documents, because it is that function that will help us access the set of CCD documents to process.

I'm not quite ready to show the work on the XQuery implementation, but I will say this:  It turns out that only a very simple model of patient data will be needed across all of the hQuery, XQuery and SQL implementations that I manage to put together.  That model is one where you have a patient associated with a number of demographic fields (e.g., addresses, gender, age, et cetera), and links from them to collections of encounters, procedures,  problems, medications (and immunizations), results (labs and vital signs) and other observations.  So, I've stopped looking for publicly available SQL database models for clinical data, and will just use a brain-dead simple model.