hibernate search cannot be used for count query if non-active parameters are present (#6027)

fixing count query with fulltextsearch bug
This commit is contained in:
TipzCM 2024-06-19 18:21:20 -04:00 committed by GitHub
parent 60f456c655
commit 5799c6b42b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 478 additions and 101 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.dao.search.LastNOperation;
import ca.uhn.fhir.jpa.model.dao.JpaPid; import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.model.entity.ResourceTable; 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.ExtendedHSearchIndexData;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.search.autocomplete.ValueSetAutocompleteOptions; import ca.uhn.fhir.jpa.search.autocomplete.ValueSetAutocompleteOptions;
@ -141,17 +142,17 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
} }
@Override @Override
public boolean supportsSomeOf(SearchParameterMap myParams) { public boolean canUseHibernateSearch(String theResourceType, SearchParameterMap myParams) {
// keep this in sync with the guts of doSearch
boolean requiresHibernateSearchAccess = myParams.containsKey(Constants.PARAM_CONTENT) boolean requiresHibernateSearchAccess = myParams.containsKey(Constants.PARAM_CONTENT)
|| myParams.containsKey(Constants.PARAM_TEXT) || myParams.containsKey(Constants.PARAM_TEXT)
|| myParams.isLastN(); || myParams.isLastN();
// we have to use it - _text and _content searches only use hibernate
if (requiresHibernateSearchAccess) {
return true;
}
requiresHibernateSearchAccess |= return myStorageSettings.isAdvancedHSearchIndexing()
myStorageSettings.isAdvancedHSearchIndexing() && myAdvancedIndexQueryBuilder.isSupportsSomeOf(myParams); && myAdvancedIndexQueryBuilder.canUseHibernateSearch(theResourceType, myParams, mySearchParamRegistry);
return requiresHibernateSearchAccess;
} }
@Override @Override
@ -174,6 +175,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
} }
// keep this in sync with supportsSomeOf(); // keep this in sync with supportsSomeOf();
@SuppressWarnings("rawtypes")
private ISearchQueryExecutor doSearch( private ISearchQueryExecutor doSearch(
String theResourceType, String theResourceType,
SearchParameterMap theParams, SearchParameterMap theParams,
@ -208,6 +210,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
return DEFAULT_MAX_NON_PAGED_SIZE; return DEFAULT_MAX_NON_PAGED_SIZE;
} }
@SuppressWarnings("rawtypes")
private SearchQueryOptionsStep<?, Long, SearchLoadingOptionsStep, ?, ?> getSearchQueryOptionsStep( private SearchQueryOptionsStep<?, Long, SearchLoadingOptionsStep, ?, ?> getSearchQueryOptionsStep(
String theResourceType, SearchParameterMap theParams, IResourcePersistentId theReferencingPid) { String theResourceType, SearchParameterMap theParams, IResourcePersistentId theReferencingPid) {
@ -230,6 +233,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
return query; return query;
} }
@SuppressWarnings("rawtypes")
private PredicateFinalStep buildWhereClause( private PredicateFinalStep buildWhereClause(
SearchPredicateFactory f, SearchPredicateFactory f,
String theResourceType, String theResourceType,
@ -271,8 +275,12 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
* Handle other supported parameters * Handle other supported parameters
*/ */
if (myStorageSettings.isAdvancedHSearchIndexing() && theParams.getEverythingMode() == null) { if (myStorageSettings.isAdvancedHSearchIndexing() && theParams.getEverythingMode() == null) {
myAdvancedIndexQueryBuilder.addAndConsumeAdvancedQueryClauses( ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams params =
builder, theResourceType, theParams, mySearchParamRegistry); new ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams();
params.setSearchParamRegistry(mySearchParamRegistry)
.setResourceType(theResourceType)
.setSearchParameterMap(theParams);
myAdvancedIndexQueryBuilder.addAndConsumeAdvancedQueryClauses(builder, params);
} }
// DROP EARLY HERE IF BOOL IS EMPTY? // DROP EARLY HERE IF BOOL IS EMPTY?
}); });
@ -283,11 +291,13 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
return Search.session(myEntityManager); return Search.session(myEntityManager);
} }
@SuppressWarnings("rawtypes")
private List<IResourcePersistentId> convertLongsToResourcePersistentIds(List<Long> theLongPids) { private List<IResourcePersistentId> convertLongsToResourcePersistentIds(List<Long> theLongPids) {
return theLongPids.stream().map(JpaPid::fromId).collect(Collectors.toList()); return theLongPids.stream().map(JpaPid::fromId).collect(Collectors.toList());
} }
@Override @Override
@SuppressWarnings({"rawtypes", "unchecked"})
public List<IResourcePersistentId> everything( public List<IResourcePersistentId> everything(
String theResourceName, String theResourceName,
SearchParameterMap theParams, SearchParameterMap theParams,
@ -336,6 +346,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
@Transactional() @Transactional()
@Override @Override
@SuppressWarnings("unchecked")
public List<IResourcePersistentId> search( public List<IResourcePersistentId> search(
String theResourceName, SearchParameterMap theParams, RequestDetails theRequestDetails) { String theResourceName, SearchParameterMap theParams, RequestDetails theRequestDetails) {
validateHibernateSearchIsEnabled(); validateHibernateSearchIsEnabled();
@ -347,6 +358,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
/** /**
* Adapt our async interface to the legacy concrete List * Adapt our async interface to the legacy concrete List
*/ */
@SuppressWarnings("rawtypes")
private List<IResourcePersistentId> toList(ISearchQueryExecutor theSearchResultStream, long theMaxSize) { private List<IResourcePersistentId> toList(ISearchQueryExecutor theSearchResultStream, long theMaxSize) {
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(theSearchResultStream, 0), false) return StreamSupport.stream(Spliterators.spliteratorUnknownSize(theSearchResultStream, 0), false)
.map(JpaPid::fromId) .map(JpaPid::fromId)
@ -384,6 +396,7 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
} }
@Override @Override
@SuppressWarnings("rawtypes")
public List<IResourcePersistentId> lastN(SearchParameterMap theParams, Integer theMaximumResults) { public List<IResourcePersistentId> lastN(SearchParameterMap theParams, Integer theMaximumResults) {
ensureElastic(); ensureElastic();
dispatchEvent(IHSearchEventListener.HSearchEventType.SEARCH); 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.Collection;
import java.util.List; import java.util.List;
@SuppressWarnings({"rawtypes"})
public interface IFulltextSearchSvc { public interface IFulltextSearchSvc {
/** /**
@ -79,11 +80,18 @@ public interface IFulltextSearchSvc {
ExtendedHSearchIndexData extractLuceneIndexData( ExtendedHSearchIndexData extractLuceneIndexData(
IBaseResource theResource, ResourceIndexedSearchParams theNewParams); 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. * Re-publish the resource to the full-text index.
* * -
* During update, hibernate search only republishes the entity if it has changed. * During update, hibernate search only republishes the entity if it has changed.
* During $reindex, we want to force the re-index. * During $reindex, we want to force the re-index.
* *

View File

@ -20,6 +20,7 @@
package ca.uhn.fhir.jpa.dao.search; package ca.uhn.fhir.jpa.dao.search;
import ca.uhn.fhir.context.RuntimeSearchParam; 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.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil; import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
import ca.uhn.fhir.model.api.IQueryParameterType; 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.TokenParam;
import ca.uhn.fhir.rest.param.UriParam; import ca.uhn.fhir.rest.param.UriParam;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 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.Lists;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
@ -44,6 +46,7 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import static ca.uhn.fhir.rest.api.Constants.PARAMQUALIFIER_MISSING; import static ca.uhn.fhir.rest.api.Constants.PARAMQUALIFIER_MISSING;
@ -59,19 +62,57 @@ public class ExtendedHSearchSearchBuilder {
public static final Set<String> ourUnsafeSearchParmeters = Sets.newHashSet("_id", "_meta"); 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) { public boolean supportsSearchParameter(String theParamName, ResourceSearchParams theActiveParamsForResourceType) {
return myParams.getSort() != null if (theActiveParamsForResourceType == null) {
|| myParams.getLastUpdated() != null return false;
|| myParams.entrySet().stream() }
.filter(e -> !ourUnsafeSearchParmeters.contains(e.getKey())) if (ourUnsafeSearchParmeters.contains(theParamName)) {
// each and clause may have a different modifier, so split down to the ORs return false;
.flatMap(andList -> andList.getValue().stream()) }
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) .flatMap(Collection::stream)
.collect(Collectors.toList())
.stream()
.anyMatch(this::isParamTypeSupported); .anyMatch(this::isParamTypeSupported);
} }
// if not supported, don't use
if (!canUseHibernate) {
return false;
}
}
return canUseHibernate;
}
/** /**
* Are all the queries supported by our indexing? * Are all the queries supported by our indexing?
*/ */
@ -166,86 +207,91 @@ public class ExtendedHSearchSearchBuilder {
} }
public void addAndConsumeAdvancedQueryClauses( public void addAndConsumeAdvancedQueryClauses(
ExtendedHSearchClauseBuilder builder, ExtendedHSearchClauseBuilder theBuilder,
String theResourceType, ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams theMethodParams) {
SearchParameterMap theParams, SearchParameterMap searchParameterMap = theMethodParams.getSearchParameterMap();
ISearchParamRegistry theSearchParamRegistry) { String resourceType = theMethodParams.getResourceType();
ISearchParamRegistry searchParamRegistry = theMethodParams.getSearchParamRegistry();
// copy the keys to avoid concurrent modification error // 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) { for (String nextParam : paramNames) {
if (ourUnsafeSearchParmeters.contains(nextParam)) { if (!supportsSearchParameter(nextParam, activeSearchParams)) {
continue;
}
RuntimeSearchParam activeParam = theSearchParamRegistry.getActiveSearchParam(theResourceType, nextParam);
if (activeParam == null) {
// ignore magic params handled in JPA // ignore magic params handled in JPA
continue; continue;
} }
RuntimeSearchParam activeParam = activeSearchParams.get(nextParam);
// NOTE - keep this in sync with isParamSupported() above. // NOTE - keep this in sync with isParamSupported() above.
switch (activeParam.getParamType()) { switch (activeParam.getParamType()) {
case TOKEN: case TOKEN:
List<List<IQueryParameterType>> tokenTextAndOrTerms = List<List<IQueryParameterType>> tokenTextAndOrTerms =
theParams.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_TOKEN_TEXT); searchParameterMap.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_TOKEN_TEXT);
builder.addStringTextSearch(nextParam, tokenTextAndOrTerms); theBuilder.addStringTextSearch(nextParam, tokenTextAndOrTerms);
List<List<IQueryParameterType>> tokenUnmodifiedAndOrTerms = List<List<IQueryParameterType>> tokenUnmodifiedAndOrTerms =
theParams.removeByNameUnmodified(nextParam); searchParameterMap.removeByNameUnmodified(nextParam);
builder.addTokenUnmodifiedSearch(nextParam, tokenUnmodifiedAndOrTerms); theBuilder.addTokenUnmodifiedSearch(nextParam, tokenUnmodifiedAndOrTerms);
break; break;
case STRING: case STRING:
List<List<IQueryParameterType>> stringTextAndOrTerms = List<List<IQueryParameterType>> stringTextAndOrTerms =
theParams.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_TOKEN_TEXT); searchParameterMap.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_TOKEN_TEXT);
builder.addStringTextSearch(nextParam, stringTextAndOrTerms); theBuilder.addStringTextSearch(nextParam, stringTextAndOrTerms);
List<List<IQueryParameterType>> stringExactAndOrTerms = List<List<IQueryParameterType>> stringExactAndOrTerms = searchParameterMap.removeByNameAndModifier(
theParams.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_STRING_EXACT); nextParam, Constants.PARAMQUALIFIER_STRING_EXACT);
builder.addStringExactSearch(nextParam, stringExactAndOrTerms); theBuilder.addStringExactSearch(nextParam, stringExactAndOrTerms);
List<List<IQueryParameterType>> stringContainsAndOrTerms = List<List<IQueryParameterType>> stringContainsAndOrTerms =
theParams.removeByNameAndModifier(nextParam, Constants.PARAMQUALIFIER_STRING_CONTAINS); searchParameterMap.removeByNameAndModifier(
builder.addStringContainsSearch(nextParam, stringContainsAndOrTerms); nextParam, Constants.PARAMQUALIFIER_STRING_CONTAINS);
theBuilder.addStringContainsSearch(nextParam, stringContainsAndOrTerms);
List<List<IQueryParameterType>> stringAndOrTerms = theParams.removeByNameUnmodified(nextParam); List<List<IQueryParameterType>> stringAndOrTerms =
builder.addStringUnmodifiedSearch(nextParam, stringAndOrTerms); searchParameterMap.removeByNameUnmodified(nextParam);
theBuilder.addStringUnmodifiedSearch(nextParam, stringAndOrTerms);
break; break;
case QUANTITY: case QUANTITY:
List<List<IQueryParameterType>> quantityAndOrTerms = theParams.removeByNameUnmodified(nextParam); List<List<IQueryParameterType>> quantityAndOrTerms =
builder.addQuantityUnmodifiedSearch(nextParam, quantityAndOrTerms); searchParameterMap.removeByNameUnmodified(nextParam);
theBuilder.addQuantityUnmodifiedSearch(nextParam, quantityAndOrTerms);
break; break;
case REFERENCE: case REFERENCE:
List<List<IQueryParameterType>> referenceAndOrTerms = theParams.removeByNameUnmodified(nextParam); List<List<IQueryParameterType>> referenceAndOrTerms =
builder.addReferenceUnchainedSearch(nextParam, referenceAndOrTerms); searchParameterMap.removeByNameUnmodified(nextParam);
theBuilder.addReferenceUnchainedSearch(nextParam, referenceAndOrTerms);
break; break;
case DATE: case DATE:
List<List<IQueryParameterType>> dateAndOrTerms = nextParam.equalsIgnoreCase("_lastupdated") List<List<IQueryParameterType>> dateAndOrTerms = nextParam.equalsIgnoreCase("_lastupdated")
? getLastUpdatedAndOrList(theParams) ? getLastUpdatedAndOrList(searchParameterMap)
: theParams.removeByNameUnmodified(nextParam); : searchParameterMap.removeByNameUnmodified(nextParam);
builder.addDateUnmodifiedSearch(nextParam, dateAndOrTerms); theBuilder.addDateUnmodifiedSearch(nextParam, dateAndOrTerms);
break; break;
case COMPOSITE: 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 // RuntimeSearchParam only points to the subs by reference. Resolve here while we have
// ISearchParamRegistry // ISearchParamRegistry
List<RuntimeSearchParam> subSearchParams = List<RuntimeSearchParam> subSearchParams =
JpaParamUtil.resolveCompositeComponentsDeclaredOrder(theSearchParamRegistry, activeParam); JpaParamUtil.resolveCompositeComponentsDeclaredOrder(searchParamRegistry, activeParam);
builder.addCompositeUnmodifiedSearch(activeParam, subSearchParams, compositeAndOrTerms); theBuilder.addCompositeUnmodifiedSearch(activeParam, subSearchParams, compositeAndOrTerms);
break; break;
case URI: case URI:
List<List<IQueryParameterType>> uriUnmodifiedAndOrTerms = List<List<IQueryParameterType>> uriUnmodifiedAndOrTerms =
theParams.removeByNameUnmodified(nextParam); searchParameterMap.removeByNameUnmodified(nextParam);
builder.addUriUnmodifiedSearch(nextParam, uriUnmodifiedAndOrTerms); theBuilder.addUriUnmodifiedSearch(nextParam, uriUnmodifiedAndOrTerms);
break; break;
case NUMBER: case NUMBER:
List<List<IQueryParameterType>> numberUnmodifiedAndOrTerms = theParams.remove(nextParam); List<List<IQueryParameterType>> numberUnmodifiedAndOrTerms = searchParameterMap.remove(nextParam);
builder.addNumberUnmodifiedSearch(nextParam, numberUnmodifiedAndOrTerms); theBuilder.addNumberUnmodifiedSearch(nextParam, numberUnmodifiedAndOrTerms);
break; break;
default: default:

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.dao.search;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.entity.StorageSettings; 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.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.util.LastNParameterHelper; import ca.uhn.fhir.jpa.searchparam.util.LastNParameterHelper;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 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)); b.must(f.match().field("myResourceType").matching(OBSERVATION_RES_TYPE));
ExtendedHSearchClauseBuilder builder = ExtendedHSearchClauseBuilder builder =
new ExtendedHSearchClauseBuilder(myFhirContext, myStorageSettings, b, f); new ExtendedHSearchClauseBuilder(myFhirContext, myStorageSettings, b, f);
myExtendedHSearchSearchBuilder.addAndConsumeAdvancedQueryClauses( ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams params =
builder, OBSERVATION_RES_TYPE, theParams.clone(), mySearchParamRegistry); new ExtendedHSearchBuilderConsumeAdvancedQueryClausesParams();
params.setResourceType(OBSERVATION_RES_TYPE)
.setSearchParameterMap(theParams.clone())
.setSearchParamRegistry(mySearchParamRegistry);
myExtendedHSearchSearchBuilder.addAndConsumeAdvancedQueryClauses(builder, params);
})) }))
.aggregation(observationsByCodeKey, f -> f.fromJson(lastNAggregation.toAggregation())) .aggregation(observationsByCodeKey, f -> f.fromJson(lastNAggregation.toAggregation()))
.fetch(0); .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"); ourLog.trace("Done fetching search resource PIDs");
int countOfPids = pids.size(); int countOfPids = pids.size();
;
int maxSize = Math.min(theToIndex - theFromIndex, countOfPids); int maxSize = Math.min(theToIndex - theFromIndex, countOfPids);
thePageBuilder.setTotalRequestedResourcesFetched(countOfPids); thePageBuilder.setTotalRequestedResourcesFetched(countOfPids);

View File

@ -95,6 +95,7 @@ import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.StringUtil; import ca.uhn.fhir.util.StringUtil;
import ca.uhn.fhir.util.UrlUtil; import ca.uhn.fhir.util.UrlUtil;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Streams; import com.google.common.collect.Streams;
import com.healthmarketscience.sqlbuilder.Condition; import com.healthmarketscience.sqlbuilder.Condition;
import jakarta.annotation.Nonnull; import jakarta.annotation.Nonnull;
@ -165,7 +166,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
public static boolean myUseMaxPageSize50ForTest = false; public static boolean myUseMaxPageSize50ForTest = false;
protected final IInterceptorBroadcaster myInterceptorBroadcaster; protected final IInterceptorBroadcaster myInterceptorBroadcaster;
protected final IResourceTagDao myResourceTagDao; protected final IResourceTagDao myResourceTagDao;
String myResourceName; private String myResourceName;
private final Class<? extends IBaseResource> myResourceType; private final Class<? extends IBaseResource> myResourceType;
private final HapiFhirLocalContainerEntityManagerFactoryBean myEntityManagerFactory; private final HapiFhirLocalContainerEntityManagerFactoryBean myEntityManagerFactory;
private final SqlObjectFactory mySqlBuilderFactory; private final SqlObjectFactory mySqlBuilderFactory;
@ -206,6 +207,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
/** /**
* Constructor * Constructor
*/ */
@SuppressWarnings({"rawtypes", "unchecked"})
public SearchBuilder( public SearchBuilder(
IDao theDao, IDao theDao,
String theResourceName, String theResourceName,
@ -240,6 +242,11 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
myIdHelperService = theIdHelperService; myIdHelperService = theIdHelperService;
} }
@VisibleForTesting
void setResourceName(String theName) {
myResourceName = theName;
}
@Override @Override
public void setMaxResultsToFetch(Integer theMaxResultsToFetch) { public void setMaxResultsToFetch(Integer theMaxResultsToFetch) {
myMaxResultsToFetch = theMaxResultsToFetch; myMaxResultsToFetch = theMaxResultsToFetch;
@ -265,8 +272,6 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
attemptComboUniqueSpProcessing(theQueryStack, theParams, theRequest); attemptComboUniqueSpProcessing(theQueryStack, theParams, theRequest);
} }
SearchContainedModeEnum searchContainedMode = theParams.getSearchContainedMode();
// Handle _id and _tag last, since they can typically be tacked onto a different parameter // Handle _id and _tag last, since they can typically be tacked onto a different parameter
List<String> paramNames = myParams.keySet().stream() List<String> paramNames = myParams.keySet().stream()
.filter(t -> !t.equals(IAnyResource.SP_RES_ID)) .filter(t -> !t.equals(IAnyResource.SP_RES_ID))
@ -399,7 +404,8 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
} }
if (fulltextExecutor == null) { if (fulltextExecutor == null) {
fulltextExecutor = SearchQueryExecutors.from(fulltextMatchIds); fulltextExecutor =
SearchQueryExecutors.from(fulltextMatchIds != null ? fulltextMatchIds : new ArrayList<>());
} }
if (theSearchRuntimeDetails != null) { if (theSearchRuntimeDetails != null) {
@ -486,7 +492,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
return fulltextEnabled return fulltextEnabled
&& myParams != null && myParams != null
&& myParams.getSearchContainedMode() == SearchContainedModeEnum.FALSE && myParams.getSearchContainedMode() == SearchContainedModeEnum.FALSE
&& myFulltextSearchSvc.supportsSomeOf(myParams) && myFulltextSearchSvc.canUseHibernateSearch(myResourceName, myParams)
&& myFulltextSearchSvc.supportsAllSortTerms(myResourceName, myParams); && myFulltextSearchSvc.supportsAllSortTerms(myResourceName, myParams);
} }
@ -538,8 +544,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
pid = myIdHelperService.resolveResourcePersistentIds(myRequestPartitionId, myResourceName, idParamValue); pid = myIdHelperService.resolveResourcePersistentIds(myRequestPartitionId, myResourceName, idParamValue);
} }
List<JpaPid> pids = myFulltextSearchSvc.everything(myResourceName, myParams, pid, theRequestDetails); return myFulltextSearchSvc.everything(myResourceName, myParams, pid, theRequestDetails);
return pids;
} }
private void doCreateChunkedQueries( private void doCreateChunkedQueries(
@ -862,13 +867,8 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
theQueryStack.addSortOnLastUpdated(ascending); theQueryStack.addSortOnLastUpdated(ascending);
} else { } else {
RuntimeSearchParam param =
RuntimeSearchParam param = null; mySearchParamRegistry.getActiveSearchParam(myResourceName, theSort.getParamName());
if (param == null) {
// do we have a composition param defined for the whole chain?
param = mySearchParamRegistry.getActiveSearchParam(myResourceName, theSort.getParamName());
}
/* /*
* If we have a sort like _sort=subject.name and we have an * If we have a sort like _sort=subject.name and we have an
@ -896,9 +896,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
mySearchParamRegistry.getActiveSearchParam(myResourceName, referenceParam); mySearchParamRegistry.getActiveSearchParam(myResourceName, referenceParam);
if (outerParam == null) { if (outerParam == null) {
throwInvalidRequestExceptionForUnknownSortParameter(myResourceName, referenceParam); throwInvalidRequestExceptionForUnknownSortParameter(myResourceName, referenceParam);
} } else if (outerParam.hasUpliftRefchain(targetParam)) {
if (outerParam.hasUpliftRefchain(targetParam)) {
for (String nextTargetType : outerParam.getTargets()) { for (String nextTargetType : outerParam.getTargets()) {
if (referenceParamTargetType != null && !referenceParamTargetType.equals(nextTargetType)) { if (referenceParamTargetType != null && !referenceParamTargetType.equals(nextTargetType)) {
continue; continue;
@ -945,6 +943,9 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
throwInvalidRequestExceptionForUnknownSortParameter(getResourceName(), paramName); 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) { if (isNotBlank(chainName) && param.getParamType() != RestSearchParameterTypeEnum.REFERENCE) {
throw new InvalidRequestException( throw new InvalidRequestException(
Msg.code(2285) + "Invalid chain, " + paramName + " is not a reference SearchParameter"); Msg.code(2285) + "Invalid chain, " + paramName + " is not a reference SearchParameter");
@ -1121,11 +1122,15 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
resourceType, next, tagMap.get(next.getId()), theForHistoryOperation); resourceType, next, tagMap.get(next.getId()), theForHistoryOperation);
} }
if (resource == null) { if (resource == null) {
if (next != null) {
ourLog.warn( ourLog.warn(
"Unable to find resource {}/{}/_history/{} in database", "Unable to find resource {}/{}/_history/{} in database",
next.getResourceType(), next.getResourceType(),
next.getIdDt().getIdPart(), next.getIdDt().getIdPart(),
next.getVersion()); next.getVersion());
} else {
ourLog.warn("Unable to find resource in database.");
}
continue; continue;
} }
@ -1196,7 +1201,6 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
RequestDetails theDetails) { RequestDetails theDetails) {
if (thePids.isEmpty()) { if (thePids.isEmpty()) {
ourLog.debug("The include pids are empty"); ourLog.debug("The include pids are empty");
// return;
} }
// Dupes will cause a crash later anyhow, but this is expensive so only do it // Dupes will cause a crash later anyhow, but this is expensive so only do it
@ -1256,10 +1260,9 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
// only impl // only impl
// to handle lastN? // to handle lastN?
if (myStorageSettings.isAdvancedHSearchIndexing() && myStorageSettings.isStoreResourceInHSearchIndex()) { 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 myFulltextSearchSvc.getResources(pidList);
return resources;
} else if (!Objects.isNull(myParams) && myParams.isLastN()) { } else if (!Objects.isNull(myParams) && myParams.isLastN()) {
// legacy LastN implementation // legacy LastN implementation
return myIElasticsearchSvc.getObservationResources(thePids); return myIElasticsearchSvc.getObservationResources(thePids);
@ -1344,7 +1347,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
for (Iterator<Include> iter = includes.iterator(); iter.hasNext(); ) { for (Iterator<Include> iter = includes.iterator(); iter.hasNext(); ) {
Include nextInclude = iter.next(); Include nextInclude = iter.next();
if (nextInclude.isRecurse() == false) { if (!nextInclude.isRecurse()) {
iter.remove(); iter.remove();
} }
@ -1707,6 +1710,8 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
} }
/** /**
* Calls Performance Trace Hook
* @param request the request deatils
* Sends a raw SQL query to the Pointcut for raw SQL queries. * Sends a raw SQL query to the Pointcut for raw SQL queries.
*/ */
private void callRawSqlHookWithCurrentThreadQueries(RequestDetails request) { private void callRawSqlHookWithCurrentThreadQueries(RequestDetails request) {
@ -1890,7 +1895,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
for (RuntimeSearchParam nextCandidate : candidateComboParams) { for (RuntimeSearchParam nextCandidate : candidateComboParams) {
List<String> nextCandidateParamNames = List<String> nextCandidateParamNames =
JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, nextCandidate).stream() JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, nextCandidate).stream()
.map(t -> t.getName()) .map(RuntimeSearchParam::getName)
.collect(Collectors.toList()); .collect(Collectors.toList());
if (theParams.keySet().containsAll(nextCandidateParamNames)) { if (theParams.keySet().containsAll(nextCandidateParamNames)) {
comboParam = nextCandidate; comboParam = nextCandidate;
@ -1902,7 +1907,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
if (comboParam != null) { if (comboParam != null) {
// Since we're going to remove elements below // Since we're going to remove elements below
theParams.values().forEach(nextAndList -> ensureSubListsAreWritable(nextAndList)); theParams.values().forEach(this::ensureSubListsAreWritable);
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append(myResourceName); sb.append(myResourceName);

View File

@ -39,7 +39,7 @@ class SearchBuilderTest {
@BeforeEach @BeforeEach
public void beforeEach() { public void beforeEach() {
mySearchBuilder.myResourceName = "QuestionnaireResponse"; mySearchBuilder.setResourceName("QuestionnaireResponse");
when(myDaoRegistry.getRegisteredDaoTypes()).thenReturn(ourCtx.getResourceTypes()); when(myDaoRegistry.getRegisteredDaoTypes()).thenReturn(ourCtx.getResourceTypes());
} }

View File

@ -26,6 +26,7 @@ import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.search.BaseSourceSearchParameterTestCases; import ca.uhn.fhir.jpa.search.BaseSourceSearchParameterTestCases;
import ca.uhn.fhir.jpa.search.CompositeSearchParameterTestCases; import ca.uhn.fhir.jpa.search.CompositeSearchParameterTestCases;
import ca.uhn.fhir.jpa.search.QuantitySearchParameterTestCases; import ca.uhn.fhir.jpa.search.QuantitySearchParameterTestCases;
import ca.uhn.fhir.jpa.search.builder.SearchBuilder;
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc; import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
@ -54,6 +55,9 @@ import ca.uhn.fhir.test.utilities.LogbackLevelOverrideExtension;
import ca.uhn.fhir.test.utilities.docker.RequiresDocker; import ca.uhn.fhir.test.utilities.docker.RequiresDocker;
import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.FhirValidator;
import ca.uhn.fhir.validation.ValidationResult; import ca.uhn.fhir.validation.ValidationResult;
import ca.uhn.test.util.LogbackTestExtension;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import jakarta.annotation.Nonnull; import jakarta.annotation.Nonnull;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import org.hl7.fhir.instance.model.api.IBaseCoding; import org.hl7.fhir.instance.model.api.IBaseCoding;
@ -118,6 +122,7 @@ import static ca.uhn.fhir.jpa.model.util.UcumServiceUtil.UCUM_CODESYSTEM_URL;
import static ca.uhn.fhir.rest.api.Constants.CHARSET_UTF8; import static ca.uhn.fhir.rest.api.Constants.CHARSET_UTF8;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@ -168,6 +173,9 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest impl
TestDaoSearch myTestDaoSearch; TestDaoSearch myTestDaoSearch;
@RegisterExtension @RegisterExtension
LogbackLevelOverrideExtension myLogbackLevelOverrideExtension = new LogbackLevelOverrideExtension(); LogbackLevelOverrideExtension myLogbackLevelOverrideExtension = new LogbackLevelOverrideExtension();
@RegisterExtension
LogbackTestExtension myLogbackTestExtension = new LogbackTestExtension();
@Autowired @Autowired
@Qualifier("myCodeSystemDaoR4") @Qualifier("myCodeSystemDaoR4")
private IFhirResourceDao<CodeSystem> myCodeSystemDao; private IFhirResourceDao<CodeSystem> myCodeSystemDao;
@ -742,19 +750,21 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest impl
*/ */
@Test @Test
public void testDirectPathWholeResourceNotIndexedWorks() { public void testDirectPathWholeResourceNotIndexedWorks() {
// setup
myLogbackLevelOverrideExtension.setLogLevel(SearchBuilder.class, Level.WARN);
IIdType id1 = myTestDataBuilder.createObservation(List.of(myTestDataBuilder.withObservationCode("http://example.com/", "theCode"))); 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 // set it after creating resource, so search doesn't find it in the index
myStorageSettings.setStoreResourceInHSearchIndex(true); myStorageSettings.setStoreResourceInHSearchIndex(true);
myCaptureQueriesListener.clear(); List<IBaseResource> result = searchForFastResources("Observation?code=theCode&_count=10&_total=accurate");
List<IBaseResource> result = searchForFastResources("Observation?code=theCode");
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
assertThat(result).hasSize(1); assertThat(result).hasSize(1);
assertEquals(((Observation) result.get(0)).getIdElement().getIdPart(), id1.getIdPart()); assertEquals(((Observation) result.get(0)).getIdElement().getIdPart(), id1.getIdPart());
assertThat(myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()).as("JPA search for IDs and for resources").isEqualTo(2);
List<ILoggingEvent> events = myLogbackTestExtension.filterLoggingEventsWithPredicate(e -> e.getLevel() == Level.WARN);
assertFalse(events.isEmpty());
assertTrue(events.stream().anyMatch(e -> e.getFormattedMessage().contains("Some resources were not found in index. Make sure all resources were indexed. Resorting to database search.")));
// restore changed property // restore changed property
JpaStorageSettings defaultConfig = new JpaStorageSettings(); JpaStorageSettings defaultConfig = new JpaStorageSettings();

View File

@ -2120,6 +2120,8 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
@SuppressWarnings("unused") @SuppressWarnings("unused")
@Test @Test
public void testFullTextSearch() throws Exception { public void testFullTextSearch() throws Exception {
IParser parser = myFhirContext.newJsonParser();
Observation obs1 = new Observation(); Observation obs1 = new Observation();
obs1.getCode().setText("Systolic Blood Pressure"); obs1.getCode().setText("Systolic Blood Pressure");
obs1.setStatus(ObservationStatus.FINAL); obs1.setStatus(ObservationStatus.FINAL);
@ -2131,13 +2133,21 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
obs2.setStatus(ObservationStatus.FINAL); obs2.setStatus(ObservationStatus.FINAL);
obs2.setValue(new Quantity(81)); obs2.setValue(new Quantity(81));
IIdType id2 = myObservationDao.create(obs2, mySrd).getId().toUnqualifiedVersionless(); IIdType id2 = myObservationDao.create(obs2, mySrd).getId().toUnqualifiedVersionless();
obs2.setId(id2);
myStorageSettings.setAdvancedHSearchIndexing(true);
HttpGet get = new HttpGet(myServerBase + "/Observation?_content=systolic&_pretty=true"); HttpGet get = new HttpGet(myServerBase + "/Observation?_content=systolic&_pretty=true");
get.addHeader("Content-Type", "application/json");
try (CloseableHttpResponse response = ourHttpClient.execute(get)) { try (CloseableHttpResponse response = ourHttpClient.execute(get)) {
assertEquals(200, response.getStatusLine().getStatusCode()); assertEquals(200, response.getStatusLine().getStatusCode());
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseString); ourLog.info(responseString);
assertThat(responseString).contains(id1.getIdPart()); Bundle bundle = parser.parseResource(Bundle.class, responseString);
assertEquals(1, bundle.getTotal());
Resource resource = bundle.getEntry().get(0).getResource();
assertEquals("Observation", resource.fhirType());
assertEquals(id1.getIdPart(), resource.getIdPart());
} }
} }

View File

@ -398,7 +398,7 @@ public abstract class BaseJpaR5Test extends BaseJpaTest implements ITestDataBuil
private PerformanceTracingLoggingInterceptor myPerformanceTracingLoggingInterceptor; private PerformanceTracingLoggingInterceptor myPerformanceTracingLoggingInterceptor;
@Autowired @Autowired
private DaoRegistry myDaoRegistry; protected DaoRegistry myDaoRegistry;
@Autowired @Autowired
private IBulkDataExportJobSchedulingHelper myBulkDataSchedulerHelper; 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.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.BundleBuilder; import ca.uhn.fhir.util.BundleBuilder;
import ca.uhn.fhir.util.HapiExtensions; 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.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; 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.Bundle;
import org.hl7.fhir.r5.model.CodeType; 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.DateType;
import org.hl7.fhir.r5.model.Encounter; import org.hl7.fhir.r5.model.Encounter;
import org.hl7.fhir.r5.model.Enumerations; import org.hl7.fhir.r5.model.Enumerations;
import org.hl7.fhir.r5.model.Extension; 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.Identifier;
import org.hl7.fhir.r5.model.Organization; import org.hl7.fhir.r5.model.Organization;
import org.hl7.fhir.r5.model.Patient; 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.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import jakarta.annotation.Nonnull;
import java.util.List; import java.util.List;
import static org.apache.commons.lang3.StringUtils.countMatches; import static org.apache.commons.lang3.StringUtils.countMatches;

View File

@ -1,11 +1,26 @@
package ca.uhn.fhir.jpa.provider.r5; 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.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.entity.Search;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum; 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.parser.StrictErrorHandler;
import ca.uhn.fhir.rest.api.Constants; 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.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.NotImplementedOperationException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.util.BundleBuilder; import ca.uhn.fhir.util.BundleBuilder;
@ -24,13 +39,18 @@ import org.hl7.fhir.r5.model.CarePlan;
import org.hl7.fhir.r5.model.CodeableConcept; import org.hl7.fhir.r5.model.CodeableConcept;
import org.hl7.fhir.r5.model.Condition; import org.hl7.fhir.r5.model.Condition;
import org.hl7.fhir.r5.model.DateTimeType; 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.MedicationRequest;
import org.hl7.fhir.r5.model.MedicinalProductDefinition;
import org.hl7.fhir.r5.model.Observation; import org.hl7.fhir.r5.model.Observation;
import org.hl7.fhir.r5.model.Observation.ObservationComponentComponent; import org.hl7.fhir.r5.model.Observation.ObservationComponentComponent;
import org.hl7.fhir.r5.model.Organization; import org.hl7.fhir.r5.model.Organization;
import org.hl7.fhir.r5.model.Parameters; import org.hl7.fhir.r5.model.Parameters;
import org.hl7.fhir.r5.model.Patient; import org.hl7.fhir.r5.model.Patient;
import org.hl7.fhir.r5.model.Quantity; 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.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -43,11 +63,13 @@ import org.springframework.util.comparator.Comparators;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.leftPad; import static org.apache.commons.lang3.StringUtils.leftPad;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@SuppressWarnings("Duplicates") @SuppressWarnings("Duplicates")
public class ResourceProviderR5Test extends BaseResourceProviderR5Test { public class ResourceProviderR5Test extends BaseResourceProviderR5Test {
@ -206,7 +228,181 @@ public class ResourceProviderR5Test extends BaseResourceProviderR5Test {
assertEquals(501, e.getStatusCode()); assertEquals(501, e.getStatusCode());
assertThat(e.getMessage()).contains("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 @Test
@ -609,4 +805,5 @@ public class ResourceProviderR5Test extends BaseResourceProviderR5Test {
} }
return retVal; return retVal;
} }
} }