From 78a068ab45108205301f2e697d3da7e2508e1105 Mon Sep 17 00:00:00 2001 From: Jens Kristian Villadsen Date: Sun, 8 Feb 2026 17:18:28 +0100 Subject: [PATCH] Feature/elastic back in green (#893) * Getting automated tests back into green * using native API's * formatting * refactoring * getting 8.X ES working * Removed sleep * Update src/test/java/ca/uhn/fhir/jpa/starter/ElasticsearchLastNR4IT.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Reintroduced sleep as indicies are a bit slow ? * Using the production bean instead of a test one * updating default parameters for es * Making separate profile for ES * works from here * remove all not needed code anymore * removing parent defined version * fixed tests --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pom.xml | 15 +-- .../jpa/starter/common/StarterJpaConfig.java | 2 + .../elastic/ElasticsearchBootSvcImpl.java | 12 +-- src/main/resources/application-elastic.yaml | 50 +++++++++ src/main/resources/application.yaml | 37 +++++-- .../jpa/starter/ElasticsearchLastNR4IT.java | 101 +++++++++--------- 6 files changed, 142 insertions(+), 75 deletions(-) create mode 100644 src/main/resources/application-elastic.yaml diff --git a/pom.xml b/pom.xml index a22875e..521e3b7 100644 --- a/pom.xml +++ b/pom.xml @@ -307,25 +307,18 @@ org.testcontainers testcontainers - 2.0.3 test org.testcontainers - elasticsearch - 1.21.4 + testcontainers-elasticsearch + 2.0.2 test org.testcontainers - postgresql - 1.21.4 - test - - - org.testcontainers - junit-jupiter - 1.21.4 + testcontainers-junit-jupiter + 2.0.2 test diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/common/StarterJpaConfig.java b/src/main/java/ca/uhn/fhir/jpa/starter/common/StarterJpaConfig.java index 1c1d2c7..1e3bc6a 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/common/StarterJpaConfig.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/common/StarterJpaConfig.java @@ -50,6 +50,7 @@ import ca.uhn.fhir.jpa.starter.AppProperties; import ca.uhn.fhir.jpa.starter.annotations.OnCorsPresent; import ca.uhn.fhir.jpa.starter.annotations.OnImplementationGuidesPresent; import ca.uhn.fhir.jpa.starter.common.validation.IRepositoryValidationInterceptorFactory; +import ca.uhn.fhir.jpa.starter.elastic.ElasticsearchBootSvcImpl; import ca.uhn.fhir.jpa.starter.ig.ExtendedPackageInstallationSpec; import ca.uhn.fhir.jpa.starter.ig.IImplementationGuideOperationProvider; import ca.uhn.fhir.jpa.subscription.util.SubscriptionDebugLogInterceptor; @@ -149,6 +150,7 @@ public class StarterJpaConfig { @Primary @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory( + Optional elasticsearchSvc, JpaProperties theJpaProperties, DataSource myDataSource, ConfigurableListableBeanFactory myConfigurableListableBeanFactory, diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/elastic/ElasticsearchBootSvcImpl.java b/src/main/java/ca/uhn/fhir/jpa/starter/elastic/ElasticsearchBootSvcImpl.java index a6299f7..fdc197f 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/elastic/ElasticsearchBootSvcImpl.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/elastic/ElasticsearchBootSvcImpl.java @@ -50,7 +50,7 @@ public class ElasticsearchBootSvcImpl implements IElasticsearchSvc { private static final String OBSERVATION_RESOURCE_NAME = "Observation"; - private final ElasticsearchClient myRestHighLevelClient; + private final ElasticsearchClient myElasticsearchClient; private final FhirContext myContext; @@ -61,7 +61,7 @@ public class ElasticsearchBootSvcImpl implements IElasticsearchSvc { public ElasticsearchBootSvcImpl(ElasticsearchClient client, FhirContext fhirContext, AppProperties appProperties) { myContext = fhirContext; - myRestHighLevelClient = client; + myElasticsearchClient = client; // Determine index prefix from configuration if (appProperties.getElasticsearch() != null) { @@ -144,7 +144,7 @@ public class ElasticsearchBootSvcImpl implements IElasticsearchSvc { } private boolean createIndex(String theIndexName, String theMapping) throws IOException { - return myRestHighLevelClient + return myElasticsearchClient .indices() .create(cir -> cir.index(theIndexName).withJson(new StringReader(theMapping))) .acknowledged(); @@ -152,7 +152,7 @@ public class ElasticsearchBootSvcImpl implements IElasticsearchSvc { private boolean indexExists(String theIndexName) throws IOException { ExistsRequest request = new ExistsRequest.Builder().index(theIndexName).build(); - return myRestHighLevelClient.indices().exists(request).value(); + return myElasticsearchClient.indices().exists(request).value(); } @Override @@ -165,7 +165,7 @@ public class ElasticsearchBootSvcImpl implements IElasticsearchSvc { SearchRequest searchRequest = buildObservationResourceSearchRequest(thePids); try { SearchResponse observationDocumentResponse = - myRestHighLevelClient.search(searchRequest, ObservationJson.class); + myElasticsearchClient.search(searchRequest, ObservationJson.class); List> observationDocumentHits = observationDocumentResponse.hits().hits(); IParser parser = TolerantJsonParser.createWithLenientErrorHandling(myContext, null); @@ -202,7 +202,7 @@ public class ElasticsearchBootSvcImpl implements IElasticsearchSvc { @VisibleForTesting public void refreshIndex(String theIndexName) throws IOException { - myRestHighLevelClient.indices().refresh(fn -> fn.index(theIndexName)); + myElasticsearchClient.indices().refresh(fn -> fn.index(theIndexName)); } /** diff --git a/src/main/resources/application-elastic.yaml b/src/main/resources/application-elastic.yaml new file mode 100644 index 0000000..68b07be --- /dev/null +++ b/src/main/resources/application-elastic.yaml @@ -0,0 +1,50 @@ +spring: + elasticsearch: + uris: http://localhost:9200 + username: elastic + password: elastic + + autoconfigure: + # This empty exclude is needed to override the default exclusion of the Elasticsearch configuration. + exclude: + + jpa: + properties: + hibernate: + # --- Hibernate Search (Lucene/Elasticsearch) --- + # Note: the following values should be kept in sync with ca.uhn.fhir.jpa.search.elastic.ElasticsearchHibernatePropertiesBuilder + search: + schema_management: + strategy: CREATE + enabled: true + backend: + layout: + strategy: ca.uhn.fhir.jpa.search.elastic.IndexNamePrefixLayoutStrategy + type: elasticsearch + protocol: http + analysis: + configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticsearchAnalysisConfigurer + scroll_timeout: 60 + schema_management: + settings_file: ca/uhn/fhir/jpa/elastic/index-settings.json + minimal_required_status_wait_timeout: 10000 + minimal_required_status: YELLOW + + dynamic_mapping: true + indexing: + plan: + synchronization: + strategy: async + +# ------------------------------------------------------------------------------------- +# HAPI FHIR — grouped by domain +# ------------------------------------------------------------------------------------- +hapi: + fhir: + # ------------------------------------------------------------------------------- + # D. Search & Indexing + # ------------------------------------------------------------------------------- + # NOTE: Extended Lucene/Elasticsearch indexing is experimental. + # See https://hapifhir.io/hapi-fhir/docs/server_jpa/elastic.html + advanced_lucene_indexing: true + search_index_full_text_enabled: true diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 8afc883..07e3961 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -37,6 +37,10 @@ management: enabled: true spring: +# elasticsearch: +# uris: http://localhost:9200 +# username: elastic +# password: elastic # ------------------------------------------------------------------------------- # Application Name # ------------------------------------------------------------------------------- @@ -130,8 +134,10 @@ spring: use_minimal_puts: false # --- Hibernate Search (Lucene/Elasticsearch) --- - search: - enabled: false + #search: + # schema_management: + # strategy: CREATE + # enabled: true # Lucene backend (default example) # backend: # type: lucene @@ -142,10 +148,25 @@ spring: # root: target/lucenefiles # lucene_version: lucene_current # Elasticsearch backend (alternative) — see also hapi.fhir.elasticsearch section in docs - # backend: - # type: elasticsearch - # analysis: - # configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticAnalysisConfigurer +# backend: +# layout: +# strategy: ca.uhn.fhir.jpa.search.elastic.IndexNamePrefixLayoutStrategy +# type: elasticsearch +# protocol: http +# analysis: +# configurer: ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers$HapiElasticsearchAnalysisConfigurer +# scroll_timeout: 60 +# schema_management: +# settings_file: ca/uhn/fhir/jpa/elastic/index-settings.json +# minimal_required_status_wait_timeout: 10000 +# minimal_required_status: YELLOW +# +# dynamic_mapping: true +# indexing: +# plan: +# synchronization: +# strategy: async + # ------------------------------------------------------------------------------------- # HAPI FHIR — grouped by domain @@ -249,8 +270,8 @@ hapi: # ------------------------------------------------------------------------------- # NOTE: Extended Lucene/Elasticsearch indexing is experimental. # See https://hapifhir.io/hapi-fhir/docs/server_jpa/elastic.html - advanced_lucene_indexing: false - search_index_full_text_enabled: false + # advanced_lucene_indexing: true + # search_index_full_text_enabled: true # language_search_parameter_enabled: true # upliftedRefchains_enabled: true # index_storage_optimized: false diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/ElasticsearchLastNR4IT.java b/src/test/java/ca/uhn/fhir/jpa/starter/ElasticsearchLastNR4IT.java index 42c3198..c2ed69a 100644 --- a/src/test/java/ca/uhn/fhir/jpa/starter/ElasticsearchLastNR4IT.java +++ b/src/test/java/ca/uhn/fhir/jpa/starter/ElasticsearchLastNR4IT.java @@ -1,7 +1,5 @@ package ca.uhn.fhir.jpa.starter; -import static org.junit.jupiter.api.Assertions.assertEquals; - import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.search.lastn.ElasticsearchSvcImpl; import ca.uhn.fhir.jpa.starter.common.TestContainerHelper; @@ -36,69 +34,72 @@ import org.testcontainers.elasticsearch.ElasticsearchContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; +import static org.junit.jupiter.api.Assertions.assertEquals; + @Testcontainers @ActiveProfiles("test") @TestPropertySource(locations = "classpath:test-elasticsearch-lastn.yaml") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class}) class ElasticsearchLastNR4IT { - private IGenericClient ourClient; + private IGenericClient ourClient; - @Container - private static final ElasticsearchContainer ELASTICSEARCH = TestContainerHelper.newElasticsearchContainer() - // Set index defaults to handle HAPI FHIR's MAX_SUBSCRIPTION_RESULTS (50000) - .withEnv("indices.query.bool.max_clause_count", "50000"); + @Container + private static final ElasticsearchContainer ELASTICSEARCH = TestContainerHelper.newElasticsearchContainer() + // Set index defaults to handle HAPI FHIR's MAX_SUBSCRIPTION_RESULTS (50000) + .withEnv("indices.query.bool.max_clause_count", "50000"); - @DynamicPropertySource - static void registerElasticsearchProperties(DynamicPropertyRegistry registry) { - TestContainerHelper.registerElasticsearchProperties(registry, ELASTICSEARCH); - // Also register spring.elasticsearch.uris for ElasticConfigCondition to enable ElasticsearchBootSvcImpl - registry.add("spring.elasticsearch.uris", () -> TestContainerHelper.getElasticsearchHttpUrl(ELASTICSEARCH)); - } + @DynamicPropertySource + static void registerElasticsearchProperties(DynamicPropertyRegistry registry) { + TestContainerHelper.registerElasticsearchProperties(registry, ELASTICSEARCH); + // Also register spring.elasticsearch.uris for ElasticConfigCondition to enable ElasticsearchBootSvcImpl + registry.add("spring.elasticsearch.uris", () -> TestContainerHelper.getElasticsearchHttpUrl(ELASTICSEARCH)); - @Autowired - private ElasticsearchBootSvcImpl myElasticsearchSvc; + registry.add("spring.jpa.properties.hibernate.search.backend.hosts", ELASTICSEARCH::getHttpHostAddress); + registry.add("spring.jpa.properties.hibernate.search.backend.protocol", () -> "http"); + registry.add("spring.jpa.properties.hibernate.search.backend.username", () -> ""); + registry.add("spring.jpa.properties.hibernate.search.backend.password", () -> ""); + } - @LocalServerPort - private int port; + @Autowired + private ElasticsearchBootSvcImpl myElasticsearchSvc; - @Test - void testLastN() throws IOException, InterruptedException { - Thread.sleep(2000); + @LocalServerPort + private int port; - Patient pt = new Patient(); - pt.addName().setFamily("Lastn").addGiven("Arthur"); - IIdType id = ourClient.create().resource(pt).execute().getId().toUnqualifiedVersionless(); + @Test + void testLastN() throws IOException, InterruptedException { + Patient pt = new Patient(); + pt.addName().setFamily("Lastn").addGiven("Arthur"); + IIdType id = ourClient.create().resource(pt).execute().getId().toUnqualifiedVersionless(); - Observation obs = new Observation(); - obs.getSubject().setReferenceElement(id); - String observationCode = "testobservationcode"; + Observation obs = new Observation(); + obs.getSubject().setReferenceElement(id); + String observationCode = "testobservationcode"; - obs.getCode().addCoding().setCode(observationCode).setSystem("http://testobservationcodesystem"); - obs.setValue(new StringType(observationCode)); - Date effectiveDtm = new GregorianCalendar().getTime(); - obs.setEffective(new DateTimeType(effectiveDtm)); - obs.getCategoryFirstRep().addCoding().setCode("testcategorycode").setSystem("http://testcategorycodesystem"); - IIdType obsId = ourClient.create().resource(obs).execute().getId().toUnqualifiedVersionless(); + obs.getCode().addCoding().setCode(observationCode).setSystem("http://testobservationcodesystem"); + obs.setValue(new StringType(observationCode)); - myElasticsearchSvc.refreshIndex(ElasticsearchSvcImpl.OBSERVATION_INDEX); - Thread.sleep(2000); + Date effectiveDtm = new GregorianCalendar().getTime(); + obs.setEffective(new DateTimeType(effectiveDtm)); + obs.getCategoryFirstRep().addCoding().setCode("testcategorycode").setSystem("http://testcategorycodesystem"); + IIdType obsId = ourClient.create().resource(obs).execute().getId().toUnqualifiedVersionless(); - Parameters output = ourClient.operation().onType(Observation.class).named("lastn") - .withParameter(Parameters.class, "max", new IntegerType(1)) - .andParameter("subject", new StringType("Patient/" + id.getIdPart())) - .execute(); - Bundle b = (Bundle) output.getParameter().get(0).getResource(); - assertEquals(1, b.getTotal()); - assertEquals(obsId, b.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless()); - } + myElasticsearchSvc.refreshIndex(ElasticsearchSvcImpl.OBSERVATION_INDEX); - @BeforeEach - void beforeEach() { - FhirContext ctx = FhirContext.forR4(); - ctx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); - ctx.getRestfulClientFactory().setSocketTimeout((int) Duration.ofMinutes(20).toMillis()); - ourClient = ctx.newRestfulGenericClient("http://localhost:" + port + "/fhir/"); - ourClient.registerInterceptor(new LoggingInterceptor(true)); - } + Thread.sleep(2000); + Parameters output = ourClient.operation().onType(Observation.class).named("lastn").withParameter(Parameters.class, "max", new IntegerType(1)).andParameter("subject", new StringType("Patient/" + id.getIdPart())).execute(); + Bundle b = (Bundle) output.getParameter().get(0).getResource(); + assertEquals(1, b.getTotal()); + assertEquals(obsId, b.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless()); + } + + @BeforeEach + void beforeEach() { + FhirContext ctx = FhirContext.forR4(); + ctx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); + ctx.getRestfulClientFactory().setSocketTimeout((int) Duration.ofMinutes(20).toMillis()); + ourClient = ctx.newRestfulGenericClient("http://localhost:" + port + "/fhir/"); + ourClient.registerInterceptor(new LoggingInterceptor(true)); + } }