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
2 changes: 2 additions & 0 deletions api/src/main/java/org/openmrs/PersonName.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public class PersonName extends BaseChangeableOpenmrsData implements java.io.Ser
@FullTextField(name = "givenNameStart", analyzer = SearchAnalysis.START_ANALYZER, searchAnalyzer = SearchAnalysis.EXACT_ANALYZER)
@FullTextField(name = "givenNameAnywhere", analyzer = SearchAnalysis.ANYWHERE_ANALYZER, searchAnalyzer = SearchAnalysis.EXACT_ANALYZER)
@FullTextField(name = "givenNameSoundex", analyzer = SearchAnalysis.SOUNDEX_ANALYZER)
@FullTextField(name = "givenNameNickname", analyzer = SearchAnalysis.NICKNAME_ANALYZER, searchAnalyzer = SearchAnalysis.EXACT_ANALYZER)
private String givenName;

private String prefix;
Expand All @@ -77,6 +78,7 @@ public class PersonName extends BaseChangeableOpenmrsData implements java.io.Ser
@FullTextField(name = "middleNameStart", analyzer = SearchAnalysis.START_ANALYZER, searchAnalyzer = SearchAnalysis.EXACT_ANALYZER)
@FullTextField(name = "middleNameAnywhere", analyzer = SearchAnalysis.ANYWHERE_ANALYZER, searchAnalyzer = SearchAnalysis.EXACT_ANALYZER)
@FullTextField(name = "middleNameSoundex", analyzer = SearchAnalysis.SOUNDEX_ANALYZER)
@FullTextField(name = "middleNameNickname", analyzer = SearchAnalysis.NICKNAME_ANALYZER, searchAnalyzer = SearchAnalysis.EXACT_ANALYZER)
private String middleName;

private String familyNamePrefix;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,12 @@ private SearchPredicate getPersonNameQuery(SearchPredicateFactory predicateFacto
Arrays.asList("givenNameAnywhere", "middleNameAnywhere", "familyNameAnywhere", "familyName2Anywhere"));
}

boolean nickname = Boolean.parseBoolean(Context.getAdministrationService()
.getGlobalProperty(OpenmrsConstants.GLOBAL_PROPERTY_PATIENT_SEARCH_MATCH_NICKNAME, "false"));
if (nickname) {
fields.addAll(Arrays.asList("givenNameNickname", "middleNameNickname"));
}

return newPersonNameSearchQuery(predicateFactory, fields, query, orQueryParser, includeVoided, patientsOnly, dead,
null, null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,6 @@ private SearchAnalysis() {
public static final String SOUNDEX_ANALYZER = "soundexAnalyzer";

public static final String NAME_ANALYZER = "nameAnalyzer";

public static final String NICKNAME_ANALYZER = "nicknameAnalyzer";
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
package org.openmrs.api.db.hibernate.search.lucene;

import org.apache.lucene.analysis.classic.ClassicFilterFactory;
import org.apache.lucene.analysis.core.FlattenGraphFilterFactory;
import org.apache.lucene.analysis.core.KeywordTokenizerFactory;
import org.apache.lucene.analysis.core.LowerCaseFilterFactory;
import org.apache.lucene.analysis.core.WhitespaceTokenizerFactory;
Expand All @@ -18,6 +19,7 @@
import org.apache.lucene.analysis.ngram.NGramFilterFactory;
import org.apache.lucene.analysis.phonetic.PhoneticFilterFactory;
import org.apache.lucene.analysis.standard.StandardTokenizerFactory;
import org.apache.lucene.analysis.synonym.SynonymGraphFilterFactory;
import org.hibernate.search.backend.lucene.analysis.LuceneAnalysisConfigurationContext;
import org.hibernate.search.backend.lucene.analysis.LuceneAnalysisConfigurer;
import org.openmrs.api.db.hibernate.search.SearchAnalysis;
Expand Down Expand Up @@ -61,5 +63,15 @@ public void configure(LuceneAnalysisConfigurationContext context) {
context.analyzer(SearchAnalysis.SOUNDEX_ANALYZER).custom().tokenizer(StandardTokenizerFactory.class)
.tokenFilter(ClassicFilterFactory.class).tokenFilter(LowerCaseFilterFactory.class)
.tokenFilter(PhoneticFilterFactory.class).param("encoder", "Soundex");

// Same backbone as EXACT_ANALYZER, plus a SynonymGraphFilter that expands a name into its
// common nicknames at index time using the curated nicknames.txt dictionary. The graph the
// synonym filter produces must be flattened with FlattenGraphFilter before it can be indexed.
// Queries use the plain EXACT_ANALYZER (see PersonName), so expansion happens only at index time.
context.analyzer(SearchAnalysis.NICKNAME_ANALYZER).custom().tokenizer(WhitespaceTokenizerFactory.class)
.tokenFilter(ClassicFilterFactory.class).tokenFilter(LowerCaseFilterFactory.class)
.tokenFilter(ASCIIFoldingFilterFactory.class).tokenFilter(SynonymGraphFilterFactory.class)
.param("synonyms", "org/openmrs/api/db/hibernate/search/nicknames.txt").param("expand", "true")
.param("ignoreCase", "true").tokenFilter(FlattenGraphFilterFactory.class);
}
}
6 changes: 6 additions & 0 deletions api/src/main/java/org/openmrs/util/OpenmrsConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,8 @@ public static final Collection<String> AUTO_ROLES() {

public static final String GLOBAL_PROPERTY_PATIENT_SEARCH_MATCH_SOUNDEX = "SOUNDEX";

public static final String GLOBAL_PROPERTY_PATIENT_SEARCH_MATCH_NICKNAME = "patientSearch.matchMode.nickname";

public static final String GLOBAL_PROPERTY_PROVIDER_SEARCH_MATCH_MODE = "providerSearch.matchMode";

public static final String GLOBAL_PROPERTY_DEFAULT_SERIALIZER = "serialization.defaultSerializer";
Expand Down Expand Up @@ -881,6 +883,10 @@ public static final List<GlobalProperty> CORE_GLOBAL_PROPERTIES() {
props.add(new GlobalProperty(GLOBAL_PROPERTY_PATIENT_SEARCH_MATCH_MODE, GLOBAL_PROPERTY_PATIENT_SEARCH_MATCH_START,
"Specifies how patient names are matched while searching patient. Valid values are 'ANYWHERE' or 'START'. Defaults to start if missing or invalid value is present."));

props.add(new GlobalProperty(GLOBAL_PROPERTY_PATIENT_SEARCH_MATCH_NICKNAME, "false",
"When set to true, patient name search also matches common English nicknames (e.g. searching 'Bob' finds a patient registered as 'Robert' and vice versa). Additive to the patientSearch.matchMode setting. Requires a search index rebuild to take effect. Defaults to false.",
BooleanDatatype.class, null));

props.add(new GlobalProperty(GP_ENABLE_CONCEPT_MAP_TYPE_MANAGEMENT, "false",
"Enables or disables management of concept map types", BooleanDatatype.class, null));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Curated English given-name nickname dictionary (Solr synonym format).
#
# Used by the "nicknameAnalyzer" defined in LuceneConfig to expand a person's
# given/middle name into its common nicknames at index time, so that a search
# for a nickname (e.g. "Bob") matches a patient registered under the formal
# name (e.g. "Robert") and vice versa.
#
# Each comma-separated line is an equivalence group: every term is treated as a
# synonym of every other term in that line (SynonymGraphFilter with expand=true).
# Keep entries lowercase; the analyzer lowercases tokens before this filter runs.
#
# Scope (v1): English given/middle names only. Surnames and locale-specific
# dictionaries are out of scope.

robert, rob, bob, bobby, robbie
william, will, bill, billy, liam
richard, rick, rich, dick, ricky
james, jim, jimmy, jamie
john, johnny, jack
michael, mike, mikey, mick
charles, charlie, chuck, chas
joseph, joe, joey
thomas, tom, tommy
edward, ed, eddie, ted, ned
anthony, tony
daniel, dan, danny
matthew, matt
christopher, chris
nicholas, nick, nicky
margaret, maggie, peggy, meg, marge
elizabeth, liz, beth, betty, eliza, lizzie
katherine, catherine, kate, katie, kathy, cathy
jennifer, jen, jenny
patricia, pat, patty, tricia
deborah, deb, debbie
susan, sue, suzie
barbara, barb, babs
victoria, vicky, tori
samantha, sam, sammy
alexandra, alex, lexi, sandra
44 changes: 44 additions & 0 deletions api/src/test/java/org/openmrs/api/db/PatientDAOTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,50 @@
OpenmrsConstants.GLOBAL_PROPERTY_PERSON_ATTRIBUTE_SEARCH_MATCH_EXACT);
}

/**
* Verifies the nickname-aware name search: with the nickname GlobalProperty enabled, searching for
* a common English nickname matches a patient registered under the formal given name.
*
* @see PatientDAO#getPatients(String,Integer,Integer)
*/
@Test
public void getPatients_shouldMatchNicknameWhenNicknameSearchIsEnabled() {
// register an existing patient under the formal given name "Robert"
Patient patient = patientService.getPatient(2);
patient.getPersonName().setGivenName("Robert");
patientService.savePatient(patient);
updateSearchIndex();

globalPropertiesTestHelper.setGlobalProperty(OpenmrsConstants.GLOBAL_PROPERTY_PATIENT_SEARCH_MATCH_NICKNAME, "true");

List<Patient> results = dao.getPatients("Bob", 0, null);

assertTrue(results.contains(patient),
"searching the nickname 'Bob' should match the patient registered as 'Robert'");
}

/**
* Regression guard: with the nickname GlobalProperty off (the default), name search behaves exactly
* as before and a nickname does not match the formal name.
*
* @see PatientDAO#getPatients(String,Integer,Integer)
*/
@Test
public void getPatients_shouldNotMatchNicknameWhenNicknameSearchIsDisabled() {
Patient patient = patientService.getPatient(2);
patient.getPersonName().setGivenName("Robert");
patientService.savePatient(patient);
updateSearchIndex();

globalPropertiesTestHelper.setGlobalProperty(OpenmrsConstants.GLOBAL_PROPERTY_PATIENT_SEARCH_MATCH_NICKNAME,
"false");

List<Patient> results = dao.getPatients("Bob", 0, null);

assertFalse(results.contains(patient),
"with nickname search disabled, 'Bob' must not match the patient registered as 'Robert'");
}

/**
* @see PatientDAO#getPatients(String,String,List<QPatientIdentifierType;>,null)
*/
Expand Down Expand Up @@ -214,7 +258,7 @@
* @see PatientDAO#getPatients(String,String,List<QPatientIdentifierType;>,null)
*/
@Test
public void getPatients_shouldEscapePercentageCharacterInNamePhrase() {

Check warning on line 261 in api/src/test/java/org/openmrs/api/db/PatientDAOTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace these 3 tests with a single Parameterized one.

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-core&issues=AZ6hcoBoiIVtV7kfLvPn&open=AZ6hcoBoiIVtV7kfLvPn&pullRequest=6170

Patient patient2 = patientService.getPatient(2);
PersonName name = new PersonName("%cats", "and", "dogs");
Expand Down Expand Up @@ -514,7 +558,7 @@
List<PatientIdentifierType> patientIdentifierTypes = dao.getPatientIdentifierTypes("Old Identification Number", null,
null, null);

assertEquals(patientIdentifierTypes.size(), 1);

Check warning on line 561 in api/src/test/java/org/openmrs/api/db/PatientDAOTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Swap these 2 arguments so they are in the correct order: expected value, actual value.

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-core&issues=AZ6hcoBoiIVtV7kfLvPp&open=AZ6hcoBoiIVtV7kfLvPp&pullRequest=6170
assertEquals(oldIdNumberNonRetired, patientIdentifierTypes.get(0));
}

Expand All @@ -529,7 +573,7 @@

List<PatientIdentifierType> patientIdentifierTypes = dao.getPatientIdentifierTypes(null, "1", null, null);

assertEquals(patientIdentifierTypes.size(), 1);

Check warning on line 576 in api/src/test/java/org/openmrs/api/db/PatientDAOTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Swap these 2 arguments so they are in the correct order: expected value, actual value.

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-core&issues=AZ6hcoBoiIVtV7kfLvPq&open=AZ6hcoBoiIVtV7kfLvPq&pullRequest=6170
assertEquals(formatOneNonRetired, patientIdentifierTypes.get(0));
}

Expand All @@ -544,7 +588,7 @@

List<PatientIdentifierType> patientIdentifierTypes = dao.getPatientIdentifierTypes(null, null, false, null);

assertEquals(patientIdentifierTypes.size(), 3);

Check warning on line 591 in api/src/test/java/org/openmrs/api/db/PatientDAOTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Swap these 2 arguments so they are in the correct order: expected value, actual value.

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-core&issues=AZ6hcoBoiIVtV7kfLvPr&open=AZ6hcoBoiIVtV7kfLvPr&pullRequest=6170
assertTrue(patientIdentifierTypes.contains(nonRetiredNonRequired1));
assertTrue(patientIdentifierTypes.contains(nonRetiredNonRequired2));
assertTrue(patientIdentifierTypes.contains(nonRetiredNonRequired3));
Expand All @@ -562,7 +606,7 @@

List<PatientIdentifierType> patientIdentifierTypes = dao.getPatientIdentifierTypes(null, null, true, null);

assertEquals(patientIdentifierTypes.size(), 1);

Check warning on line 609 in api/src/test/java/org/openmrs/api/db/PatientDAOTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Swap these 2 arguments so they are in the correct order: expected value, actual value.

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-core&issues=AZ6hcoBoiIVtV7kfLvPs&open=AZ6hcoBoiIVtV7kfLvPs&pullRequest=6170
assertEquals(nonRetiredRequired, patientIdentifierTypes.get(0));
}

Expand All @@ -572,20 +616,20 @@
@Test
public void getPatientIdentifierTypes_shouldReturnOnlyNonRetiredPatientIdentifierTypes() {
PatientIdentifierType nonRetiredType1 = dao.getPatientIdentifierType(1);
assertEquals(nonRetiredType1.getRetired(), false);

Check warning on line 619 in api/src/test/java/org/openmrs/api/db/PatientDAOTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Swap these 2 arguments so they are in the correct order: expected value, actual value.

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-core&issues=AZ6hcoBoiIVtV7kfLvPt&open=AZ6hcoBoiIVtV7kfLvPt&pullRequest=6170

PatientIdentifierType nonRetiredType2 = dao.getPatientIdentifierType(2);
assertEquals(nonRetiredType2.getRetired(), false);

Check warning on line 622 in api/src/test/java/org/openmrs/api/db/PatientDAOTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Swap these 2 arguments so they are in the correct order: expected value, actual value.

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-core&issues=AZ6hcoBoiIVtV7kfLvPu&open=AZ6hcoBoiIVtV7kfLvPu&pullRequest=6170

PatientIdentifierType nonRetiredType3 = dao.getPatientIdentifierType(5);
assertEquals(nonRetiredType3.getRetired(), false);

Check warning on line 625 in api/src/test/java/org/openmrs/api/db/PatientDAOTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Swap these 2 arguments so they are in the correct order: expected value, actual value.

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-core&issues=AZ6hcoBoiIVtV7kfLvPv&open=AZ6hcoBoiIVtV7kfLvPv&pullRequest=6170

PatientIdentifierType retiredType = dao.getPatientIdentifierType(4);
assertEquals(retiredType.getRetired(), true);

Check warning on line 628 in api/src/test/java/org/openmrs/api/db/PatientDAOTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Swap these 2 arguments so they are in the correct order: expected value, actual value.

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-core&issues=AZ6hcoBoiIVtV7kfLvPw&open=AZ6hcoBoiIVtV7kfLvPw&pullRequest=6170

List<PatientIdentifierType> patientIdentifierTypes = dao.getPatientIdentifierTypes(null, null, null, null);

assertEquals(patientIdentifierTypes.size(), 3);

Check warning on line 632 in api/src/test/java/org/openmrs/api/db/PatientDAOTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Swap these 2 arguments so they are in the correct order: expected value, actual value.

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-core&issues=AZ6hcoBoiIVtV7kfLvPx&open=AZ6hcoBoiIVtV7kfLvPx&pullRequest=6170
assertTrue(patientIdentifierTypes.contains(nonRetiredType1));
assertTrue(patientIdentifierTypes.contains(nonRetiredType2));
assertTrue(patientIdentifierTypes.contains(nonRetiredType3));
Expand Down Expand Up @@ -940,7 +984,7 @@
* boolean)
*/
@Test
public void getPatients_shouldGetPatientByShortMiddleName_SignatureNo1() {

Check warning on line 987 in api/src/test/java/org/openmrs/api/db/PatientDAOTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace these 6 tests with a single Parameterized one.

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-core&issues=AZ6hcoBoiIVtV7kfLvPo&open=AZ6hcoBoiIVtV7kfLvPo&pullRequest=6170
List<Patient> patients = dao.getPatients("ec", 0, 11);

assertEquals(1, patients.size());
Expand Down