Improved Documentation of the $closure operation for #FHIR Connectathon 11

Dec 22, 2015

One of the stated goals of FHIR Connectathon 11 in Orlando in a few weeks time is for the terminology services stream participants to test out the $closure operation. The $closure operation is an important one because it enables terminology consumers to close the loop – to integrate terminological based reasoning into their internal search capabilities, without having to split the search across multiple technologies – which is a real challenge. For additional documentation about the closure table process, see http://hl7.org/fhir/terminology-service.html#closure

However, up to now, the terminology connectathons have focused on other parts of the terminology services, and $closure hasn’t been tested – or even implemented.

In preparation for the Connectathon, I’ve implemented the $closure operation. Here’s some additional API documentation for the $closure operation, which will be the basis for the connectathon. Note that all this documentation is in JSON, but XML should work the same for servers that support XML.

Note: this is additional API documentation for connectathon implementers. Before using the specific documentation below, you should be familiar with the $closure operation concepts and description.

To help connectathon participants, I’ve published both a client and server that implement this operation. The server is at http://fhir2.healthintersections.com.au/open, and the client is in my ValueSet editor (Tools…Closure Manager). Note that the value set editor is still rough - I haven’t got the rest of it up to speed for DSTU2 quite, but the closure table part works.

Initialising a Closure Table

Before it can be used, a closure table has to be initialised. To initialise a closure table, POST the following to http://fhir2.healthintersections.com.au/open/ConceptMap/$closure:

{ 
  "resourceType" : "Parameters", 
   "parameter" : [{ 
     "name" : "name", 
     "valueString" : "eaab9a21-db9a-45e0-a400-0075eb19f795" 
  }]
 }

A successful response from the server looks like this:

{
 "resourceType": "Parameters",
 "parameter": [
    {
      "name": "outcome",
      "valueBoolean": true
    }
  ]
}

If there is an error – usually involving the closure name, the server returns a HTTP status 400 with an operation outcome:

{
  "resourceType": "OperationOutcome",
  "text": {
    "status": "generated",
    "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p>invalid closure name \"invalid-id!\":</p></div>"
  },
  "issue": [
    {
      "severity": "error",
      "details": {
        "text" : "invalid closure name \"invalid-id!\""
      }
    }
  ]
}

Note: As a matter of policy, my server requires valid allows UUIDs for closure names unless you log in using SMART on FHIR first, in which case you can use any valid id value for the closure name, but the closure is unique to the login behind the OAuth unless it’s a UUID. Other servers may choose other policies.

Adding to a closure Table

When the consumer (client) encounters a new code, it POST the following to http://fhir2.healthintersections.com.au/open/ConceptMap/$closure:

{
  "resourceType" : "Parameters",
  "parameter" : [{
    "name" : "name",
    "valueString" : "eaab9a21-db9a-45e0-a400-0075eb19f795"
  }, {
    "name" : "concept",
    "valueCoding" : {
       "system" : "http://snomed.info/sct",
       "code" : "22298006",
       "display" : "Myocardial infarction"
    }
  }]
}

Note that this example only includes one concept, but more than one is allowed:

{
  "resourceType" : "Parameters",
  "parameter" : [{
    "name" : "name",
    "valueString" : "eaab9a21-db9a-45e0-a400-0075eb19f795"
  }, {
    "name" : "concept",
    "valueCoding" : {
       "system" : "http://snomed.info/sct",
       "code" : "22298006",
       "display" : "Myocardial infarction"
    }
  }, {
    "name" : "concept",
    "valueCoding" : {
       "system" : "http://snomed.info/sct",
       "code" : "128599005",
       "display" : "Structural disorder of heart"
    }
  }]
}

The response varies depending on the conditions on the server. Possible responses:

If the closure table has not been initialised:

Return a 404 Not Found with

{
  "resourceType": "OperationOutcome",
  "text": {
    "status": "generated",
    "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p>invalid closure name \"eaab9a21-db9a-45e0-a400-0075eb19f795\":</p></div>"
  },
  "issue": [
    {
      "severity": "error",
      "details": {
        "text" : "invalid closure name \"eaab9a21-db9a-45e0-a400-0075eb19f795\""
      }
    }
  ]
}

If the closure table needs to be reinitialised:

Return a 422 Unprocessable Entity with

{
  "resourceType": "OperationOutcome",
  "text": {
    "status": "generated",
    "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p>closure \" eaab9a21-db9a-45e0-a400-0075eb19f795\" must be reinitialised</p></div>"
   },   
   "issue": [{
       "severity": "error",
       "details": {
         "text" : "closure \" eaab9a21-db9a-45e0-a400-0075eb19f795\" must be reinitialised"
       }
     }
   ]
}

The server should only send this when it’s underlying terminology conditions have been changed (e.g. a new version of SNOMED CT has been loaded). When a client gets this, it’s only choice is to initialise the closure table, and process all the codes in the closure table again (the assumption here is that the system has some external source of ‘all the codes’ so it can rebuild the table again).

If the concept(s) submitted are processed ok, but there’s no new concepts, or no new entries in the table, return a 200 OK with :

{
    "resourceType": "ConceptMap",
    "id": "42dc941b-7ff9-4859-a4cb-9592007a26e5",
    "version": "1",
    "name": "Updates for Closure Table eaab9a21-db9a-45e0-a400-0075eb19f795",
    "status": "active",
    "experimental": true,
    "date": "2015-12-20T23:12:55Z"
}

Note that in the $closure operation, the response never explicitly states that a code is subsumed by itself. Clients should assume that this is implicit.

If there’s new entries in the closure table:

The server returns a 200 ok with:

{
    "resourceType": "ConceptMap",
    "id": "b87db127-9996-4d0c-bda9-a278d7a24a69",
    "version": "2",
    "name": "Updates for Closure Table eaab9a21-db9a-45e0-a400-0075eb19f795",
    "status": "active",
    "experimental": true,
    "date": "2015-12-20T23:16:24Z",
    "element": [{
        "codeSystem": "http://snomed.info/sct",
        "code": "22298006",
        "target": [{
           "codeSystem": "http://snomed.info/sct",
           "code": "128599005",
           "equivalence": "subsumes"
        }]
    }]
}

Note: it’s important to get this the right way around. From the spec: The equivalence is read from target to source (e.g. the target is ‘wider’ than the source). So in this case, 128599005 (Structural disorder of heart) subsumes 22298006 (Myocardial infarction).

Notes:

  • The server can return multiple elements, each with 1 or more targets
  • servers may return the relationship represented in either direction.

Re-running a closure operation

The way that the closure operation functions, it’s possible for a client to lose a response from the server before it is committed to safe storage (or the client may not have particularly safe storage). For this reason, when a client is starting up, it should check that there has been no missing operations. It can do this by passing the last version it is sure it processed in the request:

{ 
  "resourceType" : "Parameters", 
   "parameter" : [{ 
     "name" : "name", 
     "valueString" : "eaab9a21-db9a-45e0-a400-0075eb19f795" 
  }, {
     "name" : "version", 
     "valueString" : "3" 
  }]
 }

That’s a request to return all the additions to the closure table since version 3. The server returns its latest version in the concept map, along with anything added to the closure table since version 3 (not including version 3)

Notes:

  • The client can pass a concept or version, but not both
  • These examples (and my server) use a serially incrementing sequential integer, but this is not required, and clients should not assume that there is any meaning or order in the version. Just recall the last version, and treat it as a magic value. There is, however, one special value: ‘0’. Passing a last version of 0 should be understood as resyncing the entire closure table