Jr 20211018 chained references 2 (#3099)

* Create index entries for outbound references of contained resources

* build query for chained reference

* fix case where the contained reference is an explicit id rather than a continued chain

* fix contained index to use path names not search param names

* make qualified search work

* cleanup and changelog

* recurse while creating indexes on contained resources

* double link both contained

* longer contained subchains

* adding some failing test cases to illustrate the limitations of qualified searches

* clean up merge cruft

* changelog
This commit is contained in:
JasonRoberts-smile 2021-10-22 08:41:05 -04:00 committed by GitHub
parent d94284b2e8
commit b267fdb752
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 433 additions and 17 deletions

View File

@ -0,0 +1,8 @@
---
type: add
issue: 3100
jira: SMILE-3151
title: "Previously, only contained resources that are referenced directly by the containing resource were being indexed.
This enhancement indexes the fields of contained resources that are referenced by other contained resources and
uses these new indices in chained searches. Note: in order to make use of this new capability, it must be enabled via
a configuration parameter and the repository must be re-indexed."

View File

@ -685,7 +685,7 @@ public class QueryStack {
} }
public Condition createPredicateReferenceForContainedResource(@Nullable DbColumn theSourceJoinColumn, public Condition createPredicateReferenceForContainedResource(@Nullable DbColumn theSourceJoinColumn,
String theResourceName, String theParamName, RuntimeSearchParam theSearchParam, String theResourceName, String theParamName, List<String> theQualifiers, RuntimeSearchParam theSearchParam,
List<? extends IQueryParameterType> theList, SearchFilterParser.CompareOperation theOperation, List<? extends IQueryParameterType> theList, SearchFilterParser.CompareOperation theOperation,
RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
@ -760,7 +760,7 @@ public class QueryStack {
throw new InvalidRequestException("Unknown search parameter name: " + theSearchParam.getName() + "."); throw new InvalidRequestException("Unknown search parameter name: " + theSearchParam.getName() + ".");
} }
List<String> qualifiers= Collections.singletonList(headQualifier); theQualifiers.add(headQualifier);
// 3. create the query // 3. create the query
Condition containedCondition = null; Condition containedCondition = null;
@ -796,7 +796,11 @@ public class QueryStack {
break; break;
case REFERENCE: case REFERENCE:
String chainedParamName = theParamName + "." + targetParamName; String chainedParamName = theParamName + "." + targetParamName;
containedCondition = createPredicateReference(theSourceJoinColumn, theResourceName, chainedParamName, qualifiers, trimmedParameters, theOperation, theRequest, theRequestPartitionId); containedCondition = createPredicateReference(theSourceJoinColumn, theResourceName, chainedParamName, theQualifiers, trimmedParameters, theOperation, theRequest, theRequestPartitionId);
if (myModelConfig.isIndexOnContainedResourcesRecursively()) {
containedCondition = toOrPredicate(containedCondition,
createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, chainedParamName, theQualifiers, theSearchParam, trimmedParameters, theOperation, theRequest, theRequestPartitionId));
}
break; break;
case HAS: case HAS:
case SPECIAL: case SPECIAL:
@ -1140,11 +1144,11 @@ public class QueryStack {
// See SMILE-2898 for details. // See SMILE-2898 for details.
// For now, leave the incorrect implementation alone, just in case someone is relying on it, // For now, leave the incorrect implementation alone, just in case someone is relying on it,
// until the complete fix is available. // until the complete fix is available.
andPredicates.add(createPredicateReferenceForContainedResource(null, theResourceName, theParamName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId)); andPredicates.add(createPredicateReferenceForContainedResource(null, theResourceName, theParamName, new ArrayList<>(), nextParamDef, nextAnd, null, theRequest, theRequestPartitionId));
} else if (isEligibleForContainedResourceSearch(nextAnd)) { } else if (isEligibleForContainedResourceSearch(nextAnd)) {
andPredicates.add(toOrPredicate( andPredicates.add(toOrPredicate(
createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId), createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId),
createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, theParamName, nextParamDef, nextAnd, null, theRequest, theRequestPartitionId) createPredicateReferenceForContainedResource(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextParamDef, nextAnd, null, theRequest, theRequestPartitionId)
)); ));
} else { } else {
andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId)); andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId));

View File

@ -569,6 +569,10 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder {
String qualifier = theParamQualifiers.get(0); String qualifier = theParamQualifiers.get(0);
RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, paramNameHead); RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, paramNameHead);
if (param == null) {
// This can happen during recursion, if not all the possible target types of one link in the chain support the next link
return new ArrayList<>();
}
Set<String> tailPaths = param.getTargets().stream() Set<String> tailPaths = param.getTargets().stream()
.filter(t -> isBlank(qualifier) || qualifier.equals(t)) .filter(t -> isBlank(qualifier) || qualifier.equals(t))
.map(t -> createResourceLinkPaths(t, paramNameTail, theParamQualifiers.subList(1, theParamQualifiers.size()))) .map(t -> createResourceLinkPaths(t, paramNameTail, theParamQualifiers.subList(1, theParamQualifiers.size())))

View File

@ -10,12 +10,14 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Device; import org.hl7.fhir.r4.model.Device;
import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Location;
import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.StringType;
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.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -46,6 +48,7 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
myModelConfig.setIndexOnContainedResources(false); myModelConfig.setIndexOnContainedResources(false);
myModelConfig.setIndexOnContainedResources(new ModelConfig().isIndexOnContainedResources()); myModelConfig.setIndexOnContainedResources(new ModelConfig().isIndexOnContainedResources());
myModelConfig.setIndexOnContainedResourcesRecursively(new ModelConfig().isIndexOnContainedResourcesRecursively());
} }
@BeforeEach @BeforeEach
@ -116,6 +119,50 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
assertThat(oids, contains(oid1.getIdPart())); assertThat(oids, contains(oid1.getIdPart()));
} }
@Test
@Disabled
public void testShouldResolveATwoLinkChainWithQualifiersWithAContainedResource() throws Exception {
// TODO: This test fails because of a known limitation in qualified searches over contained resources.
// Type information for intermediate resources in the chain is not being retained in the indexes.
// setup
IIdType oid1;
{
Patient p = new Patient();
p.setId("pat");
p.addName().setFamily("Smith").addGiven("John");
Observation obs = new Observation();
obs.getContained().add(p);
obs.getCode().setText("Observation 1");
obs.setValue(new StringType("Test"));
obs.getSubject().setReference("#pat");
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
Location loc = new Location();
loc.setId("loc");
loc.setName("Smith");
Observation obs2 = new Observation();
obs2.getContained().add(loc);
obs2.getCode().setText("Observation 2");
obs2.setValue(new StringType("Test"));
obs2.getSubject().setReference("#loc");
myObservationDao.create(obs2, mySrd);
}
String url = "/Observation?subject:Patient.name=Smith";
// execute
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
// validate
assertEquals(1L, oids.size());
assertThat(oids, contains(oid1.getIdPart()));
}
@Test @Test
public void testShouldResolveATwoLinkChainToAContainedReference() throws Exception { public void testShouldResolveATwoLinkChainToAContainedReference() throws Exception {
// Adding support for this case in SMILE-3151 // Adding support for this case in SMILE-3151
@ -260,6 +307,45 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
assertThat(oids, contains(oid1.getIdPart())); assertThat(oids, contains(oid1.getIdPart()));
} }
@Test
public void testShouldResolveAThreeLinkChainWithAllContainedResources() throws Exception {
// setup
myModelConfig.setIndexOnContainedResourcesRecursively(true);
IIdType oid1;
{
Organization org = new Organization();
org.setId("org");
org.setName("HealthCo");
Patient p = new Patient();
p.setId("pat");
p.addName().setFamily("Smith").addGiven("John");
p.getManagingOrganization().setReference("#org");
Observation obs = new Observation();
obs.getContained().add(p);
obs.getContained().add(org);
obs.getCode().setText("Observation 1");
obs.getSubject().setReference("#pat");
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
}
String url = "/Observation?subject.organization.name=HealthCo";
// execute
myCaptureQueriesListener.clear();
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
myCaptureQueriesListener.logSelectQueries();
// validate
assertEquals(1L, oids.size());
assertThat(oids, contains(oid1.getIdPart()));
}
@Test @Test
public void testShouldResolveAThreeLinkChainWithQualifiersWhereAllResourcesStandAlone() throws Exception { public void testShouldResolveAThreeLinkChainWithQualifiersWhereAllResourcesStandAlone() throws Exception {
@ -405,6 +491,117 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
assertThat(oids, contains(oid1.getIdPart())); assertThat(oids, contains(oid1.getIdPart()));
} }
@Test
@Disabled
public void testShouldResolveAThreeLinkChainWithQualifiersWithAContainedResourceAtTheBeginning_NotDistinctSourcePaths() throws Exception {
// TODO: This test fails because of a known limitation in qualified searches over contained resources.
// Type information for intermediate resources in the chain is not being retained in the indexes.
// Adding support for this case in SMILE-3151
// setup
IIdType oid1;
{
Organization org = new Organization();
org.setId(IdType.newRandomUuid());
org.setName("HealthCo");
myOrganizationDao.create(org, mySrd);
Patient p = new Patient();
p.setId("pat");
p.addName().setFamily("Smith").addGiven("John");
p.getManagingOrganization().setReference(org.getId());
Observation obs = new Observation();
obs.getContained().add(p);
obs.getCode().setText("Observation 1");
obs.getSubject().setReference("#pat");
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
Location loc = new Location();
loc.setId("loc");
loc.getManagingOrganization().setReference(org.getId());
Observation obs2 = new Observation();
obs2.getContained().add(loc);
obs2.getCode().setText("Observation 2");
obs2.getSubject().setReference("#loc");
myObservationDao.create(obs2, mySrd);
}
String url = "/Observation?subject:Patient.organization:Organization.name=HealthCo";
// execute
myCaptureQueriesListener.clear();
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
myCaptureQueriesListener.logSelectQueries();
// validate
assertEquals(1L, oids.size());
assertThat(oids, contains(oid1.getIdPart()));
}
@Test
@Disabled
public void testShouldResolveAThreeLinkChainWithQualifiersWithAllContainedResources() throws Exception {
// TODO: This test fails because of a known limitation in qualified searches over contained resources.
// Type information for intermediate resources in the chain is not being retained in the indexes.
// setup
myModelConfig.setIndexOnContainedResourcesRecursively(true);
IIdType oid1;
{
Organization org = new Organization();
org.setId("org");
org.setName("HealthCo");
Patient p = new Patient();
p.setId("pat");
p.addName().setFamily("Smith").addGiven("John");
p.getManagingOrganization().setReference("#org");
Observation obs = new Observation();
obs.getContained().add(p);
obs.getContained().add(org);
obs.getCode().setText("Observation 1");
obs.getSubject().setReference("#pat");
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
Organization org2 = new Organization();
org2.setId("org");
org2.setName("HealthCo");
Device d = new Device();
d.setId("dev");
d.getOwner().setReference("#org");
Observation obs2 = new Observation();
obs2.getContained().add(d);
obs2.getContained().add(org2);
obs2.getCode().setText("Observation 2");
obs2.getSubject().setReference("#dev");
myObservationDao.create(obs2, mySrd);
}
String url = "/Observation?subject:Patient.organization:Organization.name=HealthCo";
// execute
myCaptureQueriesListener.clear();
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
myCaptureQueriesListener.logSelectQueries();
// validate
assertEquals(1L, oids.size());
assertThat(oids, contains(oid1.getIdPart()));
}
@Test @Test
public void testShouldResolveAFourLinkChainWhereAllResourcesStandAlone() throws Exception { public void testShouldResolveAFourLinkChainWhereAllResourcesStandAlone() throws Exception {
@ -485,6 +682,47 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
assertThat(oids, contains(oid1.getIdPart())); assertThat(oids, contains(oid1.getIdPart()));
} }
@Test
public void testShouldResolveAFourLinkChainWhereTheLastTwoReferencesAreContained() throws Exception {
// setup
myModelConfig.setIndexOnContainedResourcesRecursively(true);
IIdType oid1;
{
Organization org = new Organization();
org.setId("parent");
org.setName("HealthCo");
Organization partOfOrg = new Organization();
partOfOrg.setId("child");
partOfOrg.getPartOf().setReference("#parent");
Patient p = new Patient();
p.getContained().add(org);
p.getContained().add(partOfOrg);
p.setId(IdType.newRandomUuid());
p.addName().setFamily("Smith").addGiven("John");
p.getManagingOrganization().setReference("#child");
myPatientDao.create(p, mySrd);
Observation obs = new Observation();
obs.getCode().setText("Observation 1");
obs.getSubject().setReference(p.getId());
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
}
String url = "/Observation?subject.organization.partof.name=HealthCo";
// execute
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
// validate
assertEquals(1L, oids.size());
assertThat(oids, contains(oid1.getIdPart()));
}
@Test @Test
public void testShouldResolveAFourLinkChainWithAContainedResourceInTheMiddle() throws Exception { public void testShouldResolveAFourLinkChainWithAContainedResourceInTheMiddle() throws Exception {
@ -531,6 +769,47 @@ public class ChainingR4SearchTest extends BaseJpaR4Test {
assertThat(oids, contains(oid1.getIdPart())); assertThat(oids, contains(oid1.getIdPart()));
} }
@Test
public void testShouldResolveAFourLinkChainWhereAllReferencesAreContained() throws Exception {
// setup
myModelConfig.setIndexOnContainedResourcesRecursively(true);
IIdType oid1;
{
Organization org = new Organization();
org.setId("parent");
org.setName("HealthCo");
Organization partOfOrg = new Organization();
partOfOrg.setId("child");
partOfOrg.getPartOf().setReference("#parent");
Patient p = new Patient();
p.setId("pat");
p.addName().setFamily("Smith").addGiven("John");
p.getManagingOrganization().setReference("#child");
Observation obs = new Observation();
obs.getContained().add(org);
obs.getContained().add(partOfOrg);
obs.getContained().add(p);
obs.getCode().setText("Observation 1");
obs.getSubject().setReference("#pat");
oid1 = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless();
}
String url = "/Observation?subject.organization.partof.name=HealthCo";
// execute
List<String> oids = searchAndReturnUnqualifiedVersionlessIdValues(url);
// validate
assertEquals(1L, oids.size());
assertThat(oids, contains(oid1.getIdPart()));
}
private List<String> searchAndReturnUnqualifiedVersionlessIdValues(String theUrl) throws IOException { private List<String> searchAndReturnUnqualifiedVersionlessIdValues(String theUrl) throws IOException {
List<String> ids = new ArrayList<>(); List<String> ids = new ArrayList<>();

View File

@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel; import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.jpa.model.entity.ResourceLink; import ca.uhn.fhir.jpa.model.entity.ResourceLink;
import ca.uhn.fhir.jpa.model.util.UcumServiceUtil; import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
import ca.uhn.fhir.jpa.partition.SystemRequestDetails; import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
@ -67,6 +68,7 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden()); myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden());
myModelConfig.setNormalizedQuantitySearchLevel(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED); myModelConfig.setNormalizedQuantitySearchLevel(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED);
myModelConfig.setIndexOnContainedResources(new ModelConfig().isIndexOnContainedResources()); myModelConfig.setIndexOnContainedResources(new ModelConfig().isIndexOnContainedResources());
myModelConfig.setIndexOnContainedResourcesRecursively(new ModelConfig().isIndexOnContainedResourcesRecursively());
} }
@ -122,6 +124,87 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test {
}); });
} }
@Test
public void testCreateLinkCreatesAppropriatePaths_ContainedResourceRecursive() {
myModelConfig.setIndexOnContainedResources(true);
myModelConfig.setIndexOnContainedResourcesRecursively(true);
Patient p = new Patient();
p.setId("pat");
p.setActive(true);
p.getNameFirstRep().setFamily("Smith");
Observation containedObs = new Observation();
containedObs.setId("#obs");
containedObs.setSubject(new Reference("#pat"));
Encounter enc = new Encounter();
enc.getContained().add(containedObs);
enc.getContained().add(p);
enc.addReasonReference(new Reference("#obs"));
myEncounterDao.create(enc, mySrd);
runInTransaction(() ->{
List<ResourceIndexedSearchParamString> allParams = myResourceIndexedSearchParamStringDao.findAll();
Optional<ResourceIndexedSearchParamString> link = allParams
.stream()
.filter(t -> "reason-reference.subject.family".equals(t.getParamName()))
.findFirst();
assertTrue(link.isPresent());
assertEquals("Smith", link.get().getValueExact());
});
}
@Test
public void testCreateLinkCreatesAppropriatePaths_ContainedResourceRecursive_DoesNotLoop() {
myModelConfig.setIndexOnContainedResources(true);
myModelConfig.setIndexOnContainedResourcesRecursively(true);
Organization org1 = new Organization();
org1.setId("org1");
org1.setName("EscherCorp");
org1.setPartOf(new Reference("#org2"));
Organization org2 = new Organization();
org2.setId("org2");
org2.setName("M.C.Escher Unlimited");
org2.setPartOf(new Reference("#org1"));
Observation containedObs = new Observation();
containedObs.setId("#obs");
containedObs.addPerformer(new Reference("#org1"));
Encounter enc = new Encounter();
enc.getContained().add(containedObs);
enc.getContained().add(org1);
enc.getContained().add(org2);
enc.addReasonReference(new Reference("#obs"));
myEncounterDao.create(enc, mySrd);
runInTransaction(() ->{
List<ResourceIndexedSearchParamString> allParams = myResourceIndexedSearchParamStringDao.findAll();
Optional<ResourceIndexedSearchParamString> firstOrg = allParams
.stream()
.filter(t -> "reason-reference.performer.name".equals(t.getParamName()))
.findFirst();
assertTrue(firstOrg.isPresent());
assertEquals("EscherCorp", firstOrg.get().getValueExact());
Optional<ResourceIndexedSearchParamString> secondOrg = allParams
.stream()
.filter(t -> "reason-reference.performer.partof.name".equals(t.getParamName()))
.findFirst();
assertTrue(secondOrg.isPresent());
assertEquals("M.C.Escher Unlimited", secondOrg.get().getValueExact());
Optional<ResourceIndexedSearchParamString> thirdOrg = allParams
.stream()
.filter(t -> "reason-reference.performer.partof.partof.name".equals(t.getParamName()))
.findFirst();
assertFalse(thirdOrg.isPresent());
});
}
@Test @Test
public void testConditionalCreateWithPlusInUrl() { public void testConditionalCreateWithPlusInUrl() {

View File

@ -100,6 +100,7 @@ public class ModelConfig {
private Map<String, Set<String>> myTypeToAutoVersionReferenceAtPaths = Collections.emptyMap(); private Map<String, Set<String>> myTypeToAutoVersionReferenceAtPaths = Collections.emptyMap();
private boolean myRespectVersionsForSearchIncludes; private boolean myRespectVersionsForSearchIncludes;
private boolean myIndexOnContainedResources = false; private boolean myIndexOnContainedResources = false;
private boolean myIndexOnContainedResourcesRecursively = false;
private boolean myAllowMdmExpansion = false; private boolean myAllowMdmExpansion = false;
/** /**
@ -785,6 +786,26 @@ public class ModelConfig {
myIndexOnContainedResources = theIndexOnContainedResources; myIndexOnContainedResources = theIndexOnContainedResources;
} }
/**
* Should recursive indexing and searching on contained resources be enabled on this server.
* This may have performance impacts, and should be enabled only if it is needed. Default is <code>false</code>.
*
* @since 5.6.0
*/
public boolean isIndexOnContainedResourcesRecursively() {
return myIndexOnContainedResourcesRecursively;
}
/**
* Should indexing and searching on contained resources be enabled on this server.
* This may have performance impacts, and should be enabled only if it is needed. Default is <code>false</code>.
*
* @since 5.6.0
*/
public void setIndexOnContainedResourcesRecursively(boolean theIndexOnContainedResourcesRecursively) {
myIndexOnContainedResourcesRecursively = theIndexOnContainedResourcesRecursively;
}
private static void validateTreatBaseUrlsAsLocal(String theUrl) { private static void validateTreatBaseUrlsAsLocal(String theUrl) {
Validate.notBlank(theUrl, "Base URL must not be null or empty"); Validate.notBlank(theUrl, "Base URL must not be null or empty");

View File

@ -64,6 +64,7 @@ import javax.annotation.Nonnull;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.util.Collection; import java.util.Collection;
import java.util.Date; import java.util.Date;
import java.util.HashSet;
import java.util.List; import java.util.List;
import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isBlank;
@ -133,41 +134,57 @@ public class SearchParamExtractorService {
// 1. get all contained resources // 1. get all contained resources
Collection<IBaseResource> containedResources = terser.getAllEmbeddedResources(theResource, false); Collection<IBaseResource> containedResources = terser.getAllEmbeddedResources(theResource, false);
extractSearchIndexParametersForContainedResources(theRequestDetails, theParams, theResource, theEntity, containedResources, new HashSet<>());
}
private void extractSearchIndexParametersForContainedResources(RequestDetails theRequestDetails, ResourceIndexedSearchParams theParams, IBaseResource theResource, ResourceTable theEntity, Collection<IBaseResource> containedResources, Collection<IBaseResource> theAlreadySeenResources) {
// 2. Find referenced search parameters // 2. Find referenced search parameters
ISearchParamExtractor.SearchParamSet<PathAndRef> referencedSearchParamSet = mySearchParamExtractor.extractResourceLinks(theResource, true); ISearchParamExtractor.SearchParamSet<PathAndRef> referencedSearchParamSet = mySearchParamExtractor.extractResourceLinks(theResource, true);
String spnamePrefix = null; String spnamePrefix = null;
ResourceIndexedSearchParams currParams; ResourceIndexedSearchParams currParams;
// 3. for each referenced search parameter, create an index // 3. for each referenced search parameter, create an index
for (PathAndRef nextPathAndRef : referencedSearchParamSet) { for (PathAndRef nextPathAndRef : referencedSearchParamSet) {
// 3.1 get the search parameter name as spname prefix // 3.1 get the search parameter name as spname prefix
spnamePrefix = nextPathAndRef.getSearchParamName(); spnamePrefix = nextPathAndRef.getSearchParamName();
if (spnamePrefix == null || nextPathAndRef.getRef() == null) if (spnamePrefix == null || nextPathAndRef.getRef() == null)
continue; continue;
// 3.2 find the contained resource // 3.2 find the contained resource
IBaseResource containedResource = findContainedResource(containedResources, nextPathAndRef.getRef()); IBaseResource containedResource = findContainedResource(containedResources, nextPathAndRef.getRef());
if (containedResource == null) if (containedResource == null)
continue; continue;
// 3.2.1 if we've already processed this resource upstream, do not process it again, to prevent infinite loops
if (theAlreadySeenResources.contains(containedResource)) {
continue;
}
currParams = new ResourceIndexedSearchParams(); currParams = new ResourceIndexedSearchParams();
// 3.3 create indexes for the current contained resource // 3.3 create indexes for the current contained resource
extractSearchIndexParameters(theRequestDetails, currParams, containedResource, theEntity); extractSearchIndexParameters(theRequestDetails, currParams, containedResource, theEntity);
// 3.4 added reference name as a prefix for the contained resource if any // 3.4 recurse to process any other contained resources referenced by this one
if (myModelConfig.isIndexOnContainedResourcesRecursively()) {
HashSet<IBaseResource> nextAlreadySeenResources = new HashSet<>(theAlreadySeenResources);
nextAlreadySeenResources.add(containedResource);
extractSearchIndexParametersForContainedResources(theRequestDetails, currParams, containedResource, theEntity, containedResources, nextAlreadySeenResources);
}
// 3.5 added reference name as a prefix for the contained resource if any
// e.g. for Observation.subject contained reference // e.g. for Observation.subject contained reference
// the SP_NAME = subject.family // the SP_NAME = subject.family
currParams.updateSpnamePrefixForIndexedOnContainedResource(spnamePrefix); currParams.updateSpnamePrefixForIndexedOnContainedResource(spnamePrefix);
// 3.5 merge to the mainParams // 3.6 merge to the mainParams
// NOTE: the spname prefix is different // NOTE: the spname prefix is different
mergeParams(currParams, theParams); mergeParams(currParams, theParams);
} }
} }
private IBaseResource findContainedResource(Collection<IBaseResource> resources, IBaseReference reference) { private IBaseResource findContainedResource(Collection<IBaseResource> resources, IBaseReference reference) {
for (IBaseResource resource : resources) { for (IBaseResource resource : resources) {
if (resource.getIdElement().equals(reference.getReferenceElement())) if (resource.getIdElement().equals(reference.getReferenceElement()))