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:
parent
dce330eb01
commit
e7ce41e5b8
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -53,4 +53,8 @@ public class ExtendedLuceneResourceProjection {
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
public long getPid() {
|
||||
return myPid;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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("")
|
||||
|
|
|
@ -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()))));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue