Issue 4486 mdm inconsistent possible match score values (#4487)

* Extract method for readability

* Save always normalized score values in POSSIBLE_MATCH links.

* Avoid setting properties to null values. Adjust test.

* Simplify fix

* Fix test. Add RangeTestHelper.

---------

Co-authored-by: juan.marchionatto <juan.marchionatto@smilecdr.com>
This commit is contained in:
jmarchionatto 2023-02-02 14:42:38 -05:00 committed by GitHub
parent b1770abb85
commit 0a213a52d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 305 additions and 30 deletions

View File

@ -0,0 +1,3 @@
type: fix
issue: 4486
title: "Previously, some MDM links of type `POSSIBLE_MATCH` were saved with unnormalized score values. This has been fixed."

View File

@ -73,11 +73,9 @@ public class MdmLinkDaoSvc<P extends IResourcePersistentId, M extends IMdmLink<P
mdmLink.setEidMatch(theMatchOutcome.isEidMatch() | mdmLink.isEidMatchPresent());
mdmLink.setHadToCreateNewGoldenResource(theMatchOutcome.isCreatedNewResource() | mdmLink.getHadToCreateNewGoldenResource());
mdmLink.setMdmSourceType(myFhirContext.getResourceType(theSourceResource));
if (mdmLink.getScore() != null) {
mdmLink.setScore(Math.max(theMatchOutcome.score, mdmLink.getScore()));
} else {
mdmLink.setScore(theMatchOutcome.score);
}
setScoreProperties(theMatchOutcome, mdmLink);
// Add partition for the mdm link if it's available in the source resource
RequestPartitionId partitionId = (RequestPartitionId) theSourceResource.getUserData(Constants.RESOURCE_PARTITION_ID);
if (partitionId != null && partitionId.getFirstPartitionIdOrNull() != null) {
@ -91,6 +89,24 @@ public class MdmLinkDaoSvc<P extends IResourcePersistentId, M extends IMdmLink<P
return mdmLink;
}
private void setScoreProperties(MdmMatchOutcome theMatchOutcome, M mdmLink) {
if (theMatchOutcome.getScore() != null) {
mdmLink.setScore( mdmLink.getScore() != null
? Math.max(theMatchOutcome.getNormalizedScore(), mdmLink.getScore())
: theMatchOutcome.getNormalizedScore() );
}
if (theMatchOutcome.getVector() != null) {
mdmLink.setVector( mdmLink.getVector() != null
? Math.max(theMatchOutcome.getVector(), mdmLink.getVector())
: theMatchOutcome.getVector() );
}
mdmLink.setRuleCount( mdmLink.getRuleCount() != null
? Math.max(theMatchOutcome.getMdmRuleCount(), mdmLink.getRuleCount())
: theMatchOutcome.getMdmRuleCount() );
}
@Nonnull
public M getOrCreateMdmLinkByGoldenResourceAndSourceResource(
IAnyResource theGoldenResource, IAnyResource theSourceResource
@ -127,7 +143,6 @@ public class MdmLinkDaoSvc<P extends IResourcePersistentId, M extends IMdmLink<P
* @param theSourceResourcePid The ResourcepersistenceId of the Source resource
* @return The {@link IMdmLink} entity that matches these criteria if exists
*/
@SuppressWarnings("unchecked")
public Optional<M> getLinkByGoldenResourcePidAndSourceResourcePid(P theGoldenResourcePid, P theSourceResourcePid) {
if (theSourceResourcePid == null || theGoldenResourcePid == null) {
return Optional.empty();

View File

@ -94,7 +94,7 @@ public class MdmMatchLinkSvc {
private void handleMdmWithMultipleCandidates(IAnyResource theResource, CandidateList theCandidateList, MdmTransactionContext theMdmTransactionContext) {
MatchedGoldenResourceCandidate firstMatch = theCandidateList.getFirstMatch();
IResourcePersistentId sampleGoldenResourcePid = firstMatch.getCandidateGoldenResourcePid();
IResourcePersistentId<?> sampleGoldenResourcePid = firstMatch.getCandidateGoldenResourcePid();
boolean allSameGoldenResource = theCandidateList.stream()
.allMatch(candidate -> candidate.getCandidateGoldenResourcePid().equals(sampleGoldenResourcePid));
@ -105,17 +105,7 @@ public class MdmMatchLinkSvc {
log(theMdmTransactionContext, "MDM received multiple match candidates, that were linked to different Golden Resources. Setting POSSIBLE_DUPLICATES and POSSIBLE_MATCHES.");
//Set them all as POSSIBLE_MATCH
List<IAnyResource> goldenResources = new ArrayList<>();
for (MatchedGoldenResourceCandidate matchedGoldenResourceCandidate : theCandidateList.getCandidates()) {
IAnyResource goldenResource = myMdmGoldenResourceFindingSvc
.getGoldenResourceFromMatchedGoldenResourceCandidate(matchedGoldenResourceCandidate, theMdmTransactionContext.getResourceType());
MdmMatchOutcome outcome = new MdmMatchOutcome(matchedGoldenResourceCandidate.getMatchResult().vector,
matchedGoldenResourceCandidate.getMatchResult().getNormalizedScore());
outcome.setMatchResultEnum(MdmMatchResultEnum.POSSIBLE_MATCH);
outcome.setEidMatch(theCandidateList.isEidMatch());
myMdmLinkSvc.updateLink(goldenResource, theResource, outcome, MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
goldenResources.add(goldenResource);
}
List<IAnyResource> goldenResources = createPossibleMatches(theResource, theCandidateList, theMdmTransactionContext);
//Set all GoldenResources as POSSIBLE_DUPLICATE of the last GoldenResource.
IAnyResource firstGoldenResource = goldenResources.get(0);
@ -129,6 +119,26 @@ public class MdmMatchLinkSvc {
}
}
private List<IAnyResource> createPossibleMatches(IAnyResource theResource, CandidateList theCandidateList, MdmTransactionContext theMdmTransactionContext) {
List<IAnyResource> goldenResources = new ArrayList<>();
for (MatchedGoldenResourceCandidate matchedGoldenResourceCandidate : theCandidateList.getCandidates()) {
IAnyResource goldenResource = myMdmGoldenResourceFindingSvc
.getGoldenResourceFromMatchedGoldenResourceCandidate(matchedGoldenResourceCandidate, theMdmTransactionContext.getResourceType());
MdmMatchOutcome outcome = new MdmMatchOutcome(matchedGoldenResourceCandidate.getMatchResult().getVector(),
matchedGoldenResourceCandidate.getMatchResult().getScore())
.setMdmRuleCount( matchedGoldenResourceCandidate.getMatchResult().getMdmRuleCount());
outcome.setMatchResultEnum(MdmMatchResultEnum.POSSIBLE_MATCH);
outcome.setEidMatch(theCandidateList.isEidMatch());
myMdmLinkSvc.updateLink(goldenResource, theResource, outcome, MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
goldenResources.add(goldenResource);
}
return goldenResources;
}
private void handleMdmWithNoCandidates(IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) {
log(theMdmTransactionContext, String.format("There were no matched candidates for MDM, creating a new %s Golden Resource.", theResource.getIdElement().getResourceType()));
IAnyResource newGoldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(theResource, theMdmTransactionContext);

View File

@ -100,7 +100,6 @@ public class MdmLinkDaoSvcTest extends BaseMdmR4Test {
mdmLink.setUpdated(new Date());
mdmLink.setGoldenResourcePersistenceId(JpaPid.fromId(thePatientPid));
mdmLink.setSourcePersistenceId(runInTransaction(()->myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), patient)));
MdmLink saved= myMdmLinkDao.save(mdmLink);
return saved;
return myMdmLinkDao.save(mdmLink);
}
}

View File

@ -10,6 +10,7 @@ import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.test.utilities.RangeTestHelper;
import ca.uhn.fhir.util.ParametersUtil;
import ca.uhn.fhir.util.StopWatch;
import org.apache.commons.lang3.StringUtils;
@ -377,7 +378,7 @@ public class MdmProviderQueryLinkR4Test extends BaseLinkR4Test {
List<Parameters.ParametersParameterComponent> list = getParametersByName(result, "link");
assertThat(list, hasSize(4));
List<Parameters.ParametersParameterComponent> part = list.get(3).getPart();
assertMdmLink(MDM_LINK_PROPERTY_COUNT, part, goldenResourceId.getValue(), patientId.getValue(), MdmMatchResultEnum.MATCH, "false", "false", "2");
assertMdmLink(MDM_LINK_PROPERTY_COUNT, part, goldenResourceId.getValue(), patientId.getValue(), MdmMatchResultEnum.MATCH, "false", "false", ".666");
}
@Test
@ -459,7 +460,7 @@ public class MdmProviderQueryLinkR4Test extends BaseLinkR4Test {
assertThat(thePart.get(5).getValue().primitiveValue(), is(theNewGoldenResource));
assertThat(thePart.get(6).getName(), is("score"));
assertThat(thePart.get(6).getValue().primitiveValue(), is(theScore));
RangeTestHelper.checkInRange(theScore, thePart.get(6).getValue().primitiveValue());
}
}

View File

@ -16,6 +16,7 @@ import java.util.List;
import static ca.uhn.fhir.mdm.api.MdmMatchResultEnum.MATCH;
import static ca.uhn.fhir.mdm.api.MdmMatchResultEnum.NO_MATCH;
import static ca.uhn.fhir.mdm.api.MdmMatchResultEnum.POSSIBLE_MATCH;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@ -52,6 +53,23 @@ class MdmLinkUpdaterSvcImplTest extends BaseMdmR4Test {
assertLinksMatchedByEid(false, false);
}
@Test
public void testUpdateLinkPossibleMatchSavesNormalizedScore() {
final Patient goldenPatient = createGoldenPatient(buildJanePatient());
final Patient patient1 = createPatient(buildJanePatient());
buildUpdateLinkMdmTransactionContext();
MdmMatchOutcome matchOutcome = new MdmMatchOutcome(61L, 5.0).setMdmRuleCount(6).setMatchResultEnum(POSSIBLE_MATCH);
myMdmLinkDaoSvc.createOrUpdateLinkEntity(goldenPatient, patient1, matchOutcome, MdmLinkSourceEnum.MANUAL, createContextForCreate("Patient"));
final List<MdmLink> targets = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(goldenPatient);
assertFalse(targets.isEmpty());
assertEquals(1, targets.size());
final MdmLink mdmLink = targets.get(0);
assertEquals(matchOutcome.getNormalizedScore(), mdmLink.getScore());
}
@Test
public void testUpdateLinkMatchAfterVersionChange() {
myMdmSettings.getMdmRules().setVersion("1");

View File

@ -36,13 +36,13 @@ public final class MdmMatchOutcome {
/**
* A bitmap that indicates which rules matched
*/
public final Long vector;
private final Long vector;
/**
* The sum of all scores for all rules evaluated. Similarity rules add the similarity score (between 0.0 and 1.0) whereas
* matcher rules add either a 0.0 or 1.0.
*/
public final Double score;
private final Double score;
/**
* Did the MDM match operation result in creating a new golden resource resource?
@ -134,6 +134,10 @@ public final class MdmMatchOutcome {
return this;
}
public Double getScore() { return score; }
public Long getVector() { return vector; }
/**
* Gets normalized score that is in the range from zero to one
*

View File

@ -89,12 +89,12 @@ public class MdmResourceMatcherSvc {
MdmMatchOutcome match(IBaseResource theLeftResource, IBaseResource theRightResource) {
MdmMatchOutcome matchResult = getMatchOutcome(theLeftResource, theRightResource);
MdmMatchResultEnum matchResultEnum = myMdmRulesJson.getMatchResult(matchResult.vector);
MdmMatchResultEnum matchResultEnum = myMdmRulesJson.getMatchResult(matchResult.getVector());
matchResult.setMatchResultEnum(matchResultEnum);
if (ourLog.isDebugEnabled()) {
ourLog.debug("{} {}: {}", matchResult.getMatchResultEnum(), theRightResource.getIdElement().toUnqualifiedVersionless(), matchResult);
if (ourLog.isTraceEnabled()) {
ourLog.trace("Field matcher results:\n{}", myMdmRulesJson.getDetailedFieldMatchResultWithSuccessInformation(matchResult.vector));
ourLog.trace("Field matcher results:\n{}", myMdmRulesJson.getDetailedFieldMatchResultWithSuccessInformation(matchResult.getVector()));
}
}
return matchResult;
@ -133,8 +133,8 @@ public class MdmResourceMatcherSvc {
ourLog.trace("Matcher {} is valid for resource type: {}. Evaluating match.", fieldComparator.getName(), resourceType);
MdmMatchEvaluation matchEvaluation = fieldComparator.match(theLeftResource, theRightResource);
if (matchEvaluation.match) {
vector |= (1 << i);
ourLog.trace("Match: Successfully matched matcher {} with score {}.", fieldComparator.getName(), matchEvaluation.score);
vector |= (1L << i);
ourLog.trace("Match: Successfully matched matcher {} with score {}. New vector: {}", fieldComparator.getName(), matchEvaluation.score, vector);
} else {
ourLog.trace("No match: Matcher {} did not match (score: {}).", fieldComparator.getName(), matchEvaluation.score);
}

View File

@ -43,8 +43,8 @@ public abstract class BaseR4Test {
}
protected void assertMatchResult(MdmMatchResultEnum theExpectedMatchEnum, long theExpectedVector, double theExpectedScore, boolean theExpectedNewGoldenResource, boolean theExpectedEidMatch, MdmMatchOutcome theMatchResult) {
assertEquals(theExpectedScore, theMatchResult.score, 0.001);
assertEquals(theExpectedVector, theMatchResult.vector);
assertEquals(theExpectedScore, theMatchResult.getScore(), 0.001);
assertEquals(theExpectedVector, theMatchResult.getVector());
assertEquals(theExpectedEidMatch, theMatchResult.isEidMatch());
assertEquals(theExpectedNewGoldenResource, theMatchResult.isCreatedNewResource());
assertEquals(theExpectedMatchEnum, theMatchResult.getMatchResultEnum());

View File

@ -0,0 +1,62 @@
package ca.uhn.fhir.test.utilities;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.both;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class RangeTestHelper {
public static final double THOUSANDTH = .001d;
public static void checkInRange(double base, double value) {
checkInRange(base, THOUSANDTH, value);
}
public static void checkInRange(double theBase, double theRange, double theValue) {
double lowerBound = theBase - theRange;
double upperBound = theBase + theRange;
checkWithinBounds(lowerBound, upperBound, theValue);
}
public static void checkInRange(String theBase, String theValue) {
// ease tests
if (theBase == null && theValue == null) {
return;
}
double value = Double.parseDouble(theValue);
double base = Double.parseDouble(theBase);
checkInRange(base, THOUSANDTH, value);
}
public static void checkInRange(String theBase, double theRange, String theValue) {
// ease tests
if (theBase == null && theValue == null) {
return;
}
double value = Double.parseDouble(theValue);
double base = Double.parseDouble(theBase);
checkInRange(base, theRange, value);
}
public static void checkWithinBounds(double theLowerBound, double theUpperBound, double theValue) {
assertThat(theValue, is(both(greaterThanOrEqualTo(theLowerBound)).and(lessThanOrEqualTo(theUpperBound))));
}
public static void checkWithinBounds(String theLowerBound, String theUpperBound, String theValue) {
assertNotNull(theLowerBound, "theLowerBound");
assertNotNull(theUpperBound, "theUpperBound");
assertNotNull(theValue, "theValue");
double lowerBound = Double.parseDouble(theLowerBound);
double upperBound = Double.parseDouble(theUpperBound);
double value = Double.parseDouble(theValue);
checkWithinBounds(lowerBound, upperBound, value);
}
}

View File

@ -0,0 +1,163 @@
package ca.uhn.fhir.test.utilities;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
class RangeTestHelperTest {
@Nested
public class DefaultRange {
@Test
void checkInRange() {
RangeTestHelper.checkInRange(.83d, .829999999d);
}
@Test
void checkLower() {
AssertionError thrown = assertThrows(
AssertionError.class,
() -> RangeTestHelper.checkInRange(.91, .83)
);
}
@Test
void checkHigher() {
AssertionError thrown = assertThrows(
AssertionError.class,
() -> RangeTestHelper.checkInRange(.26, .25)
);
}
@Nested
public class WithinBounds {
@Test
void checkInRange() {
RangeTestHelper.checkWithinBounds(.91001, .91002, .910013);
}
@Test
void checkLower() {
AssertionError thrown = assertThrows(
AssertionError.class,
() -> RangeTestHelper.checkWithinBounds(.91001, .91002, .9013)
);
}
@Test
void checkHigher() {
AssertionError thrown = assertThrows(
AssertionError.class,
() -> RangeTestHelper.checkWithinBounds(.87, .88, .9)
);
}
@Nested
public class PassingStrings {
@Test
void checkInRange() {
RangeTestHelper.checkWithinBounds(".91001", ".91002", ".910013");
}
@Test
void checkLower() {
AssertionError thrown = assertThrows(
AssertionError.class,
() -> RangeTestHelper.checkWithinBounds(".91001", ".91002", ".9013")
);
}
@Test
void checkHigher() {
AssertionError thrown = assertThrows(
AssertionError.class,
() -> RangeTestHelper.checkWithinBounds(".87", ".88", ".9")
);
}
}
}
@Nested
public class PassingStrings {
@Test
void checkInRange() {
RangeTestHelper.checkInRange("0.83", "0.829999999");
}
@Test
void checkLower() {
AssertionError thrown = assertThrows(
AssertionError.class,
() -> RangeTestHelper.checkInRange(".91", ".83")
);
}
@Test
void checkHigher() {
AssertionError thrown = assertThrows(
AssertionError.class,
() -> RangeTestHelper.checkInRange(".26", "0.25")
);
}
}
}
@Nested
public class ProvidedRange {
@Test
void checkInRange() {
// equals to higher bound
RangeTestHelper.checkInRange(.83, .1, .83);
RangeTestHelper.checkInRange(.831, .02, .833);
}
@Test
void checkLower() {
AssertionError thrown = assertThrows(
AssertionError.class,
() -> RangeTestHelper.checkInRange(.84, .01, .82)
);
}
@Test
void checkHigher() {
AssertionError thrown = assertThrows(
AssertionError.class,
() -> RangeTestHelper.checkInRange(.2511,.0001, .2513)
);
}
@Nested
public class PassingStrings {
@Test
void checkInRange() {
RangeTestHelper.checkInRange(".82", .01, ".83");
RangeTestHelper.checkInRange(".83d", .829999999d, ".8312d");
}
@Test
void checkLower() {
AssertionError thrown = assertThrows(
AssertionError.class,
() -> RangeTestHelper.checkInRange(".91", .02, ".83")
);
}
@Test
void checkHigher() {
AssertionError thrown = assertThrows(
AssertionError.class,
() -> RangeTestHelper.checkInRange(".26", .03, "0.3")
);
}
}
}
}