Merge pull request #2149 from jamesagnew/EMPI_-_Patient_matching_using_MPI_-_return_mode,_score,_and_grade

#2148 Initial implementation
This commit is contained in:
nvg-smile 2020-10-28 17:43:20 -04:00 committed by GitHub
commit 590d3c468b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 187 additions and 31 deletions

View File

@ -35,6 +35,7 @@ import java.util.stream.Collectors;
@Service @Service
public class EmpiMatchFinderSvcImpl implements IEmpiMatchFinderSvc { public class EmpiMatchFinderSvcImpl implements IEmpiMatchFinderSvc {
@Autowired @Autowired
private EmpiCandidateSearchSvc myEmpiCandidateSearchSvc; private EmpiCandidateSearchSvc myEmpiCandidateSearchSvc;
@Autowired @Autowired
@ -50,13 +51,4 @@ public class EmpiMatchFinderSvcImpl implements IEmpiMatchFinderSvc {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@Override
@Nonnull
public List<IAnyResource> findMatches(String theResourceType, IAnyResource theResource) {
List<MatchedTarget> targetCandidates = getMatchedTargets(theResourceType, theResource);
return targetCandidates.stream()
.filter(candidate -> candidate.isMatch())
.map(MatchedTarget::getTarget)
.collect(Collectors.toList());
}
} }

View File

@ -1,14 +1,27 @@
package ca.uhn.fhir.jpa.empi.provider; package ca.uhn.fhir.jpa.empi.provider;
import ca.uhn.fhir.empi.api.EmpiConstants;
import com.google.common.collect.Ordering;
import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.codesystems.MatchGrade;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.List;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.*;
public class EmpiProviderMatchR4Test extends BaseProviderR4Test { public class EmpiProviderMatchR4Test extends BaseProviderR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(EmpiProviderMatchR4Test.class);
public static final String NAME_GIVEN_JANET = NAME_GIVEN_JANE + "t";
@Override @Override
@BeforeEach @BeforeEach
@ -26,7 +39,56 @@ public class EmpiProviderMatchR4Test extends BaseProviderR4Test {
Bundle result = myEmpiProviderR4.match(newJane); Bundle result = myEmpiProviderR4.match(newJane);
assertEquals(1, result.getEntry().size()); assertEquals(1, result.getEntry().size());
assertEquals(createdJane.getId(), result.getEntryFirstRep().getResource().getId());
Bundle.BundleEntryComponent entry0 = result.getEntry().get(0);
assertEquals(createdJane.getId(), entry0.getResource().getId());
Bundle.BundleEntrySearchComponent searchComponent = entry0.getSearch();
assertEquals(Bundle.SearchEntryMode.MATCH, searchComponent.getMode());
assertEquals(2.0 / 3.0, searchComponent.getScore().doubleValue(), 0.01);
Extension matchGradeExtension = searchComponent.getExtensionByUrl(EmpiConstants.FIHR_STRUCTURE_DEF_MATCH_GRADE_URL_NAMESPACE);
assertNotNull(matchGradeExtension);
assertEquals(MatchGrade.CERTAIN.toCode(), matchGradeExtension.getValue().toString());
}
@Test
public void testMatchOrder() throws Exception {
Patient jane0 = buildJanePatient();
Patient createdJane1 = createPatient(jane0);
Patient jane1 = buildPatientWithNameAndId(NAME_GIVEN_JANET, JANE_ID);
jane1.setActive(true);
Patient createdJane2 = createPatient(jane1);
Patient newJane = buildJanePatient();
Bundle result = myEmpiProviderR4.match(newJane);
assertEquals(2, result.getEntry().size());
Bundle.BundleEntryComponent entry0 = result.getEntry().get(0);
assertTrue(jane0.getId().equals(((Patient) entry0.getResource()).getId()), "First match should be Jane");
Bundle.BundleEntryComponent entry1 = result.getEntry().get(1);
assertTrue(jane1.getId().equals(((Patient) entry1.getResource()).getId()), "Second match should be Janet");
List<Double> scores = result.getEntry()
.stream()
.map(bec -> bec.getSearch().getScore().doubleValue())
.collect(Collectors.toList());
assertTrue(Ordering.<Double>natural().reverse().isOrdered(scores), "Match scores must be descending");
}
@Test
public void testMismatch() throws Exception {
Patient jane = buildJanePatient();
jane.setActive(true);
Patient createdJane = createPatient(jane);
Patient paul = buildPaulPatient();
paul.setActive(true);
Bundle result = myEmpiProviderR4.match(paul);
assertEquals(0, result.getEntry().size());
} }
@Test @Test

View File

@ -32,5 +32,6 @@ public class EmpiConstants {
public static final String HAPI_ENTERPRISE_IDENTIFIER_SYSTEM = "http://hapifhir.io/fhir/NamingSystem/empi-person-enterprise-id"; public static final String HAPI_ENTERPRISE_IDENTIFIER_SYSTEM = "http://hapifhir.io/fhir/NamingSystem/empi-person-enterprise-id";
public static final String ALL_RESOURCE_SEARCH_PARAM_TYPE = "*"; public static final String ALL_RESOURCE_SEARCH_PARAM_TYPE = "*";
public static final String FIHR_STRUCTURE_DEF_MATCH_GRADE_URL_NAMESPACE = "http://hl7.org/fhir/StructureDefinition/match-grade";
} }

View File

@ -105,6 +105,26 @@ public final class EmpiMatchOutcome {
return this; return this;
} }
/**
* Gets normalized score that is in the range from zero to one
*
* @return
* Returns the normalized score
*/
public Double getNormalizedScore() {
if (vector == 0) {
return 0.0;
} else if (score > vector) {
return 1.0;
}
double retVal = score / vector;
if (retVal < 0) {
retVal = 0.0;
}
return retVal;
}
@Override @Override
public String toString() { public String toString() {
return new ToStringBuilder(this) return new ToStringBuilder(this)

View File

@ -37,16 +37,4 @@ public interface IEmpiMatchFinderSvc {
*/ */
@Nonnull @Nonnull
List<MatchedTarget> getMatchedTargets(String theResourceType, IAnyResource theResource); List<MatchedTarget> getMatchedTargets(String theResourceType, IAnyResource theResource);
/**
* Used by the $match operation.
* Retrieve a list of Patient/Practitioner matches, based on the given {@link IAnyResource}
* Internally, performs all EMPI matching rules on the type of the resource then returns only those
* with a match result of MATCHED.
*
* @param theResourceType the type of the resource.
* @param theResource the resource that we are attempting to find matches for.
* @return a List of {@link IAnyResource} representing all people who had a MATCH outcome.
*/
List<IAnyResource> findMatches(String theResourceType, IAnyResource theResource);
} }

View File

@ -21,11 +21,13 @@ package ca.uhn.fhir.empi.provider;
*/ */
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiConstants;
import ca.uhn.fhir.empi.api.EmpiLinkJson; import ca.uhn.fhir.empi.api.EmpiLinkJson;
import ca.uhn.fhir.empi.api.IEmpiControllerSvc; import ca.uhn.fhir.empi.api.IEmpiControllerSvc;
import ca.uhn.fhir.empi.api.IEmpiExpungeSvc; import ca.uhn.fhir.empi.api.IEmpiExpungeSvc;
import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc; import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc;
import ca.uhn.fhir.empi.api.IEmpiSubmitSvc; import ca.uhn.fhir.empi.api.IEmpiSubmitSvc;
import ca.uhn.fhir.empi.api.MatchedTarget;
import ca.uhn.fhir.empi.model.EmpiTransactionContext; import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.Operation;
@ -37,6 +39,7 @@ import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.ParametersUtil; import ca.uhn.fhir.util.ParametersUtil;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.CodeType;
import org.hl7.fhir.dstu3.model.DecimalType; import org.hl7.fhir.dstu3.model.DecimalType;
import org.hl7.fhir.dstu3.model.InstantType; import org.hl7.fhir.dstu3.model.InstantType;
import org.hl7.fhir.dstu3.model.Parameters; import org.hl7.fhir.dstu3.model.Parameters;
@ -45,10 +48,13 @@ import org.hl7.fhir.dstu3.model.Person;
import org.hl7.fhir.dstu3.model.Practitioner; import org.hl7.fhir.dstu3.model.Practitioner;
import org.hl7.fhir.dstu3.model.Resource; import org.hl7.fhir.dstu3.model.Resource;
import org.hl7.fhir.dstu3.model.StringType; import org.hl7.fhir.dstu3.model.StringType;
import org.hl7.fhir.dstu3.model.codesystems.MatchGrade;
import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -73,24 +79,51 @@ public class EmpiProviderDstu3 extends BaseEmpiProvider {
} }
@Operation(name = ProviderConstants.EMPI_MATCH, type = Patient.class) @Operation(name = ProviderConstants.EMPI_MATCH, type = Patient.class)
public Bundle match(@OperationParam(name=ProviderConstants.EMPI_MATCH_RESOURCE, min = 1, max = 1) Patient thePatient) { public Bundle match(@OperationParam(name = ProviderConstants.EMPI_MATCH_RESOURCE, min = 1, max = 1) Patient thePatient) {
if (thePatient == null) { if (thePatient == null) {
throw new InvalidRequestException("resource may not be null"); throw new InvalidRequestException("resource may not be null");
} }
Collection<IAnyResource> matches = myEmpiMatchFinderSvc.findMatches("Patient", thePatient);
List<MatchedTarget> matches = myEmpiMatchFinderSvc.getMatchedTargets("Patient", thePatient);
matches.sort(Comparator.comparing((MatchedTarget m) -> m.getMatchResult().getNormalizedScore()).reversed());
Bundle retVal = new Bundle(); Bundle retVal = new Bundle();
retVal.setType(Bundle.BundleType.SEARCHSET); retVal.setType(Bundle.BundleType.SEARCHSET);
retVal.setId(UUID.randomUUID().toString()); retVal.setId(UUID.randomUUID().toString());
retVal.getMeta().setLastUpdatedElement(InstantType.now()); retVal.getMeta().setLastUpdatedElement(InstantType.now());
for (IAnyResource next : matches) { for (MatchedTarget next : matches) {
retVal.addEntry().setResource((Resource) next); boolean shouldKeepThisEntry = next.isMatch() || next.isPossibleMatch();
if (!shouldKeepThisEntry) {
continue;
}
Bundle.BundleEntryComponent entry = new Bundle.BundleEntryComponent();
entry.setResource((Resource) next.getTarget());
entry.setSearch(toBundleEntrySearchComponent(next));
retVal.addEntry(entry);
} }
return retVal; return retVal;
} }
private Bundle.BundleEntrySearchComponent toBundleEntrySearchComponent(MatchedTarget theMatchedTarget) {
Bundle.BundleEntrySearchComponent searchComponent = new Bundle.BundleEntrySearchComponent();
searchComponent.setMode(Bundle.SearchEntryMode.MATCH);
searchComponent.setScore(theMatchedTarget.getMatchResult().getNormalizedScore());
MatchGrade matchGrade = MatchGrade.PROBABLE;
if (theMatchedTarget.isMatch()) {
matchGrade = MatchGrade.CERTAIN;
} else if (theMatchedTarget.isPossibleMatch()) {
matchGrade = MatchGrade.POSSIBLE;
}
searchComponent.addExtension(EmpiConstants.FIHR_STRUCTURE_DEF_MATCH_GRADE_URL_NAMESPACE, new CodeType(matchGrade.toCode()));
return searchComponent;
}
@Operation(name = ProviderConstants.EMPI_MERGE_PERSONS, type = Person.class) @Operation(name = ProviderConstants.EMPI_MERGE_PERSONS, type = Person.class)
public Person mergePerson(@OperationParam(name=ProviderConstants.EMPI_MERGE_PERSONS_FROM_PERSON_ID, min = 1, max = 1) StringType theFromPersonId, public Person mergePerson(@OperationParam(name=ProviderConstants.EMPI_MERGE_PERSONS_FROM_PERSON_ID, min = 1, max = 1) StringType theFromPersonId,
@OperationParam(name=ProviderConstants.EMPI_MERGE_PERSONS_TO_PERSON_ID, min = 1, max = 1) StringType theToPersonId, @OperationParam(name=ProviderConstants.EMPI_MERGE_PERSONS_TO_PERSON_ID, min = 1, max = 1) StringType theToPersonId,

View File

@ -21,11 +21,13 @@ package ca.uhn.fhir.empi.provider;
*/ */
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiConstants;
import ca.uhn.fhir.empi.api.EmpiLinkJson; import ca.uhn.fhir.empi.api.EmpiLinkJson;
import ca.uhn.fhir.empi.api.IEmpiControllerSvc; import ca.uhn.fhir.empi.api.IEmpiControllerSvc;
import ca.uhn.fhir.empi.api.IEmpiExpungeSvc; import ca.uhn.fhir.empi.api.IEmpiExpungeSvc;
import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc; import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc;
import ca.uhn.fhir.empi.api.IEmpiSubmitSvc; import ca.uhn.fhir.empi.api.IEmpiSubmitSvc;
import ca.uhn.fhir.empi.api.MatchedTarget;
import ca.uhn.fhir.empi.model.EmpiTransactionContext; import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.Operation;
@ -39,7 +41,9 @@ import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.DecimalType; import org.hl7.fhir.r4.model.DecimalType;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.InstantType; import org.hl7.fhir.r4.model.InstantType;
import org.hl7.fhir.r4.model.IntegerType; import org.hl7.fhir.r4.model.IntegerType;
import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Parameters;
@ -48,8 +52,11 @@ import org.hl7.fhir.r4.model.Person;
import org.hl7.fhir.r4.model.Practitioner; import org.hl7.fhir.r4.model.Practitioner;
import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.codesystems.MatchGrade;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -74,25 +81,51 @@ public class EmpiProviderR4 extends BaseEmpiProvider {
} }
@Operation(name = ProviderConstants.EMPI_MATCH, type = Patient.class) @Operation(name = ProviderConstants.EMPI_MATCH, type = Patient.class)
public Bundle match(@OperationParam(name=ProviderConstants.EMPI_MATCH_RESOURCE, min = 1, max = 1) Patient thePatient) { public Bundle match(@OperationParam(name = ProviderConstants.EMPI_MATCH_RESOURCE, min = 1, max = 1) Patient thePatient) {
if (thePatient == null) { if (thePatient == null) {
throw new InvalidRequestException("resource may not be null"); throw new InvalidRequestException("resource may not be null");
} }
Collection<IAnyResource> matches = myEmpiMatchFinderSvc.findMatches("Patient", thePatient); List<MatchedTarget> matches = myEmpiMatchFinderSvc.getMatchedTargets("Patient", thePatient);
matches.sort(Comparator.comparing((MatchedTarget m) -> m.getMatchResult().getNormalizedScore()).reversed());
Bundle retVal = new Bundle(); Bundle retVal = new Bundle();
retVal.setType(Bundle.BundleType.SEARCHSET); retVal.setType(Bundle.BundleType.SEARCHSET);
retVal.setId(UUID.randomUUID().toString()); retVal.setId(UUID.randomUUID().toString());
retVal.getMeta().setLastUpdatedElement(InstantType.now()); retVal.getMeta().setLastUpdatedElement(InstantType.now());
for (IAnyResource next : matches) { for (MatchedTarget next : matches) {
retVal.addEntry().setResource((Resource) next); boolean shouldKeepThisEntry = next.isMatch() || next.isPossibleMatch();
if (!shouldKeepThisEntry) {
continue;
}
Bundle.BundleEntryComponent entry = new Bundle.BundleEntryComponent();
entry.setResource((Resource) next.getTarget());
entry.setSearch(toBundleEntrySearchComponent(next));
retVal.addEntry(entry);
} }
return retVal; return retVal;
} }
private Bundle.BundleEntrySearchComponent toBundleEntrySearchComponent(MatchedTarget theMatchedTarget) {
Bundle.BundleEntrySearchComponent searchComponent = new Bundle.BundleEntrySearchComponent();
searchComponent.setMode(Bundle.SearchEntryMode.MATCH);
searchComponent.setScore(theMatchedTarget.getMatchResult().getNormalizedScore());
MatchGrade matchGrade = MatchGrade.PROBABLE;
if (theMatchedTarget.isMatch()) {
matchGrade = MatchGrade.CERTAIN;
} else if (theMatchedTarget.isPossibleMatch()) {
matchGrade = MatchGrade.POSSIBLE;
}
searchComponent.addExtension(EmpiConstants.FIHR_STRUCTURE_DEF_MATCH_GRADE_URL_NAMESPACE, new CodeType(matchGrade.toCode()));
return searchComponent;
}
@Operation(name = ProviderConstants.EMPI_MERGE_PERSONS, type = Person.class) @Operation(name = ProviderConstants.EMPI_MERGE_PERSONS, type = Person.class)
public Person mergePersons(@OperationParam(name=ProviderConstants.EMPI_MERGE_PERSONS_FROM_PERSON_ID, min = 1, max = 1) StringType theFromPersonId, public Person mergePersons(@OperationParam(name=ProviderConstants.EMPI_MERGE_PERSONS_FROM_PERSON_ID, min = 1, max = 1) StringType theFromPersonId,
@OperationParam(name=ProviderConstants.EMPI_MERGE_PERSONS_TO_PERSON_ID, min = 1, max = 1) StringType theToPersonId, @OperationParam(name=ProviderConstants.EMPI_MERGE_PERSONS_TO_PERSON_ID, min = 1, max = 1) StringType theToPersonId,

View File

@ -0,0 +1,27 @@
package ca.uhn.fhir.empi.api;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class EmpiMatchOutcomeTest {
@Test
void testNormalizedScore() {
EmpiMatchOutcome outcome = new EmpiMatchOutcome(0l, 0.0);
assertEquals(0.0, outcome.getNormalizedScore());
outcome = new EmpiMatchOutcome(10l, 10.0);
assertEquals(1.0, outcome.getNormalizedScore());
outcome = new EmpiMatchOutcome(10l, -10.0);
assertEquals(0.0, outcome.getNormalizedScore());
outcome = new EmpiMatchOutcome(3l, 2.0);
assertEquals(2.0 / 3.0, outcome.getNormalizedScore(), 0.0001);
outcome = new EmpiMatchOutcome(5l, 19.0);
assertEquals(1.0, outcome.getNormalizedScore());
}
}