cherry-picked 6027
This commit is contained in:
parent
6f683e0990
commit
d064172195
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
type: fix
|
||||
issue: 6024
|
||||
title: "Fixed a bug in search where requesting a count with HSearch indexing
|
||||
and FilterParameter enabled and using the _filter parameter would result
|
||||
in inaccurate results being returned.
|
||||
This happened because the count query would use an incorrect set of parameters
|
||||
to find the count, and the regular search when then try and ensure its results
|
||||
matched the count query (which it couldn't because it had different parameters).
|
||||
"
|
|
@ -34,6 +34,7 @@ import ca.uhn.fhir.jpa.dao.search.IHSearchSortHelper;
|
|||
import ca.uhn.fhir.jpa.dao.search.LastNOperation;
|
||||
import ca.uhn.fhir.jpa.model.dao.JpaPid;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.model.search.ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams;
|
||||
import ca.uhn.fhir.jpa.model.search.ExtendedHSearchIndexData;
|
||||
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
|
||||
import ca.uhn.fhir.jpa.search.autocomplete.ValueSetAutocompleteOptions;
|
||||
|
@ -141,17 +142,17 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSomeOf(SearchParameterMap myParams) {
|
||||
|
||||
// keep this in sync with the guts of doSearch
|
||||
public boolean canUseHibernateSearch(String theResourceType, SearchParameterMap myParams) {
|
||||
boolean requiresHibernateSearchAccess = myParams.containsKey(Constants.PARAM_CONTENT)
|
||||
|| myParams.containsKey(Constants.PARAM_TEXT)
|
||||
|| myParams.isLastN();
|
||||
// we have to use it - _text and _content searches only use hibernate
|
||||
if (requiresHibernateSearchAccess) {
|
||||
return true;
|
||||
}
|
||||
|
||||
requiresHibernateSearchAccess |=
|
||||
myStorageSettings.isAdvancedHSearchIndexing() && myAdvancedIndexQueryBuilder.isSupportsSomeOf(myParams);
|
||||
|
||||
return requiresHibernateSearchAccess;
|
||||
return myStorageSettings.isAdvancedHSearchIndexing()
|
||||
&& myAdvancedIndexQueryBuilder.canUseHibernateSearch(theResourceType, myParams, mySearchParamRegistry);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -174,6 +175,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
|
|||
}
|
||||
|
||||
// keep this in sync with supportsSomeOf();
|
||||
@SuppressWarnings("rawtypes")
|
||||
private ISearchQueryExecutor doSearch(
|
||||
String theResourceType,
|
||||
SearchParameterMap theParams,
|
||||
|
@ -208,6 +210,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
|
|||
return DEFAULT_MAX_NON_PAGED_SIZE;
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private SearchQueryOptionsStep<?, Long, SearchLoadingOptionsStep, ?, ?> getSearchQueryOptionsStep(
|
||||
String theResourceType, SearchParameterMap theParams, IResourcePersistentId theReferencingPid) {
|
||||
|
||||
|
@ -230,6 +233,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
|
|||
return query;
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private PredicateFinalStep buildWhereClause(
|
||||
SearchPredicateFactory f,
|
||||
String theResourceType,
|
||||
|
@ -271,8 +275,12 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
|
|||
* Handle other supported parameters
|
||||
*/
|
||||
if (myStorageSettings.isAdvancedHSearchIndexing() && theParams.getEverythingMode() == null) {
|
||||
myAdvancedIndexQueryBuilder.addAndConsumeAdvancedQueryClauses(
|
||||
builder, theResourceType, theParams, mySearchParamRegistry);
|
||||
ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams params =
|
||||
new ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams();
|
||||
params.setSearchParamRegistry(mySearchParamRegistry)
|
||||
.setResourceType(theResourceType)
|
||||
.setSearchParameterMap(theParams);
|
||||
myAdvancedIndexQueryBuilder.addAndConsumeAdvancedQueryClauses(builder, params);
|
||||
}
|
||||
// DROP EARLY HERE IF BOOL IS EMPTY?
|
||||
});
|
||||
|
@ -283,11 +291,13 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
|
|||
return Search.session(myEntityManager);
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private List<IResourcePersistentId> convertLongsToResourcePersistentIds(List<Long> theLongPids) {
|
||||
return theLongPids.stream().map(JpaPid::fromId).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
public List<IResourcePersistentId> everything(
|
||||
String theResourceName,
|
||||
SearchParameterMap theParams,
|
||||
|
@ -336,6 +346,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
|
|||
|
||||
@Transactional()
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<IResourcePersistentId> search(
|
||||
String theResourceName, SearchParameterMap theParams, RequestDetails theRequestDetails) {
|
||||
validateHibernateSearchIsEnabled();
|
||||
|
@ -347,6 +358,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
|
|||
/**
|
||||
* Adapt our async interface to the legacy concrete List
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
private List<IResourcePersistentId> toList(ISearchQueryExecutor theSearchResultStream, long theMaxSize) {
|
||||
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(theSearchResultStream, 0), false)
|
||||
.map(JpaPid::fromId)
|
||||
|
@ -384,6 +396,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
|
|||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("rawtypes")
|
||||
public List<IResourcePersistentId> lastN(SearchParameterMap theParams, Integer theMaximumResults) {
|
||||
ensureElastic();
|
||||
dispatchEvent(IHSearchEventListener.HSearchEventType.SEARCH);
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
|
|||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings({"rawtypes"})
|
||||
public interface IFulltextSearchSvc {
|
||||
|
||||
/**
|
||||
|
@ -79,11 +80,18 @@ public interface IFulltextSearchSvc {
|
|||
ExtendedHSearchIndexData extractLuceneIndexData(
|
||||
IBaseResource theResource, ResourceIndexedSearchParams theNewParams);
|
||||
|
||||
boolean supportsSomeOf(SearchParameterMap myParams);
|
||||
/**
|
||||
* Returns true if the parameter map can be handled for hibernate search.
|
||||
* We have to filter out any queries that might use search params
|
||||
* we only know how to handle in JPA.
|
||||
* -
|
||||
* See {@link ca.uhn.fhir.jpa.dao.search.ExtendedHSearchSearchBuilder#addAndConsumeAdvancedQueryClauses}
|
||||
*/
|
||||
boolean canUseHibernateSearch(String theResourceType, SearchParameterMap theParameterMap);
|
||||
|
||||
/**
|
||||
* Re-publish the resource to the full-text index.
|
||||
*
|
||||
* -
|
||||
* During update, hibernate search only republishes the entity if it has changed.
|
||||
* During $reindex, we want to force the re-index.
|
||||
*
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
package ca.uhn.fhir.jpa.dao.search;
|
||||
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.jpa.model.search.ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
|
||||
import ca.uhn.fhir.model.api.IQueryParameterType;
|
||||
|
@ -34,6 +35,7 @@ import ca.uhn.fhir.rest.param.StringParam;
|
|||
import ca.uhn.fhir.rest.param.TokenParam;
|
||||
import ca.uhn.fhir.rest.param.UriParam;
|
||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
|
@ -44,6 +46,7 @@ import java.util.ArrayList;
|
|||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static ca.uhn.fhir.rest.api.Constants.PARAMQUALIFIER_MISSING;
|
||||
|
||||
|
@ -59,17 +62,55 @@ public class ExtendedHSearchSearchBuilder {
|
|||
public static final Set<String> ourUnsafeSearchParmeters = Sets.newHashSet("_id", "_meta");
|
||||
|
||||
/**
|
||||
* Are any of the queries supported by our indexing?
|
||||
* Determine if ExtendedHibernateSearchBuilder can support this parameter
|
||||
* @param theParamName param name
|
||||
* @param theActiveParamsForResourceType active search parameters for the desired resource type
|
||||
* @return whether or not this search parameter is supported in hibernate
|
||||
*/
|
||||
public boolean isSupportsSomeOf(SearchParameterMap myParams) {
|
||||
return myParams.getSort() != null
|
||||
|| myParams.getLastUpdated() != null
|
||||
|| myParams.entrySet().stream()
|
||||
.filter(e -> !ourUnsafeSearchParmeters.contains(e.getKey()))
|
||||
// each and clause may have a different modifier, so split down to the ORs
|
||||
.flatMap(andList -> andList.getValue().stream())
|
||||
.flatMap(Collection::stream)
|
||||
.anyMatch(this::isParamTypeSupported);
|
||||
public boolean supportsSearchParameter(String theParamName, ResourceSearchParams theActiveParamsForResourceType) {
|
||||
if (theActiveParamsForResourceType == null) {
|
||||
return false;
|
||||
}
|
||||
if (ourUnsafeSearchParmeters.contains(theParamName)) {
|
||||
return false;
|
||||
}
|
||||
if (!theActiveParamsForResourceType.containsParamName(theParamName)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Are any of the queries supported by our indexing?
|
||||
* -
|
||||
* If not, do not use hibernate, because the results will
|
||||
* be inaccurate and wrong.
|
||||
*/
|
||||
public boolean canUseHibernateSearch(
|
||||
String theResourceType, SearchParameterMap myParams, ISearchParamRegistry theSearchParamRegistry) {
|
||||
boolean canUseHibernate = true;
|
||||
ResourceSearchParams resourceActiveSearchParams = theSearchParamRegistry.getActiveSearchParams(theResourceType);
|
||||
for (String paramName : myParams.keySet()) {
|
||||
// is this parameter supported?
|
||||
if (!supportsSearchParameter(paramName, resourceActiveSearchParams)) {
|
||||
canUseHibernate = false;
|
||||
} else {
|
||||
// are the parameter values supported?
|
||||
canUseHibernate =
|
||||
myParams.get(paramName).stream()
|
||||
.flatMap(Collection::stream)
|
||||
.collect(Collectors.toList())
|
||||
.stream()
|
||||
.anyMatch(this::isParamTypeSupported);
|
||||
}
|
||||
|
||||
// if not supported, don't use
|
||||
if (!canUseHibernate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return canUseHibernate;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -166,86 +207,91 @@ public class ExtendedHSearchSearchBuilder {
|
|||
}
|
||||
|
||||
public void addAndConsumeAdvancedQueryClauses(
|
||||
ExtendedHSearchClauseBuilder builder,
|
||||
String theResourceType,
|
||||
SearchParameterMap theParams,
|
||||
ISearchParamRegistry theSearchParamRegistry) {
|
||||
ExtendedHSearchClauseBuilder theBuilder,
|
||||
ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams theMethodParams) {
|
||||
SearchParameterMap searchParameterMap = theMethodParams.getSearchParameterMap();
|
||||
String resourceType = theMethodParams.getResourceType();
|
||||
ISearchParamRegistry searchParamRegistry = theMethodParams.getSearchParamRegistry();
|
||||
|
||||
// copy the keys to avoid concurrent modification error
|
||||
ArrayList<String> paramNames = compileParamNames(theParams);
|
||||
ArrayList<String> paramNames = compileParamNames(searchParameterMap);
|
||||
ResourceSearchParams activeSearchParams = searchParamRegistry.getActiveSearchParams(resourceType);
|
||||
for (String nextParam : paramNames) {
|
||||
if (ourUnsafeSearchParmeters.contains(nextParam)) {
|
||||
continue;
|
||||
}
|
||||
RuntimeSearchParam activeParam = theSearchParamRegistry.getActiveSearchParam(theResourceType, nextParam);
|
||||
if (activeParam == null) {
|
||||
if (!supportsSearchParameter(nextParam, activeSearchParams)) {
|
||||
// ignore magic params handled in JPA
|
||||
continue;
|
||||
}
|
||||
RuntimeSearchParam activeParam = activeSearchParams.get(nextParam);
|
||||
|
||||
// NOTE - keep this in sync with isParamSupported() above.
|
||||
switch (activeParam.getParamType()) {
|
||||
case TOKEN:
|
||||
List<List<IQueryParameterType>> tokenTextAndOrTerms =
|
||||
theParams.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_TOKEN_TEXT);
|
||||
builder.addStringTextSearch(nextParam, tokenTextAndOrTerms);
|
||||
searchParameterMap.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_TOKEN_TEXT);
|
||||
theBuilder.addStringTextSearch(nextParam, tokenTextAndOrTerms);
|
||||
|
||||
List<List<IQueryParameterType>> tokenUnmodifiedAndOrTerms =
|
||||
theParams.removeByNameUnmodified(nextParam);
|
||||
builder.addTokenUnmodifiedSearch(nextParam, tokenUnmodifiedAndOrTerms);
|
||||
searchParameterMap.removeByNameUnmodified(nextParam);
|
||||
theBuilder.addTokenUnmodifiedSearch(nextParam, tokenUnmodifiedAndOrTerms);
|
||||
break;
|
||||
|
||||
case STRING:
|
||||
List<List<IQueryParameterType>> stringTextAndOrTerms =
|
||||
theParams.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_TOKEN_TEXT);
|
||||
builder.addStringTextSearch(nextParam, stringTextAndOrTerms);
|
||||
searchParameterMap.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_TOKEN_TEXT);
|
||||
theBuilder.addStringTextSearch(nextParam, stringTextAndOrTerms);
|
||||
|
||||
List<List<IQueryParameterType>> stringExactAndOrTerms =
|
||||
theParams.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_STRING_EXACT);
|
||||
builder.addStringExactSearch(nextParam, stringExactAndOrTerms);
|
||||
List<List<IQueryParameterType>> stringExactAndOrTerms = searchParameterMap.removeByNameAndModifier(
|
||||
nextParam, Constants.PARAMQUALIFIER_STRING_EXACT);
|
||||
theBuilder.addStringExactSearch(nextParam, stringExactAndOrTerms);
|
||||
|
||||
List<List<IQueryParameterType>> stringContainsAndOrTerms =
|
||||
theParams.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_STRING_CONTAINS);
|
||||
builder.addStringContainsSearch(nextParam, stringContainsAndOrTerms);
|
||||
searchParameterMap.removeByNameAndModifier(
|
||||
nextParam, Constants.PARAMQUALIFIER_STRING_CONTAINS);
|
||||
theBuilder.addStringContainsSearch(nextParam, stringContainsAndOrTerms);
|
||||
|
||||
List<List<IQueryParameterType>> stringAndOrTerms = theParams.removeByNameUnmodified(nextParam);
|
||||
builder.addStringUnmodifiedSearch(nextParam, stringAndOrTerms);
|
||||
List<List<IQueryParameterType>> stringAndOrTerms =
|
||||
searchParameterMap.removeByNameUnmodified(nextParam);
|
||||
theBuilder.addStringUnmodifiedSearch(nextParam, stringAndOrTerms);
|
||||
break;
|
||||
|
||||
case QUANTITY:
|
||||
List<List<IQueryParameterType>> quantityAndOrTerms = theParams.removeByNameUnmodified(nextParam);
|
||||
builder.addQuantityUnmodifiedSearch(nextParam, quantityAndOrTerms);
|
||||
List<List<IQueryParameterType>> quantityAndOrTerms =
|
||||
searchParameterMap.removeByNameUnmodified(nextParam);
|
||||
theBuilder.addQuantityUnmodifiedSearch(nextParam, quantityAndOrTerms);
|
||||
break;
|
||||
|
||||
case REFERENCE:
|
||||
List<List<IQueryParameterType>> referenceAndOrTerms = theParams.removeByNameUnmodified(nextParam);
|
||||
builder.addReferenceUnchainedSearch(nextParam, referenceAndOrTerms);
|
||||
List<List<IQueryParameterType>> referenceAndOrTerms =
|
||||
searchParameterMap.removeByNameUnmodified(nextParam);
|
||||
theBuilder.addReferenceUnchainedSearch(nextParam, referenceAndOrTerms);
|
||||
break;
|
||||
|
||||
case DATE:
|
||||
List<List<IQueryParameterType>> dateAndOrTerms = nextParam.equalsIgnoreCase("_lastupdated")
|
||||
? getLastUpdatedAndOrList(theParams)
|
||||
: theParams.removeByNameUnmodified(nextParam);
|
||||
builder.addDateUnmodifiedSearch(nextParam, dateAndOrTerms);
|
||||
? getLastUpdatedAndOrList(searchParameterMap)
|
||||
: searchParameterMap.removeByNameUnmodified(nextParam);
|
||||
theBuilder.addDateUnmodifiedSearch(nextParam, dateAndOrTerms);
|
||||
break;
|
||||
|
||||
case COMPOSITE:
|
||||
List<List<IQueryParameterType>> compositeAndOrTerms = theParams.removeByNameUnmodified(nextParam);
|
||||
List<List<IQueryParameterType>> compositeAndOrTerms =
|
||||
searchParameterMap.removeByNameUnmodified(nextParam);
|
||||
// RuntimeSearchParam only points to the subs by reference. Resolve here while we have
|
||||
// ISearchParamRegistry
|
||||
List<RuntimeSearchParam> subSearchParams =
|
||||
JpaParamUtil.resolveCompositeComponentsDeclaredOrder(theSearchParamRegistry, activeParam);
|
||||
builder.addCompositeUnmodifiedSearch(activeParam, subSearchParams, compositeAndOrTerms);
|
||||
JpaParamUtil.resolveCompositeComponentsDeclaredOrder(searchParamRegistry, activeParam);
|
||||
theBuilder.addCompositeUnmodifiedSearch(activeParam, subSearchParams, compositeAndOrTerms);
|
||||
break;
|
||||
|
||||
case URI:
|
||||
List<List<IQueryParameterType>> uriUnmodifiedAndOrTerms =
|
||||
theParams.removeByNameUnmodified(nextParam);
|
||||
builder.addUriUnmodifiedSearch(nextParam, uriUnmodifiedAndOrTerms);
|
||||
searchParameterMap.removeByNameUnmodified(nextParam);
|
||||
theBuilder.addUriUnmodifiedSearch(nextParam, uriUnmodifiedAndOrTerms);
|
||||
break;
|
||||
|
||||
case NUMBER:
|
||||
List<List<IQueryParameterType>> numberUnmodifiedAndOrTerms = theParams.remove(nextParam);
|
||||
builder.addNumberUnmodifiedSearch(nextParam, numberUnmodifiedAndOrTerms);
|
||||
List<List<IQueryParameterType>> numberUnmodifiedAndOrTerms = searchParameterMap.remove(nextParam);
|
||||
theBuilder.addNumberUnmodifiedSearch(nextParam, numberUnmodifiedAndOrTerms);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
|
|
@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.dao.search;
|
|||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
|
||||
import ca.uhn.fhir.jpa.model.search.ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.searchparam.util.LastNParameterHelper;
|
||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||
|
@ -67,8 +68,12 @@ public class LastNOperation {
|
|||
b.must(f.match().field("myResourceType").matching(OBSERVATION_RES_TYPE));
|
||||
ExtendedHSearchClauseBuilder builder =
|
||||
new ExtendedHSearchClauseBuilder(myFhirContext, myStorageSettings, b, f);
|
||||
myExtendedHSearchSearchBuilder.addAndConsumeAdvancedQueryClauses(
|
||||
builder, OBSERVATION_RES_TYPE, theParams.clone(), mySearchParamRegistry);
|
||||
ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams params =
|
||||
new ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams();
|
||||
params.setResourceType(OBSERVATION_RES_TYPE)
|
||||
.setSearchParameterMap(theParams.clone())
|
||||
.setSearchParamRegistry(mySearchParamRegistry);
|
||||
myExtendedHSearchSearchBuilder.addAndConsumeAdvancedQueryClauses(builder, params);
|
||||
}))
|
||||
.aggregation(observationsByCodeKey, f -> f.fromJson(lastNAggregation.toAggregation()))
|
||||
.fetch(0);
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.fhir.jpa.model.search;
|
||||
|
||||
import ca.uhn.fhir.jpa.dao.search.ExtendedHSearchClauseBuilder;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||
|
||||
/**
|
||||
* This is a parameter class for the
|
||||
* {@link ca.uhn.fhir.jpa.dao.search.ExtendedHSearchSearchBuilder#addAndConsumeAdvancedQueryClauses(ExtendedHSearchClauseBuilder, ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams)}
|
||||
* method, so that we can keep the signature manageable (small) and allow for updates without breaking
|
||||
* implementers so often.
|
||||
*/
|
||||
public class ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams {
|
||||
/**
|
||||
* Resource type
|
||||
*/
|
||||
private String myResourceType;
|
||||
/**
|
||||
* The registered search
|
||||
*/
|
||||
private SearchParameterMap mySearchParameterMap;
|
||||
/**
|
||||
* Search param registry
|
||||
*/
|
||||
private ISearchParamRegistry mySearchParamRegistry;
|
||||
|
||||
public String getResourceType() {
|
||||
return myResourceType;
|
||||
}
|
||||
|
||||
public ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams setResourceType(String theResourceType) {
|
||||
myResourceType = theResourceType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SearchParameterMap getSearchParameterMap() {
|
||||
return mySearchParameterMap;
|
||||
}
|
||||
|
||||
public ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams setSearchParameterMap(SearchParameterMap theParams) {
|
||||
mySearchParameterMap = theParams;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ISearchParamRegistry getSearchParamRegistry() {
|
||||
return mySearchParamRegistry;
|
||||
}
|
||||
|
||||
public ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams setSearchParamRegistry(
|
||||
ISearchParamRegistry theSearchParamRegistry) {
|
||||
mySearchParamRegistry = theSearchParamRegistry;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -79,7 +79,7 @@ public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundl
|
|||
ourLog.trace("Done fetching search resource PIDs");
|
||||
|
||||
int countOfPids = pids.size();
|
||||
;
|
||||
|
||||
int maxSize = Math.min(theToIndex - theFromIndex, countOfPids);
|
||||
thePageBuilder.setTotalRequestedResourcesFetched(countOfPids);
|
||||
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
*/
|
||||
package ca.uhn.fhir.jpa.search.builder;
|
||||
|
||||
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
|
||||
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
|
||||
import ca.uhn.fhir.context.ComboSearchParamType;
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
|
@ -93,6 +95,7 @@ import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
|||
import ca.uhn.fhir.util.StopWatch;
|
||||
import ca.uhn.fhir.util.StringUtil;
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.healthmarketscience.sqlbuilder.Condition;
|
||||
import jakarta.annotation.Nonnull;
|
||||
|
@ -163,7 +166,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
public static boolean myUseMaxPageSize50ForTest = false;
|
||||
protected final IInterceptorBroadcaster myInterceptorBroadcaster;
|
||||
protected final IResourceTagDao myResourceTagDao;
|
||||
private final String myResourceName;
|
||||
private String myResourceName;
|
||||
private final Class<? extends IBaseResource> myResourceType;
|
||||
private final HapiFhirLocalContainerEntityManagerFactoryBean myEntityManagerFactory;
|
||||
private final SqlObjectFactory mySqlBuilderFactory;
|
||||
|
@ -195,12 +198,16 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
@Autowired(required = false)
|
||||
private IElasticsearchSvc myIElasticsearchSvc;
|
||||
|
||||
@Autowired
|
||||
private FhirContext myCtx;
|
||||
|
||||
@Autowired
|
||||
private IJpaStorageResourceParser myJpaStorageResourceParser;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
public SearchBuilder(
|
||||
IDao theDao,
|
||||
String theResourceName,
|
||||
|
@ -235,6 +242,11 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
myIdHelperService = theIdHelperService;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setResourceName(String theName) {
|
||||
myResourceName = theName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxResultsToFetch(Integer theMaxResultsToFetch) {
|
||||
myMaxResultsToFetch = theMaxResultsToFetch;
|
||||
|
@ -260,8 +272,6 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
attemptComboUniqueSpProcessing(theQueryStack, theParams, theRequest);
|
||||
}
|
||||
|
||||
SearchContainedModeEnum searchContainedMode = theParams.getSearchContainedMode();
|
||||
|
||||
// Handle _id and _tag last, since they can typically be tacked onto a different parameter
|
||||
List<String> paramNames = myParams.keySet().stream()
|
||||
.filter(t -> !t.equals(IAnyResource.SP_RES_ID))
|
||||
|
@ -394,7 +404,8 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
}
|
||||
|
||||
if (fulltextExecutor == null) {
|
||||
fulltextExecutor = SearchQueryExecutors.from(fulltextMatchIds);
|
||||
fulltextExecutor =
|
||||
SearchQueryExecutors.from(fulltextMatchIds != null ? fulltextMatchIds : new ArrayList<>());
|
||||
}
|
||||
|
||||
if (theSearchRuntimeDetails != null) {
|
||||
|
@ -481,7 +492,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
return fulltextEnabled
|
||||
&& myParams != null
|
||||
&& myParams.getSearchContainedMode() == SearchContainedModeEnum.FALSE
|
||||
&& myFulltextSearchSvc.supportsSomeOf(myParams)
|
||||
&& myFulltextSearchSvc.canUseHibernateSearch(myResourceName, myParams)
|
||||
&& myFulltextSearchSvc.supportsAllSortTerms(myResourceName, myParams);
|
||||
}
|
||||
|
||||
|
@ -533,8 +544,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
|
||||
pid = myIdHelperService.resolveResourcePersistentIds(myRequestPartitionId, myResourceName, idParamValue);
|
||||
}
|
||||
List<JpaPid> pids = myFulltextSearchSvc.everything(myResourceName, myParams, pid, theRequestDetails);
|
||||
return pids;
|
||||
return myFulltextSearchSvc.everything(myResourceName, myParams, pid, theRequestDetails);
|
||||
}
|
||||
|
||||
private void doCreateChunkedQueries(
|
||||
|
@ -857,13 +867,8 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
theQueryStack.addSortOnLastUpdated(ascending);
|
||||
|
||||
} else {
|
||||
|
||||
RuntimeSearchParam param = null;
|
||||
|
||||
if (param == null) {
|
||||
// do we have a composition param defined for the whole chain?
|
||||
param = mySearchParamRegistry.getActiveSearchParam(myResourceName, theSort.getParamName());
|
||||
}
|
||||
RuntimeSearchParam param =
|
||||
mySearchParamRegistry.getActiveSearchParam(myResourceName, theSort.getParamName());
|
||||
|
||||
/*
|
||||
* If we have a sort like _sort=subject.name and we have an
|
||||
|
@ -891,9 +896,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
mySearchParamRegistry.getActiveSearchParam(myResourceName, referenceParam);
|
||||
if (outerParam == null) {
|
||||
throwInvalidRequestExceptionForUnknownSortParameter(myResourceName, referenceParam);
|
||||
}
|
||||
|
||||
if (outerParam.hasUpliftRefchain(targetParam)) {
|
||||
} else if (outerParam.hasUpliftRefchain(targetParam)) {
|
||||
for (String nextTargetType : outerParam.getTargets()) {
|
||||
if (referenceParamTargetType != null && !referenceParamTargetType.equals(nextTargetType)) {
|
||||
continue;
|
||||
|
@ -940,6 +943,9 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
throwInvalidRequestExceptionForUnknownSortParameter(getResourceName(), paramName);
|
||||
}
|
||||
|
||||
// param will never be null here (the above line throws if it does)
|
||||
// this is just to prevent the warning
|
||||
assert param != null;
|
||||
if (isNotBlank(chainName) && param.getParamType() != RestSearchParameterTypeEnum.REFERENCE) {
|
||||
throw new InvalidRequestException(
|
||||
Msg.code(2285) + "Invalid chain, " + paramName + " is not a reference SearchParameter");
|
||||
|
@ -1116,11 +1122,15 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
resourceType, next, tagMap.get(next.getId()), theForHistoryOperation);
|
||||
}
|
||||
if (resource == null) {
|
||||
ourLog.warn(
|
||||
"Unable to find resource {}/{}/_history/{} in database",
|
||||
next.getResourceType(),
|
||||
next.getIdDt().getIdPart(),
|
||||
next.getVersion());
|
||||
if (next != null) {
|
||||
ourLog.warn(
|
||||
"Unable to find resource {}/{}/_history/{} in database",
|
||||
next.getResourceType(),
|
||||
next.getIdDt().getIdPart(),
|
||||
next.getVersion());
|
||||
} else {
|
||||
ourLog.warn("Unable to find resource in database.");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -1191,7 +1201,6 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
RequestDetails theDetails) {
|
||||
if (thePids.isEmpty()) {
|
||||
ourLog.debug("The include pids are empty");
|
||||
// return;
|
||||
}
|
||||
|
||||
// Dupes will cause a crash later anyhow, but this is expensive so only do it
|
||||
|
@ -1251,10 +1260,9 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
// only impl
|
||||
// to handle lastN?
|
||||
if (myStorageSettings.isAdvancedHSearchIndexing() && myStorageSettings.isStoreResourceInHSearchIndex()) {
|
||||
List<Long> pidList = thePids.stream().map(pid -> (pid).getId()).collect(Collectors.toList());
|
||||
List<Long> pidList = thePids.stream().map(JpaPid::getId).collect(Collectors.toList());
|
||||
|
||||
List<IBaseResource> resources = myFulltextSearchSvc.getResources(pidList);
|
||||
return resources;
|
||||
return myFulltextSearchSvc.getResources(pidList);
|
||||
} else if (!Objects.isNull(myParams) && myParams.isLastN()) {
|
||||
// legacy LastN implementation
|
||||
return myIElasticsearchSvc.getObservationResources(thePids);
|
||||
|
@ -1309,7 +1317,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
Pointcut.JPA_PERFTRACE_RAW_SQL, myInterceptorBroadcaster, theParameters.getRequestDetails())) {
|
||||
CurrentThreadCaptureQueriesListener.startCapturing();
|
||||
}
|
||||
if (matches.size() == 0) {
|
||||
if (matches.isEmpty()) {
|
||||
return new HashSet<>();
|
||||
}
|
||||
if (currentIncludes == null || currentIncludes.isEmpty()) {
|
||||
|
@ -1339,7 +1347,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
|
||||
for (Iterator<Include> iter = includes.iterator(); iter.hasNext(); ) {
|
||||
Include nextInclude = iter.next();
|
||||
if (nextInclude.isRecurse() == false) {
|
||||
if (!nextInclude.isRecurse()) {
|
||||
iter.remove();
|
||||
}
|
||||
|
||||
|
@ -1356,196 +1364,32 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
}
|
||||
|
||||
if (matchAll) {
|
||||
StringBuilder sqlBuilder = new StringBuilder();
|
||||
sqlBuilder.append("SELECT r.").append(findPidFieldName);
|
||||
sqlBuilder.append(", r.").append(findResourceTypeFieldName);
|
||||
if (findVersionFieldName != null) {
|
||||
sqlBuilder.append(", r.").append(findVersionFieldName);
|
||||
}
|
||||
sqlBuilder.append(" FROM ResourceLink r WHERE ");
|
||||
|
||||
sqlBuilder.append("r.");
|
||||
sqlBuilder.append(searchPidFieldName); // (rev mode) target_resource_id | source_resource_id
|
||||
sqlBuilder.append(" IN (:target_pids)");
|
||||
|
||||
/*
|
||||
* We need to set the resource type in 2 cases only:
|
||||
* 1) we are in $everything mode
|
||||
* (where we only want to fetch specific resource types, regardless of what is
|
||||
* available to fetch)
|
||||
* 2) we are doing revincludes
|
||||
*
|
||||
* Technically if the request is a qualified star (e.g. _include=Observation:*) we
|
||||
* should always be checking the source resource type on the resource link. We don't
|
||||
* actually index that column though by default, so in order to try and be efficient
|
||||
* we don't actually include it for includes (but we do for revincludes). This is
|
||||
* because for an include, it doesn't really make sense to include a different
|
||||
* resource type than the one you are searching on.
|
||||
*/
|
||||
if (wantResourceType != null
|
||||
&& (reverseMode || (myParams != null && myParams.getEverythingMode() != null))) {
|
||||
// because mySourceResourceType is not part of the HFJ_RES_LINK
|
||||
// index, this might not be the most optimal performance.
|
||||
// but it is for an $everything operation (and maybe we should update the index)
|
||||
sqlBuilder.append(" AND r.mySourceResourceType = :want_resource_type");
|
||||
} else {
|
||||
wantResourceType = null;
|
||||
}
|
||||
|
||||
// When calling $everything on a Patient instance, we don't want to recurse into new Patient
|
||||
// resources
|
||||
// (e.g. via Provenance, List, or Group) when in an $everything operation
|
||||
if (myParams != null
|
||||
&& myParams.getEverythingMode() == SearchParameterMap.EverythingModeEnum.PATIENT_INSTANCE) {
|
||||
sqlBuilder.append(" AND r.myTargetResourceType != 'Patient'");
|
||||
sqlBuilder.append(UNDESIRED_RESOURCE_LINKAGES_FOR_EVERYTHING_ON_PATIENT_INSTANCE.stream()
|
||||
.collect(Collectors.joining("', '", " AND r.mySourceResourceType NOT IN ('", "')")));
|
||||
}
|
||||
if (hasDesiredResourceTypes) {
|
||||
sqlBuilder.append(" AND r.myTargetResourceType IN (:desired_target_resource_types)");
|
||||
}
|
||||
|
||||
String sql = sqlBuilder.toString();
|
||||
List<Collection<JpaPid>> partitions = partition(nextRoundMatches, getMaximumPageSize());
|
||||
for (Collection<JpaPid> nextPartition : partitions) {
|
||||
TypedQuery<?> q = entityManager.createQuery(sql, Object[].class);
|
||||
q.setParameter("target_pids", JpaPid.toLongList(nextPartition));
|
||||
if (wantResourceType != null) {
|
||||
q.setParameter("want_resource_type", wantResourceType);
|
||||
}
|
||||
if (maxCount != null) {
|
||||
q.setMaxResults(maxCount);
|
||||
}
|
||||
if (hasDesiredResourceTypes) {
|
||||
q.setParameter("desired_target_resource_types", desiredResourceTypes);
|
||||
}
|
||||
List<?> results = q.getResultList();
|
||||
for (Object nextRow : results) {
|
||||
if (nextRow == null) {
|
||||
// This can happen if there are outgoing references which are canonical or point to
|
||||
// other servers
|
||||
continue;
|
||||
}
|
||||
|
||||
Long version = null;
|
||||
Long resourceLink = (Long) ((Object[]) nextRow)[0];
|
||||
String resourceType = (String) ((Object[]) nextRow)[1];
|
||||
if (findVersionFieldName != null) {
|
||||
version = (Long) ((Object[]) nextRow)[2];
|
||||
}
|
||||
|
||||
if (resourceLink != null) {
|
||||
JpaPid pid =
|
||||
JpaPid.fromIdAndVersionAndResourceType(resourceLink, version, resourceType);
|
||||
pidsToInclude.add(pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
loadIncludesMatchAll(
|
||||
findPidFieldName,
|
||||
findResourceTypeFieldName,
|
||||
findVersionFieldName,
|
||||
searchPidFieldName,
|
||||
wantResourceType,
|
||||
reverseMode,
|
||||
hasDesiredResourceTypes,
|
||||
nextRoundMatches,
|
||||
entityManager,
|
||||
maxCount,
|
||||
desiredResourceTypes,
|
||||
pidsToInclude,
|
||||
request);
|
||||
} else {
|
||||
List<String> paths;
|
||||
|
||||
// Start replace
|
||||
RuntimeSearchParam param;
|
||||
String resType = nextInclude.getParamType();
|
||||
if (isBlank(resType)) {
|
||||
continue;
|
||||
}
|
||||
RuntimeResourceDefinition def = fhirContext.getResourceDefinition(resType);
|
||||
if (def == null) {
|
||||
ourLog.warn("Unknown resource type in include/revinclude=" + nextInclude.getValue());
|
||||
continue;
|
||||
}
|
||||
|
||||
String paramName = nextInclude.getParamName();
|
||||
if (isNotBlank(paramName)) {
|
||||
param = mySearchParamRegistry.getActiveSearchParam(resType, paramName);
|
||||
} else {
|
||||
param = null;
|
||||
}
|
||||
if (param == null) {
|
||||
ourLog.warn("Unknown param name in include/revinclude=" + nextInclude.getValue());
|
||||
continue;
|
||||
}
|
||||
|
||||
paths = param.getPathsSplitForResourceType(resType);
|
||||
// end replace
|
||||
|
||||
Set<String> targetResourceTypes = computeTargetResourceTypes(nextInclude, param);
|
||||
|
||||
for (String nextPath : paths) {
|
||||
String findPidFieldSqlColumn = findPidFieldName.equals(MY_SOURCE_RESOURCE_PID)
|
||||
? "src_resource_id"
|
||||
: "target_resource_id";
|
||||
String fieldsToLoad = "r." + findPidFieldSqlColumn + " AS " + RESOURCE_ID_ALIAS;
|
||||
if (findVersionFieldName != null) {
|
||||
fieldsToLoad += ", r.target_resource_version AS " + RESOURCE_VERSION_ALIAS;
|
||||
}
|
||||
|
||||
// Query for includes lookup has 2 cases
|
||||
// Case 1: Where target_resource_id is available in hfj_res_link table for local references
|
||||
// Case 2: Where target_resource_id is null in hfj_res_link table and referred by a canonical
|
||||
// url in target_resource_url
|
||||
|
||||
// Case 1:
|
||||
Map<String, Object> localReferenceQueryParams = new HashMap<>();
|
||||
|
||||
String searchPidFieldSqlColumn = searchPidFieldName.equals(MY_TARGET_RESOURCE_PID)
|
||||
? "target_resource_id"
|
||||
: "src_resource_id";
|
||||
StringBuilder localReferenceQuery =
|
||||
new StringBuilder("SELECT " + fieldsToLoad + " FROM hfj_res_link r "
|
||||
+ " WHERE r.src_path = :src_path AND "
|
||||
+ " r.target_resource_id IS NOT NULL AND "
|
||||
+ " r."
|
||||
+ searchPidFieldSqlColumn + " IN (:target_pids) ");
|
||||
localReferenceQueryParams.put("src_path", nextPath);
|
||||
// we loop over target_pids later.
|
||||
if (targetResourceTypes != null) {
|
||||
if (targetResourceTypes.size() == 1) {
|
||||
localReferenceQuery.append(" AND r.target_resource_type = :target_resource_type ");
|
||||
localReferenceQueryParams.put(
|
||||
"target_resource_type",
|
||||
targetResourceTypes.iterator().next());
|
||||
} else {
|
||||
localReferenceQuery.append(" AND r.target_resource_type in (:target_resource_types) ");
|
||||
localReferenceQueryParams.put("target_resource_types", targetResourceTypes);
|
||||
}
|
||||
}
|
||||
|
||||
// Case 2:
|
||||
Pair<String, Map<String, Object>> canonicalQuery = buildCanonicalUrlQuery(
|
||||
findVersionFieldName, searchPidFieldSqlColumn, targetResourceTypes);
|
||||
|
||||
// @formatter:on
|
||||
|
||||
String sql = localReferenceQuery + " UNION " + canonicalQuery.getLeft();
|
||||
|
||||
List<Collection<JpaPid>> partitions = partition(nextRoundMatches, getMaximumPageSize());
|
||||
for (Collection<JpaPid> nextPartition : partitions) {
|
||||
Query q = entityManager.createNativeQuery(sql, Tuple.class);
|
||||
q.setParameter("target_pids", JpaPid.toLongList(nextPartition));
|
||||
localReferenceQueryParams.forEach(q::setParameter);
|
||||
canonicalQuery.getRight().forEach(q::setParameter);
|
||||
|
||||
if (maxCount != null) {
|
||||
q.setMaxResults(maxCount);
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Tuple> results = q.getResultList();
|
||||
for (Tuple result : results) {
|
||||
if (result != null) {
|
||||
Long resourceId =
|
||||
NumberUtils.createLong(String.valueOf(result.get(RESOURCE_ID_ALIAS)));
|
||||
Long resourceVersion = null;
|
||||
if (findVersionFieldName != null && result.get(RESOURCE_VERSION_ALIAS) != null) {
|
||||
resourceVersion = NumberUtils.createLong(
|
||||
String.valueOf(result.get(RESOURCE_VERSION_ALIAS)));
|
||||
}
|
||||
pidsToInclude.add(JpaPid.fromIdAndVersion(resourceId, resourceVersion));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
loadIncludesMatchSpecific(
|
||||
nextInclude,
|
||||
fhirContext,
|
||||
findPidFieldName,
|
||||
findVersionFieldName,
|
||||
searchPidFieldName,
|
||||
reverseMode,
|
||||
nextRoundMatches,
|
||||
entityManager,
|
||||
maxCount,
|
||||
pidsToInclude);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1609,9 +1453,266 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
return allAdded;
|
||||
}
|
||||
|
||||
private void loadIncludesMatchSpecific(
|
||||
Include nextInclude,
|
||||
FhirContext fhirContext,
|
||||
String findPidFieldName,
|
||||
String findVersionFieldName,
|
||||
String searchPidFieldName,
|
||||
boolean reverseMode,
|
||||
List<JpaPid> nextRoundMatches,
|
||||
EntityManager entityManager,
|
||||
Integer maxCount,
|
||||
HashSet<JpaPid> pidsToInclude) {
|
||||
List<String> paths;
|
||||
|
||||
// Start replace
|
||||
RuntimeSearchParam param;
|
||||
String resType = nextInclude.getParamType();
|
||||
if (isBlank(resType)) {
|
||||
return;
|
||||
}
|
||||
RuntimeResourceDefinition def = fhirContext.getResourceDefinition(resType);
|
||||
if (def == null) {
|
||||
ourLog.warn("Unknown resource type in include/revinclude=" + nextInclude.getValue());
|
||||
return;
|
||||
}
|
||||
|
||||
String paramName = nextInclude.getParamName();
|
||||
if (isNotBlank(paramName)) {
|
||||
param = mySearchParamRegistry.getActiveSearchParam(resType, paramName);
|
||||
} else {
|
||||
param = null;
|
||||
}
|
||||
if (param == null) {
|
||||
ourLog.warn("Unknown param name in include/revinclude=" + nextInclude.getValue());
|
||||
return;
|
||||
}
|
||||
|
||||
paths = param.getPathsSplitForResourceType(resType);
|
||||
// end replace
|
||||
|
||||
Set<String> targetResourceTypes = computeTargetResourceTypes(nextInclude, param);
|
||||
|
||||
for (String nextPath : paths) {
|
||||
String findPidFieldSqlColumn =
|
||||
findPidFieldName.equals(MY_SOURCE_RESOURCE_PID) ? "src_resource_id" : "target_resource_id";
|
||||
String fieldsToLoad = "r." + findPidFieldSqlColumn + " AS " + RESOURCE_ID_ALIAS;
|
||||
if (findVersionFieldName != null) {
|
||||
fieldsToLoad += ", r.target_resource_version AS " + RESOURCE_VERSION_ALIAS;
|
||||
}
|
||||
|
||||
// Query for includes lookup has 2 cases
|
||||
// Case 1: Where target_resource_id is available in hfj_res_link table for local references
|
||||
// Case 2: Where target_resource_id is null in hfj_res_link table and referred by a canonical
|
||||
// url in target_resource_url
|
||||
|
||||
// Case 1:
|
||||
Map<String, Object> localReferenceQueryParams = new HashMap<>();
|
||||
|
||||
String searchPidFieldSqlColumn =
|
||||
searchPidFieldName.equals(MY_TARGET_RESOURCE_PID) ? "target_resource_id" : "src_resource_id";
|
||||
StringBuilder localReferenceQuery = new StringBuilder("SELECT " + fieldsToLoad + " FROM hfj_res_link r "
|
||||
+ " WHERE r.src_path = :src_path AND "
|
||||
+ " r.target_resource_id IS NOT NULL AND "
|
||||
+ " r."
|
||||
+ searchPidFieldSqlColumn + " IN (:target_pids) ");
|
||||
localReferenceQueryParams.put("src_path", nextPath);
|
||||
// we loop over target_pids later.
|
||||
if (targetResourceTypes != null) {
|
||||
if (targetResourceTypes.size() == 1) {
|
||||
localReferenceQuery.append(" AND r.target_resource_type = :target_resource_type ");
|
||||
localReferenceQueryParams.put(
|
||||
"target_resource_type",
|
||||
targetResourceTypes.iterator().next());
|
||||
} else {
|
||||
localReferenceQuery.append(" AND r.target_resource_type in (:target_resource_types) ");
|
||||
localReferenceQueryParams.put("target_resource_types", targetResourceTypes);
|
||||
}
|
||||
}
|
||||
|
||||
// Case 2:
|
||||
Pair<String, Map<String, Object>> canonicalQuery =
|
||||
buildCanonicalUrlQuery(findVersionFieldName, targetResourceTypes, reverseMode);
|
||||
|
||||
String sql = localReferenceQuery + " UNION " + canonicalQuery.getLeft();
|
||||
|
||||
List<Collection<JpaPid>> partitions = partition(nextRoundMatches, getMaximumPageSize());
|
||||
for (Collection<JpaPid> nextPartition : partitions) {
|
||||
Query q = entityManager.createNativeQuery(sql, Tuple.class);
|
||||
q.setParameter("target_pids", JpaPid.toLongList(nextPartition));
|
||||
localReferenceQueryParams.forEach(q::setParameter);
|
||||
canonicalQuery.getRight().forEach(q::setParameter);
|
||||
|
||||
if (maxCount != null) {
|
||||
q.setMaxResults(maxCount);
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Tuple> results = q.getResultList();
|
||||
for (Tuple result : results) {
|
||||
if (result != null) {
|
||||
Long resourceId = NumberUtils.createLong(String.valueOf(result.get(RESOURCE_ID_ALIAS)));
|
||||
Long resourceVersion = null;
|
||||
if (findVersionFieldName != null && result.get(RESOURCE_VERSION_ALIAS) != null) {
|
||||
resourceVersion =
|
||||
NumberUtils.createLong(String.valueOf(result.get(RESOURCE_VERSION_ALIAS)));
|
||||
}
|
||||
pidsToInclude.add(JpaPid.fromIdAndVersion(resourceId, resourceVersion));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadIncludesMatchAll(
|
||||
String findPidFieldName,
|
||||
String findResourceTypeFieldName,
|
||||
String findVersionFieldName,
|
||||
String searchPidFieldName,
|
||||
String wantResourceType,
|
||||
boolean reverseMode,
|
||||
boolean hasDesiredResourceTypes,
|
||||
List<JpaPid> nextRoundMatches,
|
||||
EntityManager entityManager,
|
||||
Integer maxCount,
|
||||
List<String> desiredResourceTypes,
|
||||
HashSet<JpaPid> pidsToInclude,
|
||||
RequestDetails request) {
|
||||
StringBuilder sqlBuilder = new StringBuilder();
|
||||
sqlBuilder.append("SELECT r.").append(findPidFieldName);
|
||||
sqlBuilder.append(", r.").append(findResourceTypeFieldName);
|
||||
sqlBuilder.append(", r.myTargetResourceUrl");
|
||||
if (findVersionFieldName != null) {
|
||||
sqlBuilder.append(", r.").append(findVersionFieldName);
|
||||
}
|
||||
sqlBuilder.append(" FROM ResourceLink r WHERE ");
|
||||
|
||||
sqlBuilder.append("r.");
|
||||
sqlBuilder.append(searchPidFieldName); // (rev mode) target_resource_id | source_resource_id
|
||||
sqlBuilder.append(" IN (:target_pids)");
|
||||
|
||||
/*
|
||||
* We need to set the resource type in 2 cases only:
|
||||
* 1) we are in $everything mode
|
||||
* (where we only want to fetch specific resource types, regardless of what is
|
||||
* available to fetch)
|
||||
* 2) we are doing revincludes
|
||||
*
|
||||
* Technically if the request is a qualified star (e.g. _include=Observation:*) we
|
||||
* should always be checking the source resource type on the resource link. We don't
|
||||
* actually index that column though by default, so in order to try and be efficient
|
||||
* we don't actually include it for includes (but we do for revincludes). This is
|
||||
* because for an include, it doesn't really make sense to include a different
|
||||
* resource type than the one you are searching on.
|
||||
*/
|
||||
if (wantResourceType != null && (reverseMode || (myParams != null && myParams.getEverythingMode() != null))) {
|
||||
// because mySourceResourceType is not part of the HFJ_RES_LINK
|
||||
// index, this might not be the most optimal performance.
|
||||
// but it is for an $everything operation (and maybe we should update the index)
|
||||
sqlBuilder.append(" AND r.mySourceResourceType = :want_resource_type");
|
||||
} else {
|
||||
wantResourceType = null;
|
||||
}
|
||||
|
||||
// When calling $everything on a Patient instance, we don't want to recurse into new Patient
|
||||
// resources
|
||||
// (e.g. via Provenance, List, or Group) when in an $everything operation
|
||||
if (myParams != null
|
||||
&& myParams.getEverythingMode() == SearchParameterMap.EverythingModeEnum.PATIENT_INSTANCE) {
|
||||
sqlBuilder.append(" AND r.myTargetResourceType != 'Patient'");
|
||||
sqlBuilder.append(UNDESIRED_RESOURCE_LINKAGES_FOR_EVERYTHING_ON_PATIENT_INSTANCE.stream()
|
||||
.collect(Collectors.joining("', '", " AND r.mySourceResourceType NOT IN ('", "')")));
|
||||
}
|
||||
if (hasDesiredResourceTypes) {
|
||||
sqlBuilder.append(" AND r.myTargetResourceType IN (:desired_target_resource_types)");
|
||||
}
|
||||
|
||||
String sql = sqlBuilder.toString();
|
||||
List<Collection<JpaPid>> partitions = partition(nextRoundMatches, getMaximumPageSize());
|
||||
for (Collection<JpaPid> nextPartition : partitions) {
|
||||
TypedQuery<?> q = entityManager.createQuery(sql, Object[].class);
|
||||
q.setParameter("target_pids", JpaPid.toLongList(nextPartition));
|
||||
if (wantResourceType != null) {
|
||||
q.setParameter("want_resource_type", wantResourceType);
|
||||
}
|
||||
if (maxCount != null) {
|
||||
q.setMaxResults(maxCount);
|
||||
}
|
||||
if (hasDesiredResourceTypes) {
|
||||
q.setParameter("desired_target_resource_types", desiredResourceTypes);
|
||||
}
|
||||
List<?> results = q.getResultList();
|
||||
Set<String> canonicalUrls = null;
|
||||
for (Object nextRow : results) {
|
||||
if (nextRow == null) {
|
||||
// This can happen if there are outgoing references which are canonical or point to
|
||||
// other servers
|
||||
continue;
|
||||
}
|
||||
|
||||
Long version = null;
|
||||
Long resourceId = (Long) ((Object[]) nextRow)[0];
|
||||
String resourceType = (String) ((Object[]) nextRow)[1];
|
||||
String resourceCanonicalUrl = (String) ((Object[]) nextRow)[2];
|
||||
if (findVersionFieldName != null) {
|
||||
version = (Long) ((Object[]) nextRow)[3];
|
||||
}
|
||||
|
||||
if (resourceId != null) {
|
||||
JpaPid pid = JpaPid.fromIdAndVersionAndResourceType(resourceId, version, resourceType);
|
||||
pidsToInclude.add(pid);
|
||||
} else if (resourceCanonicalUrl != null) {
|
||||
if (canonicalUrls == null) {
|
||||
canonicalUrls = new HashSet<>();
|
||||
}
|
||||
canonicalUrls.add(resourceCanonicalUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if (canonicalUrls != null) {
|
||||
String message =
|
||||
"Search with _include=* can be inefficient when references using canonical URLs are detected. Use more specific _include values instead.";
|
||||
firePerformanceWarning(request, message);
|
||||
loadCanonicalUrls(canonicalUrls, entityManager, pidsToInclude, reverseMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadCanonicalUrls(
|
||||
Set<String> theCanonicalUrls,
|
||||
EntityManager theEntityManager,
|
||||
HashSet<JpaPid> thePidsToInclude,
|
||||
boolean theReverse) {
|
||||
StringBuilder sqlBuilder;
|
||||
Set<Long> identityHashesForTypes = calculateIndexUriIdentityHashesForResourceTypes(null, theReverse);
|
||||
List<Collection<String>> canonicalUrlPartitions =
|
||||
partition(theCanonicalUrls, getMaximumPageSize() - identityHashesForTypes.size());
|
||||
|
||||
sqlBuilder = new StringBuilder();
|
||||
sqlBuilder.append("SELECT i.myResourcePid ");
|
||||
sqlBuilder.append("FROM ResourceIndexedSearchParamUri i ");
|
||||
sqlBuilder.append("WHERE i.myHashIdentity IN (:hash_identity) ");
|
||||
sqlBuilder.append("AND i.myUri IN (:uris)");
|
||||
|
||||
String canonicalResSql = sqlBuilder.toString();
|
||||
|
||||
for (Collection<String> nextCanonicalUrlList : canonicalUrlPartitions) {
|
||||
TypedQuery<Long> canonicalResIdQuery = theEntityManager.createQuery(canonicalResSql, Long.class);
|
||||
canonicalResIdQuery.setParameter("hash_identity", identityHashesForTypes);
|
||||
canonicalResIdQuery.setParameter("uris", nextCanonicalUrlList);
|
||||
List<Long> resIds = canonicalResIdQuery.getResultList();
|
||||
for (var next : resIds) {
|
||||
if (next != null) {
|
||||
thePidsToInclude.add(JpaPid.fromId(next));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a
|
||||
* @param request
|
||||
* Calls Performance Trace Hook
|
||||
* @param request the request deatils
|
||||
* Sends a raw SQL query to the Pointcut for raw SQL queries.
|
||||
*/
|
||||
private void callRawSqlHookWithCurrentThreadQueries(RequestDetails request) {
|
||||
SqlQueryList capturedQueries = CurrentThreadCaptureQueriesListener.getCurrentQueueAndStopCapturing();
|
||||
|
@ -1641,30 +1742,22 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
|
||||
@Nonnull
|
||||
private Pair<String, Map<String, Object>> buildCanonicalUrlQuery(
|
||||
String theVersionFieldName, String thePidFieldSqlColumn, Set<String> theTargetResourceTypes) {
|
||||
String fieldsToLoadFromSpidxUriTable = "rUri.res_id";
|
||||
String theVersionFieldName, Set<String> theTargetResourceTypes, boolean theReverse) {
|
||||
String fieldsToLoadFromSpidxUriTable = theReverse ? "r.src_resource_id" : "rUri.res_id";
|
||||
if (theVersionFieldName != null) {
|
||||
// canonical-uri references aren't versioned, but we need to match the column count for the UNION
|
||||
fieldsToLoadFromSpidxUriTable += ", NULL";
|
||||
}
|
||||
// The logical join will be by hfj_spidx_uri on sp_name='uri' and sp_uri=target_resource_url.
|
||||
// But sp_name isn't indexed, so we use hash_identity instead.
|
||||
if (theTargetResourceTypes == null) {
|
||||
// hash_identity includes the resource type. So a null wildcard must be replaced with a list of all types.
|
||||
theTargetResourceTypes = myDaoRegistry.getRegisteredDaoTypes();
|
||||
}
|
||||
assert !theTargetResourceTypes.isEmpty();
|
||||
|
||||
Set<Long> identityHashesForTypes = theTargetResourceTypes.stream()
|
||||
.map(type -> BaseResourceIndexedSearchParam.calculateHashIdentity(
|
||||
myPartitionSettings, myRequestPartitionId, type, "url"))
|
||||
.collect(Collectors.toSet());
|
||||
Set<Long> identityHashesForTypes =
|
||||
calculateIndexUriIdentityHashesForResourceTypes(theTargetResourceTypes, theReverse);
|
||||
|
||||
Map<String, Object> canonicalUriQueryParams = new HashMap<>();
|
||||
StringBuilder canonicalUrlQuery = new StringBuilder(
|
||||
"SELECT " + fieldsToLoadFromSpidxUriTable + " FROM hfj_res_link r " + " JOIN hfj_spidx_uri rUri ON ( ");
|
||||
// join on hash_identity and sp_uri - indexed in IDX_SP_URI_HASH_IDENTITY_V2
|
||||
if (theTargetResourceTypes.size() == 1) {
|
||||
if (theTargetResourceTypes != null && theTargetResourceTypes.size() == 1) {
|
||||
canonicalUrlQuery.append(" rUri.hash_identity = :uri_identity_hash ");
|
||||
canonicalUriQueryParams.put(
|
||||
"uri_identity_hash", identityHashesForTypes.iterator().next());
|
||||
|
@ -1673,21 +1766,102 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
canonicalUriQueryParams.put("uri_identity_hashes", identityHashesForTypes);
|
||||
}
|
||||
|
||||
canonicalUrlQuery.append(" AND r.target_resource_url = rUri.sp_uri )" + " WHERE r.src_path = :src_path AND "
|
||||
+ " r.target_resource_id IS NULL AND "
|
||||
+ " r."
|
||||
+ thePidFieldSqlColumn + " IN (:target_pids) ");
|
||||
canonicalUrlQuery.append(" AND r.target_resource_url = rUri.sp_uri )");
|
||||
canonicalUrlQuery.append(" WHERE r.src_path = :src_path AND ");
|
||||
canonicalUrlQuery.append(" r.target_resource_id IS NULL ");
|
||||
canonicalUrlQuery.append(" AND ");
|
||||
if (theReverse) {
|
||||
canonicalUrlQuery.append("rUri.res_id");
|
||||
} else {
|
||||
canonicalUrlQuery.append("r.src_resource_id");
|
||||
}
|
||||
canonicalUrlQuery.append(" IN (:target_pids) ");
|
||||
|
||||
return Pair.of(canonicalUrlQuery.toString(), canonicalUriQueryParams);
|
||||
}
|
||||
|
||||
private List<Collection<JpaPid>> partition(Collection<JpaPid> theNextRoundMatches, int theMaxLoad) {
|
||||
@Nonnull
|
||||
Set<Long> calculateIndexUriIdentityHashesForResourceTypes(Set<String> theTargetResourceTypes, boolean theReverse) {
|
||||
Set<String> targetResourceTypes = theTargetResourceTypes;
|
||||
if (targetResourceTypes == null) {
|
||||
/*
|
||||
* If we don't have a list of valid target types, we need to figure out a list of all
|
||||
* possible target types in order to perform the search of the URI index table. This is
|
||||
* because the hash_identity column encodes the resource type, so we'll need a hash
|
||||
* value for each possible target type.
|
||||
*/
|
||||
targetResourceTypes = new HashSet<>();
|
||||
Set<String> possibleTypes = myDaoRegistry.getRegisteredDaoTypes();
|
||||
if (theReverse) {
|
||||
// For reverse includes, it is really hard to figure out what types
|
||||
// are actually potentially pointing to the type we're searching for
|
||||
// in this context, so let's just assume it could be anything.
|
||||
targetResourceTypes = possibleTypes;
|
||||
} else {
|
||||
for (var next : mySearchParamRegistry.getActiveSearchParams(myResourceName).values().stream()
|
||||
.filter(t -> t.getParamType().equals(RestSearchParameterTypeEnum.REFERENCE))
|
||||
.collect(Collectors.toList())) {
|
||||
|
||||
// If the reference points to a Reference (ie not a canonical or CanonicalReference)
|
||||
// then it doesn't matter here anyhow. The logic here only works for elements at the
|
||||
// root level of the document (e.g. QuestionnaireResponse.subject or
|
||||
// QuestionnaireResponse.subject.where(...)) but this is just an optimization
|
||||
// anyhow.
|
||||
if (next.getPath().startsWith(myResourceName + ".")) {
|
||||
String elementName =
|
||||
next.getPath().substring(next.getPath().indexOf('.') + 1);
|
||||
int secondDotIndex = elementName.indexOf('.');
|
||||
if (secondDotIndex != -1) {
|
||||
elementName = elementName.substring(0, secondDotIndex);
|
||||
}
|
||||
BaseRuntimeChildDefinition child =
|
||||
myContext.getResourceDefinition(myResourceName).getChildByName(elementName);
|
||||
if (child != null) {
|
||||
BaseRuntimeElementDefinition<?> childDef = child.getChildByName(elementName);
|
||||
if (childDef != null) {
|
||||
if (childDef.getName().equals("Reference")) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!next.getTargets().isEmpty()) {
|
||||
// For each reference parameter on the resource type we're searching for,
|
||||
// add all the potential target types to the list of possible target
|
||||
// resource types we can look up.
|
||||
for (var nextTarget : next.getTargets()) {
|
||||
if (possibleTypes.contains(nextTarget)) {
|
||||
targetResourceTypes.add(nextTarget);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If we have any references that don't define any target types, then
|
||||
// we need to assume that all enabled resource types are possible target
|
||||
// types
|
||||
targetResourceTypes.addAll(possibleTypes);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert !targetResourceTypes.isEmpty();
|
||||
|
||||
Set<Long> identityHashesForTypes = targetResourceTypes.stream()
|
||||
.map(type -> BaseResourceIndexedSearchParam.calculateHashIdentity(
|
||||
myPartitionSettings, myRequestPartitionId, type, "url"))
|
||||
.collect(Collectors.toSet());
|
||||
return identityHashesForTypes;
|
||||
}
|
||||
|
||||
private <T> List<Collection<T>> partition(Collection<T> theNextRoundMatches, int theMaxLoad) {
|
||||
if (theNextRoundMatches.size() <= theMaxLoad) {
|
||||
return Collections.singletonList(theNextRoundMatches);
|
||||
} else {
|
||||
|
||||
List<Collection<JpaPid>> retVal = new ArrayList<>();
|
||||
Collection<JpaPid> current = null;
|
||||
for (JpaPid next : theNextRoundMatches) {
|
||||
List<Collection<T>> retVal = new ArrayList<>();
|
||||
Collection<T> current = null;
|
||||
for (T next : theNextRoundMatches) {
|
||||
if (current == null) {
|
||||
current = new ArrayList<>(theMaxLoad);
|
||||
retVal.add(current);
|
||||
|
@ -1721,7 +1895,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
for (RuntimeSearchParam nextCandidate : candidateComboParams) {
|
||||
List<String> nextCandidateParamNames =
|
||||
JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, nextCandidate).stream()
|
||||
.map(t -> t.getName())
|
||||
.map(RuntimeSearchParam::getName)
|
||||
.collect(Collectors.toList());
|
||||
if (theParams.keySet().containsAll(nextCandidateParamNames)) {
|
||||
comboParam = nextCandidate;
|
||||
|
@ -1733,7 +1907,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
|
||||
if (comboParam != null) {
|
||||
// Since we're going to remove elements below
|
||||
theParams.values().forEach(nextAndList -> ensureSubListsAreWritable(nextAndList));
|
||||
theParams.values().forEach(this::ensureSubListsAreWritable);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(myResourceName);
|
||||
|
@ -2121,19 +2295,11 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
}
|
||||
|
||||
private void sendProcessingMsgAndFirePerformanceHook() {
|
||||
StorageProcessingMessage message = new StorageProcessingMessage();
|
||||
String msg = "Pass completed with no matching results seeking rows "
|
||||
+ myPidSet.size() + "-" + mySkipCount
|
||||
+ ". This indicates an inefficient query! Retrying with new max count of "
|
||||
+ myMaxResultsToFetch;
|
||||
ourLog.warn(msg);
|
||||
message.setMessage(msg);
|
||||
HookParams params = new HookParams()
|
||||
.add(RequestDetails.class, myRequest)
|
||||
.addIfMatchesType(ServletRequestDetails.class, myRequest)
|
||||
.add(StorageProcessingMessage.class, message);
|
||||
CompositeInterceptorBroadcaster.doCallHooks(
|
||||
myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_WARNING, params);
|
||||
firePerformanceWarning(myRequest, msg);
|
||||
}
|
||||
|
||||
private void initializeIteratorQuery(Integer theOffset, Integer theMaxResultsToFetch) {
|
||||
|
@ -2208,6 +2374,18 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
}
|
||||
}
|
||||
|
||||
private void firePerformanceWarning(RequestDetails theRequest, String theMessage) {
|
||||
ourLog.warn(theMessage);
|
||||
StorageProcessingMessage message = new StorageProcessingMessage();
|
||||
message.setMessage(theMessage);
|
||||
HookParams params = new HookParams()
|
||||
.add(RequestDetails.class, theRequest)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequest)
|
||||
.add(StorageProcessingMessage.class, message);
|
||||
CompositeInterceptorBroadcaster.doCallHooks(
|
||||
myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_WARNING, params);
|
||||
}
|
||||
|
||||
public static int getMaximumPageSize() {
|
||||
if (myUseMaxPageSize50ForTest) {
|
||||
return MAXIMUM_PAGE_SIZE_FOR_TESTING;
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
package ca.uhn.fhir.jpa.search.builder;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
|
||||
import ca.uhn.fhir.rest.server.util.FhirContextSearchParamRegistry;
|
||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SearchBuilderTest {
|
||||
|
||||
public static final FhirContext ourCtx = FhirContext.forR4Cached();
|
||||
@Spy
|
||||
private FhirContext myFhirContext = ourCtx;
|
||||
|
||||
@Spy
|
||||
private ISearchParamRegistry mySearchParamRegistry = new FhirContextSearchParamRegistry(myFhirContext);
|
||||
|
||||
@Spy
|
||||
private PartitionSettings myPartitionSettings = new PartitionSettings();
|
||||
|
||||
@Mock(strictness = Mock.Strictness.LENIENT)
|
||||
private DaoRegistry myDaoRegistry;
|
||||
|
||||
@InjectMocks
|
||||
private SearchBuilder mySearchBuilder;
|
||||
|
||||
@BeforeEach
|
||||
public void beforeEach() {
|
||||
mySearchBuilder.setResourceName("QuestionnaireResponse");
|
||||
when(myDaoRegistry.getRegisteredDaoTypes()).thenReturn(ourCtx.getResourceTypes());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCalculateIndexUriIdentityHashesForResourceTypes_Include_Null() {
|
||||
Set<Long> types = mySearchBuilder.calculateIndexUriIdentityHashesForResourceTypes(null, false);
|
||||
// There are only 12 resource types that actually can be linked to by the QuestionnaireResponse
|
||||
// resource via canonical references in any parameters
|
||||
assertThat(types).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCalculateIndexUriIdentityHashesForResourceTypes_Include_Nonnull() {
|
||||
Set<String> inputTypes = Set.of("Questionnaire");
|
||||
Set<Long> types = mySearchBuilder.calculateIndexUriIdentityHashesForResourceTypes(inputTypes, false);
|
||||
// Just the one that we actually specified
|
||||
assertThat(types).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCalculateIndexUriIdentityHashesForResourceTypes_RevInclude_Null() {
|
||||
Set<Long> types = mySearchBuilder.calculateIndexUriIdentityHashesForResourceTypes(null, true);
|
||||
// Revincludes are really hard to figure out the potential resource types for, so we just need to
|
||||
// use all active resource types
|
||||
assertThat(types).hasSize(146);
|
||||
}
|
||||
|
||||
}
|
|
@ -26,7 +26,7 @@ import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
|
|||
import ca.uhn.fhir.jpa.search.BaseSourceSearchParameterTestCases;
|
||||
import ca.uhn.fhir.jpa.search.CompositeSearchParameterTestCases;
|
||||
import ca.uhn.fhir.jpa.search.QuantitySearchParameterTestCases;
|
||||
import ca.uhn.fhir.jpa.search.lastn.ElasticsearchSvcImpl;
|
||||
import ca.uhn.fhir.jpa.search.builder.SearchBuilder;
|
||||
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
|
||||
|
@ -56,6 +56,7 @@ import ca.uhn.fhir.test.utilities.LogbackLevelOverrideExtension;
|
|||
import ca.uhn.fhir.test.utilities.docker.RequiresDocker;
|
||||
import ca.uhn.fhir.validation.FhirValidator;
|
||||
import ca.uhn.fhir.validation.ValidationResult;
|
||||
import ch.qos.logback.classic.Level;
|
||||
import jakarta.annotation.Nonnull;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
|
@ -106,7 +107,6 @@ import org.springframework.test.context.support.DirtiesContextTestExecutionListe
|
|||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import org.testcontainers.elasticsearch.ElasticsearchContainer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
|
@ -207,13 +207,9 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest impl
|
|||
private IFhirResourceDao<QuestionnaireResponse> myQuestionnaireResponseDao;
|
||||
@Autowired
|
||||
private TestHSearchEventDispatcher myHSearchEventDispatcher;
|
||||
@Autowired
|
||||
ElasticsearchContainer myElasticsearchContainer;
|
||||
|
||||
@Mock
|
||||
private IHSearchEventListener mySearchEventListener;
|
||||
@Autowired
|
||||
private ElasticsearchSvcImpl myElasticsearchSvc;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
|
@ -917,15 +913,14 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest impl
|
|||
*/
|
||||
@Test
|
||||
public void testDirectPathWholeResourceNotIndexedWorks() {
|
||||
// setup
|
||||
myLogbackLevelOverrideExtension.setLogLevel(SearchBuilder.class, Level.WARN);
|
||||
IIdType id1 = myTestDataBuilder.createObservation(List.of(myTestDataBuilder.withObservationCode("http://example.com/", "theCode")));
|
||||
|
||||
// set it after creating resource, so search doesn't find it in the index
|
||||
myStorageSettings.setStoreResourceInHSearchIndex(true);
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
|
||||
List<IBaseResource> result = searchForFastResources("Observation?code=theCode");
|
||||
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
|
||||
List<IBaseResource> result = searchForFastResources("Observation?code=theCode&_count=10&_total=accurate");
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertEquals(((Observation) result.get(0)).getIdElement().getIdPart(), id1.getIdPart());
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -398,7 +398,7 @@ public abstract class BaseJpaR5Test extends BaseJpaTest implements ITestDataBuil
|
|||
|
||||
private PerformanceTracingLoggingInterceptor myPerformanceTracingLoggingInterceptor;
|
||||
@Autowired
|
||||
private DaoRegistry myDaoRegistry;
|
||||
protected DaoRegistry myDaoRegistry;
|
||||
@Autowired
|
||||
private IBulkDataExportJobSchedulingHelper myBulkDataSchedulerHelper;
|
||||
|
||||
|
|
|
@ -13,16 +13,17 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
|||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import ca.uhn.fhir.util.BundleBuilder;
|
||||
import ca.uhn.fhir.util.HapiExtensions;
|
||||
import jakarta.annotation.Nonnull;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r5.model.Composition;
|
||||
import org.hl7.fhir.r5.model.IdType;
|
||||
import org.hl7.fhir.r5.model.Bundle;
|
||||
import org.hl7.fhir.r5.model.CodeType;
|
||||
import org.hl7.fhir.r5.model.Composition;
|
||||
import org.hl7.fhir.r5.model.DateType;
|
||||
import org.hl7.fhir.r5.model.Encounter;
|
||||
import org.hl7.fhir.r5.model.Enumerations;
|
||||
import org.hl7.fhir.r5.model.Extension;
|
||||
import org.hl7.fhir.r5.model.IdType;
|
||||
import org.hl7.fhir.r5.model.Identifier;
|
||||
import org.hl7.fhir.r5.model.Organization;
|
||||
import org.hl7.fhir.r5.model.Patient;
|
||||
|
@ -35,7 +36,6 @@ import org.junit.jupiter.api.Test;
|
|||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
|
||||
import jakarta.annotation.Nonnull;
|
||||
import java.util.List;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.countMatches;
|
||||
|
|
|
@ -1,11 +1,26 @@
|
|||
package ca.uhn.fhir.jpa.provider.r5;
|
||||
|
||||
import ca.uhn.fhir.batch2.jobs.reindex.ReindexAppCtx;
|
||||
import ca.uhn.fhir.batch2.jobs.reindex.ReindexJobParameters;
|
||||
import ca.uhn.fhir.batch2.model.JobInstanceStartRequest;
|
||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse;
|
||||
import ca.uhn.fhir.jpa.entity.Search;
|
||||
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.model.api.Include;
|
||||
import ca.uhn.fhir.parser.IParser;
|
||||
import ca.uhn.fhir.parser.StrictErrorHandler;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.api.SearchTotalModeEnum;
|
||||
import ca.uhn.fhir.rest.api.SortOrderEnum;
|
||||
import ca.uhn.fhir.rest.api.SortSpec;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
|
||||
import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor;
|
||||
import ca.uhn.fhir.rest.param.StringParam;
|
||||
import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
|
||||
import ca.uhn.fhir.util.BundleBuilder;
|
||||
|
@ -17,7 +32,6 @@ import org.apache.http.client.methods.HttpGet;
|
|||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r5.model.Bundle;
|
||||
import org.hl7.fhir.r5.model.Bundle.BundleEntryComponent;
|
||||
|
@ -25,13 +39,18 @@ import org.hl7.fhir.r5.model.CarePlan;
|
|||
import org.hl7.fhir.r5.model.CodeableConcept;
|
||||
import org.hl7.fhir.r5.model.Condition;
|
||||
import org.hl7.fhir.r5.model.DateTimeType;
|
||||
import org.hl7.fhir.r5.model.Extension;
|
||||
import org.hl7.fhir.r5.model.MedicationRequest;
|
||||
import org.hl7.fhir.r5.model.MedicinalProductDefinition;
|
||||
import org.hl7.fhir.r5.model.Observation;
|
||||
import org.hl7.fhir.r5.model.Observation.ObservationComponentComponent;
|
||||
import org.hl7.fhir.r5.model.Organization;
|
||||
import org.hl7.fhir.r5.model.Parameters;
|
||||
import org.hl7.fhir.r5.model.Patient;
|
||||
import org.hl7.fhir.r5.model.Quantity;
|
||||
import org.hl7.fhir.r5.model.SearchParameter;
|
||||
import org.hl7.fhir.r5.model.StringType;
|
||||
import org.intellij.lang.annotations.Language;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -44,15 +63,13 @@ import org.springframework.util.comparator.Comparators;
|
|||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.leftPad;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
@SuppressWarnings("Duplicates")
|
||||
public class ResourceProviderR5Test extends BaseResourceProviderR5Test {
|
||||
|
@ -110,7 +127,7 @@ public class ResourceProviderR5Test extends BaseResourceProviderR5Test {
|
|||
.returnBundle(Bundle.class)
|
||||
.execute();
|
||||
List<String> ids = output.getEntry().stream().map(t -> t.getResource().getIdElement().toUnqualifiedVersionless().getValue()).collect(Collectors.toList());
|
||||
assertThat(ids, containsInAnyOrder(pt1id));
|
||||
assertThat(ids).containsExactlyInAnyOrder(pt1id);
|
||||
|
||||
output = myClient
|
||||
.search()
|
||||
|
@ -119,7 +136,7 @@ public class ResourceProviderR5Test extends BaseResourceProviderR5Test {
|
|||
.returnBundle(Bundle.class)
|
||||
.execute();
|
||||
ids = output.getEntry().stream().map(t -> t.getResource().getIdElement().toUnqualifiedVersionless().getValue()).collect(Collectors.toList());
|
||||
assertThat(ids, containsInAnyOrder(pt1id));
|
||||
assertThat(ids).containsExactlyInAnyOrder(pt1id);
|
||||
|
||||
}
|
||||
|
||||
|
@ -135,7 +152,7 @@ public class ResourceProviderR5Test extends BaseResourceProviderR5Test {
|
|||
.where(org.hl7.fhir.r4.model.Patient.NAME.matches().value("Hello"))
|
||||
.returnBundle(Bundle.class)
|
||||
.execute();
|
||||
assertEquals(1, response0.getEntry().size());
|
||||
assertThat(response0.getEntry()).hasSize(1);
|
||||
|
||||
// Perform the search again (should return the same)
|
||||
Bundle response1 = myClient.search()
|
||||
|
@ -143,7 +160,7 @@ public class ResourceProviderR5Test extends BaseResourceProviderR5Test {
|
|||
.where(org.hl7.fhir.r4.model.Patient.NAME.matches().value("Hello"))
|
||||
.returnBundle(Bundle.class)
|
||||
.execute();
|
||||
assertEquals(1, response1.getEntry().size());
|
||||
assertThat(response1.getEntry()).hasSize(1);
|
||||
assertEquals(response0.getId(), response1.getId());
|
||||
|
||||
// Pretend the search was errored out
|
||||
|
@ -155,8 +172,8 @@ public class ResourceProviderR5Test extends BaseResourceProviderR5Test {
|
|||
.where(org.hl7.fhir.r4.model.Patient.NAME.matches().value("Hello"))
|
||||
.returnBundle(Bundle.class)
|
||||
.execute();
|
||||
assertEquals(1, response3.getEntry().size());
|
||||
assertNotEquals(response0.getId(), response3.getId());
|
||||
assertThat(response3.getEntry()).hasSize(1);
|
||||
assertThat(response3.getId()).isNotEqualTo(response0.getId());
|
||||
|
||||
}
|
||||
|
||||
|
@ -196,7 +213,7 @@ public class ResourceProviderR5Test extends BaseResourceProviderR5Test {
|
|||
.returnBundle(Bundle.class)
|
||||
.count(1)
|
||||
.execute();
|
||||
assertEquals(1, response0.getEntry().size());
|
||||
assertThat(response0.getEntry()).hasSize(1);
|
||||
|
||||
// Make sure it works for now
|
||||
myClient.loadPage().next(response0).execute();
|
||||
|
@ -209,9 +226,183 @@ public class ResourceProviderR5Test extends BaseResourceProviderR5Test {
|
|||
myClient.loadPage().next(response0).execute();
|
||||
} catch (NotImplementedOperationException e) {
|
||||
assertEquals(501, e.getStatusCode());
|
||||
assertThat(e.getMessage(), containsString("Some Failure Message"));
|
||||
assertThat(e.getMessage()).contains("Some Failure Message");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void searchForNewerResources_fullTextSearchWithFilterAndCount_shouldReturnAccurateResults() {
|
||||
IParser parser = myFhirContext.newJsonParser();
|
||||
int count = 10;
|
||||
|
||||
boolean presetFilterParameterEnabled = myStorageSettings.isFilterParameterEnabled();
|
||||
boolean presetAdvancedHSearchIndexing = myStorageSettings.isAdvancedHSearchIndexing();
|
||||
|
||||
try {
|
||||
// fullTextSearch means Advanced Hibernate Search
|
||||
myStorageSettings.setFilterParameterEnabled(true);
|
||||
myStorageSettings.setAdvancedHSearchIndexing(true);
|
||||
|
||||
// create custom search parameters - the _filter and _include are needed
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
IFhirResourceDao<SearchParameter> spDao = myDaoRegistry.getResourceDao("SearchParameter");
|
||||
SearchParameter sp;
|
||||
|
||||
@Language("JSON")
|
||||
String includeParam = """
|
||||
{
|
||||
"resourceType": "SearchParameter",
|
||||
"id": "9905463e-e817-4db0-9a3e-ff6aa3427848",
|
||||
"meta": {
|
||||
"versionId": "2",
|
||||
"lastUpdated": "2024-03-28T12:53:57.874+00:00",
|
||||
"source": "#7b34a4bfa42fe3ae"
|
||||
},
|
||||
"title": "Medicinal Product Manfacturer",
|
||||
"status": "active",
|
||||
"publisher": "MOH-IDMS",
|
||||
"code": "productmanufacturer",
|
||||
"base": [
|
||||
"MedicinalProductDefinition"
|
||||
],
|
||||
"type": "reference",
|
||||
"expression": "MedicinalProductDefinition.operation.organization"
|
||||
}
|
||||
""";
|
||||
sp = parser.parseResource(SearchParameter.class, includeParam);
|
||||
spDao.create(sp, new SystemRequestDetails());
|
||||
sp = null;
|
||||
@Language("JSON")
|
||||
String filterParam = """
|
||||
{
|
||||
"resourceType": "SearchParameter",
|
||||
"id": "SEARCH-PARAMETER-MedicinalProductDefinition-SearchableString",
|
||||
"meta": {
|
||||
"versionId": "2",
|
||||
"lastUpdated": "2024-03-27T19:20:25.200+00:00",
|
||||
"source": "#384dd6bccaeafa6c"
|
||||
},
|
||||
"url": "https://health.gov.on.ca/idms/fhir/SearchParameter/MedicinalProductDefinition-SearchableString",
|
||||
"version": "1.0.0",
|
||||
"name": "MedicinalProductDefinitionSearchableString",
|
||||
"status": "active",
|
||||
"publisher": "MOH-IDMS",
|
||||
"description": "Search Parameter for the MedicinalProductDefinition Searchable String Extension",
|
||||
"code": "MedicinalProductDefinitionSearchableString",
|
||||
"base": [
|
||||
"MedicinalProductDefinition"
|
||||
],
|
||||
"type": "string",
|
||||
"expression": "MedicinalProductDefinition.extension('https://health.gov.on.ca/idms/fhir/StructureDefinition/SearchableExtraString')",
|
||||
"target": [
|
||||
"MedicinalProductDefinition"
|
||||
]
|
||||
}
|
||||
""";
|
||||
sp = parser.parseResource(SearchParameter.class, filterParam);
|
||||
spDao.create(sp, new SystemRequestDetails());
|
||||
}
|
||||
// create MedicinalProductDefinitions
|
||||
MedicinalProductDefinition mdr;
|
||||
{
|
||||
@Language("JSON")
|
||||
String mpdstr = """
|
||||
{
|
||||
"resourceType": "MedicinalProductDefinition",
|
||||
"id": "36fb418b-4b1f-414c-bbb1-731bc8744b93",
|
||||
"meta": {
|
||||
"versionId": "17",
|
||||
"lastUpdated": "2024-06-10T16:52:23.907+00:00",
|
||||
"source": "#3a309416d5f52c5b",
|
||||
"profile": [
|
||||
"https://health.gov.on.ca/idms/fhir/StructureDefinition/IDMS_MedicinalProductDefinition"
|
||||
]
|
||||
},
|
||||
"extension": [
|
||||
{
|
||||
"url": "https://health.gov.on.ca/idms/fhir/StructureDefinition/SearchableExtraString",
|
||||
"valueString": "zahidbrand0610-2up|genupuu|qwewqe2 111|11111115|DF other des|Biologic|Oncology|Private Label"
|
||||
}
|
||||
],
|
||||
"status": {
|
||||
"coding": [
|
||||
{
|
||||
"system": "http://hl7.org/fhir/ValueSet/publication-status",
|
||||
"code": "active",
|
||||
"display": "Active"
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": [
|
||||
{
|
||||
"productName": "zahidbrand0610-2up"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
mdr = parser.parseResource(MedicinalProductDefinition.class, mpdstr);
|
||||
}
|
||||
IFhirResourceDao<MedicinalProductDefinition> mdrdao = myDaoRegistry.getResourceDao(MedicinalProductDefinition.class);
|
||||
|
||||
/*
|
||||
* We actually want a bunch of non-matching resources in the db
|
||||
* that won't match the filter before we get to the one that will.
|
||||
*
|
||||
* To this end, we're going to insert more than we plan
|
||||
* on retrieving to ensure the _filter is being used in both the
|
||||
* count query and the actual db hit
|
||||
*/
|
||||
List<MedicinalProductDefinition.MedicinalProductDefinitionNameComponent> productNames = mdr.getName();
|
||||
mdr.setName(null);
|
||||
List<Extension> extensions = mdr.getExtension();
|
||||
mdr.setExtension(null);
|
||||
// we need at least 10 of these; 20 should be good
|
||||
for (int i = 0; i < 2 * count; i++) {
|
||||
mdr.addName(new MedicinalProductDefinition.MedicinalProductDefinitionNameComponent("Product " + i));
|
||||
mdr.addExtension()
|
||||
.setUrl("https://health.gov.on.ca/idms/fhir/StructureDefinition/SearchableExtraString")
|
||||
.setValue(new StringType("Non-matching string " + i));
|
||||
mdrdao.create(mdr, new SystemRequestDetails());
|
||||
}
|
||||
mdr.setName(productNames);
|
||||
mdr.setExtension(extensions);
|
||||
mdrdao.create(mdr, new SystemRequestDetails());
|
||||
|
||||
// do a reindex
|
||||
ReindexJobParameters jobParameters = new ReindexJobParameters();
|
||||
jobParameters.setRequestPartitionId(RequestPartitionId.allPartitions());
|
||||
JobInstanceStartRequest request = new JobInstanceStartRequest();
|
||||
request.setJobDefinitionId(ReindexAppCtx.JOB_REINDEX);
|
||||
request.setParameters(jobParameters);
|
||||
Batch2JobStartResponse response = myJobCoordinator.startInstance(new SystemRequestDetails(), request);
|
||||
|
||||
myBatch2JobHelper.awaitJobCompletion(response);
|
||||
|
||||
// query like:
|
||||
// MedicinalProductDefinition?_getpagesoffset=0&_count=10&_total=accurate&_sort:asc=name&status=active&_include=MedicinalProductDefinition:productmanufacturer&_filter=MedicinalProductDefinitionSearchableString%20co%20%22zah%22
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
map.setCount(10);
|
||||
map.setSearchTotalMode(SearchTotalModeEnum.ACCURATE);
|
||||
map.setSort(new SortSpec().setOrder(SortOrderEnum.ASC).setParamName("name"));
|
||||
map.setIncludes(Set.of(
|
||||
new Include("MedicinalProductDefinition:productmanufacturer")
|
||||
));
|
||||
map.add("_filter", new StringParam("MedicinalProductDefinitionSearchableString co \"zah\""));
|
||||
|
||||
// test
|
||||
IBundleProvider result = mdrdao.search(map, new SystemRequestDetails());
|
||||
|
||||
// validate
|
||||
// we expect to find our 1 matching resource
|
||||
assertEquals(1, result.getAllResources().size());
|
||||
assertNotNull(result.size());
|
||||
assertEquals(1, result.size());
|
||||
} finally {
|
||||
// reset values
|
||||
myStorageSettings.setFilterParameterEnabled(presetFilterParameterEnabled);
|
||||
myStorageSettings.setAdvancedHSearchIndexing(presetAdvancedHSearchIndexing);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -254,7 +445,7 @@ public class ResourceProviderR5Test extends BaseResourceProviderR5Test {
|
|||
.returnBundle(Bundle.class)
|
||||
.execute();
|
||||
List<IIdType> ids = output.getEntry().stream().map(t -> t.getResource().getIdElement().toUnqualified()).collect(Collectors.toList());
|
||||
assertThat(ids, containsInAnyOrder(oid));
|
||||
assertThat(ids).containsExactlyInAnyOrder(oid);
|
||||
}
|
||||
|
||||
|
||||
|
@ -278,7 +469,7 @@ public class ResourceProviderR5Test extends BaseResourceProviderR5Test {
|
|||
myCaptureQueriesListener.logSelectQueries();
|
||||
|
||||
assertEquals(2, output.getTotal());
|
||||
assertEquals(0, output.getEntry().size());
|
||||
assertThat(output.getEntry()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -370,7 +561,7 @@ public class ResourceProviderR5Test extends BaseResourceProviderR5Test {
|
|||
ourLog.debug("Bundle: \n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(found));
|
||||
|
||||
List<IIdType> list = toUnqualifiedVersionlessIds(found);
|
||||
assertEquals(4, found.getEntry().size());
|
||||
assertThat(found.getEntry()).hasSize(4);
|
||||
assertEquals(oid3, list.get(0));
|
||||
assertEquals(oid1, list.get(1));
|
||||
assertEquals(oid4, list.get(2));
|
||||
|
@ -403,9 +594,9 @@ public class ResourceProviderR5Test extends BaseResourceProviderR5Test {
|
|||
assertEquals(Bundle.BundleType.SEARCHSET, b.getType());
|
||||
List<IIdType> ids = toUnqualifiedVersionlessIds(b);
|
||||
|
||||
assertThat(ids, containsInAnyOrder(p1Id, c1Id, obs1Id));
|
||||
assertThat(ids, Matchers.not(hasItem(o1Id)));
|
||||
assertThat(ids, Matchers.not(hasItem(m1Id)));
|
||||
assertThat(ids).containsExactlyInAnyOrder(p1Id, c1Id, obs1Id);
|
||||
assertThat(ids).doesNotContain(o1Id);
|
||||
assertThat(ids).doesNotContain(m1Id);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -434,9 +625,9 @@ public class ResourceProviderR5Test extends BaseResourceProviderR5Test {
|
|||
assertEquals(Bundle.BundleType.SEARCHSET, b.getType());
|
||||
List<IIdType> ids = toUnqualifiedVersionlessIds(b);
|
||||
|
||||
assertThat(ids, containsInAnyOrder(p1Id, c1Id, obs1Id));
|
||||
assertThat(ids, Matchers.not(hasItem(o1Id)));
|
||||
assertThat(ids, Matchers.not(hasItem(m1Id)));
|
||||
assertThat(ids).containsExactlyInAnyOrder(p1Id, c1Id, obs1Id);
|
||||
assertThat(ids).doesNotContain(o1Id);
|
||||
assertThat(ids).doesNotContain(m1Id);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -473,11 +664,11 @@ public class ResourceProviderR5Test extends BaseResourceProviderR5Test {
|
|||
assertEquals(Bundle.BundleType.SEARCHSET, b.getType());
|
||||
List<IIdType> ids = toUnqualifiedVersionlessIds(b);
|
||||
|
||||
assertThat(ids, containsInAnyOrder(p1Id, c1Id, obs1Id));
|
||||
assertThat(ids, Matchers.not(hasItem(o1Id)));
|
||||
assertThat(ids, Matchers.not(hasItem(m1Id)));
|
||||
assertThat(ids, Matchers.not(hasItem(p2Id)));
|
||||
assertThat(ids, Matchers.not(hasItem(o2Id)));
|
||||
assertThat(ids).containsExactlyInAnyOrder(p1Id, c1Id, obs1Id);
|
||||
assertThat(ids).doesNotContain(o1Id);
|
||||
assertThat(ids).doesNotContain(m1Id);
|
||||
assertThat(ids).doesNotContain(p2Id);
|
||||
assertThat(ids).doesNotContain(o2Id);
|
||||
}
|
||||
|
||||
|
||||
|
@ -614,4 +805,5 @@ public class ResourceProviderR5Test extends BaseResourceProviderR5Test {
|
|||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue