Freetext search query builder consolidation (#3556)

* Refactor query builder for Reference

* Execute loop for all entries

* Add sandbox disabled test

* Add multiple components tests

* Revert "Refactor query builder for Reference"

This reverts commit 85e0442082.

* Fix imports

* Simplify test

Co-authored-by: juan.marchionatto <juan.marchionatto@smilecdr.com>
Co-authored-by: Michael Buckley <michael.buckley@smilecdr.com>
This commit is contained in:
jmarchionatto 2022-04-21 12:50:21 -04:00 committed by GitHub
parent 1114c72f03
commit f9824d6b15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 676 additions and 2 deletions

View File

@ -131,12 +131,12 @@ public class HibernateSearchIndexWriter {
nestedQtyNode.addValue(QTY_SYSTEM, theValue.getSystem());
nestedQtyNode.addValue(QTY_VALUE, theValue.getValue());
if ( ! myModelConfig.getNormalizedQuantitySearchLevel().storageOrSearchSupported()) { return; }
if ( ! myModelConfig.getNormalizedQuantitySearchLevel().storageOrSearchSupported()) { continue; }
//-- convert the value/unit to the canonical form if any
Pair canonicalForm = UcumServiceUtil.getCanonicalForm(theValue.getSystem(),
BigDecimal.valueOf(theValue.getValue()), theValue.getCode());
if (canonicalForm == null) { return; }
if (canonicalForm == null) { continue; }
double canonicalValue = Double.parseDouble(canonicalForm.getValue().asDecimal());
String canonicalUnits = canonicalForm.getCode();

View File

@ -92,6 +92,7 @@ import static ca.uhn.fhir.jpa.model.util.UcumServiceUtil.UCUM_CODESYSTEM_URL;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
@ -1184,6 +1185,78 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
}
@Test
void testMultipleComponentsHandlesAndOr() {
Observation obs1 = getObservation();
addComponentWithCodeAndQuantity(obs1, "8480-6", 107);
addComponentWithCodeAndQuantity(obs1, "8462-4", 60);
IIdType obs1Id = myObservationDao.create(obs1, mySrd).getId().toUnqualifiedVersionless();
Observation obs2 = getObservation();
addComponentWithCodeAndQuantity(obs2, "8480-6",307);
addComponentWithCodeAndQuantity(obs2, "8462-4",260);
myObservationDao.create(obs2, mySrd).getId().toUnqualifiedVersionless();
// andClauses
{
String theUrl = "/Observation?component-value-quantity=107&component-value-quantity=60";
List<String> resourceIds = myTestDaoSearch.searchForIds(theUrl);
assertThat("when same component with qtys 107 and 60", resourceIds, hasItem(equalTo(obs1Id.getIdPart())));
}
{
String theUrl = "/Observation?component-value-quantity=107&component-value-quantity=260";
List<String> resourceIds = myTestDaoSearch.searchForIds(theUrl);
assertThat("when same component with qtys 107 and 260", resourceIds, empty());
}
//andAndOrClauses
{
String theUrl = "/Observation?component-value-quantity=107&component-value-quantity=gt50,lt70";
List<String> resourceIds = myTestDaoSearch.searchForIds(theUrl);
assertThat("when same component with qtys 107 and lt70,gt80", resourceIds, hasItem(equalTo(obs1Id.getIdPart())));
}
{
String theUrl = "/Observation?component-value-quantity=50,70&component-value-quantity=260";
List<String> resourceIds = myTestDaoSearch.searchForIds(theUrl);
assertThat("when same component with qtys 50,70 and 260", resourceIds, empty());
}
// multipleAndsWithMultipleOrsEach
{
String theUrl = "/Observation?component-value-quantity=50,60&component-value-quantity=105,107";
List<String> resourceIds = myTestDaoSearch.searchForIds(theUrl);
assertThat("when same component with qtys 50,60 and 105,107", resourceIds, hasItem(equalTo(obs1Id.getIdPart())));
}
{
String theUrl = "/Observation?component-value-quantity=50,60&component-value-quantity=250,260";
List<String> resourceIds = myTestDaoSearch.searchForIds(theUrl);
assertThat("when same component with qtys 50,60 and 250,260", resourceIds, empty());
}
}
private Observation getObservation() {
Observation obs = new Observation();
obs.getCode().addCoding().setCode("85354-9").setSystem("http://loinc.org");
obs.setStatus(Observation.ObservationStatus.FINAL);
return obs;
}
private Quantity getQuantity(double theValue) {
return new Quantity().setValue(theValue).setUnit("mmHg").setSystem("http://unitsofmeasure.org").setCode("mm[Hg]");
}
private Observation.ObservationComponentComponent addComponentWithCodeAndQuantity(Observation theObservation, String theConceptCode, double theQuantityValue) {
Observation.ObservationComponentComponent comp = theObservation.addComponent();
CodeableConcept cc1_1 = new CodeableConcept();
cc1_1.addCoding().setCode(theConceptCode).setSystem("http://loinc.org");
comp.setCode(cc1_1);
comp.setValue(getQuantity(theQuantityValue));
return comp;
}
}

View File

@ -0,0 +1,601 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportJobSchedulingHelper;
import ca.uhn.fhir.jpa.dao.TestDaoSearch;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
import ca.uhn.fhir.jpa.test.BaseJpaTest;
import ca.uhn.fhir.jpa.test.config.TestHibernateSearchAddInConfig;
import ca.uhn.fhir.jpa.test.config.TestR4Config;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.rest.param.QuantityParam;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.storage.test.DaoTestDataBuilder;
import ca.uhn.fhir.test.utilities.ITestDataBuilder;
import ca.uhn.fhir.test.utilities.docker.RequiresDocker;
import com.google.common.collect.Lists;
import org.hibernate.search.engine.search.predicate.dsl.BooleanPredicateClausesStep;
import org.hibernate.search.engine.search.predicate.dsl.MatchPredicateOptionsStep;
import org.hibernate.search.engine.search.predicate.dsl.PredicateFinalStep;
import org.hibernate.search.engine.search.predicate.dsl.SearchPredicateFactory;
import org.hibernate.search.engine.search.query.SearchResult;
import org.hibernate.search.mapper.orm.Search;
import org.hibernate.search.mapper.orm.session.SearchSession;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Meta;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Quantity;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.PlatformTransactionManager;
import javax.persistence.EntityManager;
import java.util.Collections;
import java.util.List;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.NESTED_SEARCH_PARAM_ROOT;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_CODE;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_PARAM_NAME;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_SYSTEM;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_VALUE;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/**
* Just a sandbox. Never intended to run by pipes
*/
@ExtendWith(SpringExtension.class)
@RequiresDocker
@ContextConfiguration(classes = {
TestR4Config.class,
TestHibernateSearchAddInConfig.Elasticsearch.class,
DaoTestDataBuilder.Config.class,
TestDaoSearch.Config.class
})
@Disabled
public class HibernateSearchSandboxTest extends BaseJpaTest {
@Autowired
private EntityManager myEntityManager;
@Autowired
private PlatformTransactionManager myTxManager;
@Autowired
private ITestDataBuilder myTestDataBuilder;
@Autowired
private IResourceReindexingSvc myResourceReindexingSvc;
@Autowired
@Qualifier("mySystemDaoR4")
private IFhirSystemDao<Bundle, Meta> mySystemDao;
@Autowired
protected ISearchCoordinatorSvc mySearchCoordinatorSvc;
@Autowired
protected ISearchParamRegistry mySearchParamRegistry;
@Autowired
private IBulkDataExportJobSchedulingHelper myBulkDataScheduleHelper;
@Autowired
@Qualifier("myObservationDaoR4")
private IFhirResourceDao<Observation> myObservationDao;
// @BeforeEach
// public void beforePurgeDatabase() {
// purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry, myBulkDataScheduleHelper);
// }
@BeforeEach
public void enableContainsAndLucene() {
myDaoConfig.setAllowContainsSearches(true);
myDaoConfig.setAdvancedLuceneIndexing(true);
}
@Nested
public class NotNestedObjectQueries {
/**
* Show that when there is only one and clause with "or" entries, we can add the shoulds
* at the top level
*/
@Test
public void searchModelingMultipleAndWithOneOringClauseTest() {
String system = "http://loinc.org";
Observation obs1 = new Observation();
obs1.getCode().setText("Systolic Blood Pressure");
obs1.getCode().addCoding().setCode("obs1").setSystem(system).setDisplay("Systolic Blood Pressure");
obs1.setStatus(Observation.ObservationStatus.FINAL);
obs1.setValue(new Quantity(123));
obs1.getNoteFirstRep().setText("obs1");
IIdType id1 = myObservationDao.create(obs1, mySrd).getId().toUnqualifiedVersionless();
runInTransaction(() -> {
SearchSession searchSession = Search.session(myEntityManager);
SearchResult<ResourceTable> result = searchSession.search(ResourceTable.class)
.where(f -> f.bool(b -> {
b.must(f.match().field("myResourceType").matching("Observation"));
b.must(f.match().field("sp.code.token.system").matching("http://loinc.org"));
b.should(f.match().field("sp.code.token.code").matching("obs3"));
b.should(f.match().field("sp.code.token.code").matching("obs1"));
b.minimumShouldMatchNumber(1);
}))
.fetchAll();
long totalHitCount = result.total().hitCount();
// List<ResourceTable> hits = result.hits();
});
}
/**
* Shows that when there is multiple "and" clause with "or" entries, we need to group each one in a "must" clause
* to be able to add a minimumShouldMatchNumber(1); to each group
*/
@Test
public void searchModelingMultipleAndWithMultipleOrClausesTest() {
String system = "http://loinc.org";
Observation obs1 = new Observation();
obs1.getCode().setText("Systolic Blood Pressure");
obs1.getCode().addCoding().setCode("obs1").setSystem(system).setDisplay("Systolic Blood Pressure");
obs1.setStatus(Observation.ObservationStatus.FINAL);
obs1.setValue(new Quantity(123));
obs1.getNoteFirstRep().setText("obs1");
IIdType id1 = myObservationDao.create(obs1, mySrd).getId().toUnqualifiedVersionless();
runInTransaction(() -> {
SearchSession searchSession = Search.session(myEntityManager);
SearchResult<ResourceTable> result = searchSession.search(ResourceTable.class)
.where(f -> f.bool(b -> {
b.must(f.match().field("myResourceType").matching("Observation"));
b.must(f.match().field("sp.code.token.system").matching("http://loinc.org"));
b.must(f.bool(p -> {
p.should(f.match().field("sp.code.token.code").matching("obs3"));
p.should(f.match().field("sp.code.token.code").matching("obs1"));
p.minimumShouldMatchNumber(1);
}));
b.must(f.bool(p -> {
p.should(f.match().field("sp.code.token.code").matching("obs5"));
p.should(f.match().field("sp.code.token.code").matching("obs1"));
p.minimumShouldMatchNumber(1);
}));
}))
.fetchAll();
long totalHitCount = result.total().hitCount();
// List<ResourceTable> hits = result.hits();
});
}
}
@Nested
public class NestedObjectQueries {
/**
* Show that when there is only one and clause with "or" entries, we can add the shoulds
* at the top level
*/
@Test
public void searchModelingAndMultipleAndWithOneOringClauseTest() {
IIdType myResourceId = myTestDataBuilder.createObservation(myTestDataBuilder.withElementAt("valueQuantity",
myTestDataBuilder.withPrimitiveAttribute("value", 0.6)
// myTestDataBuilder.withPrimitiveAttribute("system", UCUM_CODESYSTEM_URL),
// myTestDataBuilder.withPrimitiveAttribute("code", "mm[Hg]")
));
runInTransaction(() -> {
SearchSession searchSession = Search.session(myEntityManager);
SearchResult<ResourceTable> result = searchSession.search(ResourceTable.class)
.where(f -> f.bool(b -> {
b.must(f.match().field("myResourceType").matching("Observation"));
b.must(f.nested().objectField("nsp.value-quantity")
.nest(f.bool()
.must(f.range().field("nsp.value-quantity.quantity.value").lessThan(0.7))
.should(f.range().field("nsp.value-quantity.quantity.value").between(0.475, 0.525))
.should(f.range().field("nsp.value-quantity.quantity.value").between(0.57, 0.63))
.minimumShouldMatchNumber(1)
));
}))
.fetchAll();
// long totalHitCount = result.total().hitCount();
// List<ResourceTable> hits = result.hits();
});
}
/**
* Shows that when there is multiple "and" clause with "or" entries, we need to group each one in a "must" clause
* to be able to add a minimumShouldMatchNumber(1); to each group
*/
@Test
public void searchModelingMultipleAndWithMultipleOrClausesTest() {
IIdType myResourceId = myTestDataBuilder.createObservation(myTestDataBuilder.withElementAt("valueQuantity",
myTestDataBuilder.withPrimitiveAttribute("value", 0.6)
// myTestDataBuilder.withPrimitiveAttribute("system", UCUM_CODESYSTEM_URL),
// myTestDataBuilder.withPrimitiveAttribute("code", "mm[Hg]")
));
runInTransaction(() -> {
SearchSession searchSession = Search.session(myEntityManager);
SearchResult<ResourceTable> result = searchSession.search(ResourceTable.class)
.where(f -> f.bool(b -> {
b.must(f.match().field("myResourceType").matching("Observation"));
b.must(f.nested().objectField("nsp.value-quantity")
.nest(f.bool()
.must(f.range().field("nsp.value-quantity.quantity.value").lessThan(0.7))
.must(f.bool(p -> {
p.should(f.range().field("nsp.value-quantity.quantity.value").between(0.475, 0.525));
p.should(f.range().field("nsp.value-quantity.quantity.value").between(0.57, 0.63));
p.minimumShouldMatchNumber(1);
}))
.must(f.bool(p -> {
p.should(f.range().field("nsp.value-quantity.quantity.value").between(0.2, 0.8));
p.should(f.range().field("nsp.value-quantity.quantity.value").between(0.7, 0.9));
p.minimumShouldMatchNumber(1);
}))
.minimumShouldMatchNumber(1)
));
}))
.fetchAll();
// long totalHitCount = result.total().hitCount();
// List<ResourceTable> hits = result.hits();
});
// runInTransaction(() -> {
// SearchSession searchSession = Search.session(myEntityManager);
// SearchResult<ResourceTable> result = searchSession.search(ResourceTable.class)
// .where(f -> f.bool(b -> {
// b.must(f.match().field("myResourceType").matching("Observation"));
// b.must(f.bool()
// .must(f.range().field("nsp.value-quantity.quantity.value").lessThan(0.7))
//
// .must(f.bool(p -> {
// p.should(f.range().field("nsp.value-quantity.quantity.value").between(0.475, 0.525));
// p.should(f.range().field("nsp.value-quantity.quantity.value").between(0.57, 0.63));
// p.minimumShouldMatchNumber(1);
// }))
//
// .must(f.bool(p -> {
// p.should(f.range().field("nsp.value-quantity.quantity.value").between(0.2, 0.8));
// p.should(f.range().field("nsp.value-quantity.quantity.value").between(0.7, 0.9));
// p.minimumShouldMatchNumber(1);
// }))
//
// .minimumShouldMatchNumber(1)
// );
// }))
// .fetchAll();
//// long totalHitCount = result.total().hitCount();
//// List<ResourceTable> hits = result.hits();
// });
}
}
/**
* Following code is the beginning of refactoring the queries for cleaner structure, which means
* to try to achieve the clean query structure modeled by previous tests, but using generic methods
*/
@Nested
public class FragmentedCodeNotNested {
private SearchPredicateFactory fact;
@Test
public void searchModelingMultipleAndOneOrClauseTest() {
String system = "http://loinc.org";
Observation obs1 = new Observation();
obs1.getCode().setText("Systolic Blood Pressure");
obs1.getCode().addCoding().setCode("obs1").setSystem(system).setDisplay("Systolic Blood Pressure");
obs1.setStatus(Observation.ObservationStatus.FINAL);
obs1.setValue(new Quantity(123));
obs1.getNoteFirstRep().setText("obs1");
IIdType id1 = myObservationDao.create(obs1, mySrd).getId().toUnqualifiedVersionless();
String paramName = "value-quantity";
List<List<IQueryParameterType>> theQuantityAndOrTerms = Lists.newArrayList();
theQuantityAndOrTerms.add(Collections.singletonList(
new QuantityParam().setValue(0.7)));
theQuantityAndOrTerms.add(Lists.newArrayList(
new QuantityParam().setValue(0.5),
new QuantityParam().setValue(0.6)
));
runInTransaction(() -> {
SearchSession searchSession = Search.session(myEntityManager);
SearchResult<ResourceTable> result = searchSession.search(ResourceTable.class)
.where(f -> {
TestPredBuilder builder = new TestPredBuilder(f);
return builder.buildAndOrPredicates(paramName, theQuantityAndOrTerms);
})
.fetchAll();
long totalHitCount = result.total().hitCount();
// List<ResourceTable> hits = result.hits();
});
}
@Test
public void searchModelingMultipleAndMultipleOrClauseTest() {
String system = "http://loinc.org";
Observation obs1 = new Observation();
obs1.getCode().setText("Systolic Blood Pressure");
obs1.getCode().addCoding().setCode("obs1").setSystem(system).setDisplay("Systolic Blood Pressure");
obs1.setStatus(Observation.ObservationStatus.FINAL);
obs1.setValue(new Quantity(123));
obs1.getNoteFirstRep().setText("obs1");
IIdType id1 = myObservationDao.create(obs1, mySrd).getId().toUnqualifiedVersionless();
String paramName = "value-quantity";
List<List<IQueryParameterType>> theQuantityAndOrTerms = Lists.newArrayList();
theQuantityAndOrTerms.add(Collections.singletonList(
new QuantityParam().setValue(0.7)));
theQuantityAndOrTerms.add(Lists.newArrayList(
new QuantityParam().setValue(0.5),
new QuantityParam().setValue(0.6)
));
theQuantityAndOrTerms.add(Lists.newArrayList(
new QuantityParam().setValue(0.9),
new QuantityParam().setValue(0.6)
));
runInTransaction(() -> {
SearchSession searchSession = Search.session(myEntityManager);
SearchResult<ResourceTable> result = searchSession.search(ResourceTable.class)
.where(f -> {
TestPredBuilder builder = new TestPredBuilder(f);
return builder.buildAndOrPredicates(paramName, theQuantityAndOrTerms);
})
.fetchAll();
long totalHitCount = result.total().hitCount();
// List<ResourceTable> hits = result.hits();
});
}
}
private static class TestPredBuilder {
private static final double QTY_APPROX_TOLERANCE_PERCENT = .10;
private static final double QTY_TOLERANCE_PERCENT = .05;
SearchPredicateFactory myPredicateFactory;
public TestPredBuilder(SearchPredicateFactory theF) { myPredicateFactory = theF; }
public PredicateFinalStep buildAndOrPredicates(
String theSearchParamName, List<List<IQueryParameterType>> theAndOrTerms) {
boolean isNested = isNested(theSearchParamName);
// we need to know if there is more than one "and" predicate (outer list) with more than one "or" predicate (inner list)
long maxOrPredicateSize = theAndOrTerms.stream().map(List::size).filter(s -> s > 1).count();
BooleanPredicateClausesStep<?> topBool = myPredicateFactory.bool();
topBool.must(myPredicateFactory.match().field("myResourceType").matching("Observation"));
BooleanPredicateClausesStep<?> activeBool = topBool;
if (isNested) {
BooleanPredicateClausesStep<?> nestedBool = myPredicateFactory.bool();
activeBool = nestedBool;
}
for (List<IQueryParameterType> andTerm : theAndOrTerms) {
if (andTerm.size() == 1) {
// buildSinglePredicate
// activeBool.must(myPredicateFactory.match().field("nsp.value-quantity.quantity.value").matching(0.7));
addOnePredicate(activeBool, true, theSearchParamName, andTerm.get(0));
continue;
}
if (maxOrPredicateSize <= 1) {
// this is the only list of or predicates with more than 1 entry so
// no need to separate it in a group. Can be part of main and clauses
for (IQueryParameterType orTerm : andTerm) {
addOnePredicate(activeBool, false, theSearchParamName, orTerm);
}
activeBool.minimumShouldMatchNumber(1);
} else {
// this is not the only list of or predicates with more than 1 entry
// so all of them need to be separated in groups with a minimumShouldMatchNumber(1)
activeBool.must(myPredicateFactory.bool(p -> {
for (IQueryParameterType orTerm : andTerm) {
addOnePredicate(p, false, theSearchParamName, orTerm);
}
p.minimumShouldMatchNumber(1);
}));
}
}
if (isNested) {
topBool.must(myPredicateFactory.nested().objectField("nsp.value-quantity").nest(activeBool));
}
return topBool;
}
private boolean isNested(String theSearchParamName) {
if (theSearchParamName.equals("value-quantity")) {
return true;
}
return false;
}
private void addOnePredicate(BooleanPredicateClausesStep<?> theTopBool, boolean theIsMust,
String theParamName, IQueryParameterType theParameterType) {
if (theParameterType instanceof QuantityParam) {
addQuantityOrClauses(theTopBool, theIsMust, theParamName, theParameterType);
return;
}
throw new IllegalStateException("Shouldn't reach this code");
}
private void addQuantityOrClauses(BooleanPredicateClausesStep<?> theTopBool, boolean theIsMust,
String theSearchParamName, IQueryParameterType theParamType) {
String fieldPath = NESTED_SEARCH_PARAM_ROOT + "." + theSearchParamName + "." + QTY_PARAM_NAME;
QuantityParam qtyParam = QuantityParam.toQuantityParam(theParamType);
ParamPrefixEnum activePrefix = qtyParam.getPrefix() == null ? ParamPrefixEnum.EQUAL : qtyParam.getPrefix();
// if (myModelConfig.getNormalizedQuantitySearchLevel() == NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED) {
// QuantityParam canonicalQty = UcumServiceUtil.toCanonicalQuantityOrNull(qtyParam);
// if (canonicalQty != null) {
// String valueFieldPath = fieldPath + "." + QTY_VALUE_NORM;
// setPrefixedQuantityPredicate(orQuantityTerms, activePrefix, canonicalQty, valueFieldPath);
// orQuantityTerms.must(myPredicateFactory.match()
// .field(fieldPath + "." + QTY_CODE_NORM)
// .matching(canonicalQty.getUnits()));
// return orQuantityTerms;
// }
// }
// not NORMALIZED_QUANTITY_SEARCH_SUPPORTED or non-canonicalizable parameter
addQuantityTerms(theTopBool, theIsMust, activePrefix, qtyParam, fieldPath);
}
private void addQuantityTerms(BooleanPredicateClausesStep<?> theTopBool, boolean theIsMust,
ParamPrefixEnum theActivePrefix, QuantityParam theQtyParam, String theFieldPath) {
String valueFieldPath = theFieldPath + "." + QTY_VALUE;
PredicateFinalStep rangePred = getPrefixedRangePredicate(theActivePrefix, theQtyParam, valueFieldPath);
addMustOrShould(theIsMust, theTopBool, rangePred);
if (isNotBlank(theQtyParam.getSystem())) {
addFieldPredicate(theIsMust, theTopBool, theFieldPath + "." + QTY_SYSTEM, theQtyParam.getSystem());
}
if (isNotBlank(theQtyParam.getUnits())) {
addFieldPredicate(theIsMust, theTopBool, theFieldPath + "." + QTY_CODE, theQtyParam.getUnits());
}
}
private void addFieldPredicate(boolean theIsMust, BooleanPredicateClausesStep<?> theTopBool, String theFieldPath, String theValue) {
MatchPredicateOptionsStep<?> pred = myPredicateFactory.match().field(theFieldPath).matching(theValue);
addMustOrShould(theIsMust, theTopBool, pred);
}
private void addMustOrShould(boolean theIsMust, BooleanPredicateClausesStep<?> theTopBool, PredicateFinalStep thePredicate) {
if (theIsMust) {
theTopBool.must(thePredicate);
} else {
theTopBool.should(thePredicate);
}
}
private PredicateFinalStep getPrefixedRangePredicate(
ParamPrefixEnum thePrefix, QuantityParam theQuantity, String valueFieldPath) {
double value = theQuantity.getValue().doubleValue();
double approxTolerance = value * QTY_APPROX_TOLERANCE_PERCENT;
double defaultTolerance = value * QTY_TOLERANCE_PERCENT;
switch (thePrefix) {
// searches for resource quantity between passed param value +/- 10%
case APPROXIMATE:
return myPredicateFactory.range()
.field(valueFieldPath)
.between(value - approxTolerance, value + approxTolerance);
// searches for resource quantity between passed param value +/- 5%
case EQUAL:
return myPredicateFactory.range()
.field(valueFieldPath)
.between(value - defaultTolerance, value + defaultTolerance);
// searches for resource quantity > param value
case GREATERTHAN:
case STARTS_AFTER: // treated as GREATERTHAN because search doesn't handle ranges
return myPredicateFactory.range()
.field(valueFieldPath)
.greaterThan(value);
// searches for resource quantity not < param value
case GREATERTHAN_OR_EQUALS:
return myPredicateFactory.range()
.field(valueFieldPath)
.atLeast(value);
// searches for resource quantity < param value
case LESSTHAN:
case ENDS_BEFORE: // treated as LESSTHAN because search doesn't handle ranges
return myPredicateFactory.range()
.field(valueFieldPath)
.lessThan(value);
// searches for resource quantity not > param value
case LESSTHAN_OR_EQUALS:
return myPredicateFactory.range()
.field(valueFieldPath)
.atMost(value);
// NOT_EQUAL: searches for resource quantity not between passed param value +/- 5%
case NOT_EQUAL:
return myPredicateFactory.bool(b -> {
b.should(myPredicateFactory.range()
.field(valueFieldPath)
.between(null, value - defaultTolerance));
b.should(myPredicateFactory.range()
.field(valueFieldPath)
.between(value + defaultTolerance, null));
b.minimumShouldMatchNumber(1);
});
}
throw new IllegalStateException("Should not reach here");
}
}
@Override
protected FhirContext getFhirContext() {
return myFhirContext;
}
@Override
protected PlatformTransactionManager getTxManager() {
return myTxManager;
}
}