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:
michaelabuckley 2022-09-26 20:20:06 -04:00 committed by GitHub
parent 641007f5b6
commit 96b92153f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 2554 additions and 1011 deletions

View File

@ -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);

View File

@ -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);
}

View File

@ -0,0 +1,4 @@
---
type: add
issue: 3839
title: "Advanced Lucene indexing now fully supports composite SearchParameters."

View File

@ -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.
## 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
- `:text` provides a rich search syntax as using a [modified Simple Query Syntax](#modified-simple-query-syntax).
## 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.
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 &vert; john | frank &vert; 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.
## 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
## 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 |
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).
## Additional supported Parameters
| Parameter | Supported | type |
| ------------- | ------------- | ------------- |
| _source | yes | URI |
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 `&#92;`.
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 &vert; john | frank &vert; 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](#modified-simple-query-syntax) used by string `:text` searches.
See https://www.hl7.org/fhir/search.html#token.
## 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
@ -103,3 +102,5 @@ As an experimental feature with the extended indexing, the full resource can be
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.
This **experimental** feature is enabled via the `setStoreResourceInHSearchIndex()` option of DaoConfig.

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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;
}
}
}
}

View File

@ -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)
);

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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

View File

@ -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();

View File

@ -0,0 +1,5 @@
package ca.uhn.fhir.jpa.model.search;
public interface CompositeSearchIndexData {
void writeIndexEntry(HSearchIndexWriter theHSearchIndexWriter, HSearchElementCache theRoot);
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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);

View File

@ -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);
}
}
}

View File

@ -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() {
}

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -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;
}

View File

@ -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));

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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;
}
/**

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
};
}

View File

@ -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"/>

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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;
}