mirror of
https://github.com/hapifhir/hapi-fhir.git
synced 2025-03-09 14:33:32 +00:00
Implement support for Quantity search parameters in the Hibernate Search index (#3477)
* markers for new quantity hibernate search support * Add quantity search * Add normalized (canonical) quantity search * Add comment * Use double values instead of BigDecimal to avoid scaling * Remove multiple case labels which are not supported until source version 14 * Remove switch rules which are not supported until source version 14 * Use constant for error message used in tests * A failing test for multiple indexed quantity paths * Disallow dirtying context for nested classes * failing test for normalized units * Make sp.value-quantity.quantity a nested structure to handle correlated queries for code and value. Save units for canonical case and consider them when querying. Collapse normalized sub-structure under *.quantity. * Nest sp.value-quantity instead of sp.value-quantity.quantity for compatibility with other nested types (IE. token) * Comment out debug statements * Update hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/search/ExtendedLuceneClauseBuilder.java Co-authored-by: Olivia You <46392181+oliviayou@users.noreply.github.com> * Add test for nested correlated quantity query * Address MR comments * Add proper handling of or query predicates * Use positive conditions for better readability * Add tests to validate behavior of combined and and or clauses, including some making no sense but still syntactically correct. * Only consider normalized quantity searching if ModelConfig search level is NORMALIZED_QUANTITY_SEARCH_SUPPORTED Co-authored-by: juan.marchionatto <juan.marchionatto@smilecdr.com> Co-authored-by: jmarchionatto <60409882+jmarchionatto@users.noreply.github.com> Co-authored-by: Olivia You <46392181+oliviayou@users.noreply.github.com>
This commit is contained in:
parent
01d6e15f90
commit
1a8678cb1c
@ -25,7 +25,7 @@ import ca.uhn.fhir.rest.gclient.NumberClientParam.IMatches;
|
||||
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
|
||||
|
||||
/**
|
||||
* Token parameter type for use in fluent client interfaces
|
||||
* Quantity parameter type for use in fluent client interfaces
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public class QuantityClientParam extends BaseClientParam implements IParam {
|
||||
|
@ -29,7 +29,10 @@ public abstract class BaseParamWithPrefix<T extends BaseParam> extends BaseParam
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseParamWithPrefix.class);
|
||||
|
||||
|
||||
|
||||
public static final String MSG_PREFIX_INVALID_FORMAT = "Invalid date/time/quantity format: ";
|
||||
|
||||
private ParamPrefixEnum myPrefix;
|
||||
|
||||
/**
|
||||
@ -58,7 +61,7 @@ public abstract class BaseParamWithPrefix<T extends BaseParam> extends BaseParam
|
||||
}
|
||||
|
||||
if (offset > 0 && theString.length() == offset) {
|
||||
throw new DataFormatException(Msg.code(1940) + "Invalid date/time format: \"" + theString + "\"");
|
||||
throw new DataFormatException(Msg.code(1940) + MSG_PREFIX_INVALID_FORMAT + "\"" + theString + "\"");
|
||||
}
|
||||
|
||||
String prefix = theString.substring(0, offset);
|
||||
|
@ -29,6 +29,7 @@ import ca.uhn.fhir.jpa.dao.search.ExtendedLuceneIndexExtractor;
|
||||
import ca.uhn.fhir.jpa.dao.search.ExtendedLuceneResourceProjection;
|
||||
import ca.uhn.fhir.jpa.dao.search.ExtendedLuceneSearchBuilder;
|
||||
import ca.uhn.fhir.jpa.dao.search.LastNOperation;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.model.search.ExtendedLuceneIndexData;
|
||||
import ca.uhn.fhir.jpa.search.autocomplete.ValueSetAutocompleteOptions;
|
||||
@ -81,6 +82,9 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
|
||||
@Autowired
|
||||
IIdHelperService myIdHelperService;
|
||||
|
||||
@Autowired
|
||||
ModelConfig myModelConfig;
|
||||
|
||||
final private ExtendedLuceneSearchBuilder myAdvancedIndexQueryBuilder = new ExtendedLuceneSearchBuilder();
|
||||
|
||||
private Boolean ourDisabled;
|
||||
@ -95,7 +99,8 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
|
||||
public ExtendedLuceneIndexData extractLuceneIndexData(IBaseResource theResource, ResourceIndexedSearchParams theNewParams) {
|
||||
String resourceType = myFhirContext.getResourceType(theResource);
|
||||
ResourceSearchParams activeSearchParams = mySearchParamRegistry.getActiveSearchParams(resourceType);
|
||||
ExtendedLuceneIndexExtractor extractor = new ExtendedLuceneIndexExtractor(myDaoConfig, myFhirContext, activeSearchParams, mySearchParamExtractor);
|
||||
ExtendedLuceneIndexExtractor extractor = new ExtendedLuceneIndexExtractor(
|
||||
myDaoConfig, myFhirContext, activeSearchParams, mySearchParamExtractor, myModelConfig);
|
||||
return extractor.extract(theResource,theNewParams);
|
||||
}
|
||||
|
||||
@ -126,7 +131,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
|
||||
)
|
||||
.where(
|
||||
f -> f.bool(b -> {
|
||||
ExtendedLuceneClauseBuilder builder = new ExtendedLuceneClauseBuilder(myFhirContext, b, f);
|
||||
ExtendedLuceneClauseBuilder builder = new ExtendedLuceneClauseBuilder(myFhirContext, myModelConfig, b, f);
|
||||
|
||||
/*
|
||||
* Handle _content parameter (resource body content)
|
||||
@ -226,7 +231,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
|
||||
public IBaseResource tokenAutocompleteValueSetSearch(ValueSetAutocompleteOptions theOptions) {
|
||||
ensureElastic();
|
||||
|
||||
ValueSetAutocompleteSearch autocomplete = new ValueSetAutocompleteSearch(myFhirContext, getSearchSession());
|
||||
ValueSetAutocompleteSearch autocomplete = new ValueSetAutocompleteSearch(myFhirContext, myModelConfig, getSearchSession());
|
||||
|
||||
return autocomplete.search(theOptions);
|
||||
}
|
||||
@ -253,7 +258,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
|
||||
@Override
|
||||
public List<ResourcePersistentId> lastN(SearchParameterMap theParams, Integer theMaximumResults) {
|
||||
ensureElastic();
|
||||
List<Long> pidList = new LastNOperation(getSearchSession(), myFhirContext, mySearchParamRegistry)
|
||||
List<Long> pidList = new LastNOperation(getSearchSession(), myFhirContext, myModelConfig, mySearchParamRegistry)
|
||||
.executeLastN(theParams, theMaximumResults);
|
||||
return convertLongsToResourcePersistentIds(pidList);
|
||||
}
|
||||
|
@ -22,12 +22,16 @@ package ca.uhn.fhir.jpa.dao.search;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
|
||||
import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
|
||||
import ca.uhn.fhir.model.api.IQueryParameterType;
|
||||
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.param.DateParam;
|
||||
import ca.uhn.fhir.rest.param.DateRangeParam;
|
||||
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
|
||||
import ca.uhn.fhir.rest.param.QuantityParam;
|
||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||
import ca.uhn.fhir.rest.param.StringParam;
|
||||
import ca.uhn.fhir.rest.param.TokenParam;
|
||||
@ -56,20 +60,33 @@ import java.util.stream.Collectors;
|
||||
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.IDX_STRING_EXACT;
|
||||
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.IDX_STRING_NORMALIZED;
|
||||
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.IDX_STRING_TEXT;
|
||||
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_CODE_NORM;
|
||||
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 ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_VALUE_NORM;
|
||||
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.SEARCH_PARAM_ROOT;
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
public class ExtendedLuceneClauseBuilder {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(ExtendedLuceneClauseBuilder.class);
|
||||
|
||||
private static final double QTY_APPROX_TOLERANCE_PERCENT = .10;
|
||||
private static final double QTY_TOLERANCE_PERCENT = .05;
|
||||
|
||||
final FhirContext myFhirContext;
|
||||
public final SearchPredicateFactory myPredicateFactory;
|
||||
public final BooleanPredicateClausesStep<?> myRootClause;
|
||||
public final ModelConfig myModelConfig;
|
||||
|
||||
final List<TemporalPrecisionEnum> ordinalSearchPrecisions = Arrays.asList(TemporalPrecisionEnum.YEAR, TemporalPrecisionEnum.MONTH, TemporalPrecisionEnum.DAY);
|
||||
|
||||
public ExtendedLuceneClauseBuilder(FhirContext myFhirContext, BooleanPredicateClausesStep<?> myRootClause, SearchPredicateFactory myPredicateFactory) {
|
||||
public ExtendedLuceneClauseBuilder(FhirContext myFhirContext, ModelConfig theModelConfig,
|
||||
BooleanPredicateClausesStep<?> myRootClause, SearchPredicateFactory myPredicateFactory) {
|
||||
this.myFhirContext = myFhirContext;
|
||||
this.myModelConfig = theModelConfig;
|
||||
this.myRootClause = myRootClause;
|
||||
this.myPredicateFactory = myPredicateFactory;
|
||||
}
|
||||
@ -453,4 +470,137 @@ public class ExtendedLuceneClauseBuilder {
|
||||
|
||||
throw new IllegalArgumentException(Msg.code(2026) + "Date search param does not support prefix of type: " + prefix);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Differences with DB search:
|
||||
* _ is not all-normalized-or-all-not. Each parameter is applied on quantity or normalized quantity depending on UCUM fitness
|
||||
* _ respects ranges for equal and approximate qualifiers
|
||||
*
|
||||
* Strategy: For each parameter, if it can be canonicalized, it is, and used against 'normalized-value-quantity' index
|
||||
* otherwise it is applied as-is to 'value-quantity'
|
||||
*/
|
||||
public void addQuantityUnmodifiedSearch(String theSearchParamName, List<List<IQueryParameterType>> theQuantityAndOrTerms) {
|
||||
|
||||
for (List<IQueryParameterType> nextAnd : theQuantityAndOrTerms) {
|
||||
BooleanPredicateClausesStep<?> quantityTerms = myPredicateFactory.bool();
|
||||
quantityTerms.minimumShouldMatchNumber(1);
|
||||
|
||||
for (IQueryParameterType paramType : nextAnd) {
|
||||
BooleanPredicateClausesStep<?> orQuantityTerms = myPredicateFactory.bool();
|
||||
addQuantityOrClauses(theSearchParamName, paramType, orQuantityTerms);
|
||||
quantityTerms.should(orQuantityTerms);
|
||||
}
|
||||
|
||||
myRootClause.must(quantityTerms);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void addQuantityOrClauses(String theSearchParamName,
|
||||
IQueryParameterType theParamType, BooleanPredicateClausesStep<?> theQuantityTerms) {
|
||||
|
||||
QuantityParam qtyParam = QuantityParam.toQuantityParam(theParamType);
|
||||
ParamPrefixEnum activePrefix = qtyParam.getPrefix() == null ? ParamPrefixEnum.EQUAL : qtyParam.getPrefix();
|
||||
String fieldPath = NESTED_SEARCH_PARAM_ROOT + "." + theSearchParamName + "." + QTY_PARAM_NAME;
|
||||
|
||||
if (myModelConfig.getNormalizedQuantitySearchLevel() == NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED) {
|
||||
QuantityParam canonicalQty = UcumServiceUtil.toCanonicalQuantityOrNull(qtyParam);
|
||||
if (canonicalQty != null) {
|
||||
String valueFieldPath = fieldPath + "." + QTY_VALUE_NORM;
|
||||
setPrefixedQuantityPredicate(theQuantityTerms, activePrefix, canonicalQty, valueFieldPath);
|
||||
theQuantityTerms.must(myPredicateFactory.match()
|
||||
.field(fieldPath + "." + QTY_CODE_NORM)
|
||||
.matching(canonicalQty.getUnits()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// not NORMALIZED_QUANTITY_SEARCH_SUPPORTED or non-canonicalizable parameter
|
||||
String valueFieldPath = fieldPath + "." + QTY_VALUE;
|
||||
setPrefixedQuantityPredicate(theQuantityTerms, activePrefix, qtyParam, valueFieldPath);
|
||||
|
||||
if ( isNotBlank(qtyParam.getSystem()) ) {
|
||||
theQuantityTerms.must(
|
||||
myPredicateFactory.match()
|
||||
.field(fieldPath + "." + QTY_SYSTEM).matching(qtyParam.getSystem()) );
|
||||
}
|
||||
|
||||
if ( isNotBlank(qtyParam.getUnits()) ) {
|
||||
theQuantityTerms.must(
|
||||
myPredicateFactory.match()
|
||||
.field(fieldPath + "." + QTY_CODE).matching(qtyParam.getUnits()) );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void setPrefixedQuantityPredicate(BooleanPredicateClausesStep<?> theQuantityTerms,
|
||||
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:
|
||||
theQuantityTerms.must(
|
||||
myPredicateFactory.range()
|
||||
.field(valueFieldPath)
|
||||
.between(value-approxTolerance, value+approxTolerance));
|
||||
break;
|
||||
|
||||
// searches for resource quantity between passed param value +/- 5%
|
||||
case EQUAL:
|
||||
theQuantityTerms.must(
|
||||
myPredicateFactory.range()
|
||||
.field(valueFieldPath)
|
||||
.between(value-defaultTolerance, value+defaultTolerance));
|
||||
break;
|
||||
|
||||
// searches for resource quantity > param value
|
||||
case GREATERTHAN:
|
||||
case STARTS_AFTER: // treated as GREATERTHAN because search doesn't handle ranges
|
||||
theQuantityTerms.must(
|
||||
myPredicateFactory.range()
|
||||
.field(valueFieldPath)
|
||||
.greaterThan(value));
|
||||
break;
|
||||
|
||||
// searches for resource quantity not < param value
|
||||
case GREATERTHAN_OR_EQUALS:
|
||||
theQuantityTerms.must(
|
||||
myPredicateFactory.range()
|
||||
.field(valueFieldPath)
|
||||
.atLeast(value));
|
||||
break;
|
||||
|
||||
// searches for resource quantity < param value
|
||||
case LESSTHAN:
|
||||
case ENDS_BEFORE: // treated as LESSTHAN because search doesn't handle ranges
|
||||
theQuantityTerms.must(
|
||||
myPredicateFactory.range()
|
||||
.field(valueFieldPath)
|
||||
.lessThan(value));
|
||||
break;
|
||||
|
||||
// searches for resource quantity not > param value
|
||||
case LESSTHAN_OR_EQUALS:
|
||||
theQuantityTerms.must(
|
||||
myPredicateFactory.range()
|
||||
.field(valueFieldPath)
|
||||
.atMost(value));
|
||||
break;
|
||||
|
||||
// NOT_EQUAL: searches for resource quantity not between passed param value +/- 5%
|
||||
case NOT_EQUAL:
|
||||
theQuantityTerms.mustNot(
|
||||
myPredicateFactory.range()
|
||||
.field(valueFieldPath)
|
||||
.between(value-defaultTolerance, value+defaultTolerance));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.dao.search;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceLink;
|
||||
import ca.uhn.fhir.jpa.model.search.ExtendedLuceneIndexData;
|
||||
@ -54,17 +55,20 @@ public class ExtendedLuceneIndexExtractor {
|
||||
private final FhirContext myContext;
|
||||
private final ResourceSearchParams myParams;
|
||||
private final ISearchParamExtractor mySearchParamExtractor;
|
||||
private final ModelConfig myModelConfig;
|
||||
|
||||
public ExtendedLuceneIndexExtractor(DaoConfig theDaoConfig, FhirContext theContext, ResourceSearchParams theActiveParams, ISearchParamExtractor theSearchParamExtractor) {
|
||||
public ExtendedLuceneIndexExtractor(DaoConfig theDaoConfig, FhirContext theContext, ResourceSearchParams theActiveParams,
|
||||
ISearchParamExtractor theSearchParamExtractor, ModelConfig theModelConfig) {
|
||||
myDaoConfig = theDaoConfig;
|
||||
myContext = theContext;
|
||||
myParams = theActiveParams;
|
||||
mySearchParamExtractor = theSearchParamExtractor;
|
||||
myModelConfig = theModelConfig;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public ExtendedLuceneIndexData extract(IBaseResource theResource, ResourceIndexedSearchParams theNewParams) {
|
||||
ExtendedLuceneIndexData retVal = new ExtendedLuceneIndexData(myContext);
|
||||
ExtendedLuceneIndexData retVal = new ExtendedLuceneIndexData(myContext, myModelConfig);
|
||||
|
||||
if(myDaoConfig.isStoreResourceInLuceneIndex()) {
|
||||
retVal.setRawResourceData(myContext.newJsonParser().encodeResourceToString(theResource));
|
||||
@ -84,6 +88,10 @@ public class ExtendedLuceneIndexExtractor {
|
||||
retVal.addDateIndexData(nextParam.getParamName(), nextParam.getValueLow(), nextParam.getValueLowDateOrdinal(),
|
||||
nextParam.getValueHigh(), nextParam.getValueHighDateOrdinal()));
|
||||
|
||||
theNewParams.myQuantityParams.forEach(nextParam ->
|
||||
retVal.addQuantityIndexData(nextParam.getParamName(), nextParam.getUnits(), nextParam.getSystem(), nextParam.getValue().doubleValue()));
|
||||
|
||||
|
||||
if (!theNewParams.myLinks.isEmpty()) {
|
||||
|
||||
// awkwardly, links are indexed by jsonpath, not by search param.
|
||||
|
@ -91,7 +91,8 @@ public class ExtendedLuceneSearchBuilder {
|
||||
return false;
|
||||
}
|
||||
} else if (param instanceof QuantityParam) {
|
||||
return false;
|
||||
return modifier.equals(EMPTY_MODIFIER);
|
||||
|
||||
} else if (param instanceof ReferenceParam) {
|
||||
//We cannot search by chain.
|
||||
if (((ReferenceParam) param).getChain() != null) {
|
||||
@ -152,6 +153,8 @@ public class ExtendedLuceneSearchBuilder {
|
||||
break;
|
||||
|
||||
case QUANTITY:
|
||||
List<List<IQueryParameterType>> quantityAndOrTerms = theParams.removeByNameUnmodified(nextParam);
|
||||
builder.addQuantityUnmodifiedSearch(nextParam, quantityAndOrTerms);
|
||||
break;
|
||||
|
||||
case REFERENCE:
|
||||
|
@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.dao.search;
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.searchparam.util.LastNParameterHelper;
|
||||
@ -38,12 +39,15 @@ public class LastNOperation {
|
||||
public static final String OBSERVATION_RES_TYPE = "Observation";
|
||||
private final SearchSession mySession;
|
||||
private final FhirContext myFhirContext;
|
||||
private final ModelConfig myModelConfig;
|
||||
private final ISearchParamRegistry mySearchParamRegistry;
|
||||
private final ExtendedLuceneSearchBuilder myExtendedLuceneSearchBuilder = new ExtendedLuceneSearchBuilder();
|
||||
|
||||
public LastNOperation(SearchSession theSession, FhirContext theFhirContext, ISearchParamRegistry theSearchParamRegistry) {
|
||||
public LastNOperation(SearchSession theSession, FhirContext theFhirContext, ModelConfig theModelConfig,
|
||||
ISearchParamRegistry theSearchParamRegistry) {
|
||||
mySession = theSession;
|
||||
myFhirContext = theFhirContext;
|
||||
myModelConfig = theModelConfig;
|
||||
mySearchParamRegistry = theSearchParamRegistry;
|
||||
}
|
||||
|
||||
@ -57,7 +61,7 @@ public class LastNOperation {
|
||||
.where(f -> f.bool(b -> {
|
||||
// Must match observation type
|
||||
b.must(f.match().field("myResourceType").matching(OBSERVATION_RES_TYPE));
|
||||
ExtendedLuceneClauseBuilder builder = new ExtendedLuceneClauseBuilder(myFhirContext, b, f);
|
||||
ExtendedLuceneClauseBuilder builder = new ExtendedLuceneClauseBuilder(myFhirContext, myModelConfig, b, f);
|
||||
myExtendedLuceneSearchBuilder.addAndConsumeAdvancedQueryClauses(builder, OBSERVATION_RES_TYPE, theParams.clone(), mySearchParamRegistry);
|
||||
}))
|
||||
.aggregation(observationsByCodeKey, f -> f.fromJson(lastNAggregation.toAggregation()))
|
||||
|
@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.search.autocomplete;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.dao.search.ExtendedLuceneClauseBuilder;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import com.google.gson.JsonObject;
|
||||
import org.hibernate.search.backend.elasticsearch.ElasticsearchExtension;
|
||||
@ -47,10 +48,12 @@ class TokenAutocompleteSearch {
|
||||
private static final AggregationKey<JsonObject> AGGREGATION_KEY = AggregationKey.of("autocomplete");
|
||||
|
||||
private final FhirContext myFhirContext;
|
||||
private final ModelConfig myModelConfig;
|
||||
private final SearchSession mySession;
|
||||
|
||||
public TokenAutocompleteSearch(FhirContext theFhirContext, SearchSession theSession) {
|
||||
public TokenAutocompleteSearch(FhirContext theFhirContext, ModelConfig theModelConfig, SearchSession theSession) {
|
||||
myFhirContext = theFhirContext;
|
||||
myModelConfig = theModelConfig;
|
||||
mySession = theSession;
|
||||
}
|
||||
|
||||
@ -71,7 +74,7 @@ class TokenAutocompleteSearch {
|
||||
// compose the query json
|
||||
SearchQueryOptionsStep<?, ?, SearchLoadingOptionsStep, ?, ?> query = mySession.search(ResourceTable.class)
|
||||
.where(predFactory -> predFactory.bool(boolBuilder -> {
|
||||
ExtendedLuceneClauseBuilder clauseBuilder = new ExtendedLuceneClauseBuilder(myFhirContext, boolBuilder, predFactory);
|
||||
ExtendedLuceneClauseBuilder clauseBuilder = new ExtendedLuceneClauseBuilder(myFhirContext, myModelConfig, boolBuilder, predFactory);
|
||||
|
||||
// we apply resource-level predicates here, at the top level
|
||||
if (isNotBlank(theResourceName)) {
|
||||
|
@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.search.autocomplete;
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.rest.param.TokenParam;
|
||||
import org.hibernate.search.mapper.orm.session.SearchSession;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
@ -34,12 +35,14 @@ import java.util.List;
|
||||
*/
|
||||
public class ValueSetAutocompleteSearch {
|
||||
private final FhirContext myFhirContext;
|
||||
private final ModelConfig myModelConfig;
|
||||
private final TokenAutocompleteSearch myAutocompleteSearch;
|
||||
static final int DEFAULT_SIZE = 30;
|
||||
|
||||
public ValueSetAutocompleteSearch(FhirContext theFhirContext, SearchSession theSession) {
|
||||
public ValueSetAutocompleteSearch(FhirContext theFhirContext, ModelConfig theModelConfig, SearchSession theSession) {
|
||||
myFhirContext = theFhirContext;
|
||||
myAutocompleteSearch = new TokenAutocompleteSearch(myFhirContext, theSession);
|
||||
myModelConfig = theModelConfig;
|
||||
myAutocompleteSearch = new TokenAutocompleteSearch(myFhirContext, myModelConfig, theSession);
|
||||
}
|
||||
|
||||
public IBaseResource search(ValueSetAutocompleteOptions theOptions) {
|
||||
|
@ -5,6 +5,7 @@ import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
|
||||
import ca.uhn.fhir.jpa.search.autocomplete.ValueSetAutocompleteOptions;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.param.QuantityParam;
|
||||
import ca.uhn.fhir.rest.param.StringAndListParam;
|
||||
import ca.uhn.fhir.rest.param.StringOrListParam;
|
||||
import ca.uhn.fhir.rest.param.StringParam;
|
||||
@ -30,6 +31,7 @@ import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.hl7.fhir.r4.model.Observation.SP_VALUE_QUANTITY;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@ -125,8 +127,8 @@ public class FhirResourceDaoR4SearchFtTest extends BaseJpaR4Test {
|
||||
IIdType id1 = myObservationDao.create(obs1, mockSrd()).getId().toUnqualifiedVersionless();
|
||||
|
||||
Observation obs2 = new Observation();
|
||||
obs1.getCode().setText("AAAAA");
|
||||
obs1.setValue(new StringType("Diastolic Blood Pressure"));
|
||||
obs2.getCode().setText("AAAAA");
|
||||
obs2.setValue(new StringType("Diastolic Blood Pressure"));
|
||||
obs2.setStatus(ObservationStatus.FINAL);
|
||||
IIdType id2 = myObservationDao.create(obs2, mockSrd()).getId().toUnqualifiedVersionless();
|
||||
|
||||
@ -483,4 +485,41 @@ public class FhirResourceDaoR4SearchFtTest extends BaseJpaR4Test {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testResourceQuantitySearch() {
|
||||
Observation obs1 = new Observation();
|
||||
obs1.getCode().setText("Systolic Blood Pressure");
|
||||
obs1.setStatus(ObservationStatus.FINAL);
|
||||
obs1.setValue(new Quantity(123));
|
||||
obs1.getNoteFirstRep().setText("obs1");
|
||||
IIdType id1 = myObservationDao.create(obs1, mySrd).getId().toUnqualifiedVersionless();
|
||||
|
||||
Observation obs2 = new Observation();
|
||||
obs2.getCode().setText("Diastolic Blood Pressure");
|
||||
obs2.setStatus(ObservationStatus.FINAL);
|
||||
obs2.setValue(new Quantity(81));
|
||||
IIdType id2 = myObservationDao.create(obs2, mySrd).getId().toUnqualifiedVersionless();
|
||||
|
||||
SearchParameterMap map;
|
||||
|
||||
map = new SearchParameterMap();
|
||||
map.add(SP_VALUE_QUANTITY, new QuantityParam("ap122"));
|
||||
assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(map)), contains(toValues(id1)));
|
||||
|
||||
map = new SearchParameterMap();
|
||||
map.add(SP_VALUE_QUANTITY, new QuantityParam("le90"));
|
||||
assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(map)), contains(toValues(id2)));
|
||||
|
||||
map = new SearchParameterMap();
|
||||
map.add(SP_VALUE_QUANTITY, new QuantityParam("gt80"));
|
||||
assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(map)), containsInAnyOrder(toValues(id1, id2)));
|
||||
|
||||
map = new SearchParameterMap();
|
||||
map.add(SP_VALUE_QUANTITY, new QuantityParam("gt80"));
|
||||
map.add(SP_VALUE_QUANTITY, new QuantityParam("lt90"));
|
||||
assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(map)), contains(toValues(id2)));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion;
|
||||
import ca.uhn.fhir.jpa.entity.TermConcept;
|
||||
import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
|
||||
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
|
||||
@ -28,6 +29,7 @@ import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
|
||||
import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc;
|
||||
import ca.uhn.fhir.jpa.term.api.ITermReadSvcR4;
|
||||
import ca.uhn.fhir.parser.DataFormatException;
|
||||
import ca.uhn.fhir.parser.IParser;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
@ -39,6 +41,7 @@ import ca.uhn.fhir.rest.param.TokenParam;
|
||||
import ca.uhn.fhir.rest.param.TokenParamModifier;
|
||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.test.utilities.ITestDataBuilder;
|
||||
import ca.uhn.fhir.test.utilities.LogbackLevelOverrideExtension;
|
||||
import ca.uhn.fhir.test.utilities.docker.RequiresDocker;
|
||||
import ca.uhn.fhir.validation.FhirValidator;
|
||||
import ca.uhn.fhir.validation.ValidationResult;
|
||||
@ -67,11 +70,16 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.TestContext;
|
||||
import org.springframework.test.context.TestExecutionListeners;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
|
||||
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
@ -80,14 +88,18 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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.equalTo;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.stringContainsInOrder;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@RequiresDocker
|
||||
@ -98,6 +110,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
TestDaoSearch.Config.class
|
||||
})
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
|
||||
@TestExecutionListeners(listeners = {
|
||||
DependencyInjectionTestExecutionListener.class
|
||||
,FhirResourceDaoR4SearchWithElasticSearchIT.TestDirtiesContextTestExecutionListener.class
|
||||
})
|
||||
public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
|
||||
public static final String URL_MY_CODE_SYSTEM = "http://example.com/my_code_system";
|
||||
public static final String URL_MY_VALUE_SET = "http://example.com/my_value_set";
|
||||
@ -156,6 +172,8 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
|
||||
@Autowired
|
||||
@Qualifier("myQuestionnaireResponseDaoR4")
|
||||
private IFhirResourceDao<QuestionnaireResponse> myQuestionnaireResponseDao;
|
||||
@RegisterExtension
|
||||
LogbackLevelOverrideExtension myLogbackLevelOverrideExtension = new LogbackLevelOverrideExtension();
|
||||
|
||||
|
||||
@BeforeEach
|
||||
@ -702,7 +720,7 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
|
||||
.getExpansion()
|
||||
.getContains()
|
||||
.stream()
|
||||
.map(t -> t.getCode())
|
||||
.map(ValueSet.ValueSetExpansionContainsComponent::getCode)
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
assertThat(codes.toString(), codes, Matchers.contains("advice", "message", "note", "notification"));
|
||||
@ -727,7 +745,7 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
|
||||
.getExpansion()
|
||||
.getContains()
|
||||
.stream()
|
||||
.map(t -> t.getCode())
|
||||
.map(ValueSet.ValueSetExpansionContainsComponent::getCode)
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
assertThat(codes.toString(), codes, Matchers.contains("advice", "note"));
|
||||
@ -942,4 +960,475 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
public class QuantityAndNormalizedQuantitySearch {
|
||||
|
||||
private IIdType myResourceId;
|
||||
|
||||
|
||||
@Nested
|
||||
public class QuantitySearch {
|
||||
|
||||
@Nested
|
||||
public class SimpleQueries {
|
||||
|
||||
@Test
|
||||
public void noQuantityThrows() {
|
||||
String invalidQtyParam = "|http://another.org";
|
||||
DataFormatException thrown = assertThrows(DataFormatException.class,
|
||||
() -> myTestDaoSearch.searchForIds("/Observation?value-quantity=" + invalidQtyParam));
|
||||
|
||||
assertTrue(thrown.getMessage().startsWith("HAPI-1940: Invalid"));
|
||||
assertTrue(thrown.getMessage().contains(invalidQtyParam));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidPrefixThrows() {
|
||||
DataFormatException thrown = assertThrows(DataFormatException.class,
|
||||
() -> myTestDaoSearch.searchForIds("/Observation?value-quantity=st5.35"));
|
||||
|
||||
assertEquals("HAPI-1941: Invalid prefix: \"st\"", thrown.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void eq() {
|
||||
withObservationWithValueQuantity(0.6);
|
||||
|
||||
assertNotFind("when lt unitless", "/Observation?value-quantity=0.5");
|
||||
assertNotFind("when wrong system", "/Observation?value-quantity=0.6|http://another.org");
|
||||
assertNotFind("when wrong units", "/Observation?value-quantity=0.6||mmHg");
|
||||
assertNotFind("when gt unitless", "/Observation?value-quantity=0.7");
|
||||
assertNotFind("when gt", "/Observation?value-quantity=0.7||mmHg");
|
||||
|
||||
assertFind("when a little gt - default is approx", "/Observation?value-quantity=0.599");
|
||||
assertFind("when a little lt - default is approx", "/Observation?value-quantity=0.601");
|
||||
|
||||
assertFind("when eq unitless", "/Observation?value-quantity=0.6");
|
||||
assertFind("when eq with units", "/Observation?value-quantity=0.6||mm[Hg]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ne() {
|
||||
withObservationWithValueQuantity(0.6);
|
||||
|
||||
assertFind("when gt", "/Observation?value-quantity=ne0.5");
|
||||
assertNotFind("when eq", "/Observation?value-quantity=ne0.6");
|
||||
assertFind("when lt", "/Observation?value-quantity=ne0.7");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ap() {
|
||||
withObservationWithValueQuantity(0.6);
|
||||
|
||||
assertNotFind("when gt", "/Observation?value-quantity=ap0.5");
|
||||
assertFind("when a little gt", "/Observation?value-quantity=ap0.58");
|
||||
assertFind("when eq", "/Observation?value-quantity=ap0.6");
|
||||
assertFind("when a little lt", "/Observation?value-quantity=ap0.62");
|
||||
assertNotFind("when lt", "/Observation?value-quantity=ap0.7");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gt() {
|
||||
withObservationWithValueQuantity(0.6);
|
||||
|
||||
assertFind("when gt", "/Observation?value-quantity=gt0.5");
|
||||
assertNotFind("when eq", "/Observation?value-quantity=gt0.6");
|
||||
assertNotFind("when lt", "/Observation?value-quantity=gt0.7");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ge() {
|
||||
withObservationWithValueQuantity(0.6);
|
||||
|
||||
assertFind("when gt", "/Observation?value-quantity=ge0.5");
|
||||
assertFind("when eq", "/Observation?value-quantity=ge0.6");
|
||||
assertNotFind("when lt", "/Observation?value-quantity=ge0.7");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lt() {
|
||||
withObservationWithValueQuantity(0.6);
|
||||
|
||||
assertNotFind("when gt", "/Observation?value-quantity=lt0.5");
|
||||
assertNotFind("when eq", "/Observation?value-quantity=lt0.6");
|
||||
assertFind("when lt", "/Observation?value-quantity=lt0.7");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void le() {
|
||||
withObservationWithValueQuantity(0.6);
|
||||
|
||||
assertNotFind("when gt", "/Observation?value-quantity=le0.5");
|
||||
assertFind("when eq", "/Observation?value-quantity=le0.6");
|
||||
assertFind("when lt", "/Observation?value-quantity=le0.7");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
public class CombinedQueries {
|
||||
|
||||
@Test
|
||||
void gtAndLt() {
|
||||
withObservationWithValueQuantity(0.6);
|
||||
|
||||
assertFind("when gt0.5 and lt0.7", "/Observation?value-quantity=gt0.5&value-quantity=lt0.7");
|
||||
assertNotFind("when gt0.5 and lt0.6", "/Observation?value-quantity=gt0.5&value-quantity=lt0.6");
|
||||
assertNotFind("when gt6.5 and lt0.7", "/Observation?value-quantity=gt6.5&value-quantity=lt0.7");
|
||||
assertNotFind("impossible matching", "/Observation?value-quantity=gt0.7&value-quantity=lt0.5");
|
||||
}
|
||||
|
||||
@Test
|
||||
void orClauses() {
|
||||
withObservationWithValueQuantity(0.6);
|
||||
|
||||
assertFind("when gt0.5 and lt0.7", "/Observation?value-quantity=0.5,0.6");
|
||||
// make sure it doesn't find everything when using or clauses
|
||||
assertNotFind("when gt0.5 and lt0.7", "/Observation?value-quantity=0.5,0.7");
|
||||
}
|
||||
|
||||
@Nested
|
||||
public class CombinedAndPlusOr {
|
||||
|
||||
@Test
|
||||
void ltAndOrClauses() {
|
||||
withObservationWithValueQuantity(0.6);
|
||||
|
||||
assertFind("when lt0.7 and eq (0.5 or 0.6)", "/Observation?value-quantity=lt0.7&value-quantity=0.5,0.6");
|
||||
// make sure it doesn't find everything when using or clauses
|
||||
assertNotFind("when lt0.4 and eq (0.5 or 0.6)", "/Observation?value-quantity=lt0.4&value-quantity=0.5,0.6");
|
||||
assertNotFind("when lt0.7 and eq (0.4 or 0.5)", "/Observation?value-quantity=lt0.7&value-quantity=0.4,0.5");
|
||||
}
|
||||
|
||||
@Test
|
||||
void gtAndOrClauses() {
|
||||
withObservationWithValueQuantity(0.6);
|
||||
|
||||
assertFind("when gt0.4 and eq (0.5 or 0.6)", "/Observation?value-quantity=gt0.4&value-quantity=0.5,0.6");
|
||||
assertNotFind("when gt0.7 and eq (0.5 or 0.7)", "/Observation?value-quantity=gt0.7&value-quantity=0.5,0.7");
|
||||
assertNotFind("when gt0.3 and eq (0.4 or 0.5)", "/Observation?value-quantity=gt0.3&value-quantity=0.4,0.5");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
public class QualifiedOrClauses {
|
||||
|
||||
@Test
|
||||
void gtOrLt() {
|
||||
withObservationWithValueQuantity(0.6);
|
||||
|
||||
assertFind("when gt0.5 or lt0.3", "/Observation?value-quantity=gt0.5,lt0.3");
|
||||
assertNotFind("when gt0.6 or lt0.55", "/Observation?value-quantity=gt0.6,lt0.55");
|
||||
}
|
||||
|
||||
@Test
|
||||
void gtOrLe() {
|
||||
withObservationWithValueQuantity(0.6);
|
||||
|
||||
assertFind("when gt0.5 or le0.3", "/Observation?value-quantity=gt0.5,le0.3");
|
||||
assertNotFind("when gt0.6 or le0.55", "/Observation?value-quantity=gt0.6,le0.55");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ltOrGt() {
|
||||
withObservationWithValueQuantity(0.6);
|
||||
|
||||
assertFind("when lt0.7 or gt0.9", "/Observation?value-quantity=lt0.7,gt0.9");
|
||||
// make sure it doesn't find everything when using or clauses
|
||||
assertNotFind("when lt0.6 or gt0.6", "/Observation?value-quantity=lt0.6,gt0.6");
|
||||
assertNotFind("when lt0.3 or gt0.9", "/Observation?value-quantity=lt0.3,gt0.9");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ltOrGe() {
|
||||
withObservationWithValueQuantity(0.6);
|
||||
|
||||
assertFind("when lt0.7 or ge0.2", "/Observation?value-quantity=lt0.7,ge0.2");
|
||||
assertNotFind("when lt0.6 or ge0.8", "/Observation?value-quantity=lt0.6,ge0.8");
|
||||
}
|
||||
|
||||
@Test
|
||||
void gtOrGt() {
|
||||
withObservationWithValueQuantity(0.6);
|
||||
|
||||
assertFind("when gt0.5 or gt0.8", "/Observation?value-quantity=gt0.5,gt0.8");
|
||||
assertNotFind("when gt0.6 or gt0.8", "/Observation?value-quantity=gt0.6,gt0.8");
|
||||
}
|
||||
|
||||
@Test
|
||||
void geOrGe() {
|
||||
withObservationWithValueQuantity(0.6);
|
||||
|
||||
assertFind("when ge0.5 or ge0.7", "/Observation?value-quantity=ge0.5,ge0.7");
|
||||
assertNotFind("when ge0.65 or ge0.7", "/Observation?value-quantity=ge0.65,ge0.7");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ltOrLt() {
|
||||
withObservationWithValueQuantity(0.6);
|
||||
|
||||
assertFind("when lt0.5 or lt0.7", "/Observation?value-quantity=lt0.5,lt0.7");
|
||||
assertNotFind("when lt0.55 or lt0.3", "/Observation?value-quantity=lt0.55,lt0.3");
|
||||
}
|
||||
|
||||
@Test
|
||||
void leOrLe() {
|
||||
withObservationWithValueQuantity(0.6);
|
||||
|
||||
assertFind("when le0.5 or le0.6", "/Observation?value-quantity=le0.5,le0.6");
|
||||
assertNotFind("when le0.5 or le0.59", "/Observation?value-quantity=le0.5,le0.59");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
public class Sorting {
|
||||
|
||||
@Test
|
||||
public void sortByNumeric() {
|
||||
String idAlpha7 = withObservationWithValueQuantity(0.7).getIdPart();
|
||||
String idAlpha2 = withObservationWithValueQuantity(0.2).getIdPart();
|
||||
String idAlpha5 = withObservationWithValueQuantity(0.5).getIdPart();
|
||||
|
||||
List<String> allIds = myTestDaoSearch.searchForIds("/Observation?_sort=value-quantity");
|
||||
assertThat(allIds, contains(idAlpha2, idAlpha5, idAlpha7));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
public class QuantityNormalizedSearch {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
myDaoConfig.getModelConfig().setNormalizedQuantitySearchLevel(
|
||||
NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED);
|
||||
}
|
||||
|
||||
@Nested
|
||||
public class SimpleQueries {
|
||||
|
||||
@Test
|
||||
public void ne() {
|
||||
withObservationWithQuantity(0.06, UCUM_CODESYSTEM_URL, "10*6/L" );
|
||||
|
||||
assertFind("when lt UCUM", "/Observation?value-quantity=ne70|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
assertFind("when gt UCUM", "/Observation?value-quantity=ne50|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
assertNotFind("when eq UCUM", "/Observation?value-quantity=ne60|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void eq() {
|
||||
withObservationWithQuantity(0.06, UCUM_CODESYSTEM_URL, "10*6/L" );
|
||||
|
||||
assertFind("when eq UCUM 10*3/L ", "/Observation?value-quantity=60|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
assertFind("when eq UCUM 10*9/L", "/Observation?value-quantity=0.000060|" + UCUM_CODESYSTEM_URL + "|10*9/L");
|
||||
assertFind("when gt UCUM 10*3/L", "/Observation?value-quantity=eq58|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
assertFind("when le UCUM 10*3/L", "/Observation?value-quantity=eq63|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
|
||||
assertNotFind("when ne UCUM 10*3/L", "/Observation?value-quantity=80|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
assertNotFind("when gt UCUM 10*3/L", "/Observation?value-quantity=50|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
assertNotFind("when lt UCUM 10*3/L", "/Observation?value-quantity=70|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
|
||||
assertFind("Units required to match and do", "/Observation?value-quantity=60000|" + UCUM_CODESYSTEM_URL + "|/L");
|
||||
// request generates a quantity which value matches the "value-norm", but not the "code-norm"
|
||||
assertNotFind("Units required to match and don't", "/Observation?value-quantity=6000000000|" + UCUM_CODESYSTEM_URL + "|cm");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ap() {
|
||||
withObservationWithQuantity(0.06, UCUM_CODESYSTEM_URL, "10*6/L" );
|
||||
|
||||
assertNotFind("when gt UCUM", "/Observation?value-quantity=ap50|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
assertFind("when little gt UCUM", "/Observation?value-quantity=ap58|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
assertFind("when eq UCUM", "/Observation?value-quantity=ap60|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
assertFind("when a little lt UCUM", "/Observation?value-quantity=ap63|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
assertNotFind("when lt UCUM", "/Observation?value-quantity=ap71|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gt() {
|
||||
withObservationWithQuantity(0.06, UCUM_CODESYSTEM_URL, "10*6/L" );
|
||||
|
||||
assertFind("when gt UCUM", "/Observation?value-quantity=gt50|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
assertNotFind("when eq UCUM", "/Observation?value-quantity=gt60|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
assertNotFind("when lt UCUM", "/Observation?value-quantity=gt71|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ge() {
|
||||
withObservationWithQuantity(0.06, UCUM_CODESYSTEM_URL, "10*6/L" );
|
||||
|
||||
assertFind("when gt UCUM", "/Observation?value-quantity=ge50|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
assertFind("when eq UCUM", "/Observation?value-quantity=ge60|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
assertNotFind("when lt UCUM", "/Observation?value-quantity=ge62|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lt() {
|
||||
withObservationWithQuantity(0.06, UCUM_CODESYSTEM_URL, "10*6/L" );
|
||||
|
||||
assertNotFind("when gt", "/Observation?value-quantity=lt50|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
assertNotFind("when eq", "/Observation?value-quantity=lt60|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
assertFind("when lt", "/Observation?value-quantity=lt70|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void le() {
|
||||
withObservationWithQuantity(0.06, UCUM_CODESYSTEM_URL, "10*6/L" );
|
||||
|
||||
assertNotFind("when gt", "/Observation?value-quantity=le50|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
assertFind("when eq", "/Observation?value-quantity=le60|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
assertFind("when lt", "/Observation?value-quantity=le70|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* "value-quantity" data is stored in a nested object, so if not queried properly
|
||||
* it could return false positives. For instance: two Observations for following
|
||||
* combinations of code and value:
|
||||
* Obs 1 code AAA1 value: 123
|
||||
* Obs 2 code BBB2 value: 456
|
||||
* A search for code: AAA1 and value: 456 would bring both observations instead of the expected empty reply,
|
||||
* unless both predicates are enclosed in a "nested"
|
||||
* */
|
||||
@Test
|
||||
void nestedMustCorrelate() {
|
||||
withObservationWithQuantity(0.06, UCUM_CODESYSTEM_URL, "10*6/L" );
|
||||
withObservationWithQuantity(0.02, UCUM_CODESYSTEM_URL, "10*3/L" );
|
||||
|
||||
assertNotFind("when one predicate matches each object", "/Observation" +
|
||||
"?value-quantity=0.06|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
public class CombinedQueries {
|
||||
|
||||
@Test
|
||||
void gtAndLt() {
|
||||
withObservationWithQuantity(0.06, UCUM_CODESYSTEM_URL, "10*6/L" );
|
||||
|
||||
assertFind("when gt 50 and lt 70", "/Observation" +
|
||||
"?value-quantity=gt50|" + UCUM_CODESYSTEM_URL + "|10*3/L" +
|
||||
"&value-quantity=lt70|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
|
||||
assertNotFind("when gt50 and lt60", "/Observation" +
|
||||
"?value-quantity=gt50|" + UCUM_CODESYSTEM_URL + "|10*3/L" +
|
||||
"&value-quantity=lt60|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
|
||||
assertNotFind("when gt65 and lt70", "/Observation" +
|
||||
"?value-quantity=gt65|" + UCUM_CODESYSTEM_URL + "|10*3/L" +
|
||||
"&value-quantity=lt70|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
|
||||
assertNotFind("when gt 70 and lt 50", "/Observation" +
|
||||
"?value-quantity=gt70|" + UCUM_CODESYSTEM_URL + "|10*3/L" +
|
||||
"&value-quantity=lt50|" + UCUM_CODESYSTEM_URL + "|10*3/L");
|
||||
}
|
||||
|
||||
@Test
|
||||
void gtAndLtWithMixedUnits() {
|
||||
withObservationWithQuantity(0.06, UCUM_CODESYSTEM_URL, "10*6/L" );
|
||||
|
||||
assertFind("when gt 50|10*3/L and lt 70|10*9/L", "/Observation" +
|
||||
"?value-quantity=gt50|" + UCUM_CODESYSTEM_URL + "|10*3/L" +
|
||||
"&value-quantity=lt0.000070|" + UCUM_CODESYSTEM_URL + "|10*9/L");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleSearchParamsAreSeparate() {
|
||||
// for debugging
|
||||
// myLogbackLevelOverrideExtension.setLogLevel(DaoTestDataBuilder.class, Level.DEBUG);
|
||||
|
||||
// this configuration must generate a combo-value-quantity entry with both quantity objects
|
||||
myResourceId = myTestDataBuilder.createObservation(
|
||||
myTestDataBuilder.withQuantityAtPath("valueQuantity", 0.02, UCUM_CODESYSTEM_URL, "10*6/L"),
|
||||
myTestDataBuilder.withQuantityAtPath("component.valueQuantity", 0.06, UCUM_CODESYSTEM_URL, "10*6/L")
|
||||
);
|
||||
|
||||
// myLogbackLevelOverrideExtension.resetLevel(DaoTestDataBuilder.class);
|
||||
|
||||
assertFind("by value", "Observation?value-quantity=0.02|" + UCUM_CODESYSTEM_URL + "|10*6/L");
|
||||
assertFind("by component value", "Observation?component-value-quantity=0.06|" + UCUM_CODESYSTEM_URL + "|10*6/L");
|
||||
|
||||
assertNotFind("by value", "Observation?value-quantity=0.06|" + UCUM_CODESYSTEM_URL + "|10*6/L");
|
||||
assertNotFind("by component value", "Observation?component-value-quantity=0.02|" + UCUM_CODESYSTEM_URL + "|10*6/L");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorting is not implemented for normalized quantities, so quantities will be sorted
|
||||
* by their absolute values (with no unit conversions)
|
||||
*/
|
||||
@Nested
|
||||
public class Sorting {
|
||||
|
||||
@Test
|
||||
public void sortByNumeric() {
|
||||
String idAlpha6 = withObservationWithQuantity(0.06, UCUM_CODESYSTEM_URL, "10*6/L" ).getIdPart();
|
||||
String idAlpha5 = withObservationWithQuantity(50, UCUM_CODESYSTEM_URL, "10*3/L" ).getIdPart();
|
||||
String idAlpha7 = withObservationWithQuantity(0.000070, UCUM_CODESYSTEM_URL, "10*9/L" ).getIdPart();
|
||||
|
||||
// this search is not freetext because there is no freetext-known parameter name
|
||||
List<String> allIds = myTestDaoSearch.searchForIds("/Observation?_sort=value-quantity");
|
||||
assertThat(allIds, contains(idAlpha7, idAlpha6, idAlpha5));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void assertFind(String theMessage, String theUrl) {
|
||||
List<String> resourceIds = myTestDaoSearch.searchForIds(theUrl);
|
||||
assertThat(theMessage, resourceIds, hasItem(equalTo(myResourceId.getIdPart())));
|
||||
}
|
||||
|
||||
private void assertNotFind(String theMessage, String theUrl) {
|
||||
List<String> resourceIds = myTestDaoSearch.searchForIds(theUrl);
|
||||
assertThat(theMessage, resourceIds, not(hasItem(equalTo(myResourceId.getIdPart()))));
|
||||
}
|
||||
|
||||
private IIdType withObservationWithQuantity(double theValue, String theSystem, String theCode) {
|
||||
myResourceId = myTestDataBuilder.createObservation(
|
||||
myTestDataBuilder.withQuantityAtPath("valueQuantity", theValue, theSystem, theCode)
|
||||
);
|
||||
return myResourceId;
|
||||
}
|
||||
|
||||
private IIdType withObservationWithValueQuantity(double theValue) {
|
||||
myResourceId = myTestDataBuilder.createObservation(myTestDataBuilder.withElementAt("valueQuantity",
|
||||
myTestDataBuilder.withPrimitiveAttribute("value", theValue),
|
||||
myTestDataBuilder.withPrimitiveAttribute("system", UCUM_CODESYSTEM_URL),
|
||||
myTestDataBuilder.withPrimitiveAttribute("code", "mm[Hg]")
|
||||
));
|
||||
return myResourceId;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Disallow context dirtying for nested classes
|
||||
*/
|
||||
public static final class TestDirtiesContextTestExecutionListener extends DirtiesContextTestExecutionListener {
|
||||
|
||||
@Override
|
||||
protected void beforeOrAfterTestClass(TestContext testContext, DirtiesContext.ClassMode requiredClassMode) throws Exception {
|
||||
if ( ! testContext.getTestClass().getName().contains("$")) {
|
||||
super.beforeOrAfterTestClass(testContext, requiredClassMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -386,7 +386,7 @@ public class FhirResourceDaoR4StandardQueriesNoFTTest extends BaseJpaTest {
|
||||
// myDataBuilder.withPrimitiveAttribute("value", theValue),
|
||||
// myDataBuilder.withPrimitiveAttribute("unit", "mmHg"),
|
||||
// myDataBuilder.withPrimitiveAttribute("system", "http://unitsofmeasure.org"));
|
||||
myResourceId = myDataBuilder.createObservation(myDataBuilder.withAttribute("valueQuantity",
|
||||
myResourceId = myDataBuilder.createObservation(myDataBuilder.withElementAt("valueQuantity",
|
||||
myDataBuilder.withPrimitiveAttribute("value", theValue),
|
||||
myDataBuilder.withPrimitiveAttribute("unit", "mmHg"),
|
||||
myDataBuilder.withPrimitiveAttribute("system", "http://unitsofmeasure.org"),
|
||||
|
@ -164,6 +164,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import static ca.uhn.fhir.jpa.config.r4.FhirContextR4Config.DEFAULT_PRESERVE_VERSION_REFS;
|
||||
import static ca.uhn.fhir.jpa.util.TestUtil.sleepOneClick;
|
||||
import static ca.uhn.fhir.rest.param.BaseParamWithPrefix.MSG_PREFIX_INVALID_FORMAT;
|
||||
import static ca.uhn.fhir.util.TestUtil.sleepAtLeast;
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
@ -366,21 +367,21 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
|
||||
HttpGet get = new HttpGet(ourServerBase + "/Condition?onset-date=junk");
|
||||
try (CloseableHttpResponse resp = ourHttpClient.execute(get)) {
|
||||
String output = IOUtils.toString(resp.getEntity().getContent(), Charsets.UTF_8);
|
||||
assertThat(output, containsString("Invalid date/time format: "junk""));
|
||||
assertThat(output, containsString(MSG_PREFIX_INVALID_FORMAT + ""junk""));
|
||||
assertEquals(400, resp.getStatusLine().getStatusCode());
|
||||
}
|
||||
|
||||
get = new HttpGet(ourServerBase + "/Condition?onset-date=ge");
|
||||
try (CloseableHttpResponse resp = ourHttpClient.execute(get)) {
|
||||
String output = IOUtils.toString(resp.getEntity().getContent(), Charsets.UTF_8);
|
||||
assertThat(output, containsString("Invalid date/time format: "ge""));
|
||||
assertThat(output, containsString(MSG_PREFIX_INVALID_FORMAT + ""ge""));
|
||||
assertEquals(400, resp.getStatusLine().getStatusCode());
|
||||
}
|
||||
|
||||
get = new HttpGet(ourServerBase + "/Condition?onset-date=" + UrlUtil.escapeUrlParam(">"));
|
||||
try (CloseableHttpResponse resp = ourHttpClient.execute(get)) {
|
||||
String output = IOUtils.toString(resp.getEntity().getContent(), Charsets.UTF_8);
|
||||
assertThat(output, containsString("Invalid date/time format: ">""));
|
||||
assertThat(output, containsString(MSG_PREFIX_INVALID_FORMAT + "">""));
|
||||
assertEquals(400, resp.getStatusLine().getStatusCode());
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ import ca.uhn.fhir.jpa.config.TestHibernateSearchAddInConfig;
|
||||
import ca.uhn.fhir.jpa.config.TestR4Config;
|
||||
import ca.uhn.fhir.jpa.dao.BaseJpaTest;
|
||||
import ca.uhn.fhir.jpa.dao.DaoTestDataBuilder;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
|
||||
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
|
||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||
@ -81,6 +82,9 @@ public class TokenAutocompleteElasticsearchIT extends BaseJpaTest{
|
||||
@Autowired
|
||||
ITestDataBuilder myDataBuilder;
|
||||
|
||||
@Autowired
|
||||
private ModelConfig myModelConfig;
|
||||
|
||||
// a few different codes
|
||||
static final Coding mean_blood_pressure = new Coding("http://loinc.org", "8478-0", "Mean blood pressure");
|
||||
static final Coding gram_positive_culture = new Coding("http://loinc.org", "88262-1", "Gram positive blood culture panel by Probe in Positive blood culture");
|
||||
@ -180,7 +184,7 @@ public class TokenAutocompleteElasticsearchIT extends BaseJpaTest{
|
||||
|
||||
List<TokenAutocompleteHit> autocompleteSearch(String theResourceType, String theSPName, String theModifier, String theSearchText) {
|
||||
return new TransactionTemplate(myTxManager).execute(s -> {
|
||||
TokenAutocompleteSearch tokenAutocompleteSearch = new TokenAutocompleteSearch(myFhirCtx, Search.session(myEntityManager));
|
||||
TokenAutocompleteSearch tokenAutocompleteSearch = new TokenAutocompleteSearch(myFhirCtx, myModelConfig, Search.session(myEntityManager));
|
||||
return tokenAutocompleteSearch.search(theResourceType, theSPName, theSearchText, theModifier,30);
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package ca.uhn.fhir.jpa.search.autocomplete;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import org.hl7.fhir.r4.model.ValueSet;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@ -13,7 +14,8 @@ import static org.hamcrest.Matchers.nullValue;
|
||||
|
||||
class ValueSetAutocompleteSearchTest {
|
||||
FhirContext myFhirContext = FhirContext.forR4();
|
||||
ValueSetAutocompleteSearch myValueSetAutocompleteSearch = new ValueSetAutocompleteSearch(myFhirContext, null);
|
||||
private ModelConfig myModelConfig;
|
||||
ValueSetAutocompleteSearch myValueSetAutocompleteSearch = new ValueSetAutocompleteSearch(myFhirContext, myModelConfig, null);
|
||||
|
||||
@Nested
|
||||
public class HitToValueSetConversion {
|
||||
|
@ -45,7 +45,7 @@ public enum NormalizedQuantitySearchLevel {
|
||||
* and {@link ResourceIndexedSearchParamQuantityNormalized},
|
||||
* {@link ResourceIndexedSearchParamQuantityNormalized} is used by searching.
|
||||
*/
|
||||
NORMALIZED_QUANTITY_SEARCH_SUPPORTED,
|
||||
NORMALIZED_QUANTITY_SEARCH_SUPPORTED;
|
||||
|
||||
/**
|
||||
* Quantity is stored in only in {@link ResourceIndexedSearchParamQuantityNormalized},
|
||||
@ -55,4 +55,11 @@ public enum NormalizedQuantitySearchLevel {
|
||||
*/
|
||||
// When this is enabled, we can enable testSortByQuantityWithNormalizedQuantitySearchFullSupported()
|
||||
//NORMALIZED_QUANTITY_SEARCH_FULL_SUPPORTED,
|
||||
|
||||
public boolean storageOrSearchSupported() {
|
||||
return this.equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_STORAGE_SUPPORTED)
|
||||
|| this.equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -21,8 +21,10 @@ package ca.uhn.fhir.jpa.model.search;
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
|
||||
import com.google.common.collect.HashMultimap;
|
||||
import com.google.common.collect.Multimaps;
|
||||
import com.google.common.collect.SetMultimap;
|
||||
import org.hibernate.search.engine.backend.document.DocumentElement;
|
||||
import org.hl7.fhir.instance.model.api.IBaseCoding;
|
||||
@ -41,15 +43,19 @@ public class ExtendedLuceneIndexData {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(ExtendedLuceneIndexData.class);
|
||||
|
||||
final FhirContext myFhirContext;
|
||||
final ModelConfig myModelConfig;
|
||||
|
||||
final SetMultimap<String, String> mySearchParamStrings = HashMultimap.create();
|
||||
final SetMultimap<String, IBaseCoding> mySearchParamTokens = HashMultimap.create();
|
||||
final SetMultimap<String, String> mySearchParamLinks = HashMultimap.create();
|
||||
final SetMultimap<String, DateSearchIndexData> mySearchParamDates = HashMultimap.create();
|
||||
final SetMultimap<String, QuantitySearchIndexData> mySearchParamQuantities = HashMultimap.create();
|
||||
private String myForcedId;
|
||||
private String myResourceJSON;
|
||||
|
||||
public ExtendedLuceneIndexData(FhirContext theFhirContext) {
|
||||
public ExtendedLuceneIndexData(FhirContext theFhirContext, ModelConfig theModelConfig) {
|
||||
this.myFhirContext = theFhirContext;
|
||||
this.myModelConfig = theModelConfig;
|
||||
}
|
||||
|
||||
private <V> BiConsumer<String, V> ifNotContained(BiConsumer<String, V> theIndexWriter) {
|
||||
@ -70,7 +76,7 @@ public class ExtendedLuceneIndexData {
|
||||
* @param theDocument the Hibernate Search document for ResourceTable
|
||||
*/
|
||||
public void writeIndexElements(DocumentElement theDocument) {
|
||||
HibernateSearchIndexWriter indexWriter = HibernateSearchIndexWriter.forRoot(myFhirContext, theDocument);
|
||||
HibernateSearchIndexWriter indexWriter = HibernateSearchIndexWriter.forRoot(myFhirContext, myModelConfig, theDocument);
|
||||
|
||||
ourLog.debug("Writing JPA index to Hibernate Search");
|
||||
|
||||
@ -84,6 +90,8 @@ public class ExtendedLuceneIndexData {
|
||||
mySearchParamStrings.forEach(ifNotContained(indexWriter::writeStringIndex));
|
||||
mySearchParamTokens.forEach(ifNotContained(indexWriter::writeTokenIndex));
|
||||
mySearchParamLinks.forEach(ifNotContained(indexWriter::writeReferenceIndex));
|
||||
// we want to receive the whole entry collection for each invocation
|
||||
Multimaps.asMap(mySearchParamQuantities).forEach(ifNotContained(indexWriter::writeQuantityIndex));
|
||||
// TODO MB Use RestSearchParameterTypeEnum to define templates.
|
||||
mySearchParamDates.forEach(ifNotContained(indexWriter::writeDateIndex));
|
||||
}
|
||||
@ -115,6 +123,10 @@ public class ExtendedLuceneIndexData {
|
||||
mySearchParamDates.put(theSpName, new DateSearchIndexData(theLowerBound, theLowerBoundOrdinal, theUpperBound, theUpperBoundOrdinal));
|
||||
}
|
||||
|
||||
public void addQuantityIndexData(String theSpName, String theUnits, String theSystem, double theValue) {
|
||||
mySearchParamQuantities.put(theSpName, new QuantitySearchIndexData(theUnits, theSystem, theValue));
|
||||
}
|
||||
|
||||
public void setForcedId(String theForcedId) {
|
||||
myForcedId = theForcedId;
|
||||
}
|
||||
|
@ -21,12 +21,20 @@ package ca.uhn.fhir.jpa.model.search;
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.fhir.ucum.Pair;
|
||||
import org.hibernate.search.engine.backend.document.DocumentElement;
|
||||
import org.hl7.fhir.instance.model.api.IBaseCoding;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collection;
|
||||
|
||||
import static org.hl7.fhir.r4.model.Observation.SP_VALUE_QUANTITY;
|
||||
|
||||
public class HibernateSearchIndexWriter {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(HibernateSearchIndexWriter.class);
|
||||
public static final String IDX_STRING_NORMALIZED = "norm";
|
||||
@ -34,21 +42,33 @@ public class HibernateSearchIndexWriter {
|
||||
public static final String IDX_STRING_TEXT = "text";
|
||||
public static final String NESTED_SEARCH_PARAM_ROOT = "nsp";
|
||||
public static final String SEARCH_PARAM_ROOT = "sp";
|
||||
|
||||
public static final String QTY_PARAM_NAME = "quantity";
|
||||
public static final String QTY_CODE = "code";
|
||||
public static final String QTY_SYSTEM = "system";
|
||||
public static final String QTY_VALUE = "value";
|
||||
public static final String QTY_CODE_NORM = "code-norm";
|
||||
public static final String QTY_VALUE_NORM = "value-norm";
|
||||
|
||||
|
||||
|
||||
final HibernateSearchElementCache myNodeCache;
|
||||
final FhirContext myFhirContext;
|
||||
final ModelConfig myModelConfig;
|
||||
|
||||
HibernateSearchIndexWriter(FhirContext theFhirContext, DocumentElement theRoot) {
|
||||
HibernateSearchIndexWriter(FhirContext theFhirContext, ModelConfig theModelConfig, DocumentElement theRoot) {
|
||||
myFhirContext = theFhirContext;
|
||||
myModelConfig = theModelConfig;
|
||||
myNodeCache = new HibernateSearchElementCache(theRoot);
|
||||
}
|
||||
|
||||
public DocumentElement getSearchParamIndexNode(String theSearchParamName, String theIndexType) {
|
||||
return myNodeCache.getObjectElement(SEARCH_PARAM_ROOT, theSearchParamName, theIndexType);
|
||||
|
||||
}
|
||||
|
||||
public static HibernateSearchIndexWriter forRoot(FhirContext theFhirContext, DocumentElement theDocument) {
|
||||
return new HibernateSearchIndexWriter(theFhirContext, theDocument);
|
||||
public static HibernateSearchIndexWriter forRoot(
|
||||
FhirContext theFhirContext, ModelConfig theModelConfig, DocumentElement theDocument) {
|
||||
return new HibernateSearchIndexWriter(theFhirContext, theModelConfig, theDocument);
|
||||
}
|
||||
|
||||
public void writeStringIndex(String theSearchParam, String theValue) {
|
||||
@ -97,4 +117,38 @@ public class HibernateSearchIndexWriter {
|
||||
dateIndexNode.addValue("upper", theValue.getUpperBoundDate().toInstant());
|
||||
ourLog.trace("Adding Search Param Reference: {} -- {}", theSearchParam, theValue);
|
||||
}
|
||||
|
||||
|
||||
public void writeQuantityIndex(String theSearchParam, Collection<QuantitySearchIndexData> theValueCollection) {
|
||||
DocumentElement nestedRoot = myNodeCache.getObjectElement(NESTED_SEARCH_PARAM_ROOT);
|
||||
|
||||
for (QuantitySearchIndexData theValue : theValueCollection) {
|
||||
DocumentElement nestedSpNode = nestedRoot.addObject(theSearchParam);
|
||||
DocumentElement nestedQtyNode = nestedSpNode.addObject(QTY_PARAM_NAME);
|
||||
|
||||
ourLog.trace("Adding Search Param Quantity: {} -- {}", theSearchParam, theValue);
|
||||
nestedQtyNode.addValue(QTY_CODE, theValue.getCode());
|
||||
nestedQtyNode.addValue(QTY_SYSTEM, theValue.getSystem());
|
||||
nestedQtyNode.addValue(QTY_VALUE, theValue.getValue());
|
||||
|
||||
if ( ! myModelConfig.getNormalizedQuantitySearchLevel().storageOrSearchSupported()) { return; }
|
||||
|
||||
//-- 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; }
|
||||
|
||||
double canonicalValue = Double.parseDouble(canonicalForm.getValue().asDecimal());
|
||||
String canonicalUnits = canonicalForm.getCode();
|
||||
ourLog.trace("Adding search param normalized code and value: {} -- code:{}, value:{}",
|
||||
theSearchParam, canonicalUnits, canonicalValue);
|
||||
|
||||
nestedQtyNode.addValue(QTY_CODE_NORM, canonicalUnits);
|
||||
nestedQtyNode.addValue(QTY_VALUE_NORM, canonicalValue);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,45 @@
|
||||
package ca.uhn.fhir.jpa.model.search;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Model
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
class QuantitySearchIndexData {
|
||||
|
||||
// unit is also referred as code
|
||||
private final String myCode;
|
||||
private final String mySystem;
|
||||
private final double myValue;
|
||||
|
||||
|
||||
QuantitySearchIndexData(String theCode, String theSystem, double theValue) {
|
||||
myCode = theCode;
|
||||
mySystem = theSystem;
|
||||
myValue = theValue;
|
||||
}
|
||||
|
||||
|
||||
public String getCode() { return myCode; }
|
||||
|
||||
public String getSystem() { return mySystem; }
|
||||
|
||||
public double getValue() { return myValue; }
|
||||
}
|
@ -43,6 +43,11 @@ import java.time.Instant;
|
||||
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.IDX_STRING_EXACT;
|
||||
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.IDX_STRING_NORMALIZED;
|
||||
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.IDX_STRING_TEXT;
|
||||
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_CODE;
|
||||
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_CODE_NORM;
|
||||
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_SYSTEM;
|
||||
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_VALUE;
|
||||
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_VALUE_NORM;
|
||||
|
||||
/**
|
||||
* Allows hibernate search to index
|
||||
@ -60,7 +65,9 @@ public class SearchParamTextPropertyBinder implements PropertyBinder, PropertyBr
|
||||
public void bind(PropertyBindingContext thePropertyBindingContext) {
|
||||
// TODO Is it safe to use object identity of the Map to track dirty?
|
||||
// N.B. GGG I would hazard that it is not, we could potentially use Version of the resource.
|
||||
thePropertyBindingContext.dependencies().use("mySearchParamStrings");
|
||||
thePropertyBindingContext.dependencies()
|
||||
.use("mySearchParamStrings")
|
||||
.use("mySearchParamQuantities");
|
||||
|
||||
defineIndexingTemplate(thePropertyBindingContext);
|
||||
|
||||
@ -101,6 +108,10 @@ public class SearchParamTextPropertyBinder implements PropertyBinder, PropertyBr
|
||||
.projectable(Projectable.NO)
|
||||
.sortable(Sortable.YES);
|
||||
|
||||
StandardIndexFieldTypeOptionsStep<?, Double> bigDecimalFieldType = indexFieldTypeFactory.asDouble()
|
||||
.projectable(Projectable.NO)
|
||||
.sortable(Sortable.YES);
|
||||
|
||||
StringIndexFieldTypeOptionsStep<?> forcedIdType = indexFieldTypeFactory.asString()
|
||||
.projectable(Projectable.YES)
|
||||
.aggregable(Aggregable.NO);
|
||||
@ -161,11 +172,18 @@ public class SearchParamTextPropertyBinder implements PropertyBinder, PropertyBr
|
||||
nestedSpField.fieldTemplate("token-code-system", keywordFieldType).matchingPathGlob(tokenPathGlob + ".code-system").multiValued();
|
||||
nestedSpField.fieldTemplate("token-system", keywordFieldType).matchingPathGlob(tokenPathGlob + ".system").multiValued();
|
||||
|
||||
// reference
|
||||
|
||||
// reference
|
||||
spfield.fieldTemplate("reference-value", keywordFieldType).matchingPathGlob("*.reference.value").multiValued();
|
||||
|
||||
//quantity
|
||||
String quantityPathGlob = "*.quantity";
|
||||
nestedSpField.objectFieldTemplate("quantityTemplate", ObjectStructure.FLATTENED).matchingPathGlob(quantityPathGlob);
|
||||
nestedSpField.fieldTemplate(QTY_SYSTEM, keywordFieldType).matchingPathGlob(quantityPathGlob + "." + QTY_SYSTEM);
|
||||
nestedSpField.fieldTemplate(QTY_CODE, keywordFieldType).matchingPathGlob(quantityPathGlob + "." + QTY_CODE);
|
||||
nestedSpField.fieldTemplate(QTY_VALUE, bigDecimalFieldType).matchingPathGlob(quantityPathGlob + "." + QTY_VALUE);
|
||||
nestedSpField.fieldTemplate(QTY_CODE_NORM, keywordFieldType).matchingPathGlob(quantityPathGlob + "." + QTY_CODE_NORM);
|
||||
nestedSpField.fieldTemplate(QTY_VALUE_NORM, bigDecimalFieldType).matchingPathGlob(quantityPathGlob + "." + QTY_VALUE_NORM);
|
||||
|
||||
// date
|
||||
String dateTimePathGlob = "*.dt";
|
||||
spfield.objectFieldTemplate("datetimeIndex", ObjectStructure.FLATTENED).matchingPathGlob(dateTimePathGlob);
|
||||
|
@ -33,6 +33,9 @@ public class DaoTestDataBuilder implements ITestDataBuilder, AfterEachCallback {
|
||||
|
||||
@Override
|
||||
public IIdType doCreateResource(IBaseResource theResource) {
|
||||
if (ourLog.isDebugEnabled()) {
|
||||
ourLog.debug("create resource {}", myFhirCtx.newJsonParser().encodeResourceToString(theResource));
|
||||
}
|
||||
//noinspection rawtypes
|
||||
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass());
|
||||
//noinspection unchecked
|
||||
@ -74,12 +77,11 @@ public class DaoTestDataBuilder implements ITestDataBuilder, AfterEachCallback {
|
||||
|
||||
@Configuration
|
||||
public static class Config {
|
||||
@Autowired FhirContext myFhirContext;
|
||||
@Autowired DaoRegistry myDaoRegistry;
|
||||
|
||||
@Bean
|
||||
DaoTestDataBuilder testDataBuilder(
|
||||
@Autowired FhirContext myFhirContext,
|
||||
@Autowired DaoRegistry myDaoRegistry
|
||||
) {
|
||||
DaoTestDataBuilder testDataBuilder() {
|
||||
return new DaoTestDataBuilder(myFhirContext, myDaoRegistry, new SystemRequestDetails());
|
||||
}
|
||||
}
|
||||
|
@ -192,7 +192,7 @@ public interface ITestDataBuilder {
|
||||
};
|
||||
}
|
||||
|
||||
default <T extends IBase> Consumer<T> withAttribute(String thePath, Consumer<IBase>... theModifiers) {
|
||||
default <T extends IBase> Consumer<T> withElementAt(String thePath, Consumer<IBase>... theModifiers) {
|
||||
return t->{
|
||||
FhirTerser terser = getFhirContext().newTerser();
|
||||
IBase element = terser.addElement(t, thePath);
|
||||
@ -200,6 +200,15 @@ public interface ITestDataBuilder {
|
||||
};
|
||||
}
|
||||
|
||||
default Consumer<IBaseResource> withQuantityAtPath(String thePath, double theValue, String theSystem, String theCode) {
|
||||
return withElementAt(thePath,
|
||||
withPrimitiveAttribute("value", theValue),
|
||||
withPrimitiveAttribute("system", theSystem),
|
||||
withPrimitiveAttribute("code", theCode)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create an Element and apply modifiers
|
||||
* @param theElementType the FHIR Element type to create
|
||||
|
@ -0,0 +1,49 @@
|
||||
package ca.uhn.fhir.test.utilities;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import org.junit.jupiter.api.extension.AfterEachCallback;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Extension to allow temporary log elevation for a single test.
|
||||
*
|
||||
*/
|
||||
public class LogbackLevelOverrideExtension implements AfterEachCallback {
|
||||
|
||||
private final Map<String, Level> mySavedLevels = new HashMap<>();
|
||||
|
||||
public void setLogLevel(Class theClass, Level theLevel) {
|
||||
String name = theClass.getName();
|
||||
Logger logger = getClassicLogger(name);
|
||||
if (!mySavedLevels.containsKey(name)) {
|
||||
// level can be null
|
||||
mySavedLevels.put(name, logger.getLevel());
|
||||
}
|
||||
logger.setLevel(theLevel);
|
||||
}
|
||||
|
||||
private Logger getClassicLogger(String name) {
|
||||
return (Logger) LoggerFactory.getLogger(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterEach(ExtensionContext context) throws Exception {
|
||||
mySavedLevels.forEach((name,level) ->{
|
||||
getClassicLogger(name).setLevel(level);
|
||||
});
|
||||
mySavedLevels.clear();
|
||||
}
|
||||
|
||||
public void resetLevel(Class theClass) {
|
||||
String name = theClass.getName();
|
||||
if (mySavedLevels.containsKey(name)) {
|
||||
getClassicLogger(name).setLevel(mySavedLevels.get(name));
|
||||
mySavedLevels.remove(name);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user