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.
## Search Parameter Support
Extended Lucene Indexing supports all of the [core search parameter types](https://www.hl7.org/fhir/search.html).
These include:
- Number
- Date/DateTime
- String
- Token
- Reference
- Composite
- Quantity
- URI
## Date search
We support date searches using the eq, ne, lt, gt, ge, and le comparisons.
See https://www.hl7.org/fhir/search.html#date.
## String search
The Extended Lucene string search indexing supports the default search, as well as the modifiers defined in https://www.hl7.org/fhir/search.html#string.
- Default searching matches by prefix, insensitive to case or accents
- `:exact` matches the entire string, matching case and accents
- `:contains` extends the default search to match any substring of the text
The Extended Lucene string search indexing supports the default search, as well as `:contains`, `:exact`, and `:text` modifiers.
- The default (unmodified) string search matches by prefix, insensitive to case or accents.
- `:exact` matches the entire string, matching case and accents.
- `:contains` match any substring of the text, ignoring case and accents.
- `:text` provides a rich search syntax as using a [modified Simple Query Syntax](#modified-simple-query-syntax).
See https://www.hl7.org/fhir/search.html#string.
## Modified Simple Query Syntax
The `:text` modifier for token and string uses a modified version of the Simple Query Syntax provided by
[Lucene](https://lucene.apache.org/core/8_10_1/queryparser/org/apache/lucene/queryparser/simple/SimpleQueryParser.html) and
[Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html#simple-query-string-syntax).
Terms are delimited by whitespace, or query punctuation `"'()|+`.
Literal uses of these characters must be escaped by `&#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 used by string `:text` searches.
The `:text` modifier provides the same [modified Simple Query Syntax](#modified-simple-query-syntax) used by string `:text` searches.
See https://www.hl7.org/fhir/search.html#token.
## Modified Simple Query Syntax
The `:text` search for token and string, Hapi provides a modified version of the Simple Query Syntax provided by
[Lucene](https://lucene.apache.org/core/8_10_1/queryparser/org/apache/lucene/queryparser/simple/SimpleQueryParser.html) and
[Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html#simple-query-string-syntax).
Terms are delimited by whitespace, or query punctuation `"()|+'`. Literal uses of these characters must be escaped by '\'.
When the query only contains one or more bare terms, they are each converted to a prefix search to match the behaviour of a normal string search.
When multiple terms are present, they must all match (i.e. `AND`). For `OR` behaviour use the `|` operator between terms.
But if any special SQS syntax is active, the query is used as is.
To ensure that the query is used as-is, quote bare terms with the `"` or character. E.g. `without any special syntax characters
Examples:
| Fhir Query String | Executed Query | Matches | No Match | Note |
|-----------------|------------------|-------------|----------------|--------------------------------------------|
| Smit | Smit* | John Smith | John Smi | |
| Jo Smit | Jo* Smit* | John Smith | John Frank | Multiple bare terms are `AND` |
| frank &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.
## Date search
We support date searches using the eq, ne, lt, gt, ge, and le comparisons.
See https://www.hl7.org/fhir/search.html#date.
## Supported Parameters for all resources
| Parameter | Supported | type |
| ------------- | ------------- | ------------- |
| _id | no | |
| _lastUpdated | yes | date |
| _tag | yes | token |
| _profile | yes | URI |
| _security | yes | token |
| _text | yes | string |
| _content | yes | string |
| _list | no |
| _has | no |
| _type | no |
## Additional supported Parameters
| Parameter | Supported | type |
| ------------- | ------------- | ------------- |
| _source | yes | URI |
## Supported Common and Special Search Parameters
| Parameter | Supported | type |
|--------------|-----------|--------|
| _id | no | |
| _lastUpdated | yes | date |
| _tag | yes | token |
| _profile | yes | URI |
| _security | yes | token |
| _text | yes | string |
| _content | yes | string |
| _list | no | |
| _has | no | |
| _type | no | |
| _source | yes | URI |
## ValueSet autocomplete extension
@ -102,4 +101,6 @@ This extension is only valid at the type level, and requires that Extended Lucen
As an experimental feature with the extended indexing, the full resource can be stored in the
search index. This allows some queries to return results without using the relational database.
Note: This does not support the $meta-add or $meta-delete operations. Full reindexing is required
when this option is enabled after resources have been indexed.
when this option is enabled after resources have been indexed.
This **experimental** feature is enabled via the `setStoreResourceInHSearchIndex()` option of DaoConfig.

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