diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/NumericEncoder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/NumericEncoder.java
new file mode 100644
index 00000000000..1619748d470
--- /dev/null
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/NumericEncoder.java
@@ -0,0 +1,18 @@
+package ca.uhn.fhir.context.phonetic;
+
+import com.google.common.base.CharMatcher;
+
+// Useful for numerical identifiers like phone numbers, address parts etc.
+// This should not be used where decimals are important. A new "quantity encoder" should be added to handle cases like that.
+public class NumericEncoder implements IPhoneticEncoder {
+ @Override
+ public String name() {
+ return "NUMERIC";
+ }
+
+ @Override
+ public String encode(String theString) {
+ // Remove everything but the numbers
+ return CharMatcher.inRange('0', '9').retainFrom(theString);
+ }
+}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/PhoneticEncoderEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/PhoneticEncoderEnum.java
index 28549a71629..605a8ae24ca 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/PhoneticEncoderEnum.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/PhoneticEncoderEnum.java
@@ -39,7 +39,8 @@ public enum PhoneticEncoderEnum {
METAPHONE(new ApacheEncoder("METAPHONE", new Metaphone())),
NYSIIS(new ApacheEncoder("NYSIIS", new Nysiis())),
REFINED_SOUNDEX(new ApacheEncoder("REFINED_SOUNDEX", new RefinedSoundex())),
- SOUNDEX(new ApacheEncoder("SOUNDEX", new Soundex()));
+ SOUNDEX(new ApacheEncoder("SOUNDEX", new Soundex())),
+ NUMERIC(new NumericEncoder());
private final IPhoneticEncoder myPhoneticEncoder;
diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/context/phonetic/PhoneticEncoderTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/context/phonetic/PhoneticEncoderTest.java
index bca150978cb..e43327eb818 100644
--- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/context/phonetic/PhoneticEncoderTest.java
+++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/context/phonetic/PhoneticEncoderTest.java
@@ -1,14 +1,14 @@
package ca.uhn.fhir.context.phonetic;
-import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import static org.hamcrest.Matchers.startsWith;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.endsWith;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.assertEquals;
class PhoneticEncoderTest {
private static final Logger ourLog = LoggerFactory.getLogger(PhoneticEncoderTest.class);
@@ -23,7 +23,11 @@ class PhoneticEncoderTest {
public void testEncodeAddress(PhoneticEncoderEnum thePhoneticEncoderEnum) {
String encoded = thePhoneticEncoderEnum.getPhoneticEncoder().encode(ADDRESS_LINE);
ourLog.info("{}: {}", thePhoneticEncoderEnum.name(), encoded);
- assertThat(encoded, startsWith(NUMBER + " "));
- assertThat(encoded, endsWith(" " + SUITE));
+ if (thePhoneticEncoderEnum == PhoneticEncoderEnum.NUMERIC) {
+ assertEquals(NUMBER + SUITE, encoded);
+ } else {
+ assertThat(encoded, startsWith(NUMBER + " "));
+ assertThat(encoded, endsWith(" " + SUITE));
+ }
}
}
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2547-mdm-add-numeric-matcher.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2547-mdm-add-numeric-matcher.yaml
new file mode 100644
index 00000000000..24aace91fdf
--- /dev/null
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2547-mdm-add-numeric-matcher.yaml
@@ -0,0 +1,5 @@
+---
+type: add
+issue: 2547
+title: "Added new NUMERIC mdm matcher for matching phone numbers. Also added NUMERIC phonetic encoder to support
+adding NUMERIC encoded search parameter (e.g. if searching for matching phone numbers is required by mdm candidate searching)."
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_rules.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_rules.md
index a4fea7f82d0..3075b67543d 100644
--- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_rules.md
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_rules.md
@@ -292,10 +292,10 @@ The following algorithms are currently supported:
Gail = Gael, Gail != Gale, Thomas != Tom |
- CAVERPHONE1 |
+ CAVERPHONE2 |
matcher |
- Apache Caverphone1
+ Apache Caverphone2
|
Gail = Gael, Gail = Gale, Thomas != Tom |
@@ -379,6 +379,14 @@ The following algorithms are currently supported:
2019-12,Month = 2019-12-19,Day |
+
+ NUMERIC |
+ matcher |
+
+ Remove all non-numeric characters from the string before comparing.
+ |
+ 4169671111 = (416) 967-1111 |
+
NAME_ANY_ORDER |
matcher |
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3PhoneticSearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3PhoneticSearchNoFtTest.java
index bda399dbd76..75b74c87aa8 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3PhoneticSearchNoFtTest.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3PhoneticSearchNoFtTest.java
@@ -1,12 +1,13 @@
package ca.uhn.fhir.jpa.dao.dstu3;
import ca.uhn.fhir.context.phonetic.ApacheEncoder;
+import ca.uhn.fhir.context.phonetic.NumericEncoder;
import ca.uhn.fhir.context.phonetic.PhoneticEncoderEnum;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
-import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.rest.param.StringParam;
+import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.util.HapiExtensions;
import org.apache.commons.codec.language.Soundex;
import org.hl7.fhir.dstu3.model.Enumerations;
@@ -35,10 +36,14 @@ public class FhirResourceDaoDstu3PhoneticSearchNoFtTest extends BaseJpaDstu3Test
public static final String GAIL = "Gail";
public static final String NAME_SOUNDEX_SP = "nameSoundex";
public static final String ADDRESS_LINE_SOUNDEX_SP = "addressLineSoundex";
+ public static final String PHONE_NUMBER_SP = "phoneNumber";
private static final String BOB = "BOB";
private static final String ADDRESS = "123 Nohili St";
private static final String ADDRESS_CLOSE = "123 Nohily St";
private static final String ADDRESS_FAR = "123 College St";
+ private static final String PHONE = "4169671111";
+ private static final String PHONE_CLOSE = "(416) 967-1111";
+ private static final String PHONE_FAR = "416 421 0421";
@Autowired
ISearchParamRegistry mySearchParamRegistry;
@@ -49,8 +54,9 @@ public class FhirResourceDaoDstu3PhoneticSearchNoFtTest extends BaseJpaDstu3Test
myDaoConfig.setReuseCachedSearchResultsForMillis(null);
myDaoConfig.setFetchSizeDefaultMaximum(new DaoConfig().getFetchSizeDefaultMaximum());
- createSoundexSearchParameter(NAME_SOUNDEX_SP, PhoneticEncoderEnum.SOUNDEX, "Patient.name");
- createSoundexSearchParameter(ADDRESS_LINE_SOUNDEX_SP, PhoneticEncoderEnum.SOUNDEX, "Patient.address.line");
+ createPhoneticSearchParameter(NAME_SOUNDEX_SP, PhoneticEncoderEnum.SOUNDEX, "Patient.name");
+ createPhoneticSearchParameter(ADDRESS_LINE_SOUNDEX_SP, PhoneticEncoderEnum.SOUNDEX, "Patient.address.line");
+ createPhoneticSearchParameter(PHONE_NUMBER_SP, PhoneticEncoderEnum.NUMERIC, "Patient.telecom");
mySearchParamRegistry.forceRefresh();
mySearchParamRegistry.setPhoneticEncoder(new ApacheEncoder(PhoneticEncoderEnum.SOUNDEX.name(), new Soundex()));
}
@@ -70,6 +76,15 @@ public class FhirResourceDaoDstu3PhoneticSearchNoFtTest extends BaseJpaDstu3Test
ourLog.info("Encoded address: {}", soundex.encode(ADDRESS));
}
+ @Test
+ public void testNumeric() {
+ NumericEncoder numeric = new NumericEncoder();
+ assertEquals(PHONE, numeric.encode(PHONE_CLOSE));
+ assertEquals(PHONE, numeric.encode(PHONE));
+ assertEquals(numeric.encode(PHONE), numeric.encode(PHONE_CLOSE));
+ assertNotEquals(numeric.encode(PHONE), numeric.encode(PHONE_FAR));
+ }
+
@Test
public void phoneticMatch() {
Patient patient;
@@ -77,15 +92,16 @@ public class FhirResourceDaoDstu3PhoneticSearchNoFtTest extends BaseJpaDstu3Test
patient = new Patient();
patient.addName().addGiven(GALE);
patient.addAddress().addLine(ADDRESS);
+ patient.addTelecom().setValue(PHONE);
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient));
IIdType pId = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
List stringParams = myResourceIndexedSearchParamStringDao.findAll();
- assertThat(stringParams, hasSize(6));
+ assertThat(stringParams, hasSize(7));
List stringParamNames = stringParams.stream().map(ResourceIndexedSearchParamString::getParamName).collect(Collectors.toList());
- assertThat(stringParamNames, containsInAnyOrder(Patient.SP_NAME, Patient.SP_GIVEN, Patient.SP_PHONETIC, NAME_SOUNDEX_SP, Patient.SP_ADDRESS, ADDRESS_LINE_SOUNDEX_SP));
+ assertThat(stringParamNames, containsInAnyOrder(Patient.SP_NAME, Patient.SP_GIVEN, Patient.SP_PHONETIC, NAME_SOUNDEX_SP, Patient.SP_ADDRESS, ADDRESS_LINE_SOUNDEX_SP, PHONE_NUMBER_SP));
assertSearchMatch(pId, Patient.SP_PHONETIC, GALE);
assertSearchMatch(pId, Patient.SP_PHONETIC, GAIL);
@@ -98,6 +114,10 @@ public class FhirResourceDaoDstu3PhoneticSearchNoFtTest extends BaseJpaDstu3Test
assertSearchMatch(pId, ADDRESS_LINE_SOUNDEX_SP, ADDRESS);
assertSearchMatch(pId, ADDRESS_LINE_SOUNDEX_SP, ADDRESS_CLOSE);
assertNoMatch(ADDRESS_LINE_SOUNDEX_SP, ADDRESS_FAR);
+
+ assertSearchMatch(pId, PHONE_NUMBER_SP, PHONE);
+ assertSearchMatch(pId, PHONE_NUMBER_SP, PHONE_CLOSE);
+ assertNoMatch(PHONE_NUMBER_SP, PHONE_FAR);
}
private void assertSearchMatch(IIdType thePId1, String theSp, String theValue) {
@@ -114,7 +134,7 @@ public class FhirResourceDaoDstu3PhoneticSearchNoFtTest extends BaseJpaDstu3Test
assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), hasSize(0));
}
- private void createSoundexSearchParameter(String theCode, PhoneticEncoderEnum theEncoder, String theFhirPath) {
+ private void createPhoneticSearchParameter(String theCode, PhoneticEncoderEnum theEncoder, String theFhirPath) {
SearchParameter searchParameter = new SearchParameter();
searchParameter.addBase("Patient");
searchParameter.setCode(theCode);
diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/MdmMatcherEnum.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/MdmMatcherEnum.java
index 458387d14d3..f29dad1827c 100644
--- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/MdmMatcherEnum.java
+++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/MdmMatcherEnum.java
@@ -51,7 +51,8 @@ public enum MdmMatcherEnum {
IDENTIFIER(new IdentifierMatcher()),
EMPTY_FIELD(new EmptyFieldMatcher()),
- EXTENSION_ANY_ORDER(new ExtensionMatcher());
+ EXTENSION_ANY_ORDER(new ExtensionMatcher()),
+ NUMERIC(new HapiStringMatcher(new NumericMatcher()));
private final IMdmFieldMatcher myMdmFieldMatcher;
diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/NumericMatcher.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/NumericMatcher.java
new file mode 100644
index 00000000000..82bce7d59c0
--- /dev/null
+++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/NumericMatcher.java
@@ -0,0 +1,16 @@
+package ca.uhn.fhir.mdm.rules.matcher;
+
+import ca.uhn.fhir.context.phonetic.NumericEncoder;
+
+// Useful for numerical identifiers like phone numbers, address parts etc.
+// This should not be used where decimals are important. A new "quantity matcher" should be added to handle cases like that.
+public class NumericMatcher implements IMdmStringMatcher {
+ private final NumericEncoder encoder = new NumericEncoder();
+
+ @Override
+ public boolean matches(String theLeftString, String theRightString) {
+ String left = encoder.encode(theLeftString);
+ String right = encoder.encode(theRightString);
+ return left.equals(right);
+ }
+}
diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/StringMatcherR4Test.java b/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/StringMatcherR4Test.java
index 42508046adc..73aafb7aebe 100644
--- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/StringMatcherR4Test.java
+++ b/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/StringMatcherR4Test.java
@@ -14,24 +14,33 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
public class StringMatcherR4Test extends BaseMatcherR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(StringMatcherR4Test.class);
- public static final String LEFT = "namadega";
- public static final String RIGHT = "namaedga";
+ public static final String LEFT_NAME = "namadega";
+ public static final String RIGHT_NAME = "namaedga";
@Test
public void testNamadega() {
- assertTrue(match(MdmMatcherEnum.COLOGNE, LEFT, RIGHT));
- assertTrue(match(MdmMatcherEnum.DOUBLE_METAPHONE, LEFT, RIGHT));
- assertTrue(match(MdmMatcherEnum.MATCH_RATING_APPROACH, LEFT, RIGHT));
- assertTrue(match(MdmMatcherEnum.METAPHONE, LEFT, RIGHT));
- assertTrue(match(MdmMatcherEnum.SOUNDEX, LEFT, RIGHT));
- assertTrue(match(MdmMatcherEnum.METAPHONE, LEFT, RIGHT));
+ String left = LEFT_NAME;
+ String right = RIGHT_NAME;
+ assertTrue(match(MdmMatcherEnum.COLOGNE, left, right));
+ assertTrue(match(MdmMatcherEnum.DOUBLE_METAPHONE, left, right));
+ assertTrue(match(MdmMatcherEnum.MATCH_RATING_APPROACH, left, right));
+ assertTrue(match(MdmMatcherEnum.METAPHONE, left, right));
+ assertTrue(match(MdmMatcherEnum.SOUNDEX, left, right));
+ assertTrue(match(MdmMatcherEnum.METAPHONE, left, right));
- assertFalse(match(MdmMatcherEnum.CAVERPHONE1, LEFT, RIGHT));
- assertFalse(match(MdmMatcherEnum.CAVERPHONE2, LEFT, RIGHT));
- assertFalse(match(MdmMatcherEnum.NYSIIS, LEFT, RIGHT));
- assertFalse(match(MdmMatcherEnum.REFINED_SOUNDEX, LEFT, RIGHT));
- assertFalse(match(MdmMatcherEnum.STRING, LEFT, RIGHT));
- assertFalse(match(MdmMatcherEnum.SUBSTRING, LEFT, RIGHT));
+ assertFalse(match(MdmMatcherEnum.CAVERPHONE1, left, right));
+ assertFalse(match(MdmMatcherEnum.CAVERPHONE2, left, right));
+ assertFalse(match(MdmMatcherEnum.NYSIIS, left, right));
+ assertFalse(match(MdmMatcherEnum.REFINED_SOUNDEX, left, right));
+ assertFalse(match(MdmMatcherEnum.STRING, left, right));
+ assertFalse(match(MdmMatcherEnum.SUBSTRING, left, right));
+ }
+
+ @Test
+ public void testNumeric() {
+ assertTrue(match(MdmMatcherEnum.NUMERIC, "4169671111", "(416) 967-1111"));
+ assertFalse(match(MdmMatcherEnum.NUMERIC, "5169671111", "(416) 967-1111"));
+ assertFalse(match(MdmMatcherEnum.NUMERIC, "4169671111", "(416) 967-1111x123"));
}
@Test