cherry-picked 6027

This commit is contained in:
TipzCM 2024-06-19 18:21:20 -04:00 committed by longma1
parent 6f683e0990
commit d064172195
14 changed files with 1344 additions and 792 deletions

View File

@ -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).
"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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