Freetext search sort (#3695)

* Works for date and token. Cleanup required.

* Works for string and quantity.Cleanup pending.

* Works for reference and perform cleanup.

* Cleanup configuration and add missing tests

* Eliminate sort property registry (no go for a cluster) for a static property map.

* Missed in previous commit

* Implement HSearch number parameter

* Implement HSearch number sorting

* Replicate new normalizer configuration from HapiElasticsearchAnalysisConfigurer to HapiLuceneAnalysisConfigurer.
Replace deprecated word filter factory

Co-authored-by: juan.marchionatto <juan.marchionatto@smilecdr.com>
This commit is contained in:
jmarchionatto 2022-06-22 10:31:01 -04:00 committed by GitHub
parent dce330eb01
commit e7ce41e5b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1099 additions and 21 deletions

View File

@ -27,6 +27,8 @@ import ca.uhn.fhir.jpa.config.util.ResourceCountCacheUtil;
import ca.uhn.fhir.jpa.config.util.ValidationSupportConfigUtil;
import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl;
import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
import ca.uhn.fhir.jpa.dao.search.HSearchSortHelperImpl;
import ca.uhn.fhir.jpa.dao.search.IHSearchSortHelper;
import ca.uhn.fhir.jpa.provider.DaoRegistryResourceSupportedSvc;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc;
@ -34,16 +36,39 @@ import ca.uhn.fhir.jpa.search.StaleSearchDeletingSvcImpl;
import ca.uhn.fhir.jpa.util.ResourceCountCache;
import ca.uhn.fhir.jpa.validation.JpaValidationSupportChain;
import ca.uhn.fhir.rest.api.IResourceSupportedSvc;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport;
import org.springframework.batch.core.configuration.annotation.BatchConfigurer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import java.util.List;
import java.util.Map;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.IDX_STRING_LOWER;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.NESTED_SEARCH_PARAM_ROOT;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_PARAM_NAME;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_VALUE;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_VALUE_NORM;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.SEARCH_PARAM_ROOT;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.URI_VALUE;
@Configuration
@Import({JpaConfig.class})
public class HapiJpaConfig {
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
@Bean
public IHSearchSortHelper extendedFulltextSortHelper() {
return new HSearchSortHelperImpl(mySearchParamRegistry);
}
@Bean
public IFulltextSearchSvc fullTextSearchSvc() {
return new FulltextSearchSvcImpl();

View File

@ -28,6 +28,7 @@ import ca.uhn.fhir.jpa.dao.search.ExtendedLuceneClauseBuilder;
import ca.uhn.fhir.jpa.dao.search.ExtendedLuceneIndexExtractor;
import ca.uhn.fhir.jpa.dao.search.ExtendedLuceneResourceProjection;
import ca.uhn.fhir.jpa.dao.search.ExtendedLuceneSearchBuilder;
import ca.uhn.fhir.jpa.dao.search.IHSearchSortHelper;
import ca.uhn.fhir.jpa.dao.search.LastNOperation;
import ca.uhn.fhir.jpa.dao.search.SearchScrollQueryExecutorAdaptor;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
@ -43,12 +44,16 @@ import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.SortOrderEnum;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
import com.google.common.collect.Ordering;
import org.hibernate.search.backend.elasticsearch.ElasticsearchExtension;
import org.hibernate.search.engine.search.query.SearchScroll;
import org.hibernate.search.engine.search.query.dsl.SearchQueryOptionsStep;
import org.hibernate.search.engine.search.sort.dsl.SearchSortFactory;
import org.hibernate.search.engine.search.sort.dsl.SortFinalStep;
import org.hibernate.search.mapper.orm.Search;
import org.hibernate.search.mapper.orm.search.loading.dsl.SearchLoadingOptionsStep;
import org.hibernate.search.mapper.orm.session.SearchSession;
@ -64,6 +69,7 @@ import javax.annotation.Nonnull;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@ -93,6 +99,9 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
@Autowired
ModelConfig myModelConfig;
@Autowired
private IHSearchSortHelper myExtendedFulltextSortHelper;
final private ExtendedLuceneSearchBuilder myAdvancedIndexQueryBuilder = new ExtendedLuceneSearchBuilder();
private Boolean ourDisabled;
@ -163,7 +172,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
private SearchQueryOptionsStep<?, Long, SearchLoadingOptionsStep, ?, ?> getSearchQueryOptionsStep(
String theResourceType, SearchParameterMap theParams, ResourcePersistentId theReferencingPid) {
return getSearchSession().search(ResourceTable.class)
var query= getSearchSession().search(ResourceTable.class)
// The document id is the PK which is pid. We use this instead of _myId to avoid fetching the doc body.
.select(
// adapt the String docRef.id() to the Long that it really is.
@ -209,12 +218,21 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
if (myDaoConfig.isAdvancedLuceneIndexing() && theParams.getEverythingMode() == null) {
myAdvancedIndexQueryBuilder.addAndConsumeAdvancedQueryClauses(builder, theResourceType, theParams, mySearchParamRegistry);
}
//DROP EARLY HERE IF BOOL IS EMPTY?
})
);
}
if (theParams.getSort() != null) {
query.sort( f -> myExtendedFulltextSortHelper.getSortClauses(f, theParams.getSort(), theResourceType) );
// indicate parameter was processed
theParams.setSort(null);
}
return query;
}
@Nonnull
@ -321,6 +339,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
if (thePids.isEmpty()) {
return Collections.emptyList();
}
SearchSession session = getSearchSession();
List<ExtendedLuceneResourceProjection> rawResourceDataList = session.search(ResourceTable.class)
.select(
@ -331,15 +350,30 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
f.field("myRawResource", String.class))
)
.where(
f -> f.id().matchingAny(thePids) // matches '_id' from resource index
).fetchAllHits();
f -> f.id().matchingAny(thePids))
.fetchAllHits(); // matches '_id' from resource index
ArrayList<Long> pidList = new ArrayList<>(thePids);
List<ExtendedLuceneResourceProjection> orderedAsPidsResourceDataList = rawResourceDataList.stream()
.sorted( Ordering.explicit(pidList).onResultOf(ExtendedLuceneResourceProjection::getPid) ).collect( Collectors.toList() );
IParser parser = myFhirContext.newJsonParser();
return rawResourceDataList.stream()
return orderedAsPidsResourceDataList.stream()
.map(p -> p.toResource(parser))
.collect(Collectors.toList());
}
private SortFinalStep getSortOrder(SearchSortFactory theF, SortOrderEnum theOrder) {
var finalSortStep = theF.field("myId");
if (theOrder == SortOrderEnum.DESC) {
finalSortStep.desc();
} else {
finalSortStep.asc();
}
return finalSortStep;
}
@Override
public long count(String theResourceName, SearchParameterMap theParams) {

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.dao.search;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
@ -30,6 +31,7 @@ import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.NumberParam;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.rest.param.QuantityParam;
import ca.uhn.fhir.rest.param.ReferenceParam;
@ -46,6 +48,7 @@ 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.SearchPredicateFactory;
import org.hibernate.search.util.common.data.RangeBoundInclusion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -64,6 +67,7 @@ import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.IDX_STRING
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.IDX_STRING_NORMALIZED;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.IDX_STRING_TEXT;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.NESTED_SEARCH_PARAM_ROOT;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.NUMBER_VALUE;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_CODE;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_CODE_NORM;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_PARAM_NAME;
@ -72,6 +76,7 @@ import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_VALUE;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_VALUE_NORM;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.SEARCH_PARAM_ROOT;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.URI_VALUE;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class ExtendedLuceneClauseBuilder {
@ -636,4 +641,61 @@ public class ExtendedLuceneClauseBuilder {
myRootClause.must(orTermPredicate);
}
}
public void addNumberUnmodifiedSearch(String theParamName, List<List<IQueryParameterType>> theNumberUnmodifiedAndOrTerms) {
String fieldPath = String.join(".", SEARCH_PARAM_ROOT, theParamName, NUMBER_VALUE);
for (List<IQueryParameterType> nextAnd : theNumberUnmodifiedAndOrTerms) {
List<NumberParam> orTerms = nextAnd.stream().map(NumberParam.class::cast).collect(Collectors.toList());
BooleanPredicateClausesStep<?> numberPredicateStep = myPredicateFactory.bool();
numberPredicateStep.minimumShouldMatchNumber(1);
for (NumberParam orTerm : orTerms) {
double value = orTerm.getValue().doubleValue();
double approxTolerance = value * QTY_APPROX_TOLERANCE_PERCENT;
double defaultTolerance = value * QTY_TOLERANCE_PERCENT;
ParamPrefixEnum activePrefix = orTerm.getPrefix() == null ? ParamPrefixEnum.EQUAL : orTerm.getPrefix();
switch (activePrefix) {
case APPROXIMATE:
numberPredicateStep.should(myPredicateFactory.range()
.field(fieldPath).between(value - approxTolerance, value + approxTolerance));
break;
case EQUAL:
numberPredicateStep.should(myPredicateFactory.range()
.field(fieldPath).between(value - defaultTolerance, value + defaultTolerance));
break;
case STARTS_AFTER:
case GREATERTHAN:
numberPredicateStep.should(myPredicateFactory.range().field(fieldPath).greaterThan(value));
break;
case GREATERTHAN_OR_EQUALS:
numberPredicateStep.should(myPredicateFactory.range().field(fieldPath).atLeast(value));
break;
case ENDS_BEFORE:
case LESSTHAN:
numberPredicateStep.should(myPredicateFactory.range().field(fieldPath).lessThan(value));
break;
case LESSTHAN_OR_EQUALS:
numberPredicateStep.should(myPredicateFactory.range().field(fieldPath).atMost(value));
break;
case NOT_EQUAL:
numberPredicateStep.mustNot(myPredicateFactory.match().field(fieldPath).matching(value));
break;
}
}
myRootClause.must(numberPredicateStep);
}
}
}

View File

@ -87,6 +87,9 @@ public class ExtendedLuceneIndexExtractor {
theNewParams.myTokenParams.forEach(nextParam ->
retVal.addTokenIndexDataIfNotPresent(nextParam.getParamName(), nextParam.getSystem(), nextParam.getValue()));
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()));

View File

@ -53,4 +53,8 @@ public class ExtendedLuceneResourceProjection {
return result;
}
public long getPid() {
return myPid;
}
}

View File

@ -23,9 +23,9 @@ package ca.uhn.fhir.jpa.dao.search;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.NumberParam;
import ca.uhn.fhir.rest.param.QuantityParam;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.StringParam;
@ -57,6 +57,7 @@ public class ExtendedLuceneSearchBuilder {
*/
public boolean isSupportsSomeOf(SearchParameterMap myParams) {
return
myParams.getSort() != null ||
myParams.getLastUpdated() != null ||
myParams.entrySet().stream()
.filter(e -> !ourUnsafeSearchParmeters.contains(e.getKey()))
@ -117,6 +118,9 @@ public class ExtendedLuceneSearchBuilder {
} else if (param instanceof UriParam) {
return modifier.equals(EMPTY_MODIFIER);
} else if (param instanceof NumberParam) {
return modifier.equals(EMPTY_MODIFIER);
} else {
return false;
}
@ -178,6 +182,12 @@ public class ExtendedLuceneSearchBuilder {
case URI:
List<List<IQueryParameterType>> uriUnmodifiedAndOrTerms = theParams.removeByNameUnmodified(nextParam);
builder.addUriUnmodifiedSearch(nextParam, uriUnmodifiedAndOrTerms);
break;
case NUMBER:
List<List<IQueryParameterType>> numberUnmodifiedAndOrTerms = theParams.remove(nextParam);
builder.addNumberUnmodifiedSearch(nextParam, numberUnmodifiedAndOrTerms);
break;
default:
// ignore unsupported param types/modifiers. They will be processed up in SearchBuilder.
@ -191,8 +201,6 @@ public class ExtendedLuceneSearchBuilder {
? theParams.getLastUpdated().getLowerBound()
: theParams.getLastUpdated().getUpperBound();
TemporalPrecisionEnum precision = activeBound.getPrecision();
List<List<IQueryParameterType>> result = List.of( List.of(activeBound) );
// indicate parameter was processed

View File

@ -0,0 +1,145 @@
package ca.uhn.fhir.jpa.dao.search;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.api.SortOrderEnum;
import ca.uhn.fhir.rest.api.SortSpec;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
import com.google.common.annotations.VisibleForTesting;
import org.hibernate.search.engine.search.sort.dsl.SearchSortFactory;
import org.hibernate.search.engine.search.sort.dsl.SortFinalStep;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.IDX_STRING_LOWER;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.NESTED_SEARCH_PARAM_ROOT;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.NUMBER_VALUE;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_PARAM_NAME;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_VALUE;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_VALUE_NORM;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.SEARCH_PARAM_ROOT;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.URI_VALUE;
/**
* Used to build HSearch sort clauses.
*/
public class HSearchSortHelperImpl implements IHSearchSortHelper {
private static final Logger ourLog = LoggerFactory.getLogger(HSearchSortHelperImpl.class);
/** Indicates which HSearch properties must be sorted for each RestSearchParameterTypeEnum **/
private Map<RestSearchParameterTypeEnum, List<String>> mySortPropertyListMap = Map.of(
RestSearchParameterTypeEnum.STRING, List.of(SEARCH_PARAM_ROOT + ".*.string." + IDX_STRING_LOWER),
RestSearchParameterTypeEnum.TOKEN, List.of(
String.join(".", NESTED_SEARCH_PARAM_ROOT, "*", "token", "system"),
String.join(".", NESTED_SEARCH_PARAM_ROOT, "*", "token", "code") ),
RestSearchParameterTypeEnum.REFERENCE, List.of(SEARCH_PARAM_ROOT + ".*.reference.value"),
RestSearchParameterTypeEnum.DATE, List.of(SEARCH_PARAM_ROOT + ".*.dt.lower-ord"),
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) ),
RestSearchParameterTypeEnum.URI, List.of(SEARCH_PARAM_ROOT + ".*." + URI_VALUE),
RestSearchParameterTypeEnum.NUMBER, List.of(SEARCH_PARAM_ROOT + ".*." + NUMBER_VALUE)
);
private final ISearchParamRegistry mySearchParamRegistry;
public HSearchSortHelperImpl(ISearchParamRegistry theSearchParamRegistry) {
mySearchParamRegistry = theSearchParamRegistry;
}
/**
* Builds and returns sort clauses for received sort parameters
*/
@Override
public SortFinalStep getSortClauses(SearchSortFactory theSortFactory, SortSpec theSortParams, String theResourceType) {
var sortStep = theSortFactory.composite();
Optional<SortFinalStep> sortClauseOpt = getSortClause(theSortFactory, theSortParams, theResourceType);
sortClauseOpt.ifPresent(sortStep::add);
SortSpec nextParam = theSortParams.getChain();
while( nextParam != null ) {
sortClauseOpt = getSortClause(theSortFactory, nextParam, theResourceType);
sortClauseOpt.ifPresent(sortStep::add);
nextParam = nextParam.getChain();
}
return sortStep;
}
/**
* Builds sort clauses for the received SortSpec by
* _ finding out the corresponding RestSearchParameterTypeEnum for the parameter
* _ obtaining the list of properties to sort for the found parameter type
* _ building the sort clauses for the found list of properties
*/
@VisibleForTesting
Optional<SortFinalStep> getSortClause(SearchSortFactory theF, SortSpec theSortSpec, String theResourceType) {
Optional<RestSearchParameterTypeEnum> paramTypeOpt = getParamType(theResourceType, theSortSpec.getParamName());
if (paramTypeOpt.isEmpty()) {
ourLog.warn("Sprt parameter type couldn't be determined for parameter: " + theSortSpec.getParamName() +
". Result will not be properly sorted");
return Optional.empty();
}
List<String> paramFieldNameList = getSortPropertyList(paramTypeOpt.get(), theSortSpec.getParamName());
if (paramFieldNameList.isEmpty()) {
ourLog.warn("Unable to sort by parameter '" + theSortSpec.getParamName() + "'. Sort parameter ignored.");
return Optional.empty();
}
var sortFinalStep = theF.composite();
for (String fieldName : paramFieldNameList) {
var sortStep = theF.field(fieldName);
if (theSortSpec.getOrder().equals(SortOrderEnum.DESC)) {
sortStep.desc();
} else {
sortStep.asc();
}
// field could have no value
sortFinalStep.add( sortStep.missing().last() );
}
return Optional.of(sortFinalStep);
}
/**
* Finds out and returns the parameter type for each parameter name
*/
@VisibleForTesting
Optional<RestSearchParameterTypeEnum> getParamType(String theResourceTypeName, String theParamName) {
ResourceSearchParams activeSearchParams = mySearchParamRegistry.getActiveSearchParams(theResourceTypeName);
RuntimeSearchParam searchParam = activeSearchParams.get(theParamName);
if (searchParam == null) {
return Optional.empty();
}
return Optional.of(searchParam.getParamType());
}
/**
* Retrieves the generic property names (* instead of parameter name) from the configured map and
* replaces the '*' segment by theParamName before returning the final property name list
*/
@VisibleForTesting
List<String> getSortPropertyList(RestSearchParameterTypeEnum theParamType, String theParamName) {
List<String> paramFieldNameList = mySortPropertyListMap.get(theParamType);
// replace '*' names segment by theParamName
return paramFieldNameList.stream().map(s -> s.replace("*", theParamName)).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,14 @@
package ca.uhn.fhir.jpa.dao.search;
import ca.uhn.fhir.rest.api.SortSpec;
import org.hibernate.search.engine.search.sort.dsl.SearchSortFactory;
import org.hibernate.search.engine.search.sort.dsl.SortFinalStep;
/**
* Helper for building freetext sort clauses
*/
public interface IHSearchSortHelper {
SortFinalStep getSortClauses(SearchSortFactory theSortFactory, SortSpec theSort, String theResourceType);
}

View File

@ -36,6 +36,8 @@ import org.hibernate.search.backend.lucene.analysis.LuceneAnalysisConfigurationC
import org.hibernate.search.backend.lucene.analysis.LuceneAnalysisConfigurer;
import org.springframework.stereotype.Component;
import static ca.uhn.fhir.jpa.model.search.SearchParamTextPropertyBinder.LOWERCASE_ASCIIFOLDING_NORMALIZER;
/**
* Factory for defining the analysers.
*/
@ -96,5 +98,10 @@ public class HapiLuceneAnalysisConfigurer implements LuceneAnalysisConfigurer {
theLuceneCtx.analyzer("termConceptPropertyAnalyzer").custom()
.tokenizer(WhitespaceTokenizerFactory.class);
theLuceneCtx.normalizer(LOWERCASE_ASCIIFOLDING_NORMALIZER).custom()
.tokenFilter(LowerCaseFilterFactory.class)
.tokenFilter(ASCIIFoldingFilterFactory.class);
}
}

View File

@ -371,9 +371,10 @@ public class SearchBuilder implements ISearchBuilder {
theParams.getNearDistanceParam() == null &&
theParams.getLastUpdated() == null &&
theParams.getEverythingMode() == null &&
theParams.getOffset() == null &&
// or sorting?
theParams.getSort() == null
theParams.getOffset() == null
// &&
// // or sorting?
// theParams.getSort() == null
);
if (canSkipDatabase) {

View File

@ -43,6 +43,7 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.Properties;
import static org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings.Defaults.SCROLL_TIMEOUT;
import static org.slf4j.LoggerFactory.getLogger;
/**
@ -61,6 +62,7 @@ public class ElasticsearchHibernatePropertiesBuilder {
private String myUsername;
private String myPassword;
private long myIndexManagementWaitTimeoutMillis = 10000L;
private long myScrollTimeoutSecs = SCROLL_TIMEOUT;
private String myDebugSyncStrategy = AutomaticIndexingSynchronizationStrategyNames.ASYNC;
private boolean myDebugPrettyPrintJsonLog = false;
private String myProtocol;
@ -97,6 +99,7 @@ public class ElasticsearchHibernatePropertiesBuilder {
// Only for unit tests
theProperties.put(HibernateOrmMapperSettings.AUTOMATIC_INDEXING_SYNCHRONIZATION_STRATEGY, myDebugSyncStrategy);
theProperties.put(BackendSettings.backendKey(ElasticsearchBackendSettings.LOG_JSON_PRETTY_PRINTING), Boolean.toString(myDebugPrettyPrintJsonLog));
theProperties.put(BackendSettings.backendKey(ElasticsearchBackendSettings.SCROLL_TIMEOUT), Long.toString(myScrollTimeoutSecs));
//This tells elasticsearch to use our custom index naming strategy.
theProperties.put(BackendSettings.backendKey(ElasticsearchBackendSettings.LAYOUT_STRATEGY), IndexNamePrefixLayoutStrategy.class.getName());
@ -129,6 +132,11 @@ public class ElasticsearchHibernatePropertiesBuilder {
return this;
}
public ElasticsearchHibernatePropertiesBuilder setScrollTimeoutSecs(long theScrollTimeoutSecs) {
myScrollTimeoutSecs = theScrollTimeoutSecs;
return this;
}
public ElasticsearchHibernatePropertiesBuilder setDebugIndexSyncStrategy(String theSyncStrategy) {
myDebugSyncStrategy = theSyncStrategy;
return this;

View File

@ -24,6 +24,9 @@ import ca.uhn.fhir.jpa.search.HapiLuceneAnalysisConfigurer;
import org.hibernate.search.backend.elasticsearch.analysis.ElasticsearchAnalysisConfigurationContext;
import org.hibernate.search.backend.elasticsearch.analysis.ElasticsearchAnalysisConfigurer;
import static ca.uhn.fhir.jpa.model.search.SearchParamTextPropertyBinder.LOWERCASE_ASCIIFOLDING_NORMALIZER;
public class HapiElasticsearchAnalysisConfigurer implements ElasticsearchAnalysisConfigurer{
@Override
@ -89,5 +92,9 @@ public class HapiElasticsearchAnalysisConfigurer implements ElasticsearchAnalysi
theConfigCtx.analyzer("termConceptPropertyAnalyzer").custom()
.tokenizer("whitespace");
theConfigCtx.normalizer(LOWERCASE_ASCIIFOLDING_NORMALIZER).custom()
.tokenFilters("lowercase", "asciifolding");
}
}

View File

@ -0,0 +1,129 @@
package ca.uhn.fhir.jpa.dao.search;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.api.SortOrderEnum;
import ca.uhn.fhir.rest.api.SortSpec;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
import org.hamcrest.Matchers;
import org.hibernate.search.engine.search.sort.dsl.CompositeSortComponentsStep;
import org.hibernate.search.engine.search.sort.dsl.FieldSortMissingValueBehaviorStep;
import org.hibernate.search.engine.search.sort.dsl.FieldSortOptionsStep;
import org.hibernate.search.engine.search.sort.dsl.SearchSortFactory;
import org.hibernate.search.engine.search.sort.dsl.SortFinalStep;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import java.util.Optional;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@SuppressWarnings({"rawtypes", "unchecked"})
class HSearchSortHelperImplTest {
@InjectMocks
@Spy private HSearchSortHelperImpl tested;
@Mock private ISearchParamRegistry mockSearchParamRegistry;
@Mock private ResourceSearchParams mockResourceSearchParams;
@Mock private RuntimeSearchParam mockRuntimeSearchParam;
@Mock private SearchSortFactory mockSearchSortFactory;
@Mock private CompositeSortComponentsStep mockCompositeSortComponentsStep;
@Mock private FieldSortOptionsStep mockFieldSortOptionsStep;
@Mock private SortFinalStep mockSortFinalStep;
@Mock private FieldSortMissingValueBehaviorStep mockFieldSortMissingValueBehaviorStep;
/**
* Validates gets from map theParamType and replaces '*' in name by theParamName
*/
@Test
void testGetSortPropertyList() {
SortSpec sortSpec = new SortSpec();
sortSpec.setParamName("_tag");
List<String> sortPropertyList = tested.getSortPropertyList(RestSearchParameterTypeEnum.TOKEN, "the-param-name");
assertThat(sortPropertyList, Matchers.contains("nsp.the-param-name.token.system", "nsp.the-param-name.token.code"));
}
/**
* Validates invokes SearchParamRegistry.getActiveSearchParams for received resourceTypeName and returns the
* RuntimeSearchParam for the param name
*/
@Test
void testGetParamType() {
SortSpec sortSpec = new SortSpec();
sortSpec.setParamName("_tag");
when(mockSearchParamRegistry.getActiveSearchParams("Observation")).thenReturn(mockResourceSearchParams);
when(mockResourceSearchParams.get("the-param-name")).thenReturn(mockRuntimeSearchParam);
when(mockRuntimeSearchParam.getParamType()).thenReturn(RestSearchParameterTypeEnum.TOKEN);
Optional<RestSearchParameterTypeEnum> paramType = tested.getParamType("Observation", "the-param-name");
verify(mockSearchParamRegistry, times(1)).getActiveSearchParams("Observation");
verify(mockResourceSearchParams, times(1)).get("the-param-name");
assertFalse(paramType.isEmpty());
}
@Test
void testGetSortClause() {
SortSpec sortSpec = new SortSpec();
sortSpec.setParamName("_tag");
sortSpec.setOrder(SortOrderEnum.DESC);
doReturn(Optional.of(RestSearchParameterTypeEnum.TOKEN)).when(tested).getParamType("Observation", "_tag");
doReturn(List.of("aaa._tag.bbb.ccc", "ddd._tag.eee.fff")).when(tested).getSortPropertyList(RestSearchParameterTypeEnum.TOKEN, "_tag");
when(mockSearchSortFactory.composite()).thenReturn(mockCompositeSortComponentsStep);
when(mockSearchSortFactory.field("aaa._tag.bbb.ccc")).thenReturn(mockFieldSortOptionsStep);
when(mockSearchSortFactory.field("ddd._tag.eee.fff")).thenReturn(mockFieldSortOptionsStep);
when(mockFieldSortOptionsStep.missing()).thenReturn(mockFieldSortMissingValueBehaviorStep);
Optional<SortFinalStep> sortFieldStepOpt = tested.getSortClause(mockSearchSortFactory, sortSpec, "Observation");
assertFalse(sortFieldStepOpt.isEmpty());
verify(mockSearchSortFactory, times(1)).composite();
verify(mockSearchSortFactory, times(1)).field("aaa._tag.bbb.ccc");
verify(mockSearchSortFactory, times(1)).field("ddd._tag.eee.fff");
verify(mockFieldSortOptionsStep, times(2)).desc();
verify(mockFieldSortMissingValueBehaviorStep, times(2)).last();
}
@Test
void testGetSortClauses() {
SortSpec sortSpec = new SortSpec();
sortSpec.setParamName("_tag");
SortSpec sortSpec2 = new SortSpec();
sortSpec2.setParamName("param-name-B");
sortSpec2.setOrder(SortOrderEnum.ASC);
sortSpec.setChain(sortSpec2);
when(mockSearchSortFactory.composite()).thenReturn(mockCompositeSortComponentsStep);
doReturn(Optional.of(mockSortFinalStep)).when(tested).getSortClause(mockSearchSortFactory, sortSpec, "Observation");
doReturn(Optional.of(mockSortFinalStep)).when(tested).getSortClause(mockSearchSortFactory, sortSpec2, "Observation");
SortFinalStep sortFinalStep = tested.getSortClauses(mockSearchSortFactory, sortSpec, "Observation");
verify(mockSearchSortFactory, times(1)).composite();
verify(tested, times(1)).getSortClause(mockSearchSortFactory, sortSpec, "Observation");
verify(tested, times(1)).getSortClause(mockSearchSortFactory, sortSpec2, "Observation");
verify(mockCompositeSortComponentsStep, times(2)).add(mockSortFinalStep);
}
}

View File

@ -31,6 +31,7 @@ import org.hl7.fhir.instance.model.api.IBaseCoding;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import java.util.Date;
import java.util.Objects;
import java.util.function.BiConsumer;
@ -47,6 +48,7 @@ public class ExtendedLuceneIndexData {
final SetMultimap<String, String> mySearchParamStrings = HashMultimap.create();
final SetMultimap<String, IBaseCoding> mySearchParamTokens = HashMultimap.create();
final SetMultimap<String, BigDecimal> mySearchParamNumbers = HashMultimap.create();
final SetMultimap<String, String> mySearchParamLinks = HashMultimap.create();
final SetMultimap<String, String> mySearchParamUri = HashMultimap.create();
final SetMultimap<String, DateSearchIndexData> mySearchParamDates = HashMultimap.create();
@ -93,6 +95,7 @@ public class ExtendedLuceneIndexData {
mySearchParamLinks.forEach(ifNotContained(indexWriter::writeReferenceIndex));
// we want to receive the whole entry collection for each invocation
Multimaps.asMap(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));
@ -129,6 +132,10 @@ public class ExtendedLuceneIndexData {
mySearchParamDates.put(theSpName, new DateSearchIndexData(theLowerBound, theLowerBoundOrdinal, theUpperBound, theUpperBoundOrdinal));
}
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));
}

View File

@ -38,6 +38,7 @@ public class HibernateSearchIndexWriter {
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";
@ -50,6 +51,8 @@ public class HibernateSearchIndexWriter {
public static final String URI_VALUE = "uri-value";
public static final String NUMBER_VALUE = "number-value";
final HibernateSearchElementCache myNodeCache;
@ -78,6 +81,8 @@ public class HibernateSearchIndexWriter {
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);
ourLog.debug("Adding Search Param Text: {} -- {}", theSearchParam, theValue);
}
@ -85,9 +90,11 @@ public class HibernateSearchIndexWriter {
DocumentElement nestedRoot = myNodeCache.getObjectElement(NESTED_SEARCH_PARAM_ROOT);
DocumentElement nestedSpNode = nestedRoot.addObject(theSearchParam);
DocumentElement nestedTokenNode = nestedSpNode.addObject("token");
nestedTokenNode.addValue("code", theValue.getCode());
nestedTokenNode.addValue("system", theValue.getSystem());
nestedTokenNode.addValue("code-system", theValue.getSystem() + "|" + theValue.getCode());
if (StringUtils.isNotEmpty(theValue.getDisplay())) {
DocumentElement nestedStringNode = nestedSpNode.addObject("string");
nestedStringNode.addValue(IDX_STRING_TEXT, theValue.getDisplay());
@ -116,10 +123,12 @@ public class HibernateSearchIndexWriter {
// Upper bound
dateIndexNode.addValue("upper-ord", theValue.getUpperBoundOrdinal());
dateIndexNode.addValue("upper", theValue.getUpperBoundDate().toInstant());
ourLog.trace("Adding Search Param Reference: {} -- {}", theSearchParam, theValue);
ourLog.trace("Adding Search Param Date. param: {} -- {}", theSearchParam, theValue);
}
public void writeQuantityIndex(String theSearchParam, Collection<QuantitySearchIndexData> theValueCollection) {
DocumentElement nestedRoot = myNodeCache.getObjectElement(NESTED_SEARCH_PARAM_ROOT);
@ -158,4 +167,14 @@ public class HibernateSearchIndexWriter {
uriNode.addValue(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());
}
}
}

View File

@ -41,8 +41,10 @@ import org.slf4j.LoggerFactory;
import java.time.Instant;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.IDX_STRING_EXACT;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.IDX_STRING_LOWER;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.IDX_STRING_NORMALIZED;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.IDX_STRING_TEXT;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.NUMBER_VALUE;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_CODE;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_CODE_NORM;
import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.QTY_SYSTEM;
@ -58,9 +60,10 @@ import static ca.uhn.fhir.jpa.model.search.HibernateSearchIndexWriter.URI_VALUE;
* Identifier.type.text
*/
public class SearchParamTextPropertyBinder implements PropertyBinder, PropertyBridge<ExtendedLuceneIndexData> {
private static final Logger ourLog = LoggerFactory.getLogger(SearchParamTextPropertyBinder.class);
public static final String SEARCH_PARAM_TEXT_PREFIX = "text-";
private static final Logger ourLog = LoggerFactory.getLogger(SearchParamTextPropertyBinder.class);
public static final String LOWERCASE_ASCIIFOLDING_NORMALIZER = "lowercaseAsciifoldingNormalizer";
@Override
public void bind(PropertyBindingContext thePropertyBindingContext) {
@ -87,6 +90,12 @@ public class SearchParamTextPropertyBinder implements PropertyBinder, PropertyBr
.analyzer("standardAnalyzer")
.projectable(Projectable.NO);
StringIndexFieldTypeOptionsStep<?> lowerCaseNormalizer =
indexFieldTypeFactory.asString()
.normalizer(LOWERCASE_ASCIIFOLDING_NORMALIZER)
.sortable(Sortable.YES)
.projectable(Projectable.YES);
StringIndexFieldTypeOptionsStep<?> exactAnalyzer =
indexFieldTypeFactory.asString()
.analyzer("exactAnalyzer") // default max-length is 256. Is that enough for code system uris?
@ -99,6 +108,7 @@ public class SearchParamTextPropertyBinder implements PropertyBinder, PropertyBr
StringIndexFieldTypeOptionsStep<?> keywordFieldType = indexFieldTypeFactory.asString()
// TODO JB: may have to add normalizer to support case insensitive searches depending on token flags
.projectable(Projectable.NO)
.sortable(Sortable.YES)
.aggregable(Aggregable.YES);
StandardIndexFieldTypeOptionsStep<?, Instant> dateTimeFieldType = indexFieldTypeFactory.asInstant()
@ -152,6 +162,7 @@ public class SearchParamTextPropertyBinder implements PropertyBinder, PropertyBr
spfield.fieldTemplate("string-norm", normStringAnalyzer).matchingPathGlob(stringPathGlob + "." + IDX_STRING_NORMALIZED).multiValued();
spfield.fieldTemplate("string-exact", exactAnalyzer).matchingPathGlob(stringPathGlob + "." + IDX_STRING_EXACT).multiValued();
spfield.fieldTemplate("string-text", standardAnalyzer).matchingPathGlob(stringPathGlob + "." + IDX_STRING_TEXT).multiValued();
spfield.fieldTemplate("string-lower", lowerCaseNormalizer).matchingPathGlob(stringPathGlob + "." + IDX_STRING_LOWER).multiValued();
nestedSpField.objectFieldTemplate("nestedStringIndex", ObjectStructure.FLATTENED).matchingPathGlob(stringPathGlob);
nestedSpField.fieldTemplate("string-text", standardAnalyzer).matchingPathGlob(stringPathGlob + "." + IDX_STRING_TEXT).multiValued();
@ -179,6 +190,9 @@ public class SearchParamTextPropertyBinder implements PropertyBinder, PropertyBr
// uri
spfield.fieldTemplate("uriValueTemplate", keywordFieldType).matchingPathGlob("*." + URI_VALUE).multiValued();
// number
spfield.fieldTemplate("numberValueTemplate", bigDecimalFieldType).matchingPathGlob("*." + NUMBER_VALUE);
//quantity
String quantityPathGlob = "*.quantity";
nestedSpField.objectFieldTemplate("quantityTemplate", ObjectStructure.FLATTENED).matchingPathGlob(quantityPathGlob);

View File

@ -177,6 +177,8 @@ public class TestHibernateSearchAddInConfig {
.setIndexSchemaManagementStrategy(SchemaManagementStrategyName.CREATE)
.setIndexManagementWaitTimeoutMillis(10000)
.setRequiredIndexStatus(IndexStatus.YELLOW)
.setScrollTimeoutSecs(60 * 30) // 30 min for tests
.setHosts(host + ":" + httpPort)
.setProtocol("http")
.setUsername("")

View File

@ -55,6 +55,8 @@ import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CodeSystem;
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.Encounter;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Meta;
@ -65,6 +67,7 @@ import org.hl7.fhir.r4.model.Quantity;
import org.hl7.fhir.r4.model.Questionnaire;
import org.hl7.fhir.r4.model.QuestionnaireResponse;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.RiskAssessment;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.ValueSet;
import org.junit.jupiter.api.AfterEach;
@ -91,8 +94,11 @@ import javax.persistence.EntityManager;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@ -163,6 +169,11 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
@Autowired
@Qualifier("myEncounterDaoR4")
private IFhirResourceDao<Encounter> myEncounterDao;
@Autowired
@Qualifier("myRiskAssessmentDaoR4")
protected IFhirResourceDao<RiskAssessment> myRiskAssessmentDao;
@Autowired
@Qualifier("mySystemDaoR4")
private IFhirSystemDao<Bundle, Meta> mySystemDao;
@ -927,7 +938,7 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
}
@Test
public void sortStillRequiresSql() {
public void sortDoesntRequireSqlAnymore() {
IIdType id = myTestDataBuilder.createObservation(List.of(myTestDataBuilder.withObservationCode("http://example.com/", "theCode")));
myCaptureQueriesListener.clear();
@ -938,7 +949,7 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
assertThat(ids, hasSize(1));
assertThat(ids, contains(id.getIdPart()));
assertEquals(1, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "the pids come from elastic, but we use sql to sort");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "the pids come from elastic, but we use sql to sort");
}
@Test
@ -1492,21 +1503,20 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
}
/**
* Sorting is not implemented for normalized quantities, so quantities will be sorted
* by their absolute values (with no unit conversions)
* Sorting is now implemented for normalized quantities
*/
@Nested
public class Sorting {
@Test
public void sortByNumeric() {
String idAlpha6 = withObservationWithQuantity(0.06, UCUM_CODESYSTEM_URL, "10*6/L" ).getIdPart();
String idAlpha5 = withObservationWithQuantity(50, UCUM_CODESYSTEM_URL, "10*3/L" ).getIdPart();
String idAlpha7 = withObservationWithQuantity(0.000070, UCUM_CODESYSTEM_URL, "10*9/L" ).getIdPart();
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(idAlpha7, idAlpha6, idAlpha5));
assertThat(allIds, contains(idAlpha2, idAlpha1, idAlpha3));
}
}
@ -1836,6 +1846,566 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
}
}
@Nested
public class SortParameter {
@BeforeEach
void setUp() {
myDaoConfig.getModelConfig().setNormalizedQuantitySearchLevel(
NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED);
}
@BeforeEach
public void enableContainsAndLucene() {
myDaoConfig.setAllowContainsSearches(true);
myDaoConfig.setAdvancedLuceneIndexing(true);
myDaoConfig.setStoreResourceInLuceneIndex(true);
myDaoConfig.getModelConfig().setNormalizedQuantitySearchLevel(
NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED);
}
@AfterEach
public void restoreContains() {
DaoConfig defaultConfig = new DaoConfig();
myDaoConfig.setAllowContainsSearches(defaultConfig.isAllowContainsSearches());
myDaoConfig.setAdvancedLuceneIndexing(defaultConfig.isAdvancedLuceneIndexing());
myDaoConfig.setStoreResourceInLuceneIndex(defaultConfig.isStoreResourceInLuceneIndex());
myDaoConfig.getModelConfig().setNormalizedQuantitySearchLevel(
defaultConfig.getModelConfig().getNormalizedQuantitySearchLevel() );
}
@Nested
public class OneProperty {
@Nested
public class NotIncludingNulls {
@Test
public void byTokenSystemFirst() {
String id1 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withTag("http://example.orgA", "aTagD")
)).getIdPart();
String id2 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withTag("http://example.orgB", "aTagC")
)).getIdPart();
myCaptureQueriesListener.clear();
IBundleProvider result = myTestDaoSearch.searchForBundleProvider("/Observation?_sort=-_tag");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
// asked _tag (token) descending using system then code so order must be: id2, id1
assertThat(getResultIds(result), contains(id2, id1));
}
@Test
public void byTokenCode() {
String id1 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withTag("http://example.org", "aTagA")
)).getIdPart();
String id2 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withTag("http://example.org", "aTagB")
)).getIdPart();
myCaptureQueriesListener.clear();
IBundleProvider result = myTestDaoSearch.searchForBundleProvider("/Observation?_sort=-_tag");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
// asked _tag (token) descending so order must be: id2, id1
assertThat(getResultIds(result), contains(id2, id1));
}
@Test
public void byDate() {
String id1 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withEffectiveDate("2017-01-20T03:21:47")
)).getIdPart();
String id2 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withEffectiveDate("2017-01-24T03:21:47")
)).getIdPart();
myCaptureQueriesListener.clear();
IBundleProvider result = myTestDaoSearch.searchForBundleProvider("/Observation?_sort=-date");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
// requesteddate descending so order should be id2, id1
assertThat(getResultIds(result), contains(id2, id1));
}
@Test
public void byValueString() {
String id1 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withPrimitiveAttribute("valueString", "a-string-value-1")
)).getIdPart();
String id2 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withPrimitiveAttribute("valueString", "a-string-value-2")
)).getIdPart();
myCaptureQueriesListener.clear();
IBundleProvider result = myTestDaoSearch.searchForBundleProvider("/Observation?_sort=-value-string");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
// requested value-string descending so order should be id2, id1
assertThat(getResultIds(result), contains(id2, id1));
}
@Test
public void byQuantity() {
String id1 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withQuantityAtPath("valueQuantity", 50, UCUM_CODESYSTEM_URL, "10*3/L")
)).getIdPart();
String id2 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withQuantityAtPath("valueQuantity", 60, UCUM_CODESYSTEM_URL, "10*3/L")
)).getIdPart();
myCaptureQueriesListener.clear();
IBundleProvider result = myTestDaoSearch.searchForBundleProvider("/Observation?_sort=-value-quantity");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
// requested qty descending so order should be id2, id1
assertThat(getResultIds(result), contains(id2, id1));
}
@Test
public void byUri() {
String id1 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withTag("http://example.org", "aTag")
)).getIdPart();
String id2 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withProfile("http://example.com/theProfile2")
)).getIdPart();
myCaptureQueriesListener.clear();
IBundleProvider result = myTestDaoSearch.searchForBundleProvider("/Observation?_sort=-_profile");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
// requested profile (uri) descending so order should be id2, id1
assertThat(getResultIds(result), contains(id2, id1));
}
@Test
public void byReference() {
Patient patient1 = new Patient();
IIdType patId1 = myPatientDao.create(patient1, mySrd).getId();
Observation obs1 = new Observation();
obs1.setSubject(new Reference(patId1.toString()));
String obsId1 = myObservationDao.create(obs1, mySrd).getId().toUnqualifiedVersionless().getIdPart();
Patient patient2 = new Patient();
IIdType patId2 = myPatientDao.create(patient2, mySrd).getId();
Observation obs2 = new Observation();
obs2.setSubject(new Reference(patId2.toString()));
String obsId2 = myObservationDao.create(obs2, mySrd).getId().toUnqualifiedVersionless().getIdPart();
myCaptureQueriesListener.clear();
IBundleProvider result = myTestDaoSearch.searchForBundleProvider("/Observation?_sort=-subject");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
// requested reference descending so order should be id2, id1
assertThat(getResultIds(result), contains(obsId2, obsId1));
}
@Test
public void byNumber() {
String raId1 = createRiskAssessmentWithPredictionProbability(0.23).getIdPart();
String raId2 = createRiskAssessmentWithPredictionProbability(0.38).getIdPart();
String raId3 = createRiskAssessmentWithPredictionProbability(0.76).getIdPart();
myCaptureQueriesListener.clear();
IBundleProvider result = myTestDaoSearch.searchForBundleProvider("/RiskAssessment?_sort=-probability");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
// requested profile (uri) descending so order should be id2, id1
assertThat(getResultIds(result), contains(raId3, raId2, raId1));
}
}
@Nested
public class IncludingNulls {
@Test
public void byToken() {
String id1 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "the-code-1")
)).getIdPart();
String id2 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withTag("http://example.org", "aTag")
)).getIdPart();
myCaptureQueriesListener.clear();
IBundleProvider result = myTestDaoSearch.searchForBundleProvider("/Observation?_sort=_tag");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
// should use nulls last so order must be: id2, id1
assertThat(getResultIds(result), contains(id2, id1));
}
@Test
public void byDate() {
String id1 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "the-code-1")
)).getIdPart();
String id2 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withEffectiveDate("2017-01-24T03:21:47")
)).getIdPart();
myCaptureQueriesListener.clear();
IBundleProvider result = myTestDaoSearch.searchForBundleProvider("/Observation?_sort=-date");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
// should use nulls last so order must be: id2, id1
assertThat(getResultIds(result), contains(id2, id1));
}
@Test
public void byValueString() {
String id1 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "the-code-1")
)).getIdPart();
String id2 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withPrimitiveAttribute("valueString", "a-string-value-2")
)).getIdPart();
myCaptureQueriesListener.clear();
IBundleProvider result = myTestDaoSearch.searchForBundleProvider("/Observation?_sort=-value-string");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
// should use nulls last so order must be: id2, id1
assertThat(getResultIds(result), contains(id2, id1));
}
@Test
public void byQuantity() {
String id1 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "the-code-1")
)).getIdPart();
String id2 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withQuantityAtPath("valueQuantity", 60, UCUM_CODESYSTEM_URL, "10*3/L")
)).getIdPart();
myCaptureQueriesListener.clear();
IBundleProvider result = myTestDaoSearch.searchForBundleProvider("/Observation?_sort=-value-quantity");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
// requested qty descending so order should be id2, id1
assertThat(getResultIds(result), contains(id2, id1));
}
@Test
public void byUri() {
String id1 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "the-code-1")
)).getIdPart();
String id2 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withProfile("http://example.com/theProfile2")
)).getIdPart();
myCaptureQueriesListener.clear();
IBundleProvider result = myTestDaoSearch.searchForBundleProvider("/Observation?_sort=-_profile");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
// requested nulls last so order should be id2, id1
assertThat(getResultIds(result), contains(id2, id1));
}
@Test
public void byReference() {
Observation obs1 = new Observation();
String obsId1 = myObservationDao.create(obs1, mySrd).getId().toUnqualifiedVersionless().getIdPart();
Patient patient2 = new Patient();
IIdType patId2 = myPatientDao.create(patient2, mySrd).getId();
Observation obs2 = new Observation();
obs2.setSubject(new Reference(patId2.toString()));
String obsId2 = myObservationDao.create(obs2, mySrd).getId().toUnqualifiedVersionless().getIdPart();
myCaptureQueriesListener.clear();
IBundleProvider result = myTestDaoSearch.searchForBundleProvider("/Observation?_sort=-subject");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
// requested reference with nulls last so order should be: obsId2, obsId1
assertThat(getResultIds(result), contains(obsId2, obsId1));
}
@Test
public void byNumber() {
String raId1 = createRiskAssessmentWithPredictionProbability(0.23).getIdPart();
String raId2 = createRiskAssessment().getIdPart();
myCaptureQueriesListener.clear();
IBundleProvider result = myTestDaoSearch.searchForBundleProvider("/RiskAssessment?_sort=probability");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
// requested profile (uri) descending so order should be id2, id1
assertThat(getResultIds(result), contains(raId1, raId2));
}
}
}
@Nested
public class CombinedProperties {
@Test
public void byTokenAndDate() {
String id1 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "the-code-1"),
myTestDataBuilder.withEffectiveDate("2017-01-20T03:21:47"),
myTestDataBuilder.withTag("http://example.org", "aTag")
)).getIdPart();
String id2 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "the-code-2"),
myTestDataBuilder.withEffectiveDate("2017-01-24T03:21:47"),
myTestDataBuilder.withTag("http://example.org", "aTag")
)).getIdPart();
myCaptureQueriesListener.clear();
IBundleProvider result = myTestDaoSearch.searchForBundleProvider("/Observation?_sort=_tag,-date");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
assertEquals(2, result.getAllResources().size());
DateTimeType effectiveFirst = (DateTimeType) ((Observation) result.getAllResources().get(0)).getEffective();
DateTimeType effectiveSecond = (DateTimeType) ((Observation) result.getAllResources().get(1)).getEffective();
// requested date descending so first result should be the one with the latest effective date: id2
assertTrue(effectiveFirst.after(effectiveSecond));
}
@Test
public void byTokenAndValueString() {
String id1 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "the-code-1"),
myTestDataBuilder.withEffectiveDate("2017-01-20T03:21:47"),
myTestDataBuilder.withTag("http://example.org", "aTag"),
myTestDataBuilder.withPrimitiveAttribute("valueString", "a-string-value-1")
)).getIdPart();
String id2 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "the-code-2"),
myTestDataBuilder.withEffectiveDate("2017-01-24T03:21:47"),
myTestDataBuilder.withTag("http://example.org", "aTag"),
myTestDataBuilder.withPrimitiveAttribute("valueString", "a-string-value-2")
)).getIdPart();
myCaptureQueriesListener.clear();
IBundleProvider result = myTestDaoSearch.searchForBundleProvider("/Observation?_sort=_tag,-value-string");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
assertEquals(2, result.getAllResources().size());
DateTimeType effectiveFirst = (DateTimeType) ((Observation) result.getAllResources().get(0)).getEffective();
DateTimeType effectiveSecond = (DateTimeType) ((Observation) result.getAllResources().get(1)).getEffective();
// requested date descending so first result should be the one with the latest effective date: id2
assertTrue(effectiveFirst.after(effectiveSecond));
}
@Test
public void byTokenAndQuantity() {
String id1 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withTag("http://example.org", "aTag"),
myTestDataBuilder.withQuantityAtPath("valueQuantity", 50, UCUM_CODESYSTEM_URL, "10*3/L")
)).getIdPart();
String id2 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withTag("http://example.org", "aTag"),
myTestDataBuilder.withQuantityAtPath("valueQuantity", 60, UCUM_CODESYSTEM_URL, "10*3/L")
)).getIdPart();
myCaptureQueriesListener.clear();
IBundleProvider result = myTestDaoSearch.searchForBundleProvider("/Observation?_sort=_tag,-value-quantity");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
// requested qty descending so order should be id2, id1
assertThat(getResultIds(result), contains(id2, id1));
}
@Test
public void allTogetherNow() {
String id1 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "the-code-1"),
myTestDataBuilder.withEffectiveDate("2017-01-20T03:21:47"),
myTestDataBuilder.withTag("http://example.org", "aTag"),
myTestDataBuilder.withQuantityAtPath("valueQuantity", 50, UCUM_CODESYSTEM_URL, "10*3/L")
)).getIdPart();
String id2 = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "the-code-1"),
myTestDataBuilder.withEffectiveDate("2017-01-20T03:21:47"),
myTestDataBuilder.withTag("http://example.org", "aTag"),
myTestDataBuilder.withQuantityAtPath("valueQuantity", 60, UCUM_CODESYSTEM_URL, "10*3/L")
)).getIdPart();
myCaptureQueriesListener.clear();
IBundleProvider result = myTestDaoSearch.searchForBundleProvider("/Observation?_sort=code,date,_tag,_tag,-value-quantity");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
// all sorted values are the same except the last (value-quantity) so order should be id2, id1
assertThat(getResultIds(result), contains(id2, id1));
}
}
}
@Nested
public class NumberParameter {
@BeforeEach
public void enableContainsAndLucene() {
myDaoConfig.setAllowContainsSearches(true);
myDaoConfig.setAdvancedLuceneIndexing(true);
myDaoConfig.setStoreResourceInLuceneIndex(true);
myDaoConfig.getModelConfig().setNormalizedQuantitySearchLevel(
NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED);
}
@AfterEach
public void restoreContains() {
DaoConfig defaultConfig = new DaoConfig();
myDaoConfig.setAllowContainsSearches(defaultConfig.isAllowContainsSearches());
myDaoConfig.setAdvancedLuceneIndexing(defaultConfig.isAdvancedLuceneIndexing());
myDaoConfig.setStoreResourceInLuceneIndex(defaultConfig.isStoreResourceInLuceneIndex());
myDaoConfig.getModelConfig().setNormalizedQuantitySearchLevel(
defaultConfig.getModelConfig().getNormalizedQuantitySearchLevel() );
}
@Test
public void noExtraSql() {
IIdType raId1 = createRiskAssessmentWithPredictionProbability(.25);
myCaptureQueriesListener.clear();
assertFindId("when exact", raId1, "/RiskAssessment?probability=0.25");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
}
/**
* The following tests are to validate the specification implementation, but they don't work because we save parameters as BigInteger
* which invalidates the possibility to differentiate requested significant figures, which are needed to define precision ranges
* Leaving them here in case some day we fix the implementations
* We copy the JPA implementation here, which ignores precision requests and treats all numbers using default ranges
* @see TestSpecCasesNotUsingSignificantFigures
*/
@Disabled
@Nested
public class TestSpecCasesUsingSignificantFigures {
@Test
void specCase1() {
String raId1 = createRiskAssessmentWithPredictionProbability(99.4).getIdPart();
String raId2 = createRiskAssessmentWithPredictionProbability(99.6).getIdPart();
String raId3 = createRiskAssessmentWithPredictionProbability(100.4).getIdPart();
String raId4 = createRiskAssessmentWithPredictionProbability(100.6).getIdPart();
// [parameter]=100 Values that equal 100, to 3 significant figures precision, so this is actually searching for values in the range [99.5 ... 100.5)
assertFindIds("when le", Set.of(raId2, raId3), "/RiskAssessment?probability=100");
}
@Test
void specCase2() {
String raId1 = createRiskAssessmentWithPredictionProbability(99.994).getIdPart();
String raId2 = createRiskAssessmentWithPredictionProbability(99.996).getIdPart();
String raId3 = createRiskAssessmentWithPredictionProbability(100.004).getIdPart();
String raId4 = createRiskAssessmentWithPredictionProbability(100.006).getIdPart();
// [parameter]=100.00 Values that equal 100, to 5 significant figures precision, so this is actually searching for values in the range [99.995 ... 100.005)
assertFindIds("when le", Set.of(raId2, raId3), "/RiskAssessment?probability=100.00");
}
@Test
void specCase3() {
String raId1 = createRiskAssessmentWithPredictionProbability(94).getIdPart();
String raId2 = createRiskAssessmentWithPredictionProbability(96).getIdPart();
String raId3 = createRiskAssessmentWithPredictionProbability(104).getIdPart();
String raId4 = createRiskAssessmentWithPredictionProbability(106).getIdPart();
// [parameter]=1e2 Values that equal 100, to 1 significant figures precision, so this is actually searching for values in the range [95 ... 105)
assertFindIds("when le", Set.of(raId2, raId3), "/RiskAssessment?probability=1e2");
}
}
@Nested
public class TestSpecCasesNotUsingSignificantFigures {
@Test
void specCase4() {
String raId1 = createRiskAssessmentWithPredictionProbability(99).getIdPart();
String raId2 = createRiskAssessmentWithPredictionProbability(100).getIdPart();
// [parameter]=lt100 Values that are less than exactly 100
assertFindIds("when le", Set.of(raId1), "/RiskAssessment?probability=lt100");
}
@Test
void specCase5() {
String raId1 = createRiskAssessmentWithPredictionProbability(99).getIdPart();
String raId2 = createRiskAssessmentWithPredictionProbability(100).getIdPart();
String raId3 = createRiskAssessmentWithPredictionProbability(101).getIdPart();
// [parameter]=le100 Values that are less or equal to exactly 100
assertFindIds("when le", Set.of(raId1, raId2), "/RiskAssessment?probability=le100");
}
@Test
void specCase6() {
String raId1 = createRiskAssessmentWithPredictionProbability(100).getIdPart();
String raId2 = createRiskAssessmentWithPredictionProbability(101).getIdPart();
// [parameter]=gt100 Values that are greater than exactly 100
assertFindIds("when le", Set.of(raId2), "/RiskAssessment?probability=gt100");
}
@Test
void specCase7() {
String raId1 = createRiskAssessmentWithPredictionProbability(99).getIdPart();
String raId2 = createRiskAssessmentWithPredictionProbability(100).getIdPart();
String raId3 = createRiskAssessmentWithPredictionProbability(101).getIdPart();
// [parameter]=ge100 Values that are greater or equal to exactly 100
assertFindIds("when le", Set.of(raId2, raId3), "/RiskAssessment?probability=ge100");
}
@Test
void specCase8() {
String raId1 = createRiskAssessmentWithPredictionProbability(99.4).getIdPart();
String raId2 = createRiskAssessmentWithPredictionProbability(99.6).getIdPart();
String raId3 = createRiskAssessmentWithPredictionProbability(100.4).getIdPart();
String raId4 = createRiskAssessmentWithPredictionProbability(100.6).getIdPart();
String raId5 = createRiskAssessmentWithPredictionProbability(100).getIdPart();
// [parameter]=ne100 Values that are not equal to 100 (actually, in the range 99.5 to 100.5)
assertFindIds("when le", Set.of(raId1, raId2, raId3, raId4), "/RiskAssessment?probability=ne100");
}
}
@Test
void andClauses() {
String raId1 = createRiskAssessmentWithPredictionProbability(0.15).getIdPart();
String raId2 = createRiskAssessmentWithPredictionProbability(0.20).getIdPart();
String raId3 = createRiskAssessmentWithPredictionProbability(0.25).getIdPart();
String raId4 = createRiskAssessmentWithPredictionProbability(0.35).getIdPart();
String raId5 = createRiskAssessmentWithPredictionProbability(0.45).getIdPart();
String raId6 = createRiskAssessmentWithPredictionProbability(0.55).getIdPart();
assertFindIds("when le", Set.of(raId2, raId3, raId4), "/RiskAssessment?probability=ge0.2&probability=lt0.45");
}
@Test
void orClauses() {
String raId1 = createRiskAssessmentWithPredictionProbability(0.15).getIdPart();
String raId2 = createRiskAssessmentWithPredictionProbability(0.20).getIdPart();
String raId3 = createRiskAssessmentWithPredictionProbability(0.25).getIdPart();
String raId4 = createRiskAssessmentWithPredictionProbability(0.35).getIdPart();
String raId5 = createRiskAssessmentWithPredictionProbability(0.45).getIdPart();
String raId6 = createRiskAssessmentWithPredictionProbability(0.55).getIdPart();
assertFindIds("when le", Set.of(raId1, raId2, raId3, raId6), "/RiskAssessment?probability=le0.25,gt0.50");
}
}
private IIdType createRiskAssessment() {
return (createRiskAssessmentWithPredictionProbability(null));
}
private IIdType createRiskAssessmentWithPredictionProbability(Number theProbability) {
RiskAssessment ra1 = new RiskAssessment();
if (theProbability != null) {
RiskAssessment.RiskAssessmentPredictionComponent component = ra1.addPrediction();
component.setProbability(new DecimalType(theProbability.doubleValue()));
}
return myRiskAssessmentDao.create(ra1).getId().toUnqualifiedVersionless();
}
@Disabled("keeping to debug search scrolling")
@Test
@ -1882,5 +2452,24 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
}
}
private List<String> getResultIds(IBundleProvider theResult) {
return theResult.getAllResources().stream().map(r -> r.getIdElement().getIdPart()).collect(Collectors.toList());
}
private void assertFindId(String theMessage, IIdType theResourceId, String theUrl) {
List<String> resourceIds = myTestDaoSearch.searchForIds(theUrl);
assertThat(theMessage, resourceIds, hasItem(equalTo(theResourceId.getIdPart())));
}
private void assertFindIds(String theMessage, Collection<String> theResourceIds, String theUrl) {
List<String> resourceIds = myTestDaoSearch.searchForIds(theUrl);
assertEquals(theResourceIds, new HashSet<>(resourceIds), theMessage);
}
private void assertNotFindId(String theMessage, IIdType theResourceId, String theUrl) {
List<String> resourceIds = myTestDaoSearch.searchForIds(theUrl);
assertThat(theMessage, resourceIds, not(hasItem(equalTo(theResourceId.getIdPart()))));
}
}