Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
type: add
issue: 7031
title: "ValueSet expansion operations now support the EQUAL filter operator. When using filters
in ValueSet.compose.include elements, the EQUAL operator can be used to filter concepts
based on exact matches for the 'code' property or custom properties defined in the CodeSystem.
This allows for more precise ValueSet expansions when working with large terminology systems."
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,66 @@ The following are the main key columns in the TRM_CONCEPTMAP table that are used
</table>

The TRM_CONCEPTMAP table will have exactly one row for each unique combination of URL and VER.

# ValueSet Expansion Operations

HAPI FHIR supports the `$expand` operation on ValueSet resources to expand the set of concepts that are defined by a ValueSet. This operation can be used to retrieve all concepts that are included in a ValueSet, optionally filtered by various criteria.

## Filtering During Expansion

ValueSet expansion supports filtering using the `filter` parameter in the ValueSet.compose.include elements. The following filter operators are supported:

### EQUAL Filter Operator

The `EQUAL` filter operator can be used to filter concepts based on exact matches for concept properties:

#### Filtering by Code
When filtering by the `code` property, the expansion will include only concepts whose code exactly matches the filter value:

```json
{
"resourceType": "ValueSet",
"compose": {
"include": [
{
"system": "http://example.org/codes",
"filter": [
{
"property": "code",
"op": "=",
"value": "example-code"
}
]
}
]
}
}
```

#### Filtering by Custom Properties
The `EQUAL` filter can also be used with custom properties defined in the CodeSystem:

```json
{
"resourceType": "ValueSet",
"compose": {
"include": [
{
"system": "http://example.org/codes",
"filter": [
{
"property": "status",
"op": "=",
"value": "active"
}
]
}
]
}
}
```

### Other Supported Filter Operators

- **ISA**: Includes the specified concept and all its descendants
- **DESCENDENTOF**: Includes all descendant concepts of the specified concept
Original file line number Diff line number Diff line change
Expand Up @@ -1648,25 +1648,35 @@ private void handleFilterConceptAndCode(
SearchPredicateFactory f,
BooleanPredicateClausesStep<?> b,
ValueSet.ConceptSetFilterComponent theFilter) {
TermConcept code = findCodeForFilterCriteriaCodeOrConcept(theSystem, theFilter);

if (theFilter.getOp() == ValueSet.FilterOperator.ISA) {
ourLog.debug(
" * Filtering on specific code and codes with a parent of {}/{}/{}",
code.getId(),
code.getCode(),
code.getDisplay());

b.must(f.bool()
.should(f.match().field("myParentPids").matching("" + code.getId()))
.should(f.match().field("myId").matching(code.getPid())));
} else if (theFilter.getOp() == ValueSet.FilterOperator.DESCENDENTOF) {
ourLog.debug(
" * Filtering on codes with a parent of {}/{}/{}", code.getId(), code.getCode(), code.getDisplay());

b.must(f.match().field("myParentPids").matching("" + code.getId()));
if (theFilter.getOp() == ValueSet.FilterOperator.EQUAL) {
// For EQUAL filters on code/concept property, match the exact code value
b.must(f.match().field("myCode").matching(theFilter.getValue()));
} else {
throwInvalidFilter(theFilter, "");
// For other operators (ISA, DESCENDENTOF), we need to find the actual TermConcept
TermConcept code = findCodeForFilterCriteriaCodeOrConcept(theSystem, theFilter);

if (theFilter.getOp() == ValueSet.FilterOperator.ISA) {
ourLog.debug(
" * Filtering on specific code and codes with a parent of {}/{}/{}",
code.getId(),
code.getCode(),
code.getDisplay());

b.must(f.bool()
.should(f.match().field("myParentPids").matching("" + code.getId()))
.should(f.match().field("myId").matching(code.getPid())));
} else if (theFilter.getOp() == ValueSet.FilterOperator.DESCENDENTOF) {
ourLog.debug(
" * Filtering on codes with a parent of {}/{}/{}",
code.getId(),
code.getCode(),
code.getDisplay());

b.must(f.match().field("myParentPids").matching("" + code.getId()));
} else {
throwInvalidFilter(theFilter, "");
}
}
}

Expand Down Expand Up @@ -1932,6 +1942,25 @@ private void expandWithoutHibernateSearch(
addConceptAndChildren(
theValueSetCodeAccumulator, theAddedCodes, theInclude, theSystem, theAdd, code);
handled = true;
} else if (nextFilter.getOp() == ValueSet.FilterOperator.EQUAL) {
// Handle EQUAL filter on code/concept property
String filterValue = nextFilter.getValue();
Optional<TermConcept> optionalConcept = findCode(theSystem, filterValue);
if (optionalConcept.isPresent()) {
TermConcept concept = optionalConcept.get();
addCodeIfNotAlreadyAdded(
theValueSetCodeAccumulator,
theAddedCodes,
theAdd,
theSystem,
theInclude.getVersion(),
concept.getCode(),
concept.getDisplay(),
concept.getId(),
concept.getParentPidsAsString(),
concept.getDesignations());
}
handled = true;
}
break;
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,67 @@ public void testExpandInline_IncludeCodeSystem_FilterOnDisplay_LeftMatchFilter()
assertThat(expandedValueSet.getExpansion().getContains().stream().map(t -> t.getDisplay()).collect(Collectors.toList())).containsExactlyInAnyOrder("Systolic blood pressure 1 hour minimum", "Systolic blood pressure 1 hour mean", "Systolic blood pressure 1 hour maximum");
}

@Test
public void testExpandInline_IncludeCodeSystem_FilterOnCode_Equal() throws Exception {
loadAndPersistCodeSystemWithDesignations(HttpVerb.PUT);

ValueSet input = new ValueSet();
input.getCompose()
.addInclude()
.setSystem("http://acme.org")
.addFilter()
.setProperty("code")
.setOp(ValueSet.FilterOperator.EQUAL)
.setValue("8450-9");

ValueSet expandedValueSet = myTermSvc.expandValueSet(new ValueSetExpansionOptions(), input);
ourLog.debug("Expanded ValueSet:\n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(expandedValueSet));

assertEquals(1, expandedValueSet.getExpansion().getTotal());
assertThat(expandedValueSet.getExpansion().getContains().stream().map(t -> t.getCode()).collect(Collectors.toList())).containsExactlyInAnyOrder("8450-9");
}

@Test
public void testExpandInline_IncludeCodeSystem_FilterOnCustomProperty_Equal() throws Exception {
// Create CodeSystem with custom properties
CodeSystem cs = new CodeSystem();
cs.setUrl("http://example.com/cs");
cs.setContent(CodeSystem.CodeSystemContentMode.COMPLETE);

// Add concepts with custom properties
CodeSystem.ConceptDefinitionComponent concept1 = cs.addConcept()
.setCode("code1")
.setDisplay("Display 1");
concept1.addProperty()
.setCode("status")
.setValue(new StringType("active"));

CodeSystem.ConceptDefinitionComponent concept2 = cs.addConcept()
.setCode("code2")
.setDisplay("Display 2");
concept2.addProperty()
.setCode("status")
.setValue(new StringType("inactive"));

myCodeSystemDao.create(cs);

// Create ValueSet with EQUAL filter on custom property
ValueSet input = new ValueSet();
input.getCompose()
.addInclude()
.setSystem("http://example.com/cs")
.addFilter()
.setProperty("status")
.setOp(ValueSet.FilterOperator.EQUAL)
.setValue("active");

ValueSet expandedValueSet = myTermSvc.expandValueSet(new ValueSetExpansionOptions(), input);
ourLog.debug("Expanded ValueSet:\n" + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(expandedValueSet));

assertEquals(1, expandedValueSet.getExpansion().getTotal());
assertThat(expandedValueSet.getExpansion().getContains().stream().map(t -> t.getCode()).collect(Collectors.toList())).containsExactlyInAnyOrder("code1");
}

@Test
public void testExpandInline_IncludePreExpandedValueSetByUri_FilterOnDisplay_LeftMatch_SelectAll() {
myStorageSettings.setPreExpandValueSets(true);
Expand Down
Loading