Implement composite search parameters in HibernateSearch for JPA (#3839)
Implement composite search parameters in Lucene/Elasticsearch Hibernate Search indexing Co-authored-by: juan.marchionatto <juan.marchionatto@smilecdr.com>
This commit is contained in:
parent
641007f5b6
commit
96b92153f6
|
@ -111,6 +111,13 @@ public class CompositeParam<A extends IQueryParameterType, B extends IQueryParam
|
|||
return myRightType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the values of the subcomponents, in order.
|
||||
*/
|
||||
public List<IQueryParameterType> getValues() {
|
||||
return List.of(myLeftType, myRightType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
|
||||
|
|
|
@ -28,10 +28,13 @@ import java.util.Optional;
|
|||
|
||||
public class ObjectUtil {
|
||||
|
||||
// hide
|
||||
private ObjectUtil() {}
|
||||
|
||||
/**
|
||||
* @deprecated Just use Objects.equals() instead;
|
||||
*/
|
||||
@Deprecated
|
||||
@Deprecated(since = "6.2")
|
||||
public static boolean equals(Object object1, Object object2) {
|
||||
return Objects.equals(object1, object2);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
type: add
|
||||
issue: 3839
|
||||
title: "Advanced Lucene indexing now fully supports composite SearchParameters."
|
|
@ -7,81 +7,80 @@ This is required to support the `_content`, or `_text` search parameters.
|
|||
|
||||
Additional indexing is implemented for simple search parameters of type token, string, and reference.
|
||||
These implement the basic search, as well as several modifiers:
|
||||
This **experimental** feature is enabled via the `setAdvancedLuceneIndexing()` property of DaoConfig.
|
||||
This **experimental** feature is enabled via the `setAdvancedHSearchIndexing()` property of DaoConfig.
|
||||
|
||||
## Search Parameter Support
|
||||
|
||||
Extended Lucene Indexing supports all of the [core search parameter types](https://www.hl7.org/fhir/search.html).
|
||||
These include:
|
||||
- Number
|
||||
- Date/DateTime
|
||||
- String
|
||||
- Token
|
||||
- Reference
|
||||
- Composite
|
||||
- Quantity
|
||||
- URI
|
||||
|
||||
## Date search
|
||||
|
||||
We support date searches using the eq, ne, lt, gt, ge, and le comparisons.
|
||||
See https://www.hl7.org/fhir/search.html#date.
|
||||
|
||||
## String search
|
||||
|
||||
The Extended Lucene string search indexing supports the default search, as well as the modifiers defined in https://www.hl7.org/fhir/search.html#string.
|
||||
- Default searching matches by prefix, insensitive to case or accents
|
||||
- `:exact` matches the entire string, matching case and accents
|
||||
- `:contains` extends the default search to match any substring of the text
|
||||
The Extended Lucene string search indexing supports the default search, as well as `:contains`, `:exact`, and `:text` modifiers.
|
||||
- The default (unmodified) string search matches by prefix, insensitive to case or accents.
|
||||
- `:exact` matches the entire string, matching case and accents.
|
||||
- `:contains` match any substring of the text, ignoring case and accents.
|
||||
- `:text` provides a rich search syntax as using a [modified Simple Query Syntax](#modified-simple-query-syntax).
|
||||
|
||||
See https://www.hl7.org/fhir/search.html#string.
|
||||
|
||||
## Modified Simple Query Syntax
|
||||
|
||||
The `:text` modifier for token and string uses a modified version of the Simple Query Syntax provided by
|
||||
[Lucene](https://lucene.apache.org/core/8_10_1/queryparser/org/apache/lucene/queryparser/simple/SimpleQueryParser.html) and
|
||||
[Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html#simple-query-string-syntax).
|
||||
Terms are delimited by whitespace, or query punctuation `"'()|+`.
|
||||
Literal uses of these characters must be escaped by `\`.
|
||||
If the query contains any SQS query punctuation, the query is treated as a normal SQS query.
|
||||
But when the query only contains one or more bare terms, and does not use any query punctuation, a modified syntax is used.
|
||||
In modified syntax, each search term is converted to a prefix search to match standard FHIR string searching behaviour.
|
||||
When multiple terms are present, they must all match (i.e. `AND`).
|
||||
For `OR` behaviour use the `|` operator between terms.
|
||||
To match only whole words, but not match by prefix, quote bare terms with the `"` or `'` characters.
|
||||
|
||||
Examples:
|
||||
|
||||
| Fhir Query String | Executed Query | Matches | No Match | Note |
|
||||
|-------------------|-------------------|--------------|----------------|-----------------------------------------|
|
||||
| Smit | Smit* | John Smith | John Smi | |
|
||||
| Jo Smit | Jo* Smit* | John Smith | John Frank | Multiple bare terms are `AND` |
|
||||
| frank | john | frank | john | Frank Smith | Franklin Smith | SQS characters disable prefix wildcard |
|
||||
| 'frank' | 'frank' | Frank Smith | Franklin Smith | Quoted terms are exact match |
|
||||
|
||||
## Token search
|
||||
|
||||
The Extended Lucene Indexing supports the default token search by code, system, or system+code,
|
||||
as well as with the `:text` modifier.
|
||||
The `:text` modifier provides the same modified Simple Query Syntax used by string `:text` searches.
|
||||
The `:text` modifier provides the same [modified Simple Query Syntax](#modified-simple-query-syntax) used by string `:text` searches.
|
||||
See https://www.hl7.org/fhir/search.html#token.
|
||||
|
||||
## Modified Simple Query Syntax
|
||||
|
||||
The `:text` search for token and string, Hapi provides a modified version of the Simple Query Syntax provided by
|
||||
[Lucene](https://lucene.apache.org/core/8_10_1/queryparser/org/apache/lucene/queryparser/simple/SimpleQueryParser.html) and
|
||||
[Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html#simple-query-string-syntax).
|
||||
Terms are delimited by whitespace, or query punctuation `"()|+'`. Literal uses of these characters must be escaped by '\'.
|
||||
When the query only contains one or more bare terms, they are each converted to a prefix search to match the behaviour of a normal string search.
|
||||
When multiple terms are present, they must all match (i.e. `AND`). For `OR` behaviour use the `|` operator between terms.
|
||||
But if any special SQS syntax is active, the query is used as is.
|
||||
To ensure that the query is used as-is, quote bare terms with the `"` or character. E.g. `without any special syntax characters
|
||||
|
||||
Examples:
|
||||
|
||||
| Fhir Query String | Executed Query | Matches | No Match | Note |
|
||||
|-----------------|------------------|-------------|----------------|--------------------------------------------|
|
||||
| Smit | Smit* | John Smith | John Smi | |
|
||||
| Jo Smit | Jo* Smit* | John Smith | John Frank | Multiple bare terms are `AND` |
|
||||
| frank | john | frank | john | Frank Smith | Franklin Smith | SQS characters disable prefix wildcard |
|
||||
| 'frank' | 'frank' | Frank Smith | Franklin Smith | Quoted terms are exact match |
|
||||
|
||||
|
||||
|
||||
## Quantity search
|
||||
|
||||
The Extended Lucene Indexing supports the quantity search.
|
||||
See https://www.hl7.org/fhir/search.html#quantity.
|
||||
|
||||
|
||||
## URI search
|
||||
|
||||
The Extended Lucene Indexing supports the URI search.
|
||||
See https://www.hl7.org/fhir/search.html#uri.
|
||||
|
||||
|
||||
## Date search
|
||||
|
||||
We support date searches using the eq, ne, lt, gt, ge, and le comparisons.
|
||||
See https://www.hl7.org/fhir/search.html#date.
|
||||
|
||||
|
||||
## Supported Parameters for all resources
|
||||
| Parameter | Supported | type |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| _id | no | |
|
||||
| _lastUpdated | yes | date |
|
||||
| _tag | yes | token |
|
||||
| _profile | yes | URI |
|
||||
| _security | yes | token |
|
||||
| _text | yes | string |
|
||||
| _content | yes | string |
|
||||
| _list | no |
|
||||
| _has | no |
|
||||
| _type | no |
|
||||
|
||||
## Additional supported Parameters
|
||||
| Parameter | Supported | type |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| _source | yes | URI |
|
||||
## Supported Common and Special Search Parameters
|
||||
| Parameter | Supported | type |
|
||||
|--------------|-----------|--------|
|
||||
| _id | no | |
|
||||
| _lastUpdated | yes | date |
|
||||
| _tag | yes | token |
|
||||
| _profile | yes | URI |
|
||||
| _security | yes | token |
|
||||
| _text | yes | string |
|
||||
| _content | yes | string |
|
||||
| _list | no | |
|
||||
| _has | no | |
|
||||
| _type | no | |
|
||||
| _source | yes | URI |
|
||||
|
||||
## ValueSet autocomplete extension
|
||||
|
||||
|
@ -102,4 +101,6 @@ This extension is only valid at the type level, and requires that Extended Lucen
|
|||
As an experimental feature with the extended indexing, the full resource can be stored in the
|
||||
search index. This allows some queries to return results without using the relational database.
|
||||
Note: This does not support the $meta-add or $meta-delete operations. Full reindexing is required
|
||||
when this option is enabled after resources have been indexed.
|
||||
when this option is enabled after resources have been indexed.
|
||||
|
||||
This **experimental** feature is enabled via the `setStoreResourceInHSearchIndex()` option of DaoConfig.
|
||||
|
|
|
@ -285,11 +285,12 @@ public class TestUtil {
|
|||
Validate.notNull(fk);
|
||||
Validate.isTrue(isNotBlank(fk.name()), "Foreign key on " + theAnnotatedElement + " has no name()");
|
||||
|
||||
// temporarily allow the hibernate legacy sp fk names
|
||||
// Validate FK naming.
|
||||
// temporarily allow two hibernate legacy sp fk names until we fix them
|
||||
List<String> legacySPHibernateFKNames = Arrays.asList(
|
||||
"FKC97MPK37OKWU8QVTCEG2NH9VN", "FKGXSREUTYMMFJUWDSWV3Y887DO");
|
||||
Validate.isTrue(fk.name().startsWith("FK_") || legacySPHibernateFKNames.contains(fk.name()),
|
||||
"Foreign key " + fk.name() + " on " + theAnnotatedElement + " must start with FK");
|
||||
"Foreign key " + fk.name() + " on " + theAnnotatedElement + " must start with FK_");
|
||||
|
||||
if ( ! duplicateNameValidationExceptionList.contains(fk.name())) {
|
||||
assertNotADuplicateName(fk.name(), theNames);
|
||||
|
|
|
@ -810,7 +810,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
|
|||
|
||||
private <MT extends IBaseMetaType> void doMetaDelete(MT theMetaDel, BaseHasResource theEntity, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) {
|
||||
|
||||
// wipmb mb update hibernate search index if we are storing resources - it assumes inline tags.
|
||||
// todo mb update hibernate search index if we are storing resources - it assumes inline tags.
|
||||
IBaseResource oldVersion = toResource(theEntity, false);
|
||||
|
||||
List<TagDefinition> tags = toTagList(theMetaDel);
|
||||
|
|
|
@ -175,7 +175,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
|
|||
return theMax;
|
||||
}
|
||||
|
||||
// wipmb we should really pass this in.
|
||||
// todo mb we should really pass this in.
|
||||
if (theParams.getCount() != null) {
|
||||
return theParams.getCount();
|
||||
}
|
||||
|
@ -271,7 +271,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
|
|||
public List<ResourcePersistentId> everything(String theResourceName, SearchParameterMap theParams, ResourcePersistentId theReferencingPid) {
|
||||
validateHibernateSearchIsEnabled();
|
||||
|
||||
// wipmb what about max results here?
|
||||
// todo mb what about max results here?
|
||||
List<ResourcePersistentId> retVal = toList(doSearch(null, theParams, theReferencingPid, 10_000), 10_000);
|
||||
if (theReferencingPid != null) {
|
||||
retVal.add(theReferencingPid);
|
||||
|
|
|
@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.dao.search;
|
|||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
|
||||
|
@ -29,6 +30,7 @@ import ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers;
|
|||
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.CompositeParam;
|
||||
import ca.uhn.fhir.rest.param.DateParam;
|
||||
import ca.uhn.fhir.rest.param.DateRangeParam;
|
||||
import ca.uhn.fhir.rest.param.NumberParam;
|
||||
|
@ -38,18 +40,20 @@ import ca.uhn.fhir.rest.param.ReferenceParam;
|
|||
import ca.uhn.fhir.rest.param.StringParam;
|
||||
import ca.uhn.fhir.rest.param.TokenParam;
|
||||
import ca.uhn.fhir.rest.param.UriParam;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.util.DateUtils;
|
||||
import ca.uhn.fhir.util.NumericParamRangeUtil;
|
||||
import ca.uhn.fhir.util.StringUtil;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.hibernate.search.engine.search.common.BooleanOperator;
|
||||
import org.hibernate.search.engine.search.predicate.dsl.BooleanPredicateClausesStep;
|
||||
import org.hibernate.search.engine.search.predicate.dsl.PredicateFinalStep;
|
||||
import org.hibernate.search.engine.search.predicate.dsl.RangePredicateOptionsStep;
|
||||
import org.hibernate.search.engine.search.predicate.dsl.SearchPredicateFactory;
|
||||
import org.hibernate.search.engine.search.predicate.dsl.WildcardPredicateOptionsStep;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -65,18 +69,23 @@ import java.util.Optional;
|
|||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static ca.uhn.fhir.jpa.dao.search.PathContext.joinPath;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.IDX_STRING_EXACT;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.IDX_STRING_NORMALIZED;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.IDX_STRING_TEXT;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.NESTED_SEARCH_PARAM_ROOT;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.INDEX_TYPE_QUANTITY;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.INDEX_TYPE_STRING;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.INDEX_TYPE_TOKEN;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.NUMBER_VALUE;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.QTY_CODE;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.QTY_CODE_NORM;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.QTY_PARAM_NAME;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.QTY_SYSTEM;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.QTY_VALUE;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.QTY_VALUE_NORM;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.SEARCH_PARAM_ROOT;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.TOKEN_CODE;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.TOKEN_SYSTEM;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.TOKEN_SYSTEM_CODE;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.URI_VALUE;
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
|
@ -84,20 +93,21 @@ public class ExtendedHSearchClauseBuilder {
|
|||
private static final Logger ourLog = LoggerFactory.getLogger(ExtendedHSearchClauseBuilder.class);
|
||||
|
||||
private static final double QTY_APPROX_TOLERANCE_PERCENT = .10;
|
||||
public static final String PATH_JOINER = ".";
|
||||
|
||||
final FhirContext myFhirContext;
|
||||
public final SearchPredicateFactory myPredicateFactory;
|
||||
public final BooleanPredicateClausesStep<?> myRootClause;
|
||||
public final ModelConfig myModelConfig;
|
||||
final PathContext myRootContext;
|
||||
|
||||
final List<TemporalPrecisionEnum> ordinalSearchPrecisions = Arrays.asList(TemporalPrecisionEnum.YEAR, TemporalPrecisionEnum.MONTH, TemporalPrecisionEnum.DAY);
|
||||
|
||||
public ExtendedHSearchClauseBuilder(FhirContext myFhirContext, ModelConfig theModelConfig,
|
||||
BooleanPredicateClausesStep<?> myRootClause, SearchPredicateFactory myPredicateFactory) {
|
||||
BooleanPredicateClausesStep<?> theRootClause, SearchPredicateFactory thePredicateFactory) {
|
||||
this.myFhirContext = myFhirContext;
|
||||
this.myModelConfig = theModelConfig;
|
||||
this.myRootClause = myRootClause;
|
||||
this.myPredicateFactory = myPredicateFactory;
|
||||
this.myRootClause = theRootClause;
|
||||
myRootContext = PathContext.buildRootContext(theRootClause, thePredicateFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -105,7 +115,7 @@ public class ExtendedHSearchClauseBuilder {
|
|||
* @param theResourceType the type to match. e.g. "Observation"
|
||||
*/
|
||||
public void addResourceTypeClause(String theResourceType) {
|
||||
myRootClause.must(myPredicateFactory.match().field("myResourceType").matching(theResourceType));
|
||||
myRootClause.must(myRootContext.match().field("myResourceType").matching(theResourceType));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
|
@ -136,69 +146,51 @@ public class ExtendedHSearchClauseBuilder {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Provide an OR wrapper around a list of predicates.
|
||||
* Returns the sole predicate if it solo, or wrap as a bool/should for OR semantics.
|
||||
*
|
||||
* @param theOrList a list containing at least 1 predicate
|
||||
* @return a predicate providing or-sematics over the list.
|
||||
*/
|
||||
private PredicateFinalStep orPredicateOrSingle(List<? extends PredicateFinalStep> theOrList) {
|
||||
PredicateFinalStep finalClause;
|
||||
if (theOrList.size() == 1) {
|
||||
finalClause = theOrList.get(0);
|
||||
} else {
|
||||
BooleanPredicateClausesStep<?> orClause = myPredicateFactory.bool();
|
||||
theOrList.forEach(orClause::should);
|
||||
finalClause = orClause;
|
||||
}
|
||||
return finalClause;
|
||||
}
|
||||
|
||||
public void addTokenUnmodifiedSearch(String theSearchParamName, List<List<IQueryParameterType>> theAndOrTerms) {
|
||||
if (CollectionUtils.isEmpty(theAndOrTerms)) {
|
||||
return;
|
||||
}
|
||||
PathContext spContext = contextForFlatSP(theSearchParamName);
|
||||
for (List<? extends IQueryParameterType> nextAnd : theAndOrTerms) {
|
||||
|
||||
ourLog.debug("addTokenUnmodifiedSearch {} {}", theSearchParamName, nextAnd);
|
||||
List<? extends PredicateFinalStep> clauses = nextAnd.stream().map(orTerm -> {
|
||||
if (orTerm instanceof TokenParam) {
|
||||
TokenParam token = (TokenParam) orTerm;
|
||||
if (StringUtils.isBlank(token.getSystem())) {
|
||||
// bare value
|
||||
return myPredicateFactory.match().field(getTokenCodeFieldPath(theSearchParamName)).matching(token.getValue());
|
||||
} else if (StringUtils.isBlank(token.getValue())) {
|
||||
// system without value
|
||||
return myPredicateFactory.match().field(SEARCH_PARAM_ROOT + "." + theSearchParamName + ".token" + ".system").matching(token.getSystem());
|
||||
} else {
|
||||
// system + value
|
||||
return myPredicateFactory.match().field(getTokenSystemCodeFieldPath(theSearchParamName)).matching(token.getValueAsQueryToken(this.myFhirContext));
|
||||
}
|
||||
} else if (orTerm instanceof StringParam) {
|
||||
// MB I don't quite understand why FhirResourceDaoR4SearchNoFtTest.testSearchByIdParamWrongType() uses String but here we are
|
||||
StringParam string = (StringParam) orTerm;
|
||||
// treat a string as a code with no system (like _id)
|
||||
return myPredicateFactory.match().field(getTokenCodeFieldPath(theSearchParamName)).matching(string.getValue());
|
||||
} else {
|
||||
throw new IllegalArgumentException(Msg.code(1089) + "Unexpected param type for token search-param: " + orTerm.getClass().getName());
|
||||
}
|
||||
}).collect(Collectors.toList());
|
||||
List<? extends PredicateFinalStep> clauses = nextAnd.stream()
|
||||
.map(orTerm -> buildTokenUnmodifiedMatchOn(orTerm, spContext))
|
||||
.collect(Collectors.toList());
|
||||
PredicateFinalStep finalClause = spContext.orPredicateOrSingle(clauses);
|
||||
|
||||
PredicateFinalStep finalClause = orPredicateOrSingle(clauses);
|
||||
myRootClause.must(finalClause);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static String getTokenCodeFieldPath(String theSearchParamName) {
|
||||
return SEARCH_PARAM_ROOT + "." + theSearchParamName + ".token" + ".code";
|
||||
private PathContext contextForFlatSP(String theSearchParamName) {
|
||||
String path = joinPath(SEARCH_PARAM_ROOT, theSearchParamName);
|
||||
return myRootContext.forAbsolutePath(path);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static String getTokenSystemCodeFieldPath(@Nonnull String theSearchParamName) {
|
||||
return SEARCH_PARAM_ROOT + "." + theSearchParamName + ".token" + ".code-system";
|
||||
private PredicateFinalStep buildTokenUnmodifiedMatchOn(IQueryParameterType orTerm, PathContext thePathContext) {
|
||||
String pathPrefix = thePathContext.getContextPath();
|
||||
if (orTerm instanceof TokenParam) {
|
||||
TokenParam token = (TokenParam) orTerm;
|
||||
if (StringUtils.isBlank(token.getSystem())) {
|
||||
// bare value
|
||||
return thePathContext.match().field(joinPath(pathPrefix, INDEX_TYPE_TOKEN, TOKEN_CODE)).matching(token.getValue());
|
||||
} else if (StringUtils.isBlank(token.getValue())) {
|
||||
// system without value
|
||||
return thePathContext.match().field(joinPath(pathPrefix, INDEX_TYPE_TOKEN, TOKEN_SYSTEM)).matching(token.getSystem());
|
||||
} else {
|
||||
// system + value
|
||||
return thePathContext.match().field(joinPath(pathPrefix, INDEX_TYPE_TOKEN, TOKEN_SYSTEM_CODE)).matching(token.getValueAsQueryToken(this.myFhirContext));
|
||||
}
|
||||
} else if (orTerm instanceof StringParam) {
|
||||
// MB I don't quite understand why FhirResourceDaoR4SearchNoFtTest.testSearchByIdParamWrongType() uses String but here we are
|
||||
StringParam string = (StringParam) orTerm;
|
||||
// treat a string as a code with no system (like _id)
|
||||
return thePathContext.match().field(joinPath(pathPrefix, INDEX_TYPE_TOKEN, TOKEN_CODE)).matching(string.getValue());
|
||||
} else {
|
||||
throw new IllegalArgumentException(Msg.code(1089) + "Unexpected param type for token search-param: " + orTerm.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
public void addStringTextSearch(String theSearchParamName, List<List<IQueryParameterType>> stringAndOrTerms) {
|
||||
|
@ -216,56 +208,57 @@ public class ExtendedHSearchClauseBuilder {
|
|||
fieldName = "myNarrativeText";
|
||||
break;
|
||||
default:
|
||||
fieldName = SEARCH_PARAM_ROOT + "." + theSearchParamName + ".string." + IDX_STRING_TEXT;
|
||||
fieldName = joinPath(SEARCH_PARAM_ROOT, theSearchParamName, INDEX_TYPE_STRING, IDX_STRING_TEXT);
|
||||
break;
|
||||
}
|
||||
|
||||
for (List<? extends IQueryParameterType> nextAnd : stringAndOrTerms) {
|
||||
Set<String> terms = TermHelper.makePrefixSearchTerm(extractOrStringParams(nextAnd));
|
||||
ourLog.debug("addStringTextSearch {}, {}", theSearchParamName, terms);
|
||||
if (!terms.isEmpty()) {
|
||||
String query = terms.stream()
|
||||
for (List<? extends IQueryParameterType> nextOrList : stringAndOrTerms) {
|
||||
Set<String> orTerms = TermHelper.makePrefixSearchTerm(extractOrStringParams(nextOrList));
|
||||
ourLog.debug("addStringTextSearch {}, {}", theSearchParamName, orTerms);
|
||||
if (!orTerms.isEmpty()) {
|
||||
String query = orTerms.stream()
|
||||
.map(s -> "( " + s + " )")
|
||||
.collect(Collectors.joining(" | "));
|
||||
myRootClause.must(myPredicateFactory
|
||||
myRootClause.must(myRootContext
|
||||
.simpleQueryString()
|
||||
.field(fieldName)
|
||||
.matching(query)
|
||||
.defaultOperator(BooleanOperator.AND)); // term value may contain multiple tokens. Require all of them to be present.
|
||||
} else {
|
||||
ourLog.warn("No Terms found in query parameter {}", nextAnd);
|
||||
ourLog.warn("No Terms found in query parameter {}", nextOrList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void addStringExactSearch(String theSearchParamName, List<List<IQueryParameterType>> theStringAndOrTerms) {
|
||||
String fieldPath = SEARCH_PARAM_ROOT + "." + theSearchParamName + ".string." + IDX_STRING_EXACT;
|
||||
String fieldPath = joinPath(SEARCH_PARAM_ROOT, theSearchParamName, INDEX_TYPE_STRING, IDX_STRING_EXACT);
|
||||
|
||||
for (List<? extends IQueryParameterType> nextAnd : theStringAndOrTerms) {
|
||||
Set<String> terms = extractOrStringParams(nextAnd);
|
||||
ourLog.debug("addStringExactSearch {} {}", theSearchParamName, terms);
|
||||
List<? extends PredicateFinalStep> orTerms = terms.stream()
|
||||
.map(s -> myPredicateFactory.match().field(fieldPath).matching(s))
|
||||
.map(s -> myRootContext.match().field(fieldPath).matching(s))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
myRootClause.must(orPredicateOrSingle(orTerms));
|
||||
myRootClause.must(myRootContext.orPredicateOrSingle(orTerms));
|
||||
}
|
||||
}
|
||||
|
||||
public void addStringContainsSearch(String theSearchParamName, List<List<IQueryParameterType>> theStringAndOrTerms) {
|
||||
String fieldPath = SEARCH_PARAM_ROOT + "." + theSearchParamName + ".string." + IDX_STRING_NORMALIZED;
|
||||
String fieldPath = joinPath(SEARCH_PARAM_ROOT, theSearchParamName, INDEX_TYPE_STRING, IDX_STRING_NORMALIZED);
|
||||
for (List<? extends IQueryParameterType> nextAnd : theStringAndOrTerms) {
|
||||
Set<String> terms = extractOrStringParams(nextAnd);
|
||||
ourLog.debug("addStringContainsSearch {} {}", theSearchParamName, terms);
|
||||
List<? extends PredicateFinalStep> orTerms = terms.stream()
|
||||
// wildcard is a term-level query, so queries aren't analyzed. Do our own normalization first.
|
||||
.map(s-> normalize(s))
|
||||
.map(s -> myPredicateFactory
|
||||
.map(this::normalize)
|
||||
.map(s -> myRootContext
|
||||
.wildcard().field(fieldPath)
|
||||
.matching("*" + s + "*"))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
myRootClause.must(orPredicateOrSingle(orTerms));
|
||||
myRootClause.must(myRootContext.orPredicateOrSingle(orTerms));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -282,33 +275,37 @@ public class ExtendedHSearchClauseBuilder {
|
|||
}
|
||||
|
||||
public void addStringUnmodifiedSearch(String theSearchParamName, List<List<IQueryParameterType>> theStringAndOrTerms) {
|
||||
String fieldPath = SEARCH_PARAM_ROOT + "." + theSearchParamName + ".string." + IDX_STRING_NORMALIZED;
|
||||
for (List<? extends IQueryParameterType> nextAnd : theStringAndOrTerms) {
|
||||
Set<String> terms = extractOrStringParams(nextAnd);
|
||||
PathContext context = contextForFlatSP(theSearchParamName);
|
||||
for (List<? extends IQueryParameterType> nextOrList : theStringAndOrTerms) {
|
||||
Set<String> terms = extractOrStringParams(nextOrList);
|
||||
ourLog.debug("addStringUnmodifiedSearch {} {}", theSearchParamName, terms);
|
||||
List<? extends PredicateFinalStep> orTerms = terms.stream()
|
||||
List<PredicateFinalStep> orTerms = terms.stream()
|
||||
.map(s ->
|
||||
myPredicateFactory.wildcard()
|
||||
.field(fieldPath)
|
||||
// wildcard is a term-level query, so it isn't analyzed. Do our own case-folding to match the normStringAnalyzer
|
||||
.matching(normalize(s) + "*"))
|
||||
buildStringUnmodifiedClause(s, context))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
myRootClause.must(orPredicateOrSingle(orTerms));
|
||||
myRootClause.must(context.orPredicateOrSingle(orTerms));
|
||||
}
|
||||
}
|
||||
|
||||
private WildcardPredicateOptionsStep<?> buildStringUnmodifiedClause(String theString, PathContext theContext) {
|
||||
return theContext.wildcard()
|
||||
.field(joinPath(theContext.getContextPath(), INDEX_TYPE_STRING, IDX_STRING_NORMALIZED))
|
||||
// wildcard is a term-level query, so it isn't analyzed. Do our own case-folding to match the normStringAnalyzer
|
||||
.matching(normalize(theString) + "*");
|
||||
}
|
||||
|
||||
public void addReferenceUnchainedSearch(String theSearchParamName, List<List<IQueryParameterType>> theReferenceAndOrTerms) {
|
||||
String fieldPath = SEARCH_PARAM_ROOT + "." + theSearchParamName + ".reference.value";
|
||||
String fieldPath = joinPath(SEARCH_PARAM_ROOT, theSearchParamName, "reference", "value");
|
||||
for (List<? extends IQueryParameterType> nextAnd : theReferenceAndOrTerms) {
|
||||
Set<String> terms = extractOrStringParams(nextAnd);
|
||||
ourLog.trace("reference unchained search {}", terms);
|
||||
|
||||
List<? extends PredicateFinalStep> orTerms = terms.stream()
|
||||
.map(s -> myPredicateFactory.match().field(fieldPath).matching(s))
|
||||
.map(s -> myRootContext.match().field(fieldPath).matching(s))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
myRootClause.must(orPredicateOrSingle(orTerms));
|
||||
myRootClause.must(myRootContext.orPredicateOrSingle(orTerms));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -384,30 +381,34 @@ public class ExtendedHSearchClauseBuilder {
|
|||
*
|
||||
* @param theSearchParamName e.g code
|
||||
* @param theDateAndOrTerms The and/or list of DateParam values
|
||||
*
|
||||
* buildDateTermClause(subComponentPath, value);
|
||||
*/
|
||||
public void addDateUnmodifiedSearch(String theSearchParamName, List<List<IQueryParameterType>> theDateAndOrTerms) {
|
||||
for (List<? extends IQueryParameterType> nextAnd : theDateAndOrTerms) {
|
||||
// comma separated list of dates(OR list) on a date param is not applicable so grab
|
||||
// first from default list
|
||||
if (nextAnd.size() > 1) {
|
||||
throw new IllegalArgumentException(Msg.code(2032) + "OR (,) searches on DATE search parameters are not supported for ElasticSearch/Lucene");
|
||||
}
|
||||
DateParam dateParam = (DateParam) nextAnd.stream().findFirst()
|
||||
.orElseThrow(() -> new InvalidRequestException("Date param is missing value"));
|
||||
for (List<? extends IQueryParameterType> nextOrList : theDateAndOrTerms) {
|
||||
|
||||
boolean isOrdinalSearch = ordinalSearchPrecisions.contains(dateParam.getPrecision());
|
||||
PathContext spContext = contextForFlatSP(theSearchParamName);
|
||||
|
||||
PredicateFinalStep searchPredicate = isOrdinalSearch
|
||||
? generateDateOrdinalSearchTerms(theSearchParamName, dateParam)
|
||||
: generateDateInstantSearchTerms(theSearchParamName, dateParam);
|
||||
List<PredicateFinalStep> clauses = nextOrList.stream()
|
||||
.map(d -> buildDateTermClause(d, spContext))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
myRootClause.must(searchPredicate);
|
||||
myRootClause.must(myRootContext.orPredicateOrSingle(clauses));
|
||||
}
|
||||
}
|
||||
|
||||
private PredicateFinalStep generateDateOrdinalSearchTerms(String theSearchParamName, DateParam theDateParam) {
|
||||
String lowerOrdinalField = SEARCH_PARAM_ROOT + "." + theSearchParamName + ".dt.lower-ord";
|
||||
String upperOrdinalField = SEARCH_PARAM_ROOT + "." + theSearchParamName + ".dt.upper-ord";
|
||||
private PredicateFinalStep buildDateTermClause(IQueryParameterType theQueryParameter, PathContext theSpContext) {
|
||||
DateParam dateParam = (DateParam) theQueryParameter;
|
||||
boolean isOrdinalSearch = ordinalSearchPrecisions.contains(dateParam.getPrecision());
|
||||
return isOrdinalSearch
|
||||
? generateDateOrdinalSearchTerms(dateParam, theSpContext)
|
||||
: generateDateInstantSearchTerms(dateParam, theSpContext);
|
||||
}
|
||||
|
||||
private PredicateFinalStep generateDateOrdinalSearchTerms(DateParam theDateParam, PathContext theSpContext) {
|
||||
|
||||
String lowerOrdinalField = joinPath(theSpContext.getContextPath(), "dt", "lower-ord");
|
||||
String upperOrdinalField = joinPath(theSpContext.getContextPath(), "dt", "upper-ord");
|
||||
int lowerBoundAsOrdinal;
|
||||
int upperBoundAsOrdinal;
|
||||
ParamPrefixEnum prefix = theDateParam.getPrefix();
|
||||
|
@ -425,28 +426,28 @@ public class ExtendedHSearchClauseBuilder {
|
|||
if (Objects.isNull(prefix) || prefix == ParamPrefixEnum.EQUAL) {
|
||||
// For equality prefix we would like the date to fall between the lower and upper bound
|
||||
List<? extends PredicateFinalStep> predicateSteps = Arrays.asList(
|
||||
myPredicateFactory.range().field(lowerOrdinalField).atLeast(lowerBoundAsOrdinal),
|
||||
myPredicateFactory.range().field(upperOrdinalField).atMost(upperBoundAsOrdinal)
|
||||
theSpContext.range().field(lowerOrdinalField).atLeast(lowerBoundAsOrdinal),
|
||||
theSpContext.range().field(upperOrdinalField).atMost(upperBoundAsOrdinal)
|
||||
);
|
||||
BooleanPredicateClausesStep<?> booleanStep = myPredicateFactory.bool();
|
||||
BooleanPredicateClausesStep<?> booleanStep = theSpContext.bool();
|
||||
predicateSteps.forEach(booleanStep::must);
|
||||
return booleanStep;
|
||||
} else if (ParamPrefixEnum.GREATERTHAN == prefix || ParamPrefixEnum.STARTS_AFTER == prefix) {
|
||||
// TODO JB: more fine tuning needed for STARTS_AFTER
|
||||
return myPredicateFactory.range().field(upperOrdinalField).greaterThan(upperBoundAsOrdinal);
|
||||
return theSpContext.range().field(upperOrdinalField).greaterThan(upperBoundAsOrdinal);
|
||||
} else if (ParamPrefixEnum.GREATERTHAN_OR_EQUALS == prefix) {
|
||||
return myPredicateFactory.range().field(upperOrdinalField).atLeast(upperBoundAsOrdinal);
|
||||
return theSpContext.range().field(upperOrdinalField).atLeast(upperBoundAsOrdinal);
|
||||
} else if (ParamPrefixEnum.LESSTHAN == prefix || ParamPrefixEnum.ENDS_BEFORE == prefix) {
|
||||
// TODO JB: more fine tuning needed for END_BEFORE
|
||||
return myPredicateFactory.range().field(lowerOrdinalField).lessThan(lowerBoundAsOrdinal);
|
||||
return theSpContext.range().field(lowerOrdinalField).lessThan(lowerBoundAsOrdinal);
|
||||
} else if (ParamPrefixEnum.LESSTHAN_OR_EQUALS == prefix) {
|
||||
return myPredicateFactory.range().field(lowerOrdinalField).atMost(lowerBoundAsOrdinal);
|
||||
return theSpContext.range().field(lowerOrdinalField).atMost(lowerBoundAsOrdinal);
|
||||
} else if (ParamPrefixEnum.NOT_EQUAL == prefix) {
|
||||
List<? extends PredicateFinalStep> predicateSteps = Arrays.asList(
|
||||
myPredicateFactory.range().field(upperOrdinalField).lessThan(lowerBoundAsOrdinal),
|
||||
myPredicateFactory.range().field(lowerOrdinalField).greaterThan(upperBoundAsOrdinal)
|
||||
theSpContext.range().field(upperOrdinalField).lessThan(lowerBoundAsOrdinal),
|
||||
theSpContext.range().field(lowerOrdinalField).greaterThan(upperBoundAsOrdinal)
|
||||
);
|
||||
BooleanPredicateClausesStep<?> booleanStep = myPredicateFactory.bool();
|
||||
BooleanPredicateClausesStep<?> booleanStep = theSpContext.bool();
|
||||
predicateSteps.forEach(booleanStep::should);
|
||||
booleanStep.minimumShouldMatchNumber(1);
|
||||
return booleanStep;
|
||||
|
@ -454,18 +455,18 @@ public class ExtendedHSearchClauseBuilder {
|
|||
throw new IllegalArgumentException(Msg.code(2025) + "Date search param does not support prefix of type: " + prefix);
|
||||
}
|
||||
|
||||
private PredicateFinalStep generateDateInstantSearchTerms(String theSearchParamName, DateParam theDateParam) {
|
||||
String lowerInstantField = SEARCH_PARAM_ROOT + "." + theSearchParamName + ".dt.lower";
|
||||
String upperInstantField = SEARCH_PARAM_ROOT + "." + theSearchParamName + ".dt.upper";
|
||||
ParamPrefixEnum prefix = theDateParam.getPrefix();
|
||||
private PredicateFinalStep generateDateInstantSearchTerms(DateParam theDateParam, PathContext theSpContext) {
|
||||
String lowerInstantField = joinPath(theSpContext.getContextPath(), "dt", "lower");
|
||||
String upperInstantField = joinPath(theSpContext.getContextPath(), "dt", "upper");
|
||||
final ParamPrefixEnum prefix = ObjectUtils.defaultIfNull(theDateParam.getPrefix(), ParamPrefixEnum.EQUAL);
|
||||
|
||||
if (ParamPrefixEnum.NOT_EQUAL == prefix) {
|
||||
Instant dateInstant = theDateParam.getValue().toInstant();
|
||||
List<? extends PredicateFinalStep> predicateSteps = Arrays.asList(
|
||||
myPredicateFactory.range().field(upperInstantField).lessThan(dateInstant),
|
||||
myPredicateFactory.range().field(lowerInstantField).greaterThan(dateInstant)
|
||||
theSpContext.range().field(upperInstantField).lessThan(dateInstant),
|
||||
theSpContext.range().field(lowerInstantField).greaterThan(dateInstant)
|
||||
);
|
||||
BooleanPredicateClausesStep<?> booleanStep = myPredicateFactory.bool();
|
||||
BooleanPredicateClausesStep<?> booleanStep = theSpContext.bool();
|
||||
predicateSteps.forEach(booleanStep::should);
|
||||
booleanStep.minimumShouldMatchNumber(1);
|
||||
return booleanStep;
|
||||
|
@ -476,23 +477,23 @@ public class ExtendedHSearchClauseBuilder {
|
|||
Instant lowerBoundAsInstant = Optional.ofNullable(dateRange.getLowerBound()).map(param -> param.getValue().toInstant()).orElse(null);
|
||||
Instant upperBoundAsInstant = Optional.ofNullable(dateRange.getUpperBound()).map(param -> param.getValue().toInstant()).orElse(null);
|
||||
|
||||
if (Objects.isNull(prefix) || prefix == ParamPrefixEnum.EQUAL) {
|
||||
if (prefix == ParamPrefixEnum.EQUAL) {
|
||||
// For equality prefix we would like the date to fall between the lower and upper bound
|
||||
List<? extends PredicateFinalStep> predicateSteps = Arrays.asList(
|
||||
myPredicateFactory.range().field(lowerInstantField).atLeast(lowerBoundAsInstant),
|
||||
myPredicateFactory.range().field(upperInstantField).atMost(upperBoundAsInstant)
|
||||
((SearchPredicateFactory) theSpContext).range().field(lowerInstantField).atLeast(lowerBoundAsInstant),
|
||||
((SearchPredicateFactory) theSpContext).range().field(upperInstantField).atMost(upperBoundAsInstant)
|
||||
);
|
||||
BooleanPredicateClausesStep<?> booleanStep = myPredicateFactory.bool();
|
||||
BooleanPredicateClausesStep<?> booleanStep = ((SearchPredicateFactory) theSpContext).bool();
|
||||
predicateSteps.forEach(booleanStep::must);
|
||||
return booleanStep;
|
||||
} else if (ParamPrefixEnum.GREATERTHAN == prefix || ParamPrefixEnum.STARTS_AFTER == prefix) {
|
||||
return myPredicateFactory.range().field(upperInstantField).greaterThan(lowerBoundAsInstant);
|
||||
return ((SearchPredicateFactory) theSpContext).range().field(upperInstantField).greaterThan(lowerBoundAsInstant);
|
||||
} else if (ParamPrefixEnum.GREATERTHAN_OR_EQUALS == prefix) {
|
||||
return myPredicateFactory.range().field(upperInstantField).atLeast(lowerBoundAsInstant);
|
||||
return ((SearchPredicateFactory) theSpContext).range().field(upperInstantField).atLeast(lowerBoundAsInstant);
|
||||
} else if (ParamPrefixEnum.LESSTHAN == prefix || ParamPrefixEnum.ENDS_BEFORE == prefix) {
|
||||
return myPredicateFactory.range().field(lowerInstantField).lessThan(upperBoundAsInstant);
|
||||
return ((SearchPredicateFactory) theSpContext).range().field(lowerInstantField).lessThan(upperBoundAsInstant);
|
||||
} else if (ParamPrefixEnum.LESSTHAN_OR_EQUALS == prefix) {
|
||||
return myPredicateFactory.range().field(lowerInstantField).atMost(upperBoundAsInstant);
|
||||
return ((SearchPredicateFactory) theSpContext).range().field(lowerInstantField).atMost(upperBoundAsInstant);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(Msg.code(2026) + "Date search param does not support prefix of type: " + prefix);
|
||||
|
@ -509,60 +510,74 @@ public class ExtendedHSearchClauseBuilder {
|
|||
*/
|
||||
public void addQuantityUnmodifiedSearch(String theSearchParamName, List<List<IQueryParameterType>> theQuantityAndOrTerms) {
|
||||
|
||||
for (List<IQueryParameterType> nextAnd : theQuantityAndOrTerms) {
|
||||
BooleanPredicateClausesStep<?> quantityTerms = myPredicateFactory.bool();
|
||||
quantityTerms.minimumShouldMatchNumber(1);
|
||||
for (List<IQueryParameterType> nextOrList : theQuantityAndOrTerms) {
|
||||
// we build quantity predicates in a nested context so we can match units and systems with values.
|
||||
PredicateFinalStep nestedClause = myRootContext.buildPredicateInNestedContext(
|
||||
theSearchParamName,
|
||||
nextedContext -> {
|
||||
List<PredicateFinalStep> orClauses = nextOrList.stream()
|
||||
.map(quantityTerm -> buildQuantityTermClause(quantityTerm, nextedContext))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (IQueryParameterType paramType : nextAnd) {
|
||||
BooleanPredicateClausesStep<?> orQuantityTerms = myPredicateFactory.bool();
|
||||
addQuantityOrClauses(theSearchParamName, paramType, orQuantityTerms);
|
||||
quantityTerms.should(orQuantityTerms);
|
||||
}
|
||||
return nextedContext.orPredicateOrSingle(orClauses);
|
||||
});
|
||||
|
||||
myRootClause.must(quantityTerms);
|
||||
myRootClause.must(nestedClause);
|
||||
}
|
||||
}
|
||||
|
||||
private BooleanPredicateClausesStep<?> buildQuantityTermClause(IQueryParameterType theQueryParameter, PathContext thePathContext) {
|
||||
|
||||
private void addQuantityOrClauses(String theSearchParamName,
|
||||
IQueryParameterType theParamType, BooleanPredicateClausesStep<?> theQuantityTerms) {
|
||||
BooleanPredicateClausesStep<?> quantityClause = ((SearchPredicateFactory) thePathContext).bool();
|
||||
|
||||
QuantityParam qtyParam = QuantityParam.toQuantityParam(theParamType);
|
||||
QuantityParam qtyParam = QuantityParam.toQuantityParam(theQueryParameter);
|
||||
ParamPrefixEnum activePrefix = qtyParam.getPrefix() == null ? ParamPrefixEnum.EQUAL : qtyParam.getPrefix();
|
||||
String fieldPath = NESTED_SEARCH_PARAM_ROOT + "." + theSearchParamName + "." + QTY_PARAM_NAME;
|
||||
String quantityElement = joinPath(thePathContext.getContextPath(), INDEX_TYPE_QUANTITY);
|
||||
|
||||
if (myModelConfig.getNormalizedQuantitySearchLevel() == NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED) {
|
||||
QuantityParam canonicalQty = UcumServiceUtil.toCanonicalQuantityOrNull(qtyParam);
|
||||
if (canonicalQty != null) {
|
||||
String valueFieldPath = fieldPath + "." + QTY_VALUE_NORM;
|
||||
setPrefixedNumericPredicate(theQuantityTerms, activePrefix, canonicalQty.getValue(), valueFieldPath, true);
|
||||
theQuantityTerms.must(myPredicateFactory.match()
|
||||
.field(fieldPath + "." + QTY_CODE_NORM)
|
||||
String valueFieldPath = joinPath(quantityElement, QTY_VALUE_NORM);
|
||||
|
||||
quantityClause.must(buildNumericClause(valueFieldPath, activePrefix, canonicalQty.getValue(), thePathContext));
|
||||
quantityClause.must(((SearchPredicateFactory) thePathContext).match()
|
||||
.field(joinPath(quantityElement, QTY_CODE_NORM))
|
||||
.matching(canonicalQty.getUnits()));
|
||||
return;
|
||||
return quantityClause;
|
||||
}
|
||||
}
|
||||
|
||||
// not NORMALIZED_QUANTITY_SEARCH_SUPPORTED or non-canonicalizable parameter
|
||||
String valueFieldPath = fieldPath + "." + QTY_VALUE;
|
||||
setPrefixedNumericPredicate(theQuantityTerms, activePrefix, qtyParam.getValue(), valueFieldPath, true);
|
||||
String valueFieldPath = joinPath(quantityElement, QTY_VALUE);
|
||||
|
||||
quantityClause.must(buildNumericClause(valueFieldPath, activePrefix, qtyParam.getValue(), thePathContext));
|
||||
|
||||
if ( isNotBlank(qtyParam.getSystem()) ) {
|
||||
theQuantityTerms.must(
|
||||
myPredicateFactory.match()
|
||||
.field(fieldPath + "." + QTY_SYSTEM).matching(qtyParam.getSystem()) );
|
||||
quantityClause.must(
|
||||
((SearchPredicateFactory) thePathContext).match()
|
||||
.field(joinPath(quantityElement, QTY_SYSTEM)).matching(qtyParam.getSystem()) );
|
||||
}
|
||||
|
||||
if ( isNotBlank(qtyParam.getUnits()) ) {
|
||||
theQuantityTerms.must(
|
||||
myPredicateFactory.match()
|
||||
.field(fieldPath + "." + QTY_CODE).matching(qtyParam.getUnits()) );
|
||||
quantityClause.must(
|
||||
((SearchPredicateFactory) thePathContext).match()
|
||||
.field(joinPath(quantityElement, QTY_CODE)).matching(qtyParam.getUnits()) );
|
||||
}
|
||||
|
||||
return quantityClause;
|
||||
}
|
||||
|
||||
|
||||
private void setPrefixedNumericPredicate(BooleanPredicateClausesStep<?> theQuantityTerms,
|
||||
ParamPrefixEnum thePrefix, BigDecimal theNumberValue, String valueFieldPath, boolean theIsMust) {
|
||||
/**
|
||||
* Shared helper between quantity and number
|
||||
* @param valueFieldPath The path leading to index node
|
||||
* @param thePrefix the query prefix (e.g. lt). Null means eq
|
||||
* @param theNumberValue the query value
|
||||
* @param thePathContext HSearch builder
|
||||
* @return a query predicate applying the prefix to the value
|
||||
*/
|
||||
@Nonnull
|
||||
private PredicateFinalStep buildNumericClause(String valueFieldPath, ParamPrefixEnum thePrefix, BigDecimal theNumberValue, PathContext thePathContext) {
|
||||
PredicateFinalStep predicate = null;
|
||||
|
||||
double value = theNumberValue.doubleValue();
|
||||
Pair<BigDecimal, BigDecimal> range = NumericParamRangeUtil.getRange(theNumberValue);
|
||||
|
@ -572,93 +587,169 @@ public class ExtendedHSearchClauseBuilder {
|
|||
switch (activePrefix) {
|
||||
// searches for resource quantity between passed param value +/- 10%
|
||||
case APPROXIMATE:
|
||||
var predApp = myPredicateFactory.range().field(valueFieldPath)
|
||||
predicate = ((SearchPredicateFactory) thePathContext).range().field(valueFieldPath)
|
||||
.between(value-approxTolerance, value+approxTolerance);
|
||||
addMustOrShouldPredicate(theQuantityTerms, predApp, theIsMust);
|
||||
break;
|
||||
|
||||
// searches for resource quantity between passed param value +/- 5%
|
||||
case EQUAL:
|
||||
var predEq = myPredicateFactory.range().field(valueFieldPath)
|
||||
predicate = ((SearchPredicateFactory) thePathContext).range().field(valueFieldPath)
|
||||
.between(range.getLeft().doubleValue(), range.getRight().doubleValue());
|
||||
addMustOrShouldPredicate(theQuantityTerms, predEq, theIsMust);
|
||||
break;
|
||||
|
||||
// searches for resource quantity > param value
|
||||
case GREATERTHAN:
|
||||
case STARTS_AFTER: // treated as GREATERTHAN because search doesn't handle ranges
|
||||
var predGt = myPredicateFactory.range().field(valueFieldPath).greaterThan(value);
|
||||
addMustOrShouldPredicate(theQuantityTerms, predGt, theIsMust);
|
||||
predicate = ((SearchPredicateFactory) thePathContext).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));
|
||||
|
||||
var predGe = myPredicateFactory.range().field(valueFieldPath).atLeast(value);
|
||||
addMustOrShouldPredicate(theQuantityTerms, predGe, theIsMust);
|
||||
predicate = ((SearchPredicateFactory) thePathContext).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
|
||||
var predLt = myPredicateFactory.range().field(valueFieldPath).lessThan(value);
|
||||
addMustOrShouldPredicate(theQuantityTerms, predLt, theIsMust);
|
||||
predicate = ((SearchPredicateFactory) thePathContext).range().field(valueFieldPath).lessThan(value);
|
||||
break;
|
||||
|
||||
// searches for resource quantity not > param value
|
||||
case LESSTHAN_OR_EQUALS:
|
||||
var predLe = myPredicateFactory.range().field(valueFieldPath).atMost(value);
|
||||
addMustOrShouldPredicate(theQuantityTerms, predLe, theIsMust);
|
||||
predicate = ((SearchPredicateFactory) thePathContext).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(range.getLeft().doubleValue(), range.getRight().doubleValue()));
|
||||
RangePredicateOptionsStep<?> negRange = ((SearchPredicateFactory) thePathContext).range()
|
||||
.field(valueFieldPath).between(range.getLeft().doubleValue(), range.getRight().doubleValue());
|
||||
predicate = ((SearchPredicateFactory) thePathContext).bool().mustNot(negRange);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void addMustOrShouldPredicate(BooleanPredicateClausesStep<?> theQuantityTerms,
|
||||
RangePredicateOptionsStep<?> thePredicateToAdd, boolean theIsMust) {
|
||||
|
||||
if (theIsMust) {
|
||||
theQuantityTerms.must(thePredicateToAdd);
|
||||
} else {
|
||||
theQuantityTerms.should(thePredicateToAdd);
|
||||
}
|
||||
Validate.notNull(predicate, "Unsupported prefix: %s", thePrefix);
|
||||
return predicate;
|
||||
}
|
||||
|
||||
|
||||
public void addUriUnmodifiedSearch(String theParamName, List<List<IQueryParameterType>> theUriUnmodifiedAndOrTerms) {
|
||||
for (List<IQueryParameterType> nextAnd : theUriUnmodifiedAndOrTerms) {
|
||||
PathContext spContext = this.contextForFlatSP(theParamName);
|
||||
for (List<IQueryParameterType> nextOrList : theUriUnmodifiedAndOrTerms) {
|
||||
|
||||
List<String> orTerms = nextAnd.stream().map(p -> ((UriParam) p).getValue()).collect(Collectors.toList());
|
||||
PredicateFinalStep orTermPredicate = myPredicateFactory.terms()
|
||||
.field(String.join(".", SEARCH_PARAM_ROOT, theParamName, URI_VALUE))
|
||||
.matchingAny(orTerms);
|
||||
PredicateFinalStep orListPredicate = buildURIClause(nextOrList, spContext);
|
||||
|
||||
myRootClause.must(orTermPredicate);
|
||||
myRootClause.must(orListPredicate);
|
||||
}
|
||||
}
|
||||
|
||||
private PredicateFinalStep buildURIClause(List<IQueryParameterType> theOrList, PathContext thePathContext) {
|
||||
List<String> orTerms = theOrList.stream()
|
||||
.map(p -> ((UriParam) p).getValue())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ((SearchPredicateFactory) thePathContext).terms()
|
||||
.field(joinPath(thePathContext.getContextPath(), URI_VALUE))
|
||||
.matchingAny(orTerms);
|
||||
}
|
||||
|
||||
public void addNumberUnmodifiedSearch(String theParamName, List<List<IQueryParameterType>> theNumberUnmodifiedAndOrTerms) {
|
||||
String fieldPath = String.join(".", SEARCH_PARAM_ROOT, theParamName, NUMBER_VALUE);
|
||||
PathContext pathContext = contextForFlatSP(theParamName);
|
||||
String fieldPath = joinPath(SEARCH_PARAM_ROOT, theParamName, NUMBER_VALUE);
|
||||
|
||||
for (List<IQueryParameterType> nextAnd : theNumberUnmodifiedAndOrTerms) {
|
||||
List<NumberParam> orTerms = nextAnd.stream().map(NumberParam.class::cast).collect(Collectors.toList());
|
||||
for (List<IQueryParameterType> nextOrList : theNumberUnmodifiedAndOrTerms) {
|
||||
List<PredicateFinalStep> orTerms = nextOrList.stream()
|
||||
.map(NumberParam.class::cast)
|
||||
.map(orTerm -> buildNumericClause(fieldPath, orTerm.getPrefix(), orTerm.getValue(), pathContext))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
BooleanPredicateClausesStep<?> numberPredicateStep = myPredicateFactory.bool();
|
||||
numberPredicateStep.minimumShouldMatchNumber(1);
|
||||
myRootClause.must(pathContext.orPredicateOrSingle(orTerms));
|
||||
}
|
||||
}
|
||||
|
||||
private PredicateFinalStep buildNumericClause(IQueryParameterType theValue, PathContext thePathContext) {
|
||||
NumberParam p = (NumberParam) theValue;
|
||||
|
||||
return buildNumericClause(joinPath(thePathContext.getContextPath(), NUMBER_VALUE), p.getPrefix(), p.getValue(), thePathContext);
|
||||
}
|
||||
|
||||
public void addCompositeUnmodifiedSearch(RuntimeSearchParam theSearchParam, List<RuntimeSearchParam> theSubSearchParams, List<List<IQueryParameterType>> theCompositeAndOrTerms) {
|
||||
for (List<IQueryParameterType> nextOrList : theCompositeAndOrTerms) {
|
||||
|
||||
// The index data for each extracted element is stored in a separate nested HSearch document.
|
||||
// Create a nested parent node for all component predicates.
|
||||
// Each can share this nested beacuse all nested docs share a parent id.
|
||||
|
||||
PredicateFinalStep nestedClause = myRootContext.buildPredicateInNestedContext(
|
||||
theSearchParam.getName(),
|
||||
nestedContext -> {
|
||||
List<PredicateFinalStep> orClauses =
|
||||
nextOrList.stream()
|
||||
.map(term -> computeCompositeTermClause(theSearchParam, theSubSearchParams, (CompositeParam<?,?>) term, nestedContext))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return nestedContext.orPredicateOrSingle(orClauses);
|
||||
});
|
||||
myRootClause.must(nestedClause);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the match clause for all the components of theCompositeQueryParam.
|
||||
*
|
||||
* @param theSearchParam The composite SP
|
||||
* @param theSubSearchParams the composite component SPs
|
||||
* @param theCompositeQueryParam the query param values
|
||||
* @param theCompositeContext the root of the nested SP query.
|
||||
*/
|
||||
private PredicateFinalStep computeCompositeTermClause(RuntimeSearchParam theSearchParam, List<RuntimeSearchParam> theSubSearchParams, CompositeParam<?,?> theCompositeQueryParam, PathContext theCompositeContext) {
|
||||
Validate.notNull(theSearchParam);
|
||||
Validate.notNull(theSubSearchParams);
|
||||
Validate.notNull(theCompositeQueryParam);
|
||||
Validate.isTrue(theSubSearchParams.size() == 2, "Hapi only supports composite search parameters with 2 components. %s %d", theSearchParam.getName(), theSubSearchParams.size());
|
||||
List<IQueryParameterType> values = theCompositeQueryParam.getValues();
|
||||
Validate.isTrue(theSubSearchParams.size() == values.size(), "Different number of query components than defined. %s %d %d", theSearchParam.getName(), theSubSearchParams.size(), values.size());
|
||||
|
||||
// The index data for each extracted element is stored in a separate nested HSearch document.
|
||||
|
||||
// Create a nested parent node for all component predicates.
|
||||
BooleanPredicateClausesStep<?> compositeClause = ((SearchPredicateFactory) theCompositeContext).bool();
|
||||
for (int i = 0; i < theSubSearchParams.size(); i += 1) {
|
||||
RuntimeSearchParam component = theSubSearchParams.get(i);
|
||||
IQueryParameterType value = values.get(i);
|
||||
PredicateFinalStep subMatch = null;
|
||||
PathContext componentContext = theCompositeContext.getSubComponentContext(component.getName());
|
||||
switch (component.getParamType()) {
|
||||
case DATE:
|
||||
subMatch = buildDateTermClause(value, componentContext);
|
||||
break;
|
||||
case STRING:
|
||||
subMatch = buildStringUnmodifiedClause(value.getValueAsQueryToken(myFhirContext), componentContext);
|
||||
break;
|
||||
case TOKEN:
|
||||
subMatch = buildTokenUnmodifiedMatchOn(value, componentContext);
|
||||
break;
|
||||
case QUANTITY:
|
||||
subMatch = buildQuantityTermClause(value, componentContext);
|
||||
break;
|
||||
case URI:
|
||||
subMatch = buildURIClause(List.of(value), componentContext);
|
||||
break;
|
||||
case NUMBER:
|
||||
subMatch = buildNumericClause(value, componentContext);
|
||||
break;
|
||||
case REFERENCE:
|
||||
// wipmb Can we use reference in composite?
|
||||
|
||||
default:
|
||||
break;
|
||||
|
||||
for (NumberParam orTerm : orTerms) {
|
||||
setPrefixedNumericPredicate(numberPredicateStep, orTerm.getPrefix(), orTerm.getValue(), fieldPath, false);
|
||||
}
|
||||
|
||||
myRootClause.must(numberPredicateStep);
|
||||
Validate.notNull(subMatch, "Unsupported composite type in %s: %s %s", theSearchParam.getName(), component.getName(), component.getParamType());
|
||||
compositeClause.must(subMatch);
|
||||
}
|
||||
|
||||
return compositeClause;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -25,9 +25,14 @@ import ca.uhn.fhir.context.RuntimeSearchParam;
|
|||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceLink;
|
||||
import ca.uhn.fhir.jpa.model.search.CompositeSearchIndexData;
|
||||
import ca.uhn.fhir.jpa.model.search.DateSearchIndexData;
|
||||
import ca.uhn.fhir.jpa.model.search.ExtendedHSearchIndexData;
|
||||
import ca.uhn.fhir.jpa.model.search.QuantitySearchIndexData;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParamComposite;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
|
||||
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
|
||||
import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
|
||||
|
@ -71,7 +76,7 @@ public class ExtendedHSearchIndexExtractor {
|
|||
|
||||
@Nonnull
|
||||
public ExtendedHSearchIndexData extract(IBaseResource theResource, ResourceIndexedSearchParams theNewParams) {
|
||||
ExtendedHSearchIndexData retVal = new ExtendedHSearchIndexData(myContext, myModelConfig);
|
||||
ExtendedHSearchIndexData retVal = new ExtendedHSearchIndexData(myContext, myModelConfig, theResource);
|
||||
|
||||
if(myDaoConfig.isStoreResourceInHSearchIndex()) {
|
||||
retVal.setRawResourceData(myContext.newJsonParser().encodeResourceToString(theResource));
|
||||
|
@ -79,6 +84,7 @@ public class ExtendedHSearchIndexExtractor {
|
|||
|
||||
retVal.setForcedId(theResource.getIdElement().getIdPart());
|
||||
|
||||
// wipmb mb add a flag ot DaoConfig to suppress this
|
||||
extractAutocompleteTokens(theResource, retVal);
|
||||
|
||||
theNewParams.myStringParams.forEach(nextParam ->
|
||||
|
@ -90,12 +96,9 @@ public class ExtendedHSearchIndexExtractor {
|
|||
theNewParams.myNumberParams.forEach(nextParam ->
|
||||
retVal.addNumberIndexDataIfNotPresent(nextParam.getParamName(), nextParam.getValue()));
|
||||
|
||||
theNewParams.myDateParams.forEach(nextParam ->
|
||||
retVal.addDateIndexData(nextParam.getParamName(), nextParam.getValueLow(), nextParam.getValueLowDateOrdinal(),
|
||||
nextParam.getValueHigh(), nextParam.getValueHighDateOrdinal()));
|
||||
theNewParams.myDateParams.forEach(nextParam -> retVal.addDateIndexData(nextParam.getParamName(), convertDate(nextParam)));
|
||||
|
||||
theNewParams.myQuantityParams.forEach(nextParam ->
|
||||
retVal.addQuantityIndexData(nextParam.getParamName(), nextParam.getUnits(), nextParam.getSystem(), nextParam.getValue().doubleValue()));
|
||||
theNewParams.myQuantityParams.forEach(nextParam -> retVal.addQuantityIndexData(nextParam.getParamName(), convertQuantity(nextParam)));
|
||||
|
||||
theResource.getMeta().getTag().forEach(tag ->
|
||||
retVal.addTokenIndexData("_tag", tag));
|
||||
|
@ -111,6 +114,10 @@ public class ExtendedHSearchIndexExtractor {
|
|||
retVal.addUriIndexData("_source", source);
|
||||
}
|
||||
|
||||
theNewParams.myCompositeParams.forEach(nextParam ->
|
||||
retVal.addCompositeIndexData(nextParam.getSearchParamName(), buildCompositeIndexData(nextParam)));
|
||||
|
||||
|
||||
if (theResource.getMeta().getLastUpdated() != null) {
|
||||
int ordinal = ResourceIndexedSearchParamDate.calculateOrdinalValue(theResource.getMeta().getLastUpdated()).intValue();
|
||||
retVal.addDateIndexData("_lastUpdated", theResource.getMeta().getLastUpdated(), ordinal,
|
||||
|
@ -157,6 +164,21 @@ public class ExtendedHSearchIndexExtractor {
|
|||
return retVal;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static DateSearchIndexData convertDate(ResourceIndexedSearchParamDate nextParam) {
|
||||
return new DateSearchIndexData(nextParam.getValueLow(), nextParam.getValueLowDateOrdinal(), nextParam.getValueHigh(), nextParam.getValueHighDateOrdinal());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static QuantitySearchIndexData convertQuantity(ResourceIndexedSearchParamQuantity nextParam) {
|
||||
return new QuantitySearchIndexData(nextParam.getUnits(), nextParam.getSystem(), nextParam.getValue().doubleValue());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private CompositeSearchIndexData buildCompositeIndexData(ResourceIndexedSearchParamComposite theSearchParamComposite) {
|
||||
return new HSearchCompositeSearchIndexDataImpl(theSearchParamComposite);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-extract token parameters so we can distinguish
|
||||
*/
|
||||
|
@ -204,4 +226,5 @@ public class ExtendedHSearchIndexExtractor {
|
|||
private void addToken_Coding(ExtendedHSearchIndexData theRetVal, String theSpName, IBaseCoding theNextValue) {
|
||||
theRetVal.addTokenIndexData(theSpName, theNextValue);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -22,8 +22,10 @@ package ca.uhn.fhir.jpa.dao.search;
|
|||
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
|
||||
import ca.uhn.fhir.model.api.IQueryParameterType;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.param.CompositeParam;
|
||||
import ca.uhn.fhir.rest.api.SearchContainedModeEnum;
|
||||
import ca.uhn.fhir.rest.param.DateParam;
|
||||
import ca.uhn.fhir.rest.param.NumberParam;
|
||||
|
@ -124,6 +126,9 @@ public class ExtendedHSearchSearchBuilder {
|
|||
} else if (param instanceof QuantityParam) {
|
||||
return modifier.equals(EMPTY_MODIFIER);
|
||||
|
||||
} else if (param instanceof CompositeParam) {
|
||||
return true;
|
||||
|
||||
} else if (param instanceof ReferenceParam) {
|
||||
//We cannot search by chain.
|
||||
if (((ReferenceParam) param).getChain() != null) {
|
||||
|
@ -204,6 +209,13 @@ public class ExtendedHSearchSearchBuilder {
|
|||
builder.addDateUnmodifiedSearch(nextParam, dateAndOrTerms);
|
||||
break;
|
||||
|
||||
case COMPOSITE:
|
||||
List<List<IQueryParameterType>> compositeAndOrTerms = theParams.removeByNameUnmodified(nextParam);
|
||||
// RuntimeSearchParam only points to the subs by reference. Resolve here while we have ISearchParamRegistry
|
||||
List<RuntimeSearchParam> subSearchParams = JpaParamUtil.resolveCompositeComponentsDeclaredOrder(theSearchParamRegistry, activeParam);
|
||||
builder.addCompositeUnmodifiedSearch(activeParam, subSearchParams, compositeAndOrTerms);
|
||||
break;
|
||||
|
||||
case URI:
|
||||
List<List<IQueryParameterType>> uriUnmodifiedAndOrTerms = theParams.removeByNameUnmodified(nextParam);
|
||||
builder.addUriUnmodifiedSearch(nextParam, uriUnmodifiedAndOrTerms);
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
package ca.uhn.fhir.jpa.dao.search;
|
||||
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri;
|
||||
import ca.uhn.fhir.jpa.model.search.CompositeSearchIndexData;
|
||||
import ca.uhn.fhir.jpa.model.search.HSearchElementCache;
|
||||
import ca.uhn.fhir.jpa.model.search.HSearchIndexWriter;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParamComposite;
|
||||
import ca.uhn.fhir.model.api.Tag;
|
||||
import ca.uhn.fhir.util.ObjectUtil;
|
||||
import org.hibernate.search.engine.backend.document.DocumentElement;
|
||||
|
||||
/**
|
||||
* binding of HSearch apis into
|
||||
*
|
||||
* We have a diamond dependency pattern, and searchparam and hsearch aren't friends. Bring them together here.
|
||||
*/
|
||||
class HSearchCompositeSearchIndexDataImpl implements CompositeSearchIndexData {
|
||||
|
||||
final ResourceIndexedSearchParamComposite mySearchParamComposite;
|
||||
|
||||
public HSearchCompositeSearchIndexDataImpl(ResourceIndexedSearchParamComposite theSearchParamComposite) {
|
||||
mySearchParamComposite = theSearchParamComposite;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Write a nested index document for this composite.
|
||||
* We use a nested document to support correlation queries on the same parent element for
|
||||
* proper composite SP semantics. Each component will have a sub-node for each component SP.
|
||||
*
|
||||
* Example for component-code-value-quantity, which composes
|
||||
* component-code and component-value-quantity:
|
||||
<pre>
|
||||
{ "nsp: {
|
||||
"component-code-value-quantity": [
|
||||
{
|
||||
"component-code": {
|
||||
"token": {
|
||||
"code": "8480-6",
|
||||
"system": "http://loinc.org",
|
||||
"code-system": "http://loinc.org|8480-6"
|
||||
}
|
||||
},
|
||||
"component-value-quantity": {
|
||||
"quantity": {
|
||||
"code": "mmHg",
|
||||
"value": 60.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"component-code": {
|
||||
"token": {
|
||||
"code": "3421-5",
|
||||
"system": "http://loinc.org",
|
||||
"code-system": "http://loinc.org|3421-5"
|
||||
}
|
||||
},
|
||||
"component-value-quantity": {
|
||||
"quantity": {
|
||||
"code": "mmHg",
|
||||
"value": 100.0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}}
|
||||
</pre>
|
||||
*
|
||||
* @param theRoot our cache wrapper around the root HSearch DocumentElement
|
||||
*/
|
||||
@Override
|
||||
public void writeIndexEntry(HSearchIndexWriter theHSearchIndexWriter, HSearchElementCache theRoot) {
|
||||
// optimization - An empty sub-component will never match.
|
||||
// Storing the rest only wastes resources
|
||||
boolean hasAnEmptyComponent =
|
||||
mySearchParamComposite.getComponents().stream()
|
||||
.anyMatch(c->c.getParamIndexValues().isEmpty());
|
||||
|
||||
if (hasAnEmptyComponent) {
|
||||
return;
|
||||
}
|
||||
|
||||
DocumentElement nestedParamRoot = theRoot.getObjectElement(HSearchIndexWriter.NESTED_SEARCH_PARAM_ROOT);
|
||||
|
||||
// we want to re-use the `token`, `quantity` nodes for multiple values.
|
||||
DocumentElement compositeRoot = nestedParamRoot.addObject(mySearchParamComposite.getSearchParamName());
|
||||
|
||||
|
||||
for (ResourceIndexedSearchParamComposite.Component subParam : mySearchParamComposite.getComponents()) {
|
||||
// Write the various index nodes.
|
||||
// Note: we don't support modifiers with composites, so we don't bother to index :of-type, :text, etc.
|
||||
DocumentElement subParamElement = compositeRoot.addObject(subParam.getSearchParamName());
|
||||
switch (subParam.getSearchParameterType()) {
|
||||
case DATE:
|
||||
DocumentElement dateElement = subParamElement.addObject("dt");
|
||||
subParam.getParamIndexValues().stream()
|
||||
.flatMap(o->ObjectUtil.castIfInstanceof(o, ResourceIndexedSearchParamDate.class).stream())
|
||||
.map(ExtendedHSearchIndexExtractor::convertDate)
|
||||
.forEach(d-> theHSearchIndexWriter.writeDateFields(dateElement, d));
|
||||
break;
|
||||
|
||||
case QUANTITY:
|
||||
DocumentElement quantityElement = subParamElement.addObject(HSearchIndexWriter.INDEX_TYPE_QUANTITY);
|
||||
subParam.getParamIndexValues().stream()
|
||||
.flatMap(o->ObjectUtil.castIfInstanceof(o, ResourceIndexedSearchParamQuantity.class).stream())
|
||||
.map(ExtendedHSearchIndexExtractor::convertQuantity)
|
||||
.forEach(q-> theHSearchIndexWriter.writeQuantityFields(quantityElement, q));
|
||||
break;
|
||||
|
||||
case STRING:
|
||||
DocumentElement stringElement = subParamElement.addObject("string");
|
||||
subParam.getParamIndexValues().stream()
|
||||
.flatMap(o->ObjectUtil.castIfInstanceof(o, ResourceIndexedSearchParamString.class).stream())
|
||||
.forEach(risps-> theHSearchIndexWriter.writeBasicStringFields(stringElement, risps.getValueExact()));
|
||||
break;
|
||||
|
||||
case TOKEN:
|
||||
DocumentElement tokenElement = subParamElement.addObject("token");
|
||||
subParam.getParamIndexValues().stream()
|
||||
.flatMap(o->ObjectUtil.castIfInstanceof(o, ResourceIndexedSearchParamToken.class).stream())
|
||||
.forEach(rispt-> theHSearchIndexWriter.writeTokenFields(tokenElement, new Tag(rispt.getSystem(), rispt.getValue())));
|
||||
break;
|
||||
|
||||
case URI:
|
||||
subParam.getParamIndexValues().stream()
|
||||
.flatMap(o->ObjectUtil.castIfInstanceof(o, ResourceIndexedSearchParamUri.class).stream())
|
||||
.forEach(rispu->theHSearchIndexWriter.writeUriFields(subParamElement, rispu.getUri()));
|
||||
break;
|
||||
|
||||
case NUMBER:
|
||||
subParam.getParamIndexValues().stream()
|
||||
.flatMap(o->ObjectUtil.castIfInstanceof(o, ResourceIndexedSearchParamNumber.class).stream())
|
||||
.forEach(rispn->theHSearchIndexWriter.writeNumberFields(subParamElement, rispn.getValue()));
|
||||
break;
|
||||
|
||||
case COMPOSITE:
|
||||
assert false: "composite components can't be composite";
|
||||
break;
|
||||
|
||||
// wipmb Can any of these be part of a composite?
|
||||
case REFERENCE:
|
||||
break;
|
||||
|
||||
// unsupported
|
||||
case SPECIAL:
|
||||
case HAS:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -40,7 +40,7 @@ import java.util.stream.Collectors;
|
|||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.IDX_STRING_LOWER;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.NESTED_SEARCH_PARAM_ROOT;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.NUMBER_VALUE;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.QTY_PARAM_NAME;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.INDEX_TYPE_QUANTITY;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.QTY_VALUE;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.QTY_VALUE_NORM;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.SEARCH_PARAM_ROOT;
|
||||
|
@ -61,8 +61,8 @@ public class HSearchSortHelperImpl implements IHSearchSortHelper {
|
|||
RestSearchParameterTypeEnum.REFERENCE, List.of(SEARCH_PARAM_ROOT + ".*.reference.value"),
|
||||
RestSearchParameterTypeEnum.DATE, List.of(SEARCH_PARAM_ROOT + ".*.dt.lower"),
|
||||
RestSearchParameterTypeEnum.QUANTITY, List.of(
|
||||
String.join(".", NESTED_SEARCH_PARAM_ROOT, "*", QTY_PARAM_NAME, QTY_VALUE_NORM),
|
||||
String.join(".", NESTED_SEARCH_PARAM_ROOT, "*", QTY_PARAM_NAME, QTY_VALUE) ),
|
||||
String.join(".", NESTED_SEARCH_PARAM_ROOT, "*", INDEX_TYPE_QUANTITY, QTY_VALUE_NORM),
|
||||
String.join(".", NESTED_SEARCH_PARAM_ROOT, "*", INDEX_TYPE_QUANTITY, QTY_VALUE) ),
|
||||
RestSearchParameterTypeEnum.URI, List.of(SEARCH_PARAM_ROOT + ".*." + URI_VALUE),
|
||||
RestSearchParameterTypeEnum.NUMBER, List.of(SEARCH_PARAM_ROOT + ".*." + NUMBER_VALUE)
|
||||
);
|
||||
|
|
|
@ -0,0 +1,185 @@
|
|||
package ca.uhn.fhir.jpa.dao.search;
|
||||
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hibernate.search.engine.search.predicate.dsl.*;
|
||||
import org.hibernate.search.util.common.annotation.Incubating;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static ca.uhn.fhir.jpa.dao.search.ExtendedHSearchClauseBuilder.PATH_JOINER;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.NESTED_SEARCH_PARAM_ROOT;
|
||||
|
||||
/**
|
||||
* Holds current query path, boolean clause accumulating AND clauses, and a factory for new predicates.
|
||||
*
|
||||
* The Hibernate Search SearchPredicateFactory is "smart", and knows to wrap references to nested fields
|
||||
* in a nested clause. This is a problem if we want to accumulate them in a single boolean before nesting.
|
||||
* Instead, we keep track of the current query path (e.g. "nsp.value-quantity"), and the right SearchPredicateFactory
|
||||
* to use.
|
||||
*/
|
||||
class PathContext implements SearchPredicateFactory {
|
||||
private final String myPathPrefix;
|
||||
private final BooleanPredicateClausesStep<?> myRootClause;
|
||||
private final SearchPredicateFactory myPredicateFactory;
|
||||
|
||||
PathContext(String thePrefix, BooleanPredicateClausesStep<?> theClause, SearchPredicateFactory thePredicateFactory) {
|
||||
myRootClause = theClause;
|
||||
myPredicateFactory = thePredicateFactory;
|
||||
myPathPrefix = thePrefix;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
static PathContext buildRootContext(BooleanPredicateClausesStep<?> theRootClause, SearchPredicateFactory thePredicateFactory) {
|
||||
return new PathContext("", theRootClause, thePredicateFactory);
|
||||
}
|
||||
|
||||
public String getContextPath() {
|
||||
return myPathPrefix;
|
||||
}
|
||||
|
||||
public PathContext getSubComponentContext(String theName) {
|
||||
return new PathContext(joinPath(myPathPrefix, theName), myRootClause, myPredicateFactory);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
PathContext forAbsolutePath(String path) {
|
||||
return new PathContext(path, myRootClause, myPredicateFactory);
|
||||
}
|
||||
|
||||
public PredicateFinalStep buildPredicateInNestedContext(String theSubPath, Function<PathContext, PredicateFinalStep> f) {
|
||||
String nestedRootPath = joinPath(NESTED_SEARCH_PARAM_ROOT, theSubPath);
|
||||
NestedPredicateOptionsStep<?> orListPredicate = myPredicateFactory
|
||||
.nested().objectField(nestedRootPath)
|
||||
.nest(nestedRootPredicateFactory -> {
|
||||
PathContext nestedCompositeSPContext = new PathContext(nestedRootPath, myRootClause, nestedRootPredicateFactory);
|
||||
return f.apply(nestedCompositeSPContext);
|
||||
});
|
||||
return orListPredicate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide an OR wrapper around a list of predicates.
|
||||
*
|
||||
* Wrap the predicates under a bool as should clauses with minimumShouldMatch=1 for OR semantics.
|
||||
* As an optimization, when there is only one clause, we avoid the redundant boolean wrapper
|
||||
* and return the first item as is.
|
||||
*
|
||||
* @param theOrList a list containing at least 1 predicate
|
||||
* @return a predicate providing or-semantics over the list.
|
||||
*/
|
||||
public PredicateFinalStep orPredicateOrSingle(List<? extends PredicateFinalStep> theOrList) {
|
||||
PredicateFinalStep finalClause;
|
||||
if (theOrList.size() == 1) {
|
||||
finalClause = theOrList.get(0);
|
||||
} else {
|
||||
BooleanPredicateClausesStep<?> orClause = myPredicateFactory.bool();
|
||||
orClause.minimumShouldMatchNumber(1);
|
||||
theOrList.forEach(orClause::should);
|
||||
finalClause = orClause;
|
||||
}
|
||||
return finalClause;
|
||||
}
|
||||
|
||||
|
||||
// implement SearchPredicateFactory
|
||||
|
||||
public MatchAllPredicateOptionsStep<?> matchAll() {
|
||||
return myPredicateFactory.matchAll();
|
||||
}
|
||||
|
||||
public MatchIdPredicateMatchingStep<?> id() {
|
||||
return myPredicateFactory.id();
|
||||
}
|
||||
|
||||
public BooleanPredicateClausesStep<?> bool() {
|
||||
return myPredicateFactory.bool();
|
||||
}
|
||||
|
||||
public PredicateFinalStep bool(Consumer<? super BooleanPredicateClausesStep<?>> clauseContributor) {
|
||||
return myPredicateFactory.bool(clauseContributor);
|
||||
}
|
||||
|
||||
public MatchPredicateFieldStep<?> match() {
|
||||
return myPredicateFactory.match();
|
||||
}
|
||||
|
||||
public RangePredicateFieldStep<?> range() {
|
||||
return myPredicateFactory.range();
|
||||
}
|
||||
|
||||
public PhrasePredicateFieldStep<?> phrase() {
|
||||
return myPredicateFactory.phrase();
|
||||
}
|
||||
|
||||
public WildcardPredicateFieldStep<?> wildcard() {
|
||||
return myPredicateFactory.wildcard();
|
||||
}
|
||||
|
||||
public RegexpPredicateFieldStep<?> regexp() {
|
||||
return myPredicateFactory.regexp();
|
||||
}
|
||||
|
||||
public TermsPredicateFieldStep<?> terms() {
|
||||
return myPredicateFactory.terms();
|
||||
}
|
||||
|
||||
public NestedPredicateFieldStep<?> nested() {
|
||||
return myPredicateFactory.nested();
|
||||
}
|
||||
|
||||
public SimpleQueryStringPredicateFieldStep<?> simpleQueryString() {
|
||||
return myPredicateFactory.simpleQueryString();
|
||||
}
|
||||
|
||||
public ExistsPredicateFieldStep<?> exists() {
|
||||
return myPredicateFactory.exists();
|
||||
}
|
||||
|
||||
public SpatialPredicateInitialStep spatial() {
|
||||
return myPredicateFactory.spatial();
|
||||
}
|
||||
|
||||
@Incubating
|
||||
public NamedPredicateOptionsStep named(String path) {
|
||||
return myPredicateFactory.named(path);
|
||||
}
|
||||
|
||||
public <T> T extension(SearchPredicateFactoryExtension<T> extension) {
|
||||
return myPredicateFactory.extension(extension);
|
||||
}
|
||||
|
||||
public SearchPredicateFactoryExtensionIfSupportedStep extension() {
|
||||
return myPredicateFactory.extension();
|
||||
}
|
||||
|
||||
@Incubating
|
||||
public SearchPredicateFactory withRoot(String objectFieldPath) {
|
||||
return myPredicateFactory.withRoot(objectFieldPath);
|
||||
}
|
||||
|
||||
@Incubating
|
||||
public String toAbsolutePath(String relativeFieldPath) {
|
||||
return myPredicateFactory.toAbsolutePath(relativeFieldPath);
|
||||
}
|
||||
|
||||
|
||||
// HSearch uses a dotted path
|
||||
// Some private static helpers that can be inlined.
|
||||
@Nonnull
|
||||
public static String joinPath(String thePath0, String thePath1) {
|
||||
return thePath0 + PATH_JOINER + thePath1;
|
||||
}
|
||||
|
||||
public static String joinPath(String thePath0, String thePath1, String thePath2) {
|
||||
return thePath0 + PATH_JOINER + thePath1 + PATH_JOINER + thePath2;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static String joinPath(String thePath0, String thePath1, String thePath2, String thePath3) {
|
||||
return thePath0 + PATH_JOINER + thePath1 + PATH_JOINER + thePath2 + PATH_JOINER + thePath3;
|
||||
}
|
||||
}
|
|
@ -65,7 +65,7 @@ abstract public class BaseR4SearchLastN extends BaseJpaTest {
|
|||
private static final Map<String, String> observationCategoryMap = new HashMap<>();
|
||||
private static final Map<String, String> observationCodeMap = new HashMap<>();
|
||||
private static final Map<String, Date> observationEffectiveMap = new HashMap<>();
|
||||
// todo mb make thise normal fields. This static setup wasn't working
|
||||
// todo mb make these normal fields. This static setup wasn't working
|
||||
protected static IIdType patient0Id = null;
|
||||
protected static IIdType patient1Id = null;
|
||||
protected static IIdType patient2Id = null;
|
||||
|
|
|
@ -21,6 +21,8 @@ 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.CompositeSearchParameterTestCases;
|
||||
import ca.uhn.fhir.jpa.search.QuantitySearchParameterTestCases;
|
||||
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
|
||||
|
@ -59,6 +61,7 @@ import org.hl7.fhir.r4.model.CodeableConcept;
|
|||
import org.hl7.fhir.r4.model.Coding;
|
||||
import org.hl7.fhir.r4.model.DateTimeType;
|
||||
import org.hl7.fhir.r4.model.DecimalType;
|
||||
import org.hl7.fhir.r4.model.DiagnosticReport;
|
||||
import org.hl7.fhir.r4.model.Encounter;
|
||||
import org.hl7.fhir.r4.model.Identifier;
|
||||
import org.hl7.fhir.r4.model.Meta;
|
||||
|
@ -132,7 +135,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||
@ExtendWith(SpringExtension.class)
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@RequiresDocker
|
||||
// wipmb hierarchy for context
|
||||
@ContextHierarchy({
|
||||
@ContextConfiguration(classes = TestR4ConfigWithElasticHSearch.class),
|
||||
@ContextConfiguration(classes = {
|
||||
|
@ -202,12 +204,16 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest impl
|
|||
@Autowired
|
||||
private DaoRegistry myDaoRegistry;
|
||||
@Autowired
|
||||
ITestDataBuilder myTestDataBuilder;
|
||||
ITestDataBuilder.WithSupport myTestDataBuilder;
|
||||
@Autowired
|
||||
TestDaoSearch myTestDaoSearch;
|
||||
@Autowired
|
||||
@Qualifier("myQuestionnaireDaoR4")
|
||||
private IFhirResourceDao<Questionnaire> myQuestionnaireDao;
|
||||
|
||||
@Autowired
|
||||
private IFhirResourceDao<DiagnosticReport> myDiagnosticReportDao;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("myQuestionnaireResponseDaoR4")
|
||||
private IFhirResourceDao<QuestionnaireResponse> myQuestionnaireResponseDao;
|
||||
|
@ -225,7 +231,6 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest impl
|
|||
public void beforePurgeDatabase() {
|
||||
purgeDatabase(myDaoConfig, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry, myBulkDataScheduleHelper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IIdType doCreateResource(IBaseResource theResource) {
|
||||
return myTestDataBuilder.doCreateResource(theResource);
|
||||
|
@ -1313,583 +1318,11 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest impl
|
|||
|
||||
}
|
||||
|
||||
|
||||
@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 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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@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 SpecTestCases {
|
||||
@Test
|
||||
void specCase1() {
|
||||
String id1 = withObservationWithValueQuantity(5.34).getIdPart();
|
||||
String id2 = withObservationWithValueQuantity(5.36).getIdPart();
|
||||
String id3 = withObservationWithValueQuantity(5.40).getIdPart();
|
||||
String id4 = withObservationWithValueQuantity(5.44).getIdPart();
|
||||
String id5 = withObservationWithValueQuantity(5.46).getIdPart();
|
||||
// GET [base]/Observation?value-quantity=5.4 :: 5.4(+/-0.05)
|
||||
assertFindIds("when le", Set.of(id2, id3, id4), "/Observation?value-quantity=5.4");
|
||||
}
|
||||
|
||||
@Test
|
||||
void specCase2() {
|
||||
String id1 = withObservationWithValueQuantity(0.005394).getIdPart();
|
||||
String id2 = withObservationWithValueQuantity(0.005395).getIdPart();
|
||||
String id3 = withObservationWithValueQuantity(0.0054).getIdPart();
|
||||
String id4 = withObservationWithValueQuantity(0.005404).getIdPart();
|
||||
String id5 = withObservationWithValueQuantity(0.005406).getIdPart();
|
||||
// GET [base]/Observation?value-quantity=5.40e-3 :: 0.0054(+/-0.000005)
|
||||
assertFindIds("when le", Set.of(id2, id3, id4), "/Observation?value-quantity=5.40e-3");
|
||||
}
|
||||
|
||||
@Test
|
||||
void specCase6() {
|
||||
String id1 = withObservationWithValueQuantity(4.85).getIdPart();
|
||||
String id2 = withObservationWithValueQuantity(4.86).getIdPart();
|
||||
String id3 = withObservationWithValueQuantity(5.94).getIdPart();
|
||||
String id4 = withObservationWithValueQuantity(5.95).getIdPart();
|
||||
// GET [base]/Observation?value-quantity=ap5.4 :: 5.4(+/- 10%) :: [4.86 ... 5.94]
|
||||
assertFindIds("when le", Set.of(id2, id3), "/Observation?value-quantity=ap5.4");
|
||||
}
|
||||
|
||||
}
|
||||
class QuantityAndNormalizedQuantitySearch extends QuantitySearchParameterTestCases {
|
||||
QuantityAndNormalizedQuantitySearch() {
|
||||
super(myTestDataBuilder.getTestDataBuilderSupport(), myTestDaoSearch, myDaoConfig);
|
||||
}
|
||||
|
||||
|
||||
@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");
|
||||
|
||||
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 TemperatureUnitConversions {
|
||||
|
||||
@Test
|
||||
public void storeCelsiusSearchFahrenheit() {
|
||||
withObservationWithQuantity(37.5, UCUM_CODESYSTEM_URL, "Cel" );
|
||||
|
||||
assertFind( "when eq UCUM 99.5 degF", "/Observation?value-quantity=99.5|" + UCUM_CODESYSTEM_URL + "|[degF]");
|
||||
assertNotFind( "when eq UCUM 101.1 degF", "/Observation?value-quantity=101.1|" + UCUM_CODESYSTEM_URL + "|[degF]");
|
||||
assertNotFind( "when eq UCUM 97.8 degF", "/Observation?value-quantity=97.8|" + UCUM_CODESYSTEM_URL + "|[degF]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void storeFahrenheitSearchCelsius() {
|
||||
withObservationWithQuantity(99.5, UCUM_CODESYSTEM_URL, "[degF]" );
|
||||
|
||||
assertFind( "when eq UCUM 37.5 Cel", "/Observation?value-quantity=37.5|" + UCUM_CODESYSTEM_URL + "|Cel");
|
||||
assertNotFind( "when eq UCUM 37.3 Cel", "/Observation?value-quantity=37.3|" + UCUM_CODESYSTEM_URL + "|Cel");
|
||||
assertNotFind( "when eq UCUM 37.7 Cel", "/Observation?value-quantity=37.7|" + UCUM_CODESYSTEM_URL + "|Cel");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@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(List.of(
|
||||
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 now implemented for normalized quantities
|
||||
*/
|
||||
@Nested
|
||||
public class Sorting {
|
||||
|
||||
@Test
|
||||
public void sortByNumeric() {
|
||||
String idAlpha1 = withObservationWithQuantity(0.06, UCUM_CODESYSTEM_URL, "10*6/L" ).getIdPart(); // 60,000
|
||||
String idAlpha2 = withObservationWithQuantity(50, UCUM_CODESYSTEM_URL, "10*3/L" ).getIdPart(); // 50,000
|
||||
String idAlpha3 = withObservationWithQuantity(0.000070, UCUM_CODESYSTEM_URL, "10*9/L" ).getIdPart(); // 70_000
|
||||
|
||||
// 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(idAlpha2, idAlpha1, idAlpha3));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
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(asArray(
|
||||
myTestDataBuilder.withQuantityAtPath("valueQuantity", theValue, theSystem, theCode)
|
||||
));
|
||||
return myResourceId;
|
||||
}
|
||||
|
||||
private IIdType withObservationWithValueQuantity(double theValue) {
|
||||
myResourceId = myTestDataBuilder.createObservation(List.of(myTestDataBuilder.withElementAt("valueQuantity",
|
||||
myTestDataBuilder.withPrimitiveAttribute("value", theValue),
|
||||
myTestDataBuilder.withPrimitiveAttribute("system", UCUM_CODESYSTEM_URL),
|
||||
myTestDataBuilder.withPrimitiveAttribute("code", "mm[Hg]")
|
||||
)));
|
||||
return myResourceId;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
|
@ -2815,8 +2248,18 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest impl
|
|||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class CompositeSearch extends CompositeSearchParameterTestCases {
|
||||
CompositeSearch() {
|
||||
super(myTestDataBuilder.getTestDataBuilderSupport(), myTestDaoSearch);
|
||||
}
|
||||
|
||||
|
||||
/** HSearch supports it! */
|
||||
@Override
|
||||
protected boolean isCorrelatedSupported() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disallow context dirtying for nested classes
|
||||
|
|
|
@ -48,7 +48,7 @@ import java.util.List;
|
|||
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.NESTED_SEARCH_PARAM_ROOT;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.QTY_CODE;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.QTY_PARAM_NAME;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.INDEX_TYPE_QUANTITY;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.QTY_SYSTEM;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.QTY_VALUE;
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
@ -472,7 +472,7 @@ public class HSearchSandboxTest extends BaseJpaTest {
|
|||
private void addQuantityOrClauses(BooleanPredicateClausesStep<?> theTopBool, boolean theIsMust,
|
||||
String theSearchParamName, IQueryParameterType theParamType) {
|
||||
|
||||
String fieldPath = NESTED_SEARCH_PARAM_ROOT + "." + theSearchParamName + "." + QTY_PARAM_NAME;
|
||||
String fieldPath = NESTED_SEARCH_PARAM_ROOT + "." + theSearchParamName + "." + INDEX_TYPE_QUANTITY;
|
||||
|
||||
QuantityParam qtyParam = QuantityParam.toQuantityParam(theParamType);
|
||||
ParamPrefixEnum activePrefix = qtyParam.getPrefix() == null ? ParamPrefixEnum.EQUAL : qtyParam.getPrefix();
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package ca.uhn.fhir.jpa.model.search;
|
||||
|
||||
public interface CompositeSearchIndexData {
|
||||
void writeIndexEntry(HSearchIndexWriter theHSearchIndexWriter, HSearchElementCache theRoot);
|
||||
}
|
|
@ -22,13 +22,13 @@ package ca.uhn.fhir.jpa.model.search;
|
|||
|
||||
import java.util.Date;
|
||||
|
||||
class DateSearchIndexData {
|
||||
public class DateSearchIndexData {
|
||||
private final Date myLowerBoundDate;
|
||||
private final int myLowerBoundOrdinal;
|
||||
private final Date myUpperBoundDate;
|
||||
private final int myUpperBoundOrdinal;
|
||||
|
||||
DateSearchIndexData(Date theLowerBoundDate, int theLowerBoundOrdinal, Date theUpperBoundDate, int theUpperBoundOrdinal) {
|
||||
public DateSearchIndexData(Date theLowerBoundDate, int theLowerBoundOrdinal, Date theUpperBoundDate, int theUpperBoundOrdinal) {
|
||||
myLowerBoundDate = theLowerBoundDate;
|
||||
myLowerBoundOrdinal = theLowerBoundOrdinal;
|
||||
myUpperBoundDate = theUpperBoundDate;
|
||||
|
|
|
@ -28,6 +28,7 @@ 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;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -53,12 +54,15 @@ public class ExtendedHSearchIndexData {
|
|||
final SetMultimap<String, String> mySearchParamUri = HashMultimap.create();
|
||||
final SetMultimap<String, DateSearchIndexData> mySearchParamDates = HashMultimap.create();
|
||||
final SetMultimap<String, QuantitySearchIndexData> mySearchParamQuantities = HashMultimap.create();
|
||||
final SetMultimap<String, CompositeSearchIndexData> mySearchParamComposites = HashMultimap.create();
|
||||
private String myForcedId;
|
||||
private String myResourceJSON;
|
||||
private IBaseResource myResource;
|
||||
|
||||
public ExtendedHSearchIndexData(FhirContext theFhirContext, ModelConfig theModelConfig) {
|
||||
public ExtendedHSearchIndexData(FhirContext theFhirContext, ModelConfig theModelConfig, IBaseResource theResource) {
|
||||
this.myFhirContext = theFhirContext;
|
||||
this.myModelConfig = theModelConfig;
|
||||
myResource = theResource;
|
||||
}
|
||||
|
||||
private <V> BiConsumer<String, V> ifNotContained(BiConsumer<String, V> theIndexWriter) {
|
||||
|
@ -79,7 +83,7 @@ public class ExtendedHSearchIndexData {
|
|||
* @param theDocument the Hibernate Search document for ResourceTable
|
||||
*/
|
||||
public void writeIndexElements(DocumentElement theDocument) {
|
||||
HSearchIndexWriter indexWriter = HSearchIndexWriter.forRoot(myFhirContext, myModelConfig, theDocument);
|
||||
HSearchIndexWriter indexWriter = HSearchIndexWriter.forRoot(myModelConfig, theDocument);
|
||||
|
||||
ourLog.debug("Writing JPA index to Hibernate Search");
|
||||
|
||||
|
@ -94,11 +98,11 @@ public class ExtendedHSearchIndexData {
|
|||
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));
|
||||
mySearchParamQuantities.forEach(ifNotContained(indexWriter::writeQuantityIndex));
|
||||
Multimaps.asMap(mySearchParamNumbers).forEach(ifNotContained(indexWriter::writeNumberIndex));
|
||||
// TODO MB Use RestSearchParameterTypeEnum to define templates.
|
||||
mySearchParamDates.forEach(ifNotContained(indexWriter::writeDateIndex));
|
||||
Multimaps.asMap(mySearchParamUri).forEach(ifNotContained(indexWriter::writeUriIndex));
|
||||
Multimaps.asMap(mySearchParamComposites).forEach(indexWriter::writeCompositeIndex);
|
||||
}
|
||||
|
||||
public void addStringIndexData(String theSpName, String theText) {
|
||||
|
@ -109,6 +113,7 @@ public class ExtendedHSearchIndexData {
|
|||
* Add if not already present.
|
||||
*/
|
||||
public void addTokenIndexDataIfNotPresent(String theSpName, String theSystem, String theValue) {
|
||||
// todo MB create a BaseCodingDt that respects equals
|
||||
boolean isPresent = mySearchParamTokens.get(theSpName).stream()
|
||||
.anyMatch(c -> Objects.equals(c.getSystem(), theSystem) && Objects.equals(c.getCode(), theValue));
|
||||
if (!isPresent) {
|
||||
|
@ -129,15 +134,19 @@ public class ExtendedHSearchIndexData {
|
|||
}
|
||||
|
||||
public void addDateIndexData(String theSpName, Date theLowerBound, int theLowerBoundOrdinal, Date theUpperBound, int theUpperBoundOrdinal) {
|
||||
mySearchParamDates.put(theSpName, new DateSearchIndexData(theLowerBound, theLowerBoundOrdinal, theUpperBound, theUpperBoundOrdinal));
|
||||
addDateIndexData(theSpName, new DateSearchIndexData(theLowerBound, theLowerBoundOrdinal, theUpperBound, theUpperBoundOrdinal));
|
||||
}
|
||||
|
||||
public void addDateIndexData(String theSpName, DateSearchIndexData value) {
|
||||
mySearchParamDates.put(theSpName, value);
|
||||
}
|
||||
|
||||
public void addNumberIndexDataIfNotPresent(String theParamName, BigDecimal theValue) {
|
||||
mySearchParamNumbers.put(theParamName, theValue);
|
||||
}
|
||||
|
||||
public void addQuantityIndexData(String theSpName, String theUnits, String theSystem, double theValue) {
|
||||
mySearchParamQuantities.put(theSpName, new QuantitySearchIndexData(theUnits, theSystem, theValue));
|
||||
public void addQuantityIndexData(String theSpName, QuantitySearchIndexData value) {
|
||||
mySearchParamQuantities.put(theSpName, value);
|
||||
}
|
||||
|
||||
public void setForcedId(String theForcedId) {
|
||||
|
@ -148,7 +157,15 @@ public class ExtendedHSearchIndexData {
|
|||
return myForcedId;
|
||||
}
|
||||
|
||||
public void setRawResourceData(String theResourceJSON) {
|
||||
public void setRawResourceData(String theResourceJSON) {
|
||||
myResourceJSON = theResourceJSON;
|
||||
}
|
||||
|
||||
public SetMultimap<String, CompositeSearchIndexData> getSearchParamComposites() {
|
||||
return mySearchParamComposites;
|
||||
}
|
||||
|
||||
public void addCompositeIndexData(String theSearchParamName, CompositeSearchIndexData theBuildCompositeIndexData) {
|
||||
mySearchParamComposites.put(theSearchParamName, theBuildCompositeIndexData);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ package ca.uhn.fhir.jpa.model.search;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
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;
|
||||
|
@ -32,20 +31,30 @@ import org.slf4j.LoggerFactory;
|
|||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
|
||||
public class HSearchIndexWriter {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(HSearchIndexWriter.class);
|
||||
|
||||
public static final String NESTED_SEARCH_PARAM_ROOT = "nsp";
|
||||
public static final String SEARCH_PARAM_ROOT = "sp";
|
||||
public static final String INDEX_TYPE_STRING = "string";
|
||||
public static final String IDX_STRING_NORMALIZED = "norm";
|
||||
public static final String IDX_STRING_EXACT = "exact";
|
||||
public static final String IDX_STRING_TEXT = "text";
|
||||
public static final String IDX_STRING_LOWER = "lower";
|
||||
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 INDEX_TYPE_TOKEN = "token";
|
||||
public static final String TOKEN_CODE = "code";
|
||||
public static final String TOKEN_SYSTEM = "system";
|
||||
public static final String TOKEN_SYSTEM_CODE = "code-system";
|
||||
public static final String INDEX_TYPE_QUANTITY = "quantity";
|
||||
|
||||
// numeric
|
||||
public static final String VALUE_FIELD = "value";
|
||||
public static final String QTY_CODE = TOKEN_CODE;
|
||||
public static final String QTY_SYSTEM = TOKEN_SYSTEM;
|
||||
public static final String QTY_VALUE = VALUE_FIELD;
|
||||
public static final String QTY_CODE_NORM = "code-norm";
|
||||
public static final String QTY_VALUE_NORM = "value-norm";
|
||||
|
||||
|
@ -54,13 +63,16 @@ public class HSearchIndexWriter {
|
|||
public static final String NUMBER_VALUE = "number-value";
|
||||
|
||||
|
||||
public static final String DATE_LOWER_ORD = "lower-ord";
|
||||
public static final String DATE_LOWER = "lower";
|
||||
public static final String DATE_UPPER_ORD = "upper-ord";
|
||||
public static final String DATE_UPPER = "upper";
|
||||
|
||||
|
||||
final HSearchElementCache myNodeCache;
|
||||
final FhirContext myFhirContext;
|
||||
final ModelConfig myModelConfig;
|
||||
|
||||
HSearchIndexWriter(FhirContext theFhirContext, ModelConfig theModelConfig, DocumentElement theRoot) {
|
||||
myFhirContext = theFhirContext;
|
||||
HSearchIndexWriter(ModelConfig theModelConfig, DocumentElement theRoot) {
|
||||
myModelConfig = theModelConfig;
|
||||
myNodeCache = new HSearchElementCache(theRoot);
|
||||
}
|
||||
|
@ -69,94 +81,112 @@ public class HSearchIndexWriter {
|
|||
return myNodeCache.getObjectElement(SEARCH_PARAM_ROOT, theSearchParamName, theIndexType);
|
||||
}
|
||||
|
||||
public static HSearchIndexWriter forRoot(
|
||||
FhirContext theFhirContext, ModelConfig theModelConfig, DocumentElement theDocument) {
|
||||
return new HSearchIndexWriter(theFhirContext, theModelConfig, theDocument);
|
||||
public static HSearchIndexWriter forRoot(ModelConfig theModelConfig, DocumentElement theDocument) {
|
||||
return new HSearchIndexWriter(theModelConfig, theDocument);
|
||||
}
|
||||
|
||||
public void writeStringIndex(String theSearchParam, String theValue) {
|
||||
DocumentElement stringIndexNode = getSearchParamIndexNode(theSearchParam, "string");
|
||||
DocumentElement stringIndexNode = getSearchParamIndexNode(theSearchParam, INDEX_TYPE_STRING);
|
||||
|
||||
// we are assuming that our analyzer matches StringUtil.normalizeStringForSearchIndexing(theValue).toLowerCase(Locale.ROOT))
|
||||
stringIndexNode.addValue(IDX_STRING_NORMALIZED, theValue);// for default search
|
||||
stringIndexNode.addValue(IDX_STRING_EXACT, theValue);
|
||||
stringIndexNode.addValue(IDX_STRING_TEXT, theValue);
|
||||
stringIndexNode.addValue(IDX_STRING_LOWER, theValue);
|
||||
writeBasicStringFields(stringIndexNode, theValue);
|
||||
addDocumentValue(stringIndexNode, IDX_STRING_EXACT, theValue);
|
||||
addDocumentValue(stringIndexNode, IDX_STRING_TEXT, theValue);
|
||||
addDocumentValue(stringIndexNode, IDX_STRING_LOWER, theValue);
|
||||
|
||||
ourLog.debug("Adding Search Param Text: {} -- {}", theSearchParam, theValue);
|
||||
}
|
||||
|
||||
public void writeBasicStringFields(DocumentElement theIndexNode, String theValue) {
|
||||
addDocumentValue(theIndexNode, IDX_STRING_NORMALIZED, theValue);
|
||||
}
|
||||
|
||||
public void writeTokenIndex(String theSearchParam, IBaseCoding theValue) {
|
||||
DocumentElement nestedRoot = myNodeCache.getObjectElement(NESTED_SEARCH_PARAM_ROOT);
|
||||
DocumentElement nestedSpNode = nestedRoot.addObject(theSearchParam);
|
||||
DocumentElement nestedTokenNode = nestedSpNode.addObject("token");
|
||||
DocumentElement nestedTokenNode = nestedSpNode.addObject(INDEX_TYPE_TOKEN);
|
||||
|
||||
nestedTokenNode.addValue("code", theValue.getCode());
|
||||
nestedTokenNode.addValue("system", theValue.getSystem());
|
||||
nestedTokenNode.addValue("code-system", theValue.getSystem() + "|" + theValue.getCode());
|
||||
writeTokenFields(nestedTokenNode, theValue);
|
||||
|
||||
if (StringUtils.isNotEmpty(theValue.getDisplay())) {
|
||||
DocumentElement nestedStringNode = nestedSpNode.addObject("string");
|
||||
nestedStringNode.addValue(IDX_STRING_TEXT, theValue.getDisplay());
|
||||
DocumentElement nestedStringNode = nestedSpNode.addObject(INDEX_TYPE_STRING);
|
||||
addDocumentValue(nestedStringNode, IDX_STRING_TEXT, theValue.getDisplay());
|
||||
}
|
||||
|
||||
DocumentElement tokenIndexNode = getSearchParamIndexNode(theSearchParam, "token");
|
||||
DocumentElement tokenIndexNode = getSearchParamIndexNode(theSearchParam, INDEX_TYPE_TOKEN);
|
||||
// TODO mb we can use a token_filter with pattern_capture to generate all three off a single value. Do this next, after merge.
|
||||
tokenIndexNode.addValue("code", theValue.getCode());
|
||||
tokenIndexNode.addValue("system", theValue.getSystem());
|
||||
tokenIndexNode.addValue("code-system", theValue.getSystem() + "|" + theValue.getCode());
|
||||
writeTokenFields(tokenIndexNode, theValue);
|
||||
ourLog.debug("Adding Search Param Token: {} -- {}", theSearchParam, theValue);
|
||||
// TODO mb should we write the strings here too? Or leave it to the old spidx indexing?
|
||||
}
|
||||
|
||||
public void writeTokenFields(DocumentElement theDocumentElement, IBaseCoding theValue) {
|
||||
addDocumentValue(theDocumentElement, TOKEN_CODE, theValue.getCode());
|
||||
addDocumentValue(theDocumentElement, TOKEN_SYSTEM, theValue.getSystem());
|
||||
addDocumentValue(theDocumentElement, TOKEN_SYSTEM_CODE, theValue.getSystem() + "|" + theValue.getCode());
|
||||
}
|
||||
|
||||
private void addDocumentValue(DocumentElement theDocumentElement, String theKey, Object theValue) {
|
||||
if (theValue != null) {
|
||||
theDocumentElement.addValue(theKey, theValue);
|
||||
}
|
||||
}
|
||||
|
||||
public void writeReferenceIndex(String theSearchParam, String theValue) {
|
||||
DocumentElement referenceIndexNode = getSearchParamIndexNode(theSearchParam, "reference");
|
||||
referenceIndexNode.addValue("value", theValue);
|
||||
addDocumentValue(referenceIndexNode, VALUE_FIELD, theValue);
|
||||
ourLog.trace("Adding Search Param Reference: {} -- {}", theSearchParam, theValue);
|
||||
}
|
||||
|
||||
public void writeDateIndex(String theSearchParam, DateSearchIndexData theValue) {
|
||||
DocumentElement dateIndexNode = getSearchParamIndexNode(theSearchParam, "dt");
|
||||
// Lower bound
|
||||
dateIndexNode.addValue("lower-ord", theValue.getLowerBoundOrdinal());
|
||||
dateIndexNode.addValue("lower", theValue.getLowerBoundDate().toInstant());
|
||||
// Upper bound
|
||||
dateIndexNode.addValue("upper-ord", theValue.getUpperBoundOrdinal());
|
||||
dateIndexNode.addValue("upper", theValue.getUpperBoundDate().toInstant());
|
||||
writeDateFields(dateIndexNode, theValue);
|
||||
|
||||
ourLog.trace("Adding Search Param Date. param: {} -- {}", theSearchParam, theValue);
|
||||
}
|
||||
|
||||
public void writeDateFields(DocumentElement dateIndexNode, DateSearchIndexData theValue) {
|
||||
// Lower bound
|
||||
addDocumentValue(dateIndexNode, DATE_LOWER_ORD, theValue.getLowerBoundOrdinal());
|
||||
addDocumentValue(dateIndexNode, DATE_LOWER, theValue.getLowerBoundDate().toInstant());
|
||||
// Upper bound
|
||||
addDocumentValue(dateIndexNode, DATE_UPPER_ORD, theValue.getUpperBoundOrdinal());
|
||||
addDocumentValue(dateIndexNode, DATE_UPPER, theValue.getUpperBoundDate().toInstant());
|
||||
}
|
||||
|
||||
|
||||
public void writeQuantityIndex(String theSearchParam, Collection<QuantitySearchIndexData> theValueCollection) {
|
||||
public void writeQuantityIndex(String theSearchParam, QuantitySearchIndexData theValue) {
|
||||
DocumentElement nestedRoot = myNodeCache.getObjectElement(NESTED_SEARCH_PARAM_ROOT);
|
||||
|
||||
for (QuantitySearchIndexData theValue : theValueCollection) {
|
||||
DocumentElement nestedSpNode = nestedRoot.addObject(theSearchParam);
|
||||
DocumentElement nestedQtyNode = nestedSpNode.addObject(QTY_PARAM_NAME);
|
||||
DocumentElement nestedSpNode = nestedRoot.addObject(theSearchParam);
|
||||
DocumentElement nestedQtyNode = nestedSpNode.addObject(INDEX_TYPE_QUANTITY);
|
||||
|
||||
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());
|
||||
ourLog.trace("Adding Search Param Quantity: {} -- {}", theSearchParam, theValue);
|
||||
writeQuantityFields(nestedQtyNode, theValue);
|
||||
|
||||
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) { continue; }
|
||||
public void writeQuantityFields(DocumentElement nestedQtyNode, QuantitySearchIndexData theValue) {
|
||||
addDocumentValue(nestedQtyNode, QTY_CODE, theValue.getCode());
|
||||
addDocumentValue(nestedQtyNode, QTY_SYSTEM, theValue.getSystem());
|
||||
addDocumentValue(nestedQtyNode, QTY_VALUE, theValue.getValue());
|
||||
|
||||
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);
|
||||
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();
|
||||
|
||||
addDocumentValue(nestedQtyNode, QTY_CODE_NORM, canonicalUnits);
|
||||
addDocumentValue(nestedQtyNode, QTY_VALUE_NORM, canonicalValue);
|
||||
}
|
||||
|
||||
|
||||
|
@ -164,17 +194,35 @@ public class HSearchIndexWriter {
|
|||
DocumentElement uriNode = myNodeCache.getObjectElement(SEARCH_PARAM_ROOT).addObject(theParamName);
|
||||
for (String uriSearchIndexValue : theUriValueCollection) {
|
||||
ourLog.trace("Adding Search Param Uri: {} -- {}", theParamName, uriSearchIndexValue);
|
||||
uriNode.addValue(URI_VALUE, uriSearchIndexValue);
|
||||
writeUriFields(uriNode, uriSearchIndexValue);
|
||||
}
|
||||
}
|
||||
|
||||
public void writeUriFields(DocumentElement uriNode, String uriSearchIndexValue) {
|
||||
addDocumentValue(uriNode, URI_VALUE, uriSearchIndexValue);
|
||||
}
|
||||
|
||||
public void writeNumberIndex(String theParamName, Collection<BigDecimal> theNumberValueCollection) {
|
||||
DocumentElement numberNode = myNodeCache.getObjectElement(SEARCH_PARAM_ROOT).addObject(theParamName);
|
||||
for (BigDecimal numberSearchIndexValue : theNumberValueCollection) {
|
||||
ourLog.trace("Adding Search Param Number: {} -- {}", theParamName, numberSearchIndexValue);
|
||||
numberNode.addValue(NUMBER_VALUE, numberSearchIndexValue.doubleValue());
|
||||
writeNumberFields(numberNode, numberSearchIndexValue);
|
||||
}
|
||||
}
|
||||
|
||||
public void writeNumberFields(DocumentElement numberNode, BigDecimal numberSearchIndexValue) {
|
||||
addDocumentValue(numberNode, NUMBER_VALUE, numberSearchIndexValue.doubleValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ignoredParamName unused - for consistent api
|
||||
* @param theCompositeSearchIndexData extracted index data for this sp
|
||||
*/
|
||||
public void writeCompositeIndex(String ignoredParamName, Set<CompositeSearchIndexData> theCompositeSearchIndexData) {
|
||||
// must be nested.
|
||||
for (CompositeSearchIndexData compositeSearchIndexDatum : theCompositeSearchIndexData) {
|
||||
compositeSearchIndexDatum.writeIndexEntry(this, myNodeCache);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,9 +20,7 @@ package ca.uhn.fhir.jpa.model.search;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
class QuantitySearchIndexData {
|
||||
public class QuantitySearchIndexData {
|
||||
|
||||
// unit is also referred as code
|
||||
private final String myCode;
|
||||
|
@ -30,7 +28,7 @@ class QuantitySearchIndexData {
|
|||
private final double myValue;
|
||||
|
||||
|
||||
QuantitySearchIndexData(String theCode, String theSystem, double theValue) {
|
||||
public QuantitySearchIndexData(String theCode, String theSystem, double theValue) {
|
||||
myCode = theCode;
|
||||
mySystem = theSystem;
|
||||
myValue = theValue;
|
||||
|
|
|
@ -52,6 +52,7 @@ import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.QTY_VALUE;
|
|||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.QTY_VALUE_NORM;
|
||||
import static ca.uhn.fhir.jpa.model.search.HSearchIndexWriter.URI_VALUE;
|
||||
|
||||
|
||||
/**
|
||||
* Allows hibernate search to index
|
||||
* <p>
|
||||
|
@ -165,6 +166,7 @@ public class SearchParamTextPropertyBinder implements PropertyBinder, PropertyBr
|
|||
spfield.fieldTemplate("string-lower", lowerCaseNormalizer).matchingPathGlob(stringPathGlob + "." + IDX_STRING_LOWER).multiValued();
|
||||
|
||||
nestedSpField.objectFieldTemplate("nestedStringIndex", ObjectStructure.FLATTENED).matchingPathGlob(stringPathGlob);
|
||||
nestedSpField.fieldTemplate("string-norm", normStringAnalyzer).matchingPathGlob(stringPathGlob + "." + IDX_STRING_NORMALIZED).multiValued();
|
||||
nestedSpField.fieldTemplate("string-text", standardAnalyzer).matchingPathGlob(stringPathGlob + "." + IDX_STRING_TEXT).multiValued();
|
||||
|
||||
// token
|
||||
|
@ -189,9 +191,11 @@ public class SearchParamTextPropertyBinder implements PropertyBinder, PropertyBr
|
|||
|
||||
// uri
|
||||
spfield.fieldTemplate("uriValueTemplate", keywordFieldType).matchingPathGlob("*." + URI_VALUE).multiValued();
|
||||
nestedSpField.fieldTemplate("uriValueTemplate", keywordFieldType).matchingPathGlob("*." + URI_VALUE).multiValued();
|
||||
|
||||
// number
|
||||
spfield.fieldTemplate("numberValueTemplate", bigDecimalFieldType).matchingPathGlob("*." + NUMBER_VALUE);
|
||||
nestedSpField.fieldTemplate("numberValueTemplate", bigDecimalFieldType).matchingPathGlob("*." + NUMBER_VALUE);
|
||||
|
||||
//quantity
|
||||
String quantityPathGlob = "*.quantity";
|
||||
|
@ -205,15 +209,22 @@ public class SearchParamTextPropertyBinder implements PropertyBinder, PropertyBr
|
|||
// date
|
||||
String dateTimePathGlob = "*.dt";
|
||||
spfield.objectFieldTemplate("datetimeIndex", ObjectStructure.FLATTENED).matchingPathGlob(dateTimePathGlob);
|
||||
spfield.fieldTemplate("datetime-lower-ordinal", dateTimeOrdinalFieldType).matchingPathGlob(dateTimePathGlob + ".lower-ord");
|
||||
spfield.fieldTemplate("datetime-lower-value", dateTimeFieldType).matchingPathGlob(dateTimePathGlob + ".lower");
|
||||
spfield.fieldTemplate("datetime-upper-ordinal", dateTimeOrdinalFieldType).matchingPathGlob(dateTimePathGlob + ".upper-ord");
|
||||
spfield.fieldTemplate("datetime-upper-value", dateTimeFieldType).matchingPathGlob(dateTimePathGlob + ".upper");
|
||||
spfield.fieldTemplate("datetime-lower-ordinal", dateTimeOrdinalFieldType).matchingPathGlob(dateTimePathGlob + ".lower-ord").multiValued();
|
||||
spfield.fieldTemplate("datetime-lower-value", dateTimeFieldType).matchingPathGlob(dateTimePathGlob + ".lower").multiValued();
|
||||
spfield.fieldTemplate("datetime-upper-ordinal", dateTimeOrdinalFieldType).matchingPathGlob(dateTimePathGlob + ".upper-ord").multiValued();
|
||||
spfield.fieldTemplate("datetime-upper-value", dateTimeFieldType).matchingPathGlob(dateTimePathGlob + ".upper").multiValued();
|
||||
|
||||
nestedSpField.objectFieldTemplate("nestedDatetimeIndex", ObjectStructure.FLATTENED).matchingPathGlob(dateTimePathGlob);
|
||||
nestedSpField.fieldTemplate("datetime-lower-ordinal", dateTimeOrdinalFieldType).matchingPathGlob(dateTimePathGlob + ".lower-ord").multiValued();
|
||||
nestedSpField.fieldTemplate("datetime-lower-value", dateTimeFieldType).matchingPathGlob(dateTimePathGlob + ".lower").multiValued();
|
||||
nestedSpField.fieldTemplate("datetime-upper-ordinal", dateTimeOrdinalFieldType).matchingPathGlob(dateTimePathGlob + ".upper-ord").multiValued();
|
||||
nestedSpField.fieldTemplate("datetime-upper-value", dateTimeFieldType).matchingPathGlob(dateTimePathGlob + ".upper").multiValued();
|
||||
|
||||
// last, since the globs are matched in declaration order, and * matches even nested nodes.
|
||||
spfield.objectFieldTemplate("spObject", ObjectStructure.FLATTENED).matchingPathGlob("*");
|
||||
|
||||
// we use nested search params for the autocomplete search.
|
||||
nestedSpField.objectFieldTemplate("nestedSpSubObject", ObjectStructure.FLATTENED).matchingPathGlob("*.*").multiValued();
|
||||
nestedSpField.objectFieldTemplate("nestedSpObject", ObjectStructure.NESTED).matchingPathGlob("*").multiValued();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,6 +92,15 @@
|
|||
<artifactId>hapi-fhir-validation-resources-r5</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-test-utilities</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.annotation</groupId>
|
||||
<artifactId>javax.annotation-api</artifactId>
|
||||
|
|
|
@ -30,6 +30,7 @@ import ca.uhn.fhir.context.RuntimeSearchParam;
|
|||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
|
||||
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParamQuantity;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamCoords;
|
||||
|
@ -87,6 +88,8 @@ import java.util.Set;
|
|||
import java.util.TreeSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum.DATE;
|
||||
import static ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum.REFERENCE;
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
import static org.apache.commons.lang3.StringUtils.trim;
|
||||
|
@ -192,6 +195,18 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
|
||||
@Override
|
||||
public List<String> extractParamValuesAsStrings(RuntimeSearchParam theSearchParam, IBaseResource theResource) {
|
||||
IExtractor extractor = createExtractor(theSearchParam, theResource);
|
||||
|
||||
if (theSearchParam.getParamType().equals(REFERENCE)) {
|
||||
return extractReferenceParamsAsQueryTokens(theSearchParam, theResource, extractor);
|
||||
} else {
|
||||
return extractParamsAsQueryTokens(theSearchParam, theResource, extractor);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private IExtractor createExtractor(RuntimeSearchParam theSearchParam, IBaseResource theResource) {
|
||||
IExtractor extractor;
|
||||
switch (theSearchParam.getParamType()) {
|
||||
case DATE:
|
||||
|
@ -208,16 +223,9 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
break;
|
||||
case REFERENCE:
|
||||
extractor = createReferenceExtractor();
|
||||
return extractReferenceParamsAsQueryTokens(theSearchParam, theResource, extractor);
|
||||
break;
|
||||
case QUANTITY:
|
||||
if (myModelConfig.getNormalizedQuantitySearchLevel().equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED)) {
|
||||
extractor = new CompositeExtractor(
|
||||
createQuantityExtractor(theResource),
|
||||
createQuantityNormalizedExtractor(theResource)
|
||||
);
|
||||
} else {
|
||||
extractor = createQuantityExtractor(theResource);
|
||||
}
|
||||
extractor = createQuantityExtractor(theResource);
|
||||
break;
|
||||
case URI:
|
||||
extractor = createUriExtractor(theResource);
|
||||
|
@ -229,8 +237,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
default:
|
||||
throw new UnsupportedOperationException(Msg.code(503) + "Type " + theSearchParam.getParamType() + " not supported for extraction");
|
||||
}
|
||||
|
||||
return extractParamsAsQueryTokens(theSearchParam, theResource, extractor);
|
||||
return extractor;
|
||||
}
|
||||
|
||||
private List<String> extractReferenceParamsAsQueryTokens(RuntimeSearchParam theSearchParam, IBaseResource theResource, IExtractor<PathAndRef> theExtractor) {
|
||||
|
@ -258,6 +265,126 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchParamSet<ResourceIndexedSearchParamComposite> extractSearchParamComposites(IBaseResource theResource) {
|
||||
IExtractor<ResourceIndexedSearchParamComposite> extractor = createCompositeExtractor(theResource);
|
||||
return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.COMPOSITE, false);
|
||||
|
||||
}
|
||||
|
||||
private IExtractor<ResourceIndexedSearchParamComposite> createCompositeExtractor(IBaseResource theResource) {
|
||||
return new CompositeExtractor(theResource);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Extractor for composite SPs.
|
||||
* Extracts elements, and then recurses and applies extractors for each component SP using the element as the new root.
|
||||
*/
|
||||
public class CompositeExtractor implements IExtractor<ResourceIndexedSearchParamComposite> {
|
||||
final IBaseResource myResource;
|
||||
final String myResourceType;
|
||||
|
||||
public CompositeExtractor(IBaseResource theResource) {
|
||||
myResource = theResource;
|
||||
myResourceType = toRootTypeName(theResource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the subcomponent index data for each component of a composite SP from an IBase element.
|
||||
*
|
||||
* @param theParams will add 1 or 0 ResourceIndexedSearchParamComposite instances for theValue
|
||||
* @param theCompositeSearchParam the composite SP
|
||||
* @param theValue the focus element for the subcomponent extraction
|
||||
* @param thePath unused api param
|
||||
* @param theWantLocalReferences passed down to reference extraction
|
||||
*/
|
||||
@Override
|
||||
public void extract(SearchParamSet<ResourceIndexedSearchParamComposite> theParams, RuntimeSearchParam theCompositeSearchParam, IBase theValue, String thePath, boolean theWantLocalReferences) {
|
||||
|
||||
// skip broken SPs
|
||||
if (!isExtractableComposite(theCompositeSearchParam)) {
|
||||
ourLog.info("CompositeExtractor - skipping unsupported search parameter {}", theCompositeSearchParam.getName());
|
||||
return;
|
||||
}
|
||||
|
||||
String compositeSpName = theCompositeSearchParam.getName();
|
||||
ourLog.trace("CompositeExtractor - extracting {} {}", compositeSpName, theValue);
|
||||
ResourceIndexedSearchParamComposite e = new ResourceIndexedSearchParamComposite(compositeSpName, thePath);
|
||||
|
||||
// extract the index data for each component.
|
||||
for (RuntimeSearchParam.Component component : theCompositeSearchParam.getComponents()) {
|
||||
String componentSpRef = component.getReference();
|
||||
String expression = component.getExpression();
|
||||
|
||||
RuntimeSearchParam componentSp = mySearchParamRegistry.getActiveSearchParamByUrl(componentSpRef);
|
||||
Validate.notNull(componentSp, "Misconfigured SP %s - failed to load component %s", compositeSpName, componentSpRef);
|
||||
|
||||
SearchParamSet<BaseResourceIndexedSearchParam> componentIndexedSearchParams = extractCompositeComponentIndexData(theValue, componentSp, expression, theWantLocalReferences, theCompositeSearchParam);
|
||||
if (componentIndexedSearchParams.isEmpty()) {
|
||||
// If any of the components are empty, no search can ever match. Short circuit, and bail out.
|
||||
return;
|
||||
} else {
|
||||
e.addComponentIndexedSearchParams(componentSp, componentIndexedSearchParams);
|
||||
}
|
||||
}
|
||||
|
||||
// every component has data. Add it for indexing.
|
||||
theParams.add(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the subcomponent index data for a single component of a composite SP.
|
||||
*
|
||||
* @param theFocusElement the element to use as the root for sub-extraction
|
||||
* @param theComponentSearchParam the active subcomponent SP for extraction
|
||||
* @param theSubPathExpression the sub-expression to extract values from theFocusElement
|
||||
* @param theWantLocalReferences flag for URI processing
|
||||
* @param theCompositeSearchParam the parent composite SP
|
||||
* @return the extracted index beans for theFocusElement
|
||||
*/
|
||||
@Nonnull
|
||||
private SearchParamSet<BaseResourceIndexedSearchParam> extractCompositeComponentIndexData(IBase theFocusElement, RuntimeSearchParam theComponentSearchParam, String theSubPathExpression, boolean theWantLocalReferences, RuntimeSearchParam theCompositeSearchParam) {
|
||||
IExtractor componentExtractor = createExtractor(theComponentSearchParam, myResource);
|
||||
SearchParamSet<BaseResourceIndexedSearchParam> componentIndexData = new SearchParamSet<>();
|
||||
|
||||
extractSearchParam(theComponentSearchParam, theSubPathExpression, theFocusElement, componentExtractor, componentIndexData, theWantLocalReferences);
|
||||
ourLog.trace("CompositeExtractor - extracted {} index values for {}", componentIndexData.size(), theComponentSearchParam.getName());
|
||||
|
||||
return componentIndexData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this an extractable composite SP?
|
||||
*
|
||||
* @param theSearchParam of type composite
|
||||
* @return can we extract useful index data from this?
|
||||
*/
|
||||
private boolean isExtractableComposite(RuntimeSearchParam theSearchParam) {
|
||||
// this is a composite SP
|
||||
return RestSearchParameterTypeEnum.COMPOSITE.equals(theSearchParam.getParamType()) &&
|
||||
theSearchParam.getComponents().stream()
|
||||
.noneMatch(this::isNotExtractableCompositeComponent);
|
||||
}
|
||||
|
||||
private boolean isNotExtractableCompositeComponent(RuntimeSearchParam.Component c) {
|
||||
RuntimeSearchParam componentSearchParam = mySearchParamRegistry.getActiveSearchParamByUrl(c.getReference());
|
||||
return // Does the sub-param link work?
|
||||
componentSearchParam == null ||
|
||||
// Is this the right type?
|
||||
RestSearchParameterTypeEnum.COMPOSITE.equals(componentSearchParam.getParamType()) ||
|
||||
|
||||
// Bug workaround: the component expressions are null in the FhirContextSearchParamRegistry. We can't do anything with them.
|
||||
c.getExpression() == null ||
|
||||
|
||||
// wipmb hack hack alert:
|
||||
// Bug workaround: we don't support the %resource variable, but standard SPs on MolecularSequence use it.
|
||||
// Skip them for now.
|
||||
c.getExpression().contains("%resource");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamTokens(IBaseResource theResource) {
|
||||
IExtractor<BaseResourceIndexedSearchParam> extractor = createTokenExtractor(theResource);
|
||||
|
@ -340,7 +467,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
@Override
|
||||
public SearchParamSet<ResourceIndexedSearchParamDate> extractSearchParamDates(IBaseResource theResource) {
|
||||
IExtractor<ResourceIndexedSearchParamDate> extractor = createDateExtractor(theResource);
|
||||
return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.DATE, false);
|
||||
return extractSearchParams(theResource, extractor, DATE, false);
|
||||
}
|
||||
|
||||
private IExtractor<ResourceIndexedSearchParamDate> createDateExtractor(IBaseResource theResource) {
|
||||
|
@ -387,7 +514,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
|
||||
@Override
|
||||
public SearchParamSet<ResourceIndexedSearchParamQuantity> extractSearchParamQuantity(IBaseResource theResource) {
|
||||
IExtractor<ResourceIndexedSearchParamQuantity> extractor = createQuantityExtractor(theResource);
|
||||
IExtractor<ResourceIndexedSearchParamQuantity> extractor = createQuantityUnnormalizedExtractor(theResource);
|
||||
return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.QUANTITY, false);
|
||||
}
|
||||
|
||||
|
@ -398,14 +525,29 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
return extractSearchParams(theResource, extractor, RestSearchParameterTypeEnum.QUANTITY, false);
|
||||
}
|
||||
|
||||
private IExtractor<ResourceIndexedSearchParamQuantity> createQuantityExtractor(IBaseResource theResource) {
|
||||
@Nonnull
|
||||
private IExtractor<? extends BaseResourceIndexedSearchParamQuantity> createQuantityExtractor(IBaseResource theResource) {
|
||||
IExtractor<? extends BaseResourceIndexedSearchParamQuantity> result;
|
||||
if (myModelConfig.getNormalizedQuantitySearchLevel().storageOrSearchSupported()) {
|
||||
result = new MultiplexExtractor(
|
||||
createQuantityUnnormalizedExtractor(theResource),
|
||||
createQuantityNormalizedExtractor(theResource)
|
||||
);
|
||||
} else {
|
||||
result = createQuantityUnnormalizedExtractor(theResource);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private IExtractor<ResourceIndexedSearchParamQuantity> createQuantityUnnormalizedExtractor(IBaseResource theResource) {
|
||||
String resourceType = toRootTypeName(theResource);
|
||||
return (params, searchParam, value, path, theWantLocalReferences) -> {
|
||||
if (value.getClass().equals(myLocationPositionDefinition.getImplementingClass())) {
|
||||
return;
|
||||
}
|
||||
|
||||
String nextType = toRootTypeName(value);
|
||||
String resourceType = toRootTypeName(theResource);
|
||||
switch (nextType) {
|
||||
case "Quantity":
|
||||
addQuantity_Quantity(resourceType, params, searchParam, value);
|
||||
|
@ -457,8 +599,8 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
}
|
||||
|
||||
private IExtractor<ResourceIndexedSearchParamString> createStringExtractor(IBaseResource theResource) {
|
||||
String resourceType = toRootTypeName(theResource);
|
||||
return (params, searchParam, value, path, theWantLocalReferences) -> {
|
||||
String resourceType = toRootTypeName(theResource);
|
||||
|
||||
if (value instanceof IPrimitiveType) {
|
||||
IPrimitiveType<?> nextValue = (IPrimitiveType<?>) value;
|
||||
|
@ -495,7 +637,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
* Override parent because we're using FHIRPath here
|
||||
*/
|
||||
@Override
|
||||
public List<IBase> extractValues(String thePaths, IBaseResource theResource) {
|
||||
public List<IBase> extractValues(String thePaths, IBase theResource) {
|
||||
List<IBase> values = new ArrayList<>();
|
||||
if (isNotBlank(thePaths)) {
|
||||
String[] nextPathsSplit = split(thePaths);
|
||||
|
@ -503,10 +645,9 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
List<? extends IBase> allValues;
|
||||
|
||||
// This path is hard to parse and isn't likely to produce anything useful anyway
|
||||
if (myContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU2)) {
|
||||
if (nextPath.equals("Bundle.entry.resource(0)")) {
|
||||
continue;
|
||||
}
|
||||
if (myContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU2)
|
||||
&& nextPath.equals("Bundle.entry.resource(0)")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
nextPath = trim(nextPath);
|
||||
|
@ -1011,13 +1152,26 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
}
|
||||
}
|
||||
|
||||
private <T> void extractSearchParam(RuntimeSearchParam theSearchParameterDef, IBaseResource theResource, IExtractor<T> theExtractor, SearchParamSet<T> theSetToPopulate, boolean theWantLocalReferences) {
|
||||
|
||||
/**
|
||||
* extract for normal SP
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public <T> void extractSearchParam(RuntimeSearchParam theSearchParameterDef, IBase theResource, IExtractor<T> theExtractor, SearchParamSet<T> theSetToPopulate, boolean theWantLocalReferences) {
|
||||
String nextPathUnsplit = theSearchParameterDef.getPath();
|
||||
if (isBlank(nextPathUnsplit)) {
|
||||
extractSearchParam(theSearchParameterDef, nextPathUnsplit, theResource, theExtractor, theSetToPopulate, theWantLocalReferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* extract for SP, but with possibly different expression.
|
||||
* Allows composite SPs to use sub-paths.
|
||||
*/
|
||||
private <T> void extractSearchParam(RuntimeSearchParam theSearchParameterDef, String thePathExpression, IBase theResource, IExtractor<T> theExtractor, SearchParamSet<T> theSetToPopulate, boolean theWantLocalReferences) {
|
||||
if (isBlank(thePathExpression)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String[] splitPaths = split(nextPathUnsplit);
|
||||
String[] splitPaths = split(thePathExpression);
|
||||
for (String nextPath : splitPaths) {
|
||||
nextPath = trim(nextPath);
|
||||
for (IBase nextObject : extractValues(nextPath, theResource)) {
|
||||
|
@ -1131,7 +1285,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
|
||||
/**
|
||||
* Iteratively splits a string on any ` or ` or | that is ** not** contained inside a set of parentheses. e.g.
|
||||
*
|
||||
* <p>
|
||||
* "Patient.select(a or b)" --> ["Patient.select(a or b)"]
|
||||
* "Patient.select(a or b) or Patient.select(c or d )" --> ["Patient.select(a or b)", "Patient.select(c or d)"]
|
||||
* "Patient.select(a|b) or Patient.select(c or d )" --> ["Patient.select(a|b)", "Patient.select(c or d)"]
|
||||
|
@ -1139,7 +1293,6 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
*
|
||||
* @param thePaths The string to split
|
||||
* @return The split string
|
||||
|
||||
*/
|
||||
private String[] splitOutOfParensOrs(String thePaths) {
|
||||
List<String> topLevelOrExpressions = splitOutOfParensToken(thePaths, " or ");
|
||||
|
@ -1154,7 +1307,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
int index = thePath.indexOf(theToken);
|
||||
int rightIndex = 0;
|
||||
List<String> retVal = new ArrayList<>();
|
||||
while (index > -1 ) {
|
||||
while (index > -1) {
|
||||
String left = thePath.substring(rightIndex, index);
|
||||
if (allParensHaveBeenClosed(left)) {
|
||||
retVal.add(left);
|
||||
|
@ -1474,11 +1627,11 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
|
||||
private class DateExtractor implements IExtractor<ResourceIndexedSearchParamDate> {
|
||||
|
||||
String myResourceType;
|
||||
final String myResourceType;
|
||||
ResourceIndexedSearchParamDate myIndexedSearchParamDate = null;
|
||||
|
||||
public DateExtractor(IBaseResource theResource) {
|
||||
myResourceType = toRootTypeName(theResource);
|
||||
this(toRootTypeName(theResource));
|
||||
}
|
||||
|
||||
public DateExtractor(String theResourceType) {
|
||||
|
@ -1664,30 +1817,36 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
|
|||
String nextType = BaseSearchParamExtractor.this.toRootTypeName(value);
|
||||
switch (nextType) {
|
||||
case "Identifier":
|
||||
BaseSearchParamExtractor.this.addToken_Identifier(myResourceTypeName, params, searchParam, value);
|
||||
addToken_Identifier(myResourceTypeName, params, searchParam, value);
|
||||
break;
|
||||
case "CodeableConcept":
|
||||
BaseSearchParamExtractor.this.addToken_CodeableConcept(myResourceTypeName, params, searchParam, value);
|
||||
addToken_CodeableConcept(myResourceTypeName, params, searchParam, value);
|
||||
break;
|
||||
case "Coding":
|
||||
BaseSearchParamExtractor.this.addToken_Coding(myResourceTypeName, params, searchParam, value);
|
||||
addToken_Coding(myResourceTypeName, params, searchParam, value);
|
||||
break;
|
||||
case "ContactPoint":
|
||||
BaseSearchParamExtractor.this.addToken_ContactPoint(myResourceTypeName, params, searchParam, value);
|
||||
addToken_ContactPoint(myResourceTypeName, params, searchParam, value);
|
||||
break;
|
||||
default:
|
||||
BaseSearchParamExtractor.this.addUnexpectedDatatypeWarning(params, searchParam, value);
|
||||
addUnexpectedDatatypeWarning(params, searchParam, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class CompositeExtractor<T> implements IExtractor<T> {
|
||||
|
||||
/**
|
||||
* Extractor that delegates to two other extractors.
|
||||
*
|
||||
* @param <T> the type (currently only used for Numeric)
|
||||
*/
|
||||
private static class MultiplexExtractor<T> implements IExtractor<T> {
|
||||
|
||||
private final IExtractor<T> myExtractor0;
|
||||
private final IExtractor<T> myExtractor1;
|
||||
|
||||
private CompositeExtractor(IExtractor<T> theExtractor0, IExtractor<T> theExtractor1) {
|
||||
private MultiplexExtractor(IExtractor<T> theExtractor0, IExtractor<T> theExtractor1) {
|
||||
myExtractor0 = theExtractor0;
|
||||
myExtractor1 = theExtractor1;
|
||||
}
|
||||
|
|
|
@ -53,6 +53,8 @@ public interface ISearchParamExtractor {
|
|||
|
||||
SearchParamSet<ResourceIndexedSearchParamString> extractSearchParamStrings(IBaseResource theResource);
|
||||
|
||||
SearchParamSet<ResourceIndexedSearchParamComposite> extractSearchParamComposites(IBaseResource theResource);
|
||||
|
||||
SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamTokens(IBaseResource theResource);
|
||||
|
||||
SearchParamSet<BaseResourceIndexedSearchParam> extractSearchParamTokens(IBaseResource theResource, RuntimeSearchParam theSearchParam);
|
||||
|
@ -67,7 +69,7 @@ public interface ISearchParamExtractor {
|
|||
|
||||
List<String> extractParamValuesAsStrings(RuntimeSearchParam theActiveSearchParam, IBaseResource theResource);
|
||||
|
||||
List<IBase> extractValues(String thePaths, IBaseResource theResource);
|
||||
List<IBase> extractValues(String thePaths, IBase theResource);
|
||||
|
||||
String toRootTypeName(IBase nextObject);
|
||||
|
||||
|
@ -81,7 +83,7 @@ public interface ISearchParamExtractor {
|
|||
|
||||
String getDisplayTextForCoding(IBase theValue);
|
||||
|
||||
BaseSearchParamExtractor.IValueExtractor getPathValueExtractor(IBaseResource theResource, String theSinglePath);
|
||||
BaseSearchParamExtractor.IValueExtractor getPathValueExtractor(IBase theResource, String theSinglePath);
|
||||
|
||||
List<IBase> getCodingsFromCodeableConcept(IBase theValue);
|
||||
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
package ca.uhn.fhir.jpa.searchparam.extractor;
|
||||
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
|
||||
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Intermediate holder for indexing composite search parameters.
|
||||
* There will be one instance for each element extracted by the parent composite path expression.
|
||||
*/
|
||||
public class ResourceIndexedSearchParamComposite {
|
||||
|
||||
private final String mySearchParamName;
|
||||
private final String myPath;
|
||||
private final List<Component> myComponents = new ArrayList<>();
|
||||
|
||||
public ResourceIndexedSearchParamComposite(String theSearchParamName, String thePath) {
|
||||
mySearchParamName = theSearchParamName;
|
||||
myPath = thePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* the SP name for this composite SP
|
||||
*/
|
||||
public String getSearchParamName() {
|
||||
return mySearchParamName;
|
||||
}
|
||||
|
||||
/**
|
||||
* The path expression of the composite SP
|
||||
*/
|
||||
public String getPath() {
|
||||
return myPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subcomponent index data for this composite
|
||||
*/
|
||||
public List<Component> getComponents() {
|
||||
return myComponents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add subcomponent index data.
|
||||
* @param theComponentSearchParam the component SP we are extracting
|
||||
* @param theExtractedParams index data extracted by the sub-extractor
|
||||
*/
|
||||
public void addComponentIndexedSearchParams(RuntimeSearchParam theComponentSearchParam, ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> theExtractedParams) {
|
||||
addComponentIndexedSearchParams(theComponentSearchParam.getName(), theComponentSearchParam.getParamType(), theExtractedParams);
|
||||
}
|
||||
|
||||
public void addComponentIndexedSearchParams(String theComponentSearchParamName, RestSearchParameterTypeEnum theComponentSearchParamType, ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> theExtractedParams) {
|
||||
myComponents.add(new Component(theComponentSearchParamName, theComponentSearchParamType, theExtractedParams));
|
||||
}
|
||||
|
||||
/**
|
||||
* Nested holder of index data for a single component of a composite SP.
|
||||
* E.g. hold token info for component-code under parent of component-code-value-quantity.
|
||||
*/
|
||||
public static class Component {
|
||||
/**
|
||||
* The SP name of this subcomponent.
|
||||
* E.g. "component-code" when the parent composite SP is component-code-value-quantity.
|
||||
*/
|
||||
private final String mySearchParamName;
|
||||
/**
|
||||
* The SP type of this subcomponent.
|
||||
* E.g. TOKEN when indexing "component-code" of parent composite SP is component-code-value-quantity.
|
||||
*/
|
||||
private final RestSearchParameterTypeEnum mySearchParameterType;
|
||||
/**
|
||||
* Any of the extracted data of any type for this subcomponent.
|
||||
*/
|
||||
private final ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> myParamIndexValues;
|
||||
|
||||
private Component(String theComponentSearchParamName, RestSearchParameterTypeEnum theComponentSearchParamType, ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> theParamIndexValues) {
|
||||
mySearchParamName = theComponentSearchParamName;
|
||||
mySearchParameterType = theComponentSearchParamType;
|
||||
myParamIndexValues = theParamIndexValues;
|
||||
}
|
||||
|
||||
public String getSearchParamName() {
|
||||
return mySearchParamName;
|
||||
}
|
||||
|
||||
public RestSearchParameterTypeEnum getSearchParameterType() {
|
||||
return mySearchParameterType;
|
||||
}
|
||||
|
||||
public ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> getParamIndexValues() {
|
||||
return myParamIndexValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -73,6 +73,7 @@ public final class ResourceIndexedSearchParams {
|
|||
final public Collection<ResourceIndexedComboTokenNonUnique> myComboTokenNonUnique = new HashSet<>();
|
||||
final public Collection<ResourceLink> myLinks = new HashSet<>();
|
||||
final public Set<String> myPopulatedResourceLinkParameters = new HashSet<>();
|
||||
final public Collection<ResourceIndexedSearchParamComposite> myCompositeParams = new HashSet<>();
|
||||
|
||||
public ResourceIndexedSearchParams() {
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ import ca.uhn.fhir.util.FhirTerser;
|
|||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
import org.hl7.fhir.instance.model.api.IBaseDatatype;
|
||||
import org.hl7.fhir.instance.model.api.IBaseExtension;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -48,7 +47,7 @@ public class SearchParamExtractorDstu2 extends BaseSearchParamExtractor implemen
|
|||
}
|
||||
|
||||
@Override
|
||||
public IValueExtractor getPathValueExtractor(IBaseResource theResource, String theSinglePath) {
|
||||
public IValueExtractor getPathValueExtractor(IBase theResource, String theSinglePath) {
|
||||
return () -> {
|
||||
String path = theSinglePath;
|
||||
|
||||
|
|
|
@ -30,7 +30,6 @@ import org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext;
|
|||
import org.hl7.fhir.dstu3.model.Base;
|
||||
import org.hl7.fhir.dstu3.utils.FHIRPathEngine;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.ArrayList;
|
||||
|
@ -56,7 +55,7 @@ public class SearchParamExtractorDstu3 extends BaseSearchParamExtractor implemen
|
|||
}
|
||||
|
||||
@Override
|
||||
public IValueExtractor getPathValueExtractor(IBaseResource theResource, String theSinglePath) {
|
||||
public IValueExtractor getPathValueExtractor(IBase theResource, String theSinglePath) {
|
||||
return () -> {
|
||||
List<IBase> values = new ArrayList<>();
|
||||
List<Base> allValues = myFhirPathEngine.evaluate((Base) theResource, theSinglePath);
|
||||
|
|
|
@ -29,7 +29,7 @@ import com.github.benmanes.caffeine.cache.Caffeine;
|
|||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.hl7.fhir.exceptions.FHIRException;
|
||||
import org.hl7.fhir.exceptions.PathEngineException;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
import org.hl7.fhir.r4.context.IWorkerContext;
|
||||
import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext;
|
||||
import org.hl7.fhir.r4.model.Base;
|
||||
|
@ -71,7 +71,7 @@ public class SearchParamExtractorR4 extends BaseSearchParamExtractor implements
|
|||
}
|
||||
|
||||
@Override
|
||||
public IValueExtractor getPathValueExtractor(IBaseResource theResource, String theSinglePath) {
|
||||
public IValueExtractor getPathValueExtractor(IBase theResource, String theSinglePath) {
|
||||
return () -> {
|
||||
ExpressionNode parsed = myParsedFhirPathCache.get(theSinglePath, path -> myFhirPathEngine.parse(path));
|
||||
return myFhirPathEngine.evaluate((Base) theResource, parsed);
|
||||
|
|
|
@ -28,7 +28,7 @@ import com.github.benmanes.caffeine.cache.Cache;
|
|||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.hl7.fhir.exceptions.FHIRException;
|
||||
import org.hl7.fhir.exceptions.PathEngineException;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
import org.hl7.fhir.r5.context.IWorkerContext;
|
||||
import org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext;
|
||||
import org.hl7.fhir.r5.model.Base;
|
||||
|
@ -88,7 +88,7 @@ public class SearchParamExtractorR5 extends BaseSearchParamExtractor implements
|
|||
}
|
||||
|
||||
@Override
|
||||
public IValueExtractor getPathValueExtractor(IBaseResource theResource, String theSinglePath) {
|
||||
public IValueExtractor getPathValueExtractor(IBase theResource, String theSinglePath) {
|
||||
return () -> {
|
||||
ExpressionNode parsed = myParsedFhirPathCache.get(theSinglePath, path -> myFhirPathEngine.parse(path));
|
||||
return myFhirPathEngine.evaluate((Base) theResource, parsed);
|
||||
|
|
|
@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.searchparam.extractor;
|
|||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
|
@ -202,9 +203,10 @@ public class SearchParamExtractorService {
|
|||
theTargetParams.myTokenParams.addAll(theSrcParams.myTokenParams);
|
||||
theTargetParams.myStringParams.addAll(theSrcParams.myStringParams);
|
||||
theTargetParams.myCoordsParams.addAll(theSrcParams.myCoordsParams);
|
||||
theTargetParams.myCompositeParams.addAll(theSrcParams.myCompositeParams);
|
||||
}
|
||||
|
||||
private void extractSearchIndexParameters(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, IBaseResource theResource, ResourceTable theEntity) {
|
||||
void extractSearchIndexParameters(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, IBaseResource theResource, ResourceTable theEntity) {
|
||||
|
||||
// Strings
|
||||
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamString> strings = extractSearchParamStrings(theResource);
|
||||
|
@ -250,6 +252,15 @@ public class SearchParamExtractorService {
|
|||
}
|
||||
}
|
||||
|
||||
// Composites
|
||||
// wipmb should we have config to skip this cpu work? Check to see if HSearch is enabled?
|
||||
// dst2 composites use stuff like value[x] , and we don't support them.
|
||||
if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) {
|
||||
ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamComposite> composites = extractSearchParamComposites(theResource);
|
||||
handleWarnings(theRequestDetails, myInterceptorBroadcaster, composites);
|
||||
theParams.myCompositeParams.addAll(composites);
|
||||
}
|
||||
|
||||
// Specials
|
||||
ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> specials = extractSearchParamSpecial(theResource);
|
||||
for (BaseResourceIndexedSearchParam next : specials) {
|
||||
|
@ -560,6 +571,11 @@ public class SearchParamExtractorService {
|
|||
return mySearchParamExtractor.extractSearchParamUri(theResource);
|
||||
}
|
||||
|
||||
private ISearchParamExtractor.SearchParamSet<ResourceIndexedSearchParamComposite> extractSearchParamComposites(IBaseResource theResource) {
|
||||
return mySearchParamExtractor.extractSearchParamComposites(theResource);
|
||||
}
|
||||
|
||||
|
||||
@VisibleForTesting
|
||||
void setInterceptorBroadcasterForUnitTest(IInterceptorBroadcaster theInterceptorBroadcaster) {
|
||||
myInterceptorBroadcaster = theInterceptorBroadcaster;
|
||||
|
|
|
@ -51,6 +51,7 @@ import ca.uhn.fhir.rest.param.binder.QueryParameterAndBinder;
|
|||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
|
@ -147,6 +148,16 @@ public enum JpaParamUtil {
|
|||
}
|
||||
|
||||
public static List<RuntimeSearchParam> resolveComponentParameters(ISearchParamRegistry theSearchParamRegistry, RuntimeSearchParam theParamDef) {
|
||||
List<RuntimeSearchParam> compositeList = resolveCompositeComponentsDeclaredOrder(theSearchParamRegistry, theParamDef);
|
||||
|
||||
// wipmb why is this sorted? Is the param order flipped too during query-time?
|
||||
compositeList.sort((Comparator.comparing(RuntimeSearchParam::getName)));
|
||||
|
||||
return compositeList;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static List<RuntimeSearchParam> resolveCompositeComponentsDeclaredOrder(ISearchParamRegistry theSearchParamRegistry, RuntimeSearchParam theParamDef) {
|
||||
List<RuntimeSearchParam> compositeList = new ArrayList<>();
|
||||
List<RuntimeSearchParam.Component> components = theParamDef.getComponents();
|
||||
for (RuntimeSearchParam.Component next : components) {
|
||||
|
@ -157,9 +168,6 @@ public enum JpaParamUtil {
|
|||
}
|
||||
compositeList.add(componentParam);
|
||||
}
|
||||
|
||||
compositeList.sort((Comparator.comparing(RuntimeSearchParam::getName)));
|
||||
|
||||
return compositeList;
|
||||
}
|
||||
|
||||
|
|
|
@ -147,6 +147,7 @@ public class FhirResourceDaoR4SearchFtTest extends BaseJpaR4Test {
|
|||
map.add(Observation.SP_VALUE_STRING, new StringParam("Systolic Blood"));
|
||||
assertThat("Default search matches prefix, even with space", toUnqualifiedVersionlessIdValues(myObservationDao.search(map)), containsInAnyOrder(toValues(id1)));
|
||||
|
||||
|
||||
// contains doesn't work
|
||||
// map = new SearchParameterMap();
|
||||
// map.add(Observation.SP_VALUE_STRING, new StringParam("sure").setContains(true));
|
||||
|
|
|
@ -4,6 +4,8 @@ import ca.uhn.fhir.context.FhirContext;
|
|||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.dao.TestDaoSearch;
|
||||
import ca.uhn.fhir.jpa.search.CompositeSearchParameterTestCases;
|
||||
import ca.uhn.fhir.jpa.search.QuantitySearchParameterTestCases;
|
||||
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
|
||||
import ca.uhn.fhir.jpa.test.BaseJpaTest;
|
||||
import ca.uhn.fhir.jpa.test.config.TestHSearchAddInConfig;
|
||||
|
@ -13,6 +15,7 @@ import ca.uhn.fhir.storage.test.DaoTestDataBuilder;
|
|||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Observation;
|
||||
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;
|
||||
|
@ -410,4 +413,25 @@ public class FhirResourceDaoR4StandardQueriesNoFTTest extends BaseJpaTest {
|
|||
|
||||
}
|
||||
|
||||
// wipmb re-enable this. Some of these fail!
|
||||
@Disabled
|
||||
@Nested
|
||||
class QuantityAndNormalizedQuantitySearch extends QuantitySearchParameterTestCases {
|
||||
QuantityAndNormalizedQuantitySearch() {
|
||||
super(myDataBuilder, myTestDaoSearch, myDaoConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class CompositeSearch extends CompositeSearchParameterTestCases {
|
||||
CompositeSearch() {
|
||||
super(myDataBuilder, myTestDaoSearch);
|
||||
}
|
||||
|
||||
/** JPA doesn't know which sub-element matches */
|
||||
@Override
|
||||
protected boolean isCorrelatedSupported() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,11 +13,16 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
|
|||
import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.PathAndRef;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParamComposite;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorR4;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.SearchParameterCanonicalizer;
|
||||
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
|
||||
import ca.uhn.fhir.rest.server.util.FhirContextSearchParamRegistry;
|
||||
import ca.uhn.fhir.test.utilities.ITestDataBuilder;
|
||||
import ca.uhn.fhir.util.HapiExtensions;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.BooleanType;
|
||||
import org.hl7.fhir.r4.model.CodeableConcept;
|
||||
import org.hl7.fhir.r4.model.Coding;
|
||||
|
@ -32,11 +37,13 @@ import org.hl7.fhir.r4.model.Reference;
|
|||
import org.hl7.fhir.r4.model.SearchParameter;
|
||||
import org.hl7.fhir.r4.model.StringType;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
@ -45,23 +52,23 @@ import java.util.stream.Collectors;
|
|||
import static java.util.Comparator.comparing;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
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.hasProperty;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
public class SearchParamExtractorR4Test {
|
||||
public class SearchParamExtractorR4Test implements ITestDataBuilder {
|
||||
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(SearchParamExtractorR4Test.class);
|
||||
private static final FhirContext ourCtx = FhirContext.forR4Cached();
|
||||
private FhirContextSearchParamRegistry mySearchParamRegistry;
|
||||
private PartitionSettings myPartitionSettings;
|
||||
|
||||
@BeforeEach
|
||||
public void before() {
|
||||
|
||||
mySearchParamRegistry = new FhirContextSearchParamRegistry(ourCtx);
|
||||
myPartitionSettings = new PartitionSettings();
|
||||
|
||||
}
|
||||
private final FhirContextSearchParamRegistry mySearchParamRegistry = new FhirContextSearchParamRegistry(ourCtx);
|
||||
private final PartitionSettings myPartitionSettings = new PartitionSettings();
|
||||
final ModelConfig myModelConfig = new ModelConfig();
|
||||
|
||||
@Test
|
||||
public void testParamWithOrInPath() {
|
||||
|
@ -377,4 +384,174 @@ public class SearchParamExtractorR4Test {
|
|||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class CompositeSearchParameter {
|
||||
SearchParamExtractorR4 myExtractor = new SearchParamExtractorR4(myModelConfig, new PartitionSettings(), ourCtx, mySearchParamRegistry);
|
||||
|
||||
/**
|
||||
* Install a full definition of component-code-value-concept in the SP registry.
|
||||
*
|
||||
* We can't use the base FhirContext SP definitions since @SearchParamDefinition
|
||||
* only includes the sub-SP ids, and lacks the composite sub-paths.
|
||||
* @see ca.uhn.fhir.model.api.annotation.SearchParamDefinition#compositeOf
|
||||
*/
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
String spJson = """
|
||||
{
|
||||
"resourceType": "SearchParameter",
|
||||
"id": "Observation-component-code-value-concept",
|
||||
"extension": [ {
|
||||
"url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status",
|
||||
"valueCode": "trial-use"
|
||||
} ],
|
||||
"url": "http://hl7.org/fhir/SearchParameter/Observation-component-code-value-concept",
|
||||
"version": "4.0.1",
|
||||
"name": "component-code-value-concept",
|
||||
"status": "active",
|
||||
"experimental": false,
|
||||
"date": "2019-11-01T09:29:23+11:00",
|
||||
"publisher": "Health Level Seven International (Orders and Observations)",
|
||||
"contact": [ {
|
||||
"telecom": [ {
|
||||
"system": "url",
|
||||
"value": "http://hl7.org/fhir"
|
||||
} ]
|
||||
}, {
|
||||
"telecom": [ {
|
||||
"system": "url",
|
||||
"value": "http://www.hl7.org/Special/committees/orders/index.cfm"
|
||||
} ]
|
||||
} ],
|
||||
"description": "Component code and component coded value parameter pair",
|
||||
"code": "component-code-value-concept",
|
||||
"base": [ "Observation" ],
|
||||
"type": "composite",
|
||||
"expression": "Observation.component",
|
||||
"xpathUsage": "normal",
|
||||
"multipleOr": false,
|
||||
"component": [ {
|
||||
"definition": "http://hl7.org/fhir/SearchParameter/Observation-component-code",
|
||||
"expression": "code"
|
||||
}, {
|
||||
"definition": "http://hl7.org/fhir/SearchParameter/Observation-component-value-concept",
|
||||
"expression": "value.as(CodeableConcept)"
|
||||
} ]
|
||||
}
|
||||
""";
|
||||
SearchParameter sp = (SearchParameter) getFhirContext().newJsonParser().parseResource(spJson);
|
||||
RuntimeSearchParam runtimeSP = new SearchParameterCanonicalizer(getFhirContext()).canonicalizeSearchParameter(sp);
|
||||
mySearchParamRegistry.addSearchParam(runtimeSP);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractSearchParamComposites_componentCodeCode_producesOneCompositeComponentForEachElement() {
|
||||
IBaseResource resource = buildResource("Observation",
|
||||
withObservationComponent(
|
||||
withCodingAt("code.coding", "http://example.com", "code_token", null),
|
||||
withCodingAt("valueCodeableConcept.coding", null, "value_token", null)),
|
||||
withObservationComponent(
|
||||
withCodingAt("code.coding", "http://example.com", "code_token2", null),
|
||||
withCodingAt("valueCodeableConcept.coding", null, "value_toke2", null)),
|
||||
withObservationComponent(
|
||||
withCodingAt("code.coding", "http://example.com", "code_token3", null),
|
||||
withCodingAt("valueCodeableConcept.coding", null, "value_toke3", null))
|
||||
);
|
||||
|
||||
Collection<ResourceIndexedSearchParamComposite> c = myExtractor.extractSearchParamComposites(resource);
|
||||
|
||||
assertThat(c, not(empty()));
|
||||
assertThat("Extracts standard R4 composite sp", c, hasItem(hasProperty("searchParamName", equalTo("component-code-value-concept"))));
|
||||
|
||||
List<ResourceIndexedSearchParamComposite> components = c.stream()
|
||||
.filter(idx -> idx.getSearchParamName().equals("component-code-value-concept"))
|
||||
.collect(Collectors.toList());
|
||||
assertThat("one components per element", components, hasSize(3));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractSearchParamComposites_componentCodeCode_yieldsTwoSubComponents() {
|
||||
IBaseResource resource = buildResource("Observation",
|
||||
withObservationComponent(
|
||||
withCodingAt("code.coding", "http://example.com", "code_token", null),
|
||||
withCodingAt("valueCodeableConcept.coding", null, "value_token", null))
|
||||
);
|
||||
|
||||
Collection<ResourceIndexedSearchParamComposite> c = myExtractor.extractSearchParamComposites(resource);
|
||||
|
||||
List<ResourceIndexedSearchParamComposite> components = c.stream()
|
||||
.filter(idx -> idx.getSearchParamName().equals("component-code-value-concept"))
|
||||
.toList();
|
||||
assertThat(components, hasSize(1));
|
||||
ResourceIndexedSearchParamComposite componentCodeValueConcept = components.get(0);
|
||||
|
||||
// component-code-value-concept is two token params - component-code and component-value-concept
|
||||
List<ResourceIndexedSearchParamComposite.Component> indexedComponentsOfElement = componentCodeValueConcept.getComponents();
|
||||
assertThat("component-code-value-concept has two sub-params", indexedComponentsOfElement, hasSize(2));
|
||||
|
||||
final ResourceIndexedSearchParamComposite.Component component0 = indexedComponentsOfElement.get(0);
|
||||
assertThat(component0.getSearchParamName(), equalTo("component-code"));
|
||||
assertThat(component0.getSearchParameterType(), equalTo(RestSearchParameterTypeEnum.TOKEN));
|
||||
|
||||
final ResourceIndexedSearchParamComposite.Component component1 = indexedComponentsOfElement.get(1);
|
||||
assertThat(component1.getSearchParamName(), equalTo("component-value-concept"));
|
||||
assertThat(component1.getSearchParameterType(), equalTo(RestSearchParameterTypeEnum.TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExtractSearchParamComposites_componentCodeCode_subValueExtraction() {
|
||||
IBaseResource resource = buildResource("Observation",
|
||||
withObservationComponent(
|
||||
withCodingAt("code.coding", "http://example.com", "code_token", "display value"),
|
||||
withCodingAt("valueCodeableConcept.coding", null, "value_token", null))
|
||||
);
|
||||
|
||||
Collection<ResourceIndexedSearchParamComposite> c = myExtractor.extractSearchParamComposites(resource);
|
||||
|
||||
List<ResourceIndexedSearchParamComposite> components = c.stream()
|
||||
.filter(idx -> idx.getSearchParamName().equals("component-code-value-concept"))
|
||||
.toList();
|
||||
assertThat(components, hasSize(1));
|
||||
ResourceIndexedSearchParamComposite spEntry = components.get(0);
|
||||
|
||||
// this SP has two sub-components
|
||||
assertThat(spEntry.getComponents(), hasSize(2));
|
||||
|
||||
ResourceIndexedSearchParamComposite.Component indexComponent0 = spEntry.getComponents().get(0);
|
||||
assertThat(indexComponent0.getSearchParamName(), notNullValue());
|
||||
assertThat(indexComponent0.getSearchParamName(), equalTo("component-code"));
|
||||
assertThat(indexComponent0.getSearchParameterType(), equalTo(RestSearchParameterTypeEnum.TOKEN));
|
||||
assertThat(indexComponent0.getParamIndexValues(), hasSize(2));
|
||||
// token indexes both the token, and the display text
|
||||
ResourceIndexedSearchParamToken tokenIdx0 = (ResourceIndexedSearchParamToken) indexComponent0.getParamIndexValues().stream()
|
||||
.filter(i -> i instanceof ResourceIndexedSearchParamToken)
|
||||
.findFirst().orElseThrow();
|
||||
assertThat(tokenIdx0.getParamName(), equalTo("component-code"));
|
||||
assertThat(tokenIdx0.getResourceType(), equalTo("Observation"));
|
||||
assertThat(tokenIdx0.getValue(), equalTo("code_token"));
|
||||
|
||||
ResourceIndexedSearchParamString tokenDisplayIdx0 = (ResourceIndexedSearchParamString) indexComponent0.getParamIndexValues().stream()
|
||||
.filter(i -> i instanceof ResourceIndexedSearchParamString)
|
||||
.findFirst().orElseThrow();
|
||||
assertThat(tokenDisplayIdx0.getParamName(), equalTo("component-code"));
|
||||
assertThat(tokenDisplayIdx0.getResourceType(), equalTo("Observation"));
|
||||
assertThat(tokenDisplayIdx0.getValueExact(), equalTo("display value"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IIdType doCreateResource(IBaseResource theResource) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IIdType doUpdateResource(IBaseResource theResource) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FhirContext getFhirContext() {
|
||||
return ourCtx;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,14 @@
|
|||
|
||||
<name>HAPI FHIR JPA Server Test Utilities</name>
|
||||
<artifactId>hapi-fhir-jpaserver-test-utilities</artifactId>
|
||||
|
||||
<properties>
|
||||
<!-- this is a test jar, so use our test settings -->
|
||||
<maven.compiler.source>${maven.compiler.testSource}</maven.compiler.source>
|
||||
<maven.compiler.target>${maven.compiler.testTarget}</maven.compiler.target>
|
||||
<maven.compiler.release>${maven.compiler.testRelease}</maven.compiler.release>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
|
|
|
@ -32,6 +32,7 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
|||
import ca.uhn.fhir.rest.server.IPagingProvider;
|
||||
import ca.uhn.fhir.rest.server.IRestfulServerDefaults;
|
||||
import ca.uhn.fhir.rest.server.method.SortParameter;
|
||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
@ -65,23 +66,31 @@ public class TestDaoSearch {
|
|||
TestDaoSearch testDaoSearch(
|
||||
@Autowired FhirContext theFhirContext,
|
||||
@Autowired DaoRegistry theDaoRegistry,
|
||||
@Autowired MatchUrlService theMatchUrlService
|
||||
@Autowired MatchUrlService theMatchUrlService,
|
||||
@Autowired ISearchParamRegistry theSearchParamRegistry
|
||||
|
||||
) {
|
||||
return new TestDaoSearch(theFhirContext, theDaoRegistry, theMatchUrlService);
|
||||
return new TestDaoSearch(theFhirContext, theDaoRegistry, theMatchUrlService, theSearchParamRegistry);
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private IFulltextSearchSvc myFulltextSearchSvc;
|
||||
|
||||
final MatchUrlService myMatchUrlService;
|
||||
final DaoRegistry myDaoRegistry;
|
||||
final FhirContext myFhirCtx;
|
||||
final DaoRegistry myDaoRegistry;
|
||||
final MatchUrlService myMatchUrlService;
|
||||
final ISearchParamRegistry mySearchParamRegistry;
|
||||
|
||||
public TestDaoSearch(FhirContext theFhirCtx, DaoRegistry theDaoRegistry, MatchUrlService theMatchUrlService) {
|
||||
public TestDaoSearch(FhirContext theFhirCtx, DaoRegistry theDaoRegistry, MatchUrlService theMatchUrlService, ISearchParamRegistry theSearchParamRegistry) {
|
||||
myMatchUrlService = theMatchUrlService;
|
||||
myDaoRegistry = theDaoRegistry;
|
||||
myFhirCtx = theFhirCtx;
|
||||
mySearchParamRegistry = theSearchParamRegistry;
|
||||
}
|
||||
|
||||
public ISearchParamRegistry getSearchParamRegistry() {
|
||||
return mySearchParamRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,198 @@
|
|||
package ca.uhn.fhir.jpa.search;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.dao.TestDaoSearch;
|
||||
import ca.uhn.fhir.test.utilities.ITestDataBuilder;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIf;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
|
||||
/**
|
||||
* Test cases for composite search parameters.
|
||||
* https://www.hl7.org/fhir/search.html#composite
|
||||
*
|
||||
* Intended to be nested in a context that provides a ITestDataBuilder.Support and TestDaoSearch.
|
||||
*/
|
||||
public abstract class CompositeSearchParameterTestCases implements ITestDataBuilder.WithSupport {
|
||||
static final String SYSTEM_LOINC_ORG = "http://loinc.org";
|
||||
static final String CODE_8480_6 = "8480-6";
|
||||
static final String CODE_3421_5 = "3421-5";
|
||||
|
||||
final ITestDataBuilder.Support myTestDataBuilder;
|
||||
final TestDaoSearch myTestDaoSearch;
|
||||
|
||||
protected CompositeSearchParameterTestCases(ITestDataBuilder.Support theTestDataBuilder, TestDaoSearch theTestDaoSearch) {
|
||||
myTestDataBuilder = theTestDataBuilder;
|
||||
myTestDaoSearch = theTestDaoSearch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Support getTestDataBuilderSupport() {
|
||||
return myTestDataBuilder;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Should we run test cases that depend on engine support sub-element correlation?
|
||||
*
|
||||
* JPA currently reuses the extracted values from each sub-param.
|
||||
* This works fine for non-repeating elements, but can fail for repeating elements
|
||||
* like Observation.component. For the composites defined on Observation.component,
|
||||
* both sub-params must match on the specific component (eg systolic vs diasystolic readings),
|
||||
* and JPA doesn't track down to the element level.
|
||||
*/
|
||||
protected abstract boolean isCorrelatedSupported();
|
||||
|
||||
@Test
|
||||
void searchCodeQuantity_onSameComponent_found() {
|
||||
IIdType id1 = createObservation(
|
||||
withObservationComponent(
|
||||
withCodingAt("code.coding", SYSTEM_LOINC_ORG, CODE_8480_6, null),
|
||||
withQuantityAtPath("valueQuantity", 60, null, "mmHg")),
|
||||
withObservationComponent(
|
||||
withCodingAt("code.coding", SYSTEM_LOINC_ORG, CODE_3421_5, null),
|
||||
withQuantityAtPath("valueQuantity", 100, null, "mmHg"))
|
||||
);
|
||||
|
||||
myTestDaoSearch.assertSearchFinds("search matches both sps in composite",
|
||||
"Observation?component-code-value-quantity=8480-6$60", id1);
|
||||
}
|
||||
|
||||
@EnabledIf("isCorrelatedSupported")
|
||||
@Test
|
||||
void searchCodeQuantity_differentComponents_notFound() {
|
||||
createObservation(
|
||||
withObservationCode(SYSTEM_LOINC_ORG, CODE_8480_6),
|
||||
withObservationComponent(
|
||||
withCodingAt("code.coding", SYSTEM_LOINC_ORG, CODE_8480_6),
|
||||
withQuantityAtPath("valueQuantity", 60, null, "mmHg")),
|
||||
withObservationComponent(
|
||||
withCodingAt("code.coding", SYSTEM_LOINC_ORG, CODE_3421_5),
|
||||
withQuantityAtPath("valueQuantity", 100, null, "mmHg"))
|
||||
);
|
||||
|
||||
List<String> ids = myTestDaoSearch.searchForIds("Observation?component-code-value-quantity=8480-6$100");
|
||||
assertThat("Search for the value from one component, but the code from the other, so it shouldn't match", ids, empty());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void searchCodeCode_onSameComponent_found() {
|
||||
IIdType id1 = createObservation(
|
||||
withObservationComponent(
|
||||
withCodingAt("code.coding", SYSTEM_LOINC_ORG, CODE_8480_6, null),
|
||||
withCodingAt("valueCodeableConcept.coding", SYSTEM_LOINC_ORG, "some-code")),
|
||||
withObservationComponent(
|
||||
withCodingAt("code.coding", SYSTEM_LOINC_ORG, CODE_3421_5, null),
|
||||
withCodingAt("valueCodeableConcept.coding", SYSTEM_LOINC_ORG, "another-code"))
|
||||
);
|
||||
|
||||
myTestDaoSearch.assertSearchFinds("search matches both sps in composite",
|
||||
"Observation?component-code-value-concept=8480-6$some-code", id1);
|
||||
}
|
||||
|
||||
@EnabledIf("isCorrelatedSupported")
|
||||
@Test
|
||||
void searchCodeCode_differentComponents_notFound() {
|
||||
createObservation(
|
||||
withObservationCode(SYSTEM_LOINC_ORG, CODE_8480_6),
|
||||
withObservationComponent(
|
||||
withCodingAt("code.coding", SYSTEM_LOINC_ORG, CODE_8480_6, null),
|
||||
withCodingAt("valueCodeableConcept.coding", SYSTEM_LOINC_ORG, "some-code")),
|
||||
withObservationComponent(
|
||||
withCodingAt("code.coding", SYSTEM_LOINC_ORG, CODE_3421_5, null),
|
||||
withCodingAt("valueCodeableConcept.coding", SYSTEM_LOINC_ORG, "another-code"))
|
||||
);
|
||||
|
||||
List<String> ids = myTestDaoSearch.searchForIds("Observation?component-code-value-concept=8480-6$another-code");
|
||||
assertThat("Search for the value from one component, but the code from the other, so it shouldn't match", ids, empty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchCodeDate_onSameResource_found() {
|
||||
IIdType id1 = createObservation(
|
||||
withObservationCode( SYSTEM_LOINC_ORG, CODE_8480_6, null),
|
||||
withDateTimeAt("valueDateTime", "2020-01-01T12:34:56")
|
||||
);
|
||||
|
||||
myTestDaoSearch.assertSearchFinds("search matches both sps in composite",
|
||||
"Observation?code-value-date=8480-6$lt2021", id1);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void searchCodeString_onSameResource_found() {
|
||||
IIdType id1 = createObservation(
|
||||
withObservationCode( SYSTEM_LOINC_ORG, CODE_8480_6, null),
|
||||
withDateTimeAt("valueString", "ABCDEF")
|
||||
);
|
||||
|
||||
myTestDaoSearch.assertSearchFinds("token code + string prefix matches",
|
||||
"Observation?code-value-string=8480-6$ABC", id1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a goofy SP to test composite component support
|
||||
* for uri and number.
|
||||
*
|
||||
* Note - JPA doesn't support this either yet. Just piggyback on this flag.
|
||||
*/
|
||||
@EnabledIf("isCorrelatedSupported")
|
||||
@Test
|
||||
void searchUriNumber_onSameResource_found() {
|
||||
// Combine existing SPs to test uri + number
|
||||
createResourceFromJson("""
|
||||
{
|
||||
"resourceType": "SearchParameter",
|
||||
"name": "uri-number-compound-test",
|
||||
"status": "active",
|
||||
"description": "dummy to exercise uri + number",
|
||||
"code": "uri-number-compound-test",
|
||||
"base": [ "RiskAssessment" ],
|
||||
"type": "composite",
|
||||
"expression": "RiskAssessment",
|
||||
"component": [ {
|
||||
"definition": "http://hl7.org/fhir/SearchParameter/Resource-source",
|
||||
"expression": "meta.source"
|
||||
}, {
|
||||
"definition": "http://hl7.org/fhir/SearchParameter/RiskAssessment-probability",
|
||||
"expression": "prediction.probability"
|
||||
} ]
|
||||
}""");
|
||||
// enable this sp.
|
||||
myTestDaoSearch.getSearchParamRegistry().forceRefresh();
|
||||
|
||||
IIdType raId = createResourceFromJson("""
|
||||
{
|
||||
"resourceType": "RiskAssessment",
|
||||
"meta": {
|
||||
"source": "https://example.com/ourSource"
|
||||
},
|
||||
"prediction": [
|
||||
{
|
||||
"outcome": {
|
||||
"text": "Heart Attack"
|
||||
},
|
||||
"probabilityDecimal": 0.02
|
||||
}
|
||||
]
|
||||
}
|
||||
""");
|
||||
|
||||
// verify config
|
||||
myTestDaoSearch.assertSearchFinds("simple uri search works", "RiskAssessment?_source=https://example.com/ourSource", raId);
|
||||
myTestDaoSearch.assertSearchFinds("simple number search works", "RiskAssessment?probability=0.02", raId);
|
||||
// verify composite queries
|
||||
myTestDaoSearch.assertSearchFinds("composite uri + number", "RiskAssessment?uri-number-compound-test=https://example.com/ourSource$0.02", raId);
|
||||
myTestDaoSearch.assertSearchNotFound("both params must match ", "RiskAssessment?uri-number-compound-test=https://example.com/ourSource$0.08", raId);
|
||||
myTestDaoSearch.assertSearchNotFound("both params must match ", "RiskAssessment?uri-number-compound-test=https://example.com/otherUrI$0.02", raId);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,633 @@
|
|||
package ca.uhn.fhir.jpa.search;
|
||||
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.dao.TestDaoSearch;
|
||||
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
|
||||
import ca.uhn.fhir.parser.DataFormatException;
|
||||
import ca.uhn.fhir.test.utilities.ITestDataBuilder;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Observation;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
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.empty;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class QuantitySearchParameterTestCases implements ITestDataBuilder.WithSupport {
|
||||
|
||||
final Support myTestDataBuilder;
|
||||
final TestDaoSearch myTestDaoSearch;
|
||||
final DaoConfig myDaoConfig;
|
||||
|
||||
private IIdType myResourceId;
|
||||
|
||||
protected QuantitySearchParameterTestCases(Support theTestDataBuilder, TestDaoSearch theTestDaoSearch, DaoConfig theDaoConfig) {
|
||||
myTestDataBuilder = theTestDataBuilder;
|
||||
myTestDaoSearch = theTestDaoSearch;
|
||||
myDaoConfig = theDaoConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Support getTestDataBuilderSupport() {
|
||||
return myTestDataBuilder;
|
||||
}
|
||||
|
||||
@Nested
|
||||
public class QuantitySearch {
|
||||
|
||||
/**
|
||||
* Tests for each basic comparison prefix: https://www.hl7.org/fhir/search.html#prefix
|
||||
*/
|
||||
@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 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.7 or lt0.55", "/Observation?value-quantity=gt0.7,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.7 or le0.55", "/Observation?value-quantity=gt0.7,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.5 or ge0.8", "/Observation?value-quantity=lt0.5,ge0.8");
|
||||
}
|
||||
|
||||
@Test
|
||||
void gtOrGt() {
|
||||
withObservationWithValueQuantity(0.6);
|
||||
|
||||
assertFind("when gt0.5 or gt0.8", "/Observation?value-quantity=gt0.5,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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleComponentsHandlesAndOr() {
|
||||
IIdType obs1Id = createObservation(
|
||||
withObservationComponent(
|
||||
withCodingAt("code.coding", "http://loinc.org", "8480-6"),
|
||||
withQuantityAtPath("valueQuantity", 107, "http://unitsofmeasure.org", "mm[Hg]")),
|
||||
withObservationComponent(
|
||||
withCodingAt("code.coding", "http://loinc.org", "8462-4"),
|
||||
withQuantityAtPath("valueQuantity", 60, "http://unitsofmeasure.org", "mm[Hg]"))
|
||||
);
|
||||
|
||||
|
||||
IIdType obs2Id = createObservation(
|
||||
withObservationComponent(
|
||||
withCodingAt("code.coding", "http://loinc.org", "8480-6"),
|
||||
withQuantityAtPath("valueQuantity", 307, "http://unitsofmeasure.org", "mm[Hg]")),
|
||||
withObservationComponent(
|
||||
withCodingAt("code.coding", "http://loinc.org", "8462-4"),
|
||||
withQuantityAtPath("valueQuantity", 260, "http://unitsofmeasure.org", "mm[Hg]"))
|
||||
);
|
||||
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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 CorrelatedQueries {
|
||||
@Test
|
||||
public void unitsMustMatch() {
|
||||
myResourceId = createObservation(
|
||||
withObservationComponent(
|
||||
withQuantityAtPath("valueQuantity", 42, null, "cats")),
|
||||
withObservationComponent(
|
||||
withQuantityAtPath("valueQuantity", 18, null, "dogs")));
|
||||
|
||||
assertFind("no units matches value", "/Observation?component-value-quantity=42");
|
||||
assertFind("correct units matches value", "/Observation?component-value-quantity=42||cats");
|
||||
assertNotFind("mixed unit from other element in same resource", "/Observation?component-value-quantity=42||dogs");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
public class SpecTestCases {
|
||||
@Test
|
||||
void specCase1() {
|
||||
String id1 = withObservationWithValueQuantity(5.34).getIdPart();
|
||||
String id2 = withObservationWithValueQuantity(5.36).getIdPart();
|
||||
String id3 = withObservationWithValueQuantity(5.40).getIdPart();
|
||||
String id4 = withObservationWithValueQuantity(5.44).getIdPart();
|
||||
String id5 = withObservationWithValueQuantity(5.46).getIdPart();
|
||||
// GET [base]/Observation?value-quantity=5.4 :: 5.4(+/-0.05)
|
||||
assertFindIds("when le", Set.of(id2, id3, id4), "/Observation?value-quantity=5.4");
|
||||
}
|
||||
|
||||
@Test
|
||||
void specCase2() {
|
||||
String id1 = withObservationWithValueQuantity(0.005394).getIdPart();
|
||||
String id2 = withObservationWithValueQuantity(0.005395).getIdPart();
|
||||
String id3 = withObservationWithValueQuantity(0.0054).getIdPart();
|
||||
String id4 = withObservationWithValueQuantity(0.005404).getIdPart();
|
||||
String id5 = withObservationWithValueQuantity(0.005406).getIdPart();
|
||||
// GET [base]/Observation?value-quantity=5.40e-3 :: 0.0054(+/-0.000005)
|
||||
assertFindIds("when le", Set.of(id2, id3, id4), "/Observation?value-quantity=5.40e-3");
|
||||
}
|
||||
|
||||
@Test
|
||||
void specCase6() {
|
||||
String id1 = withObservationWithValueQuantity(4.85).getIdPart();
|
||||
String id2 = withObservationWithValueQuantity(4.86).getIdPart();
|
||||
String id3 = withObservationWithValueQuantity(5.94).getIdPart();
|
||||
String id4 = withObservationWithValueQuantity(5.95).getIdPart();
|
||||
// GET [base]/Observation?value-quantity=ap5.4 :: 5.4(+/- 10%) :: [4.86 ... 5.94]
|
||||
assertFindIds("when le", Set.of(id2, id3), "/Observation?value-quantity=ap5.4");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
public class QuantityNormalizedSearch {
|
||||
|
||||
NormalizedQuantitySearchLevel mySavedNomalizedSetting;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mySavedNomalizedSetting = myDaoConfig.getModelConfig().getNormalizedQuantitySearchLevel();
|
||||
myDaoConfig.getModelConfig().setNormalizedQuantitySearchLevel(
|
||||
NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
myDaoConfig.getModelConfig().setNormalizedQuantitySearchLevel(mySavedNomalizedSetting);
|
||||
}
|
||||
|
||||
@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");
|
||||
|
||||
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 TemperatureUnitConversions {
|
||||
|
||||
@Test
|
||||
public void storeCelsiusSearchFahrenheit() {
|
||||
withObservationWithQuantity(37.5, UCUM_CODESYSTEM_URL, "Cel" );
|
||||
|
||||
assertFind( "when eq UCUM 99.5 degF", "/Observation?value-quantity=99.5|" + UCUM_CODESYSTEM_URL + "|[degF]");
|
||||
assertNotFind( "when eq UCUM 101.1 degF", "/Observation?value-quantity=101.1|" + UCUM_CODESYSTEM_URL + "|[degF]");
|
||||
assertNotFind( "when eq UCUM 97.8 degF", "/Observation?value-quantity=97.8|" + UCUM_CODESYSTEM_URL + "|[degF]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void storeFahrenheitSearchCelsius() {
|
||||
withObservationWithQuantity(99.5, UCUM_CODESYSTEM_URL, "[degF]" );
|
||||
|
||||
assertFind( "when eq UCUM 37.5 Cel", "/Observation?value-quantity=37.5|" + UCUM_CODESYSTEM_URL + "|Cel");
|
||||
assertNotFind( "when eq UCUM 37.3 Cel", "/Observation?value-quantity=37.3|" + UCUM_CODESYSTEM_URL + "|Cel");
|
||||
assertNotFind( "when eq UCUM 37.7 Cel", "/Observation?value-quantity=37.7|" + UCUM_CODESYSTEM_URL + "|Cel");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@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 = createObservation(List.of(
|
||||
withQuantityAtPath("valueQuantity", 0.02, UCUM_CODESYSTEM_URL, "10*6/L"),
|
||||
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 now implemented for normalized quantities
|
||||
*/
|
||||
@Nested
|
||||
public class Sorting {
|
||||
|
||||
@Test
|
||||
public void sortByNumeric() {
|
||||
String idAlpha1 = withObservationWithQuantity(0.06, UCUM_CODESYSTEM_URL, "10*6/L" ).getIdPart(); // 60,000
|
||||
String idAlpha2 = withObservationWithQuantity(50, UCUM_CODESYSTEM_URL, "10*3/L" ).getIdPart(); // 50,000
|
||||
String idAlpha3 = withObservationWithQuantity(0.000070, UCUM_CODESYSTEM_URL, "10*9/L" ).getIdPart(); // 70_000
|
||||
|
||||
// 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(idAlpha2, idAlpha1, idAlpha3));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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 = createObservation(
|
||||
withQuantityAtPath("valueQuantity", theValue, theSystem, theCode)
|
||||
);
|
||||
return myResourceId;
|
||||
}
|
||||
|
||||
private IIdType withObservationWithValueQuantity(double theValue) {
|
||||
myResourceId = createObservation(List.of(withElementAt("valueQuantity",
|
||||
withPrimitiveAttribute("value", theValue),
|
||||
withPrimitiveAttribute("system", UCUM_CODESYSTEM_URL),
|
||||
withPrimitiveAttribute("code", "mm[Hg]")
|
||||
)));
|
||||
return myResourceId;
|
||||
}
|
||||
|
||||
private void assertFindIds(String theMessage, Collection<String> theResourceIds, String theUrl) {
|
||||
List<String> resourceIds = myTestDaoSearch.searchForIds(theUrl);
|
||||
assertEquals(theResourceIds, new HashSet<>(resourceIds), theMessage);
|
||||
}
|
||||
|
||||
}
|
|
@ -135,9 +135,12 @@ public class TestHSearchAddInConfig {
|
|||
luceneHeapProperties.put(BackendSettings.backendKey(LuceneIndexSettings.DIRECTORY_TYPE), "local-heap");
|
||||
luceneHeapProperties.put(BackendSettings.backendKey(LuceneBackendSettings.LUCENE_VERSION), "LUCENE_CURRENT");
|
||||
luceneHeapProperties.put(HibernateOrmMapperSettings.ENABLED, "true");
|
||||
luceneHeapProperties.put(BackendSettings.backendKey(LuceneIndexSettings.IO_WRITER_INFOSTREAM), "true");
|
||||
|
||||
return (theProperties) ->
|
||||
return (theProperties) -> {
|
||||
ourLog.info("Configuring Hibernate Search - {}", luceneHeapProperties);
|
||||
theProperties.putAll(luceneHeapProperties);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -20,8 +20,8 @@
|
|||
<logger name="ca.uhn.fhir.jpa.dao" level="info"/>
|
||||
|
||||
<!-- set to debug to enable term expansion logs -->
|
||||
<logger name="ca.uhn.fhir.jpa.term" level="info"/>
|
||||
|
||||
<logger name="ca.uhn.fhir.jpa.term" level="info"/>
|
||||
<!-- Set to 'trace' to enable SQL logging -->
|
||||
<logger name="org.hibernate.SQL" level="info"/>
|
||||
<!-- Set to 'trace' to enable SQL Value logging -->
|
||||
|
@ -30,7 +30,7 @@
|
|||
<logger name="org.springframework.test.context.cache" level="info"/>
|
||||
<logger name="ca.uhn.fhir.jpa.bulk" level="info"/>
|
||||
|
||||
<!-- debugging -->
|
||||
<!-- more debugging -->
|
||||
<!--
|
||||
<logger name="org.elasticsearch.client" level="trace"/>
|
||||
<logger name="org.hibernate.search.elasticsearch.request" level="TRACE"/>
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
package ca.uhn.fhir.jpa.dao.r4;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.dao.TestDaoSearch;
|
||||
import ca.uhn.fhir.jpa.search.CompositeSearchParameterTestCases;
|
||||
import ca.uhn.fhir.jpa.search.QuantitySearchParameterTestCases;
|
||||
import ca.uhn.fhir.jpa.test.BaseJpaTest;
|
||||
import ca.uhn.fhir.jpa.test.config.TestR4Config;
|
||||
import ca.uhn.fhir.storage.test.BaseDateSearchDaoTests;
|
||||
import ca.uhn.fhir.storage.test.DaoTestDataBuilder;
|
||||
import org.hl7.fhir.r4.model.Observation;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
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.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = {
|
||||
TestR4Config.class,
|
||||
DaoTestDataBuilder.Config.class,
|
||||
TestDaoSearch.Config.class
|
||||
})
|
||||
public class FhirResourceDaoR4StandardQueriesLuceneTest extends BaseJpaTest {
|
||||
FhirContext myFhirContext = FhirContext.forR4Cached();
|
||||
@Autowired
|
||||
PlatformTransactionManager myTxManager;
|
||||
@RegisterExtension
|
||||
@Autowired
|
||||
DaoTestDataBuilder myDataBuilder;
|
||||
@Autowired
|
||||
TestDaoSearch myTestDaoSearch;
|
||||
@Autowired
|
||||
@Qualifier("myObservationDaoR4")
|
||||
IFhirResourceDao<Observation> myObservationDao;
|
||||
|
||||
// todo mb create an extension to restore via clone or xstream + BeanUtils.copyProperties().
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
myDaoConfig.setAdvancedHSearchIndexing(true);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
DaoConfig defaultConfig = new DaoConfig();
|
||||
myDaoConfig.setAdvancedHSearchIndexing(defaultConfig.isAdvancedHSearchIndexing());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FhirContext getFhirContext() {
|
||||
return myFhirContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PlatformTransactionManager getTxManager() {
|
||||
return myTxManager;
|
||||
}
|
||||
|
||||
@Nested
|
||||
public class DateSearchTests extends BaseDateSearchDaoTests {
|
||||
@Override
|
||||
protected Fixture constructFixture() {
|
||||
return new TestDataBuilderFixture<>(myDataBuilder, myObservationDao);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class QuantityAndNormalizedQuantitySearch extends QuantitySearchParameterTestCases {
|
||||
QuantityAndNormalizedQuantitySearch() {
|
||||
super(myDataBuilder, myTestDaoSearch, myDaoConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class CompositeSearch extends CompositeSearchParameterTestCases {
|
||||
CompositeSearch() {
|
||||
super(myDataBuilder, myTestDaoSearch);
|
||||
}
|
||||
|
||||
/** JPA doesn't know which sub-element matches */
|
||||
@Override
|
||||
protected boolean isCorrelatedSupported() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package ca.uhn.fhir.jpa.dao.search;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
|
||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
|
||||
import ca.uhn.fhir.jpa.model.search.CompositeSearchIndexData;
|
||||
import ca.uhn.fhir.jpa.model.search.ExtendedHSearchIndexData;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParamComposite;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorR4;
|
||||
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
|
||||
import ca.uhn.fhir.rest.server.util.FhirContextSearchParamRegistry;
|
||||
import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
|
||||
import ca.uhn.fhir.test.utilities.ITestDataBuilder;
|
||||
import org.hl7.fhir.r4.model.Observation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
|
||||
class ExtendedHSearchIndexExtractorTest implements ITestDataBuilder.WithSupport {
|
||||
FhirContext myFhirContext = FhirContext.forR4Cached();
|
||||
DaoConfig myDaoConfig = new DaoConfig();
|
||||
ModelConfig myModelConfig = new ModelConfig();
|
||||
FhirContextSearchParamRegistry mySearchParamRegistry = new FhirContextSearchParamRegistry(myFhirContext);
|
||||
SearchParamExtractorR4 mySearchParamExtractor = new SearchParamExtractorR4(myModelConfig, new PartitionSettings(), myFhirContext, mySearchParamRegistry);
|
||||
|
||||
|
||||
@Test
|
||||
void testExtract_composite_producesValues() {
|
||||
// setup
|
||||
ResourceIndexedSearchParamComposite composite = new ResourceIndexedSearchParamComposite("component-code-value-concept", "Observation.component");
|
||||
|
||||
ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> codeParams = new ISearchParamExtractor.SearchParamSet<>();
|
||||
codeParams.add(new ResourceIndexedSearchParamToken(new PartitionSettings(), "Observation", "component-code", "https://example.com", "8480-6"));
|
||||
composite.addComponentIndexedSearchParams("component-code", RestSearchParameterTypeEnum.TOKEN, codeParams);
|
||||
|
||||
ISearchParamExtractor.SearchParamSet<BaseResourceIndexedSearchParam> valueParams = new ISearchParamExtractor.SearchParamSet<>();
|
||||
valueParams.add(new ResourceIndexedSearchParamToken(new PartitionSettings(), "Observation", "component-value-concept", "https://example.com", "some_other_value"));
|
||||
composite.addComponentIndexedSearchParams("component-value-concept", RestSearchParameterTypeEnum.TOKEN, valueParams);
|
||||
|
||||
ResourceIndexedSearchParams extractedParams = new ResourceIndexedSearchParams();
|
||||
extractedParams.myCompositeParams.add(composite);
|
||||
|
||||
// run: now translate to HSearch
|
||||
ResourceSearchParams activeSearchParams = mySearchParamRegistry.getActiveSearchParams("Observation");
|
||||
ExtendedHSearchIndexExtractor extractor = new ExtendedHSearchIndexExtractor(
|
||||
myDaoConfig, myFhirContext, activeSearchParams, mySearchParamExtractor, myModelConfig);
|
||||
ExtendedHSearchIndexData indexData = extractor.extract(new Observation(), extractedParams);
|
||||
|
||||
// validate
|
||||
Set<CompositeSearchIndexData> spIndexData = indexData.getSearchParamComposites().get("component-code-value-concept");
|
||||
assertThat(spIndexData, hasSize(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Support getTestDataBuilderSupport() {
|
||||
return new SupportNoDao(myFhirContext);
|
||||
}
|
||||
}
|
|
@ -43,7 +43,7 @@ import org.springframework.context.annotation.Configuration;
|
|||
* Add the inner {@link Config} to your spring context to inject this.
|
||||
* For convenience, you can still implement ITestDataBuilder on your test class, and delegate the missing methods to this bean.
|
||||
*/
|
||||
public class DaoTestDataBuilder implements ITestDataBuilder, AfterEachCallback {
|
||||
public class DaoTestDataBuilder implements ITestDataBuilder.WithSupport, ITestDataBuilder.Support, AfterEachCallback {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(DaoTestDataBuilder.class);
|
||||
|
||||
final FhirContext myFhirCtx;
|
||||
|
@ -78,6 +78,11 @@ public class DaoTestDataBuilder implements ITestDataBuilder, AfterEachCallback {
|
|||
return dao.update(theResource, mySrd).getId().toUnqualifiedVersionless();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Support getTestDataBuilderSupport() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FhirContext getFhirContext() {
|
||||
return myFhirCtx;
|
||||
|
|
|
@ -218,11 +218,24 @@ public interface ITestDataBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
default IIdType createResourceFromJson(String theJson, Consumer<IBaseResource>... theModifiers) {
|
||||
IBaseResource resource = getFhirContext().newJsonParser().parseResource(theJson);
|
||||
applyElementModifiers(resource, theModifiers);
|
||||
|
||||
if (ourLog.isDebugEnabled()) {
|
||||
ourLog.debug("Creating {}", getFhirContext().newJsonParser().encodeResourceToString(resource));
|
||||
}
|
||||
|
||||
if (isNotBlank(resource.getIdElement().getValue())) {
|
||||
return doUpdateResource(resource);
|
||||
} else {
|
||||
return doCreateResource(resource);
|
||||
}
|
||||
}
|
||||
|
||||
default IBaseResource buildResource(String theResourceType, Consumer<IBaseResource>... theModifiers) {
|
||||
IBaseResource resource = getFhirContext().getResourceDefinition(theResourceType).newInstance();
|
||||
for (Consumer<IBaseResource> next : theModifiers) {
|
||||
next.accept(resource);
|
||||
}
|
||||
applyElementModifiers(resource, theModifiers);
|
||||
return resource;
|
||||
}
|
||||
|
||||
|
@ -356,26 +369,28 @@ public interface ITestDataBuilder {
|
|||
|
||||
interface Support {
|
||||
FhirContext getFhirContext();
|
||||
IIdType createResource(IBaseResource theResource);
|
||||
IIdType updateResource(IBaseResource theResource);
|
||||
IIdType doCreateResource(IBaseResource theResource);
|
||||
IIdType doUpdateResource(IBaseResource theResource);
|
||||
}
|
||||
|
||||
// todo mb make this the norm.
|
||||
interface WithSupport extends ITestDataBuilder {
|
||||
Support getSupport();
|
||||
Support getTestDataBuilderSupport();
|
||||
|
||||
@Override
|
||||
default FhirContext getFhirContext() {
|
||||
return getSupport().getFhirContext();
|
||||
return getTestDataBuilderSupport().getFhirContext();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
default IIdType doCreateResource(IBaseResource theResource) {
|
||||
return getSupport().createResource(theResource);
|
||||
return getTestDataBuilderSupport().doCreateResource(theResource);
|
||||
}
|
||||
|
||||
@Override
|
||||
default IIdType doUpdateResource(IBaseResource theResource) {
|
||||
return getSupport().updateResource(theResource);
|
||||
return getTestDataBuilderSupport().doUpdateResource(theResource);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -396,13 +411,13 @@ public interface ITestDataBuilder {
|
|||
}
|
||||
|
||||
@Override
|
||||
public IIdType createResource(IBaseResource theResource) {
|
||||
public IIdType doCreateResource(IBaseResource theResource) {
|
||||
Validate.isTrue(false, "Create not supported");
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IIdType updateResource(IBaseResource theResource) {
|
||||
public IIdType doUpdateResource(IBaseResource theResource) {
|
||||
Validate.isTrue(false, "Update not supported");
|
||||
return null;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue