From b267fdb7527207a18a7b92a980d233a98636e326 Mon Sep 17 00:00:00 2001 From: JasonRoberts-smile <85363818+JasonRoberts-smile@users.noreply.github.com> Date: Fri, 22 Oct 2021 08:41:05 -0400 Subject: [PATCH] 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 --- ...ained-search-multi-contained-resource.yaml | 8 + .../fhir/jpa/search/builder/QueryStack.java | 14 +- .../ResourceLinkPredicateBuilder.java | 4 + .../fhir/jpa/dao/r4/ChainingR4SearchTest.java | 279 ++++++++++++++++++ .../dao/r4/FhirResourceDaoR4CreateTest.java | 83 ++++++ .../fhir/jpa/model/entity/ModelConfig.java | 21 ++ .../SearchParamExtractorService.java | 41 ++- 7 files changed, 433 insertions(+), 17 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/3100-chained-search-multi-contained-resource.yaml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/3100-chained-search-multi-contained-resource.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/3100-chained-search-multi-contained-resource.yaml new file mode 100644 index 00000000000..a03068d48f5 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_6_0/3100-chained-search-multi-contained-resource.yaml @@ -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." diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java index d1f2719ace0..3130f7cddcd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/QueryStack.java @@ -685,7 +685,7 @@ public class QueryStack { } public Condition createPredicateReferenceForContainedResource(@Nullable DbColumn theSourceJoinColumn, - String theResourceName, String theParamName, RuntimeSearchParam theSearchParam, + String theResourceName, String theParamName, List theQualifiers, RuntimeSearchParam theSearchParam, List theList, SearchFilterParser.CompareOperation theOperation, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { @@ -760,7 +760,7 @@ public class QueryStack { throw new InvalidRequestException("Unknown search parameter name: " + theSearchParam.getName() + "."); } - List qualifiers= Collections.singletonList(headQualifier); + theQualifiers.add(headQualifier); // 3. create the query Condition containedCondition = null; @@ -796,7 +796,11 @@ public class QueryStack { break; case REFERENCE: 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; case HAS: case SPECIAL: @@ -1140,11 +1144,11 @@ public class QueryStack { // See SMILE-2898 for details. // For now, leave the incorrect implementation alone, just in case someone is relying on it, // 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)) { andPredicates.add(toOrPredicate( 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 { andPredicates.add(createPredicateReference(theSourceJoinColumn, theResourceName, theParamName, new ArrayList<>(), nextAnd, null, theRequest, theRequestPartitionId)); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java index a959218cbf2..452a1e12805 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/predicate/ResourceLinkPredicateBuilder.java @@ -569,6 +569,10 @@ public class ResourceLinkPredicateBuilder extends BaseJoiningPredicateBuilder { String qualifier = theParamQualifiers.get(0); 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 tailPaths = param.getTargets().stream() .filter(t -> isBlank(qualifier) || qualifier.equals(t)) .map(t -> createResourceLinkPaths(t, paramNameTail, theParamQualifiers.subList(1, theParamQualifiers.size()))) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/ChainingR4SearchTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/ChainingR4SearchTest.java index 9ce33162512..e05458ed820 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/ChainingR4SearchTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/ChainingR4SearchTest.java @@ -10,12 +10,14 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Device; 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.Organization; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.StringType; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -46,6 +48,7 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { myModelConfig.setIndexOnContainedResources(false); myModelConfig.setIndexOnContainedResources(new ModelConfig().isIndexOnContainedResources()); + myModelConfig.setIndexOnContainedResourcesRecursively(new ModelConfig().isIndexOnContainedResourcesRecursively()); } @BeforeEach @@ -116,6 +119,50 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { 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 oids = searchAndReturnUnqualifiedVersionlessIdValues(url); + + // validate + assertEquals(1L, oids.size()); + assertThat(oids, contains(oid1.getIdPart())); + } + @Test public void testShouldResolveATwoLinkChainToAContainedReference() throws Exception { // Adding support for this case in SMILE-3151 @@ -260,6 +307,45 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { 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 oids = searchAndReturnUnqualifiedVersionlessIdValues(url); + myCaptureQueriesListener.logSelectQueries(); + + // validate + assertEquals(1L, oids.size()); + assertThat(oids, contains(oid1.getIdPart())); + } + @Test public void testShouldResolveAThreeLinkChainWithQualifiersWhereAllResourcesStandAlone() throws Exception { @@ -405,6 +491,117 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { 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 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 oids = searchAndReturnUnqualifiedVersionlessIdValues(url); + myCaptureQueriesListener.logSelectQueries(); + + // validate + assertEquals(1L, oids.size()); + assertThat(oids, contains(oid1.getIdPart())); + } + @Test public void testShouldResolveAFourLinkChainWhereAllResourcesStandAlone() throws Exception { @@ -485,6 +682,47 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { 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 oids = searchAndReturnUnqualifiedVersionlessIdValues(url); + + // validate + assertEquals(1L, oids.size()); + assertThat(oids, contains(oid1.getIdPart())); + } + @Test public void testShouldResolveAFourLinkChainWithAContainedResourceInTheMiddle() throws Exception { @@ -531,6 +769,47 @@ public class ChainingR4SearchTest extends BaseJpaR4Test { 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 oids = searchAndReturnUnqualifiedVersionlessIdValues(url); + + // validate + assertEquals(1L, oids.size()); + assertThat(oids, contains(oid1.getIdPart())); + } + private List searchAndReturnUnqualifiedVersionlessIdValues(String theUrl) throws IOException { List ids = new ArrayList<>(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java index 2682d8f97cf..d7545987b8d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java @@ -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.ResourceIndexedSearchParamQuantity; 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.util.UcumServiceUtil; import ca.uhn.fhir.jpa.partition.SystemRequestDetails; @@ -67,6 +68,7 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden()); myModelConfig.setNormalizedQuantitySearchLevel(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED); 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 allParams = myResourceIndexedSearchParamStringDao.findAll(); + Optional 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 allParams = myResourceIndexedSearchParamStringDao.findAll(); + Optional firstOrg = allParams + .stream() + .filter(t -> "reason-reference.performer.name".equals(t.getParamName())) + .findFirst(); + assertTrue(firstOrg.isPresent()); + assertEquals("EscherCorp", firstOrg.get().getValueExact()); + + Optional 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 thirdOrg = allParams + .stream() + .filter(t -> "reason-reference.performer.partof.partof.name".equals(t.getParamName())) + .findFirst(); + assertFalse(thirdOrg.isPresent()); + }); + } @Test public void testConditionalCreateWithPlusInUrl() { diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java index 043807c1212..042243eee9a 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java @@ -100,6 +100,7 @@ public class ModelConfig { private Map> myTypeToAutoVersionReferenceAtPaths = Collections.emptyMap(); private boolean myRespectVersionsForSearchIncludes; private boolean myIndexOnContainedResources = false; + private boolean myIndexOnContainedResourcesRecursively = false; private boolean myAllowMdmExpansion = false; /** @@ -785,6 +786,26 @@ public class ModelConfig { 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 false. + * + * @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 false. + * + * @since 5.6.0 + */ + public void setIndexOnContainedResourcesRecursively(boolean theIndexOnContainedResourcesRecursively) { + myIndexOnContainedResourcesRecursively = theIndexOnContainedResourcesRecursively; + } + private static void validateTreatBaseUrlsAsLocal(String theUrl) { Validate.notBlank(theUrl, "Base URL must not be null or empty"); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java index 1121c250d49..dc7c57dfe51 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/extractor/SearchParamExtractorService.java @@ -64,6 +64,7 @@ import javax.annotation.Nonnull; import javax.validation.constraints.NotNull; import java.util.Collection; import java.util.Date; +import java.util.HashSet; import java.util.List; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -133,41 +134,57 @@ public class SearchParamExtractorService { // 1. get all contained resources Collection 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 containedResources, Collection theAlreadySeenResources) { // 2. Find referenced search parameters ISearchParamExtractor.SearchParamSet referencedSearchParamSet = mySearchParamExtractor.extractResourceLinks(theResource, true); - + String spnamePrefix = null; ResourceIndexedSearchParams currParams; // 3. for each referenced search parameter, create an index for (PathAndRef nextPathAndRef : referencedSearchParamSet) { - + // 3.1 get the search parameter name as spname prefix spnamePrefix = nextPathAndRef.getSearchParamName(); - + if (spnamePrefix == null || nextPathAndRef.getRef() == null) continue; - + // 3.2 find the contained resource IBaseResource containedResource = findContainedResource(containedResources, nextPathAndRef.getRef()); if (containedResource == null) 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(); - + // 3.3 create indexes for the current contained resource 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 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 // the SP_NAME = subject.family currParams.updateSpnamePrefixForIndexedOnContainedResource(spnamePrefix); - - // 3.5 merge to the mainParams + + // 3.6 merge to the mainParams // NOTE: the spname prefix is different - mergeParams(currParams, theParams); + mergeParams(currParams, theParams); } } - + private IBaseResource findContainedResource(Collection resources, IBaseReference reference) { for (IBaseResource resource : resources) { if (resource.getIdElement().equals(reference.getReferenceElement()))