Terminology Module Incubator, published by HL7 International / Terminology Infrastructure. This guide is not an authorized publication; it is the continuous build for version 0.1.0 built by the FHIR (HL7® FHIR® Standard) CI Build. This version is based on the current content of https://github.com/HL7/txmodule-incubator/ and changes regularly. See the Directory of published versions
| Page standards status: Informative |
This content was previously published in the FHIR Terminology Service documentation. It has been moved here to the Terminology Module Incubator to allow for more rapid iteration and development.
The 5 operations Expand, Lookup, Validate, Subsumes, and Translate account for most operational requirements associated with terminology use. However, there is one difficult but important use case that they do not address, which is integrating terminologically based logic into application searches.
A typical example of this is a user that wants to find any observations for male patients over the age of 50 who attended a particular clinic within a particular 2-week period, with a diagnosis of gout, and who had an elevated serum creatinine.
In this case, both "diagnosis of gout" and "serum creatinine" involve value set and/or subsumption queries (e.g. against SNOMED CT and LOINC respectively). This search has to be executed by some logical processing engine that knows how to find patient related data in a given persistence store. Often, this is some kind of SQL query, though many other technological choices are available. However, this is done, the challenge with an operation like this is to integrate the terminological knowledge into a search execution that also covers other relationships expressed in the search criteria.
One approach to this problem would be to use the expand operation above, so that the system executing the search could generate expansions, and then search for these expansions. This has a couple of problems:
An alternative approach is to generate a transitive closure table which lists all the possible transitive subsumption relationships, and allows for rapid execution of these kind of queries . However, this has other problems:
This is the main reason why most systems do not support post-coordination or other forms of coded expressions.
In FHIR, this problem is solved by building a closure table on the fly as new codes are seen. This technique leaves the FHIR terminology server responsible for the terminological reasoning and the client responsible for the closure table maintenance. To the client, it doesn't matter whether the concept is post-coordinated or not. Here's a description of how the process works:
The $closure operation takes 2 parameters:
The operation returns a concept map which has a list of mappings that represent new entries to make in the closure table.
The subsumption testing performed when building a closure table is the same as for the $subsumes operation, and is based on the CodeSystem definition of subsumption.
The closure table can be resynchronized by passing an additional "version" parameter, which is a value taken from the version in one of the delta responses. This is a request to replay all the mapping changes since that delta was sent.
Before it can be used, a closure table has to be initialized. To initialize a closure table, POST the following to [base]/ConceptMap/$closure:
{
"resourceType" : "Parameters",
"parameter" : [{
"name" : "name",
"valueString" : "[name]"
}]
}
A successful response is a 200 OK from the server, with an associated ConceptMap:
{
"resourceType": "ConceptMap",
"id": "[name]",
"version": "0",
"name": "Closure Table [name] Creation",
"status": "active",
"experimental": true,
"date": "2015-12-20T23:10:55Z"
}
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!\""
}
}
]
}
What closure names are valid is at the discretion of the server.
When the consumer (client) encounters a new code, it POSTs the following to [base]/ConceptMap/$closure:
{
"resourceType" : "Parameters",
"parameter" : [{
"name" : "name",
"valueString" : "[name]"
}, {
"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" : "[name]"
}, {
"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 initialized: Return a 404 Not Found with
{
"resourceType": "OperationOutcome",
"text": {
"status": "generated",
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p>invalid closure name \"[name]\":</p></div>"
},
"issue": [
{
"severity": "error",
"details": {
"text" : "invalid closure name \"[name]\""
}
}
]
}
If the closure table needs to be reinitialized: Return a 422 Unprocessable Entity with
{
"resourceType": "OperationOutcome",
"text": {
"status": "generated",
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p>closure \"[name\" must be reinitialized</p></div>"
},
"issue": [{
"severity": "error",
"details": {
"text" : "closure \"[name]\" must be reinitialized"
}
}
]
}
The server should only send this when its underlying terminology conditions have been changed (e.g. a new version of SNOMED CT has been loaded). When a client gets this, its only choice is to initialize 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": "[name]",
"version": "[version]",
"name": "Updates for Closure Table [name]",
"status": "active",
"experimental": true,
"date": "2015-12-20T23:12:55Z"
}
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": "[version]",
"name": "Updates for Closure Table [name]",
"status": "active",
"experimental": true,
"date": "2015-12-20T23:16:24Z",
"group": [{
"source": "http://snomed.info/sct",
"target": "http://snomed.info/sct",
"element" : {
"code": "22298006",
"target": [{
"code": "128599005",
"relationship": " source-is-narrower-than-target"
}]
}
}]
}
Notes
Given 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 might not have particularly safe storage). For this reason, when a client is starting up, it should check that there have been no missing operations. It can do this by passing the last version (from the Concept Map response) it is sure it processed in the request:
{
"resourceType" : "Parameters",
"parameter" : [{
"name" : "name",
"valueString" : "[name]"
}, {
"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 uses the result of the closure operation to maintain a closure table. Simplistically, it might look like this:
| Scope | Source | Target | |
| patient-problems | http://snomed.info/sct|22298006 | http://snomed.info/sct|128599005 | |
| patient-problems | http://snomed.info/sct|24595009 | http://snomed.info/sct|90560007 | |
| obs-code | http://loinc.org|14682-9 | http://loinc.org|LP41281-4 |
The client can then use a table like this as part of its general search conditions. Using the example from above: "Find any observations for male patients over the age of 50 who attended a particular clinic within a particular 2-week period, with a diagnosis of gout, and who had an elevated serum creatinine." This query could be done, for instance, with an SQL query like this:
Select * from Observations, Patients, Encounters, Conditions, Observations as Obs2 where
Observations.patient = Patients.Key and Patients.Age > 50 and
Observations.encounter = Encounters.Key and Encounter.clinic = [key]
and encounter.date >= [date] and encounter.date <= [date] and
Conditions.patient = Patients.Key and Conditions.code
in (select Source From ClosureTable
where Scope = "patient-problems" and Target = "http://snomed.info/sct|90560007") and
Obs2.patient = Patients.Key and Obs2.value > 0.19 and Obs2.code
in (select Source From ClosureTable
where Scope = "obs-code" and Target = "http://loinc.org|LP41281-4")
Note that in real clinical systems, tables are usually far more structured than this example implies, and the query is correspondingly more complex. The closure table would usually be normalised - this example is kept simple to demonstrate the concept.