Add Elasticsearch index prefix configuration (#891)
* Add Elasticsearch configuration section to application.yaml * Add configuration for reindexing resources upon search parameter change * Add Elasticsearch client configuration for testing * Update logback dependencies because of a security issue and enable resource filtering in pom.xml * Add application name and tester configuration to application.yaml. Refactor hibernate properties * Add custom Elasticsearch configuration to create ElasticsearchClient bean * Add index prefix configuration for Elasticsearch indices * Fix Elasticsearch URI format in test initialization * Refactor ElasticsearchConfig for improved readability and code style consistency (spotless:apply) * Refactor ElasticsearchConfig for improved readability and code style consistency (spotless:apply) * Update index prefix in application.yaml for Elasticsearch configuration * Enhance index prefix validation and add sanitization method for Elasticsearch index names * Merged with master in upstream * Refactor ElasticsearchBootSvcImpl and ElasticsearchConfig for improved readability (mvn spotless) * Comment out custom_content_path in application.yaml for clarity * - Refactor application.yaml for improved structure and readability; - Add PostgresLucenePatientIT integration test; and - Update testcontainers dependencies to avoid compatibility issues with newer Docker APIs. * Refactor PostgresLucenePatientIT to use external configuration file for test properties * Add integration tests for PostgreSQL and Elasticsearch with dynamic configuration * Increase socket timeout in CdsHooksServletIT for improved stability during tests * Remove deprecated tester configuration from application.yaml
This commit is contained in:
@@ -72,7 +72,7 @@ class CdsHooksServletIT implements IServerSupport {
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
|
||||
ourCtx.getRestfulClientFactory().setSocketTimeout(1200 * 1000);
|
||||
ourCtx.getRestfulClientFactory().setSocketTimeout(1200 * 3000);
|
||||
ourServerBase = "http://localhost:" + port + "/fhir/";
|
||||
ourClient = ourCtx.newRestfulGenericClient(ourServerBase);
|
||||
ourCdsBase = "http://localhost:" + port + "/cds-services";
|
||||
|
||||
@@ -3,6 +3,7 @@ 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.ElasticsearchRestClientFactory;
|
||||
import ca.uhn.fhir.jpa.search.lastn.ElasticsearchSvcImpl;
|
||||
import ca.uhn.fhir.jpa.starter.elastic.ElasticsearchBootSvcImpl;
|
||||
import ca.uhn.fhir.jpa.test.config.TestElasticsearchContainerHelper;
|
||||
@@ -14,6 +15,8 @@ import java.io.IOException;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
import co.elastic.clients.elasticsearch.ElasticsearchClient;
|
||||
import co.elastic.clients.elasticsearch.indices.IndexSettings;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
@@ -25,7 +28,6 @@ import org.hl7.fhir.r4.model.Patient;
|
||||
import org.hl7.fhir.r4.model.StringType;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -35,6 +37,8 @@ import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.context.ApplicationContextInitializer;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.testcontainers.elasticsearch.ElasticsearchContainer;
|
||||
@@ -43,9 +47,8 @@ import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@Testcontainers
|
||||
@Disabled
|
||||
@ActiveProfiles("test")
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class}, properties =
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class, ElasticsearchLastNR4IT.TestConfig.class}, properties =
|
||||
{
|
||||
"spring.datasource.url=jdbc:h2:mem:dbr4",
|
||||
"hapi.fhir.fhir_version=r4",
|
||||
@@ -81,21 +84,20 @@ class ElasticsearchLastNR4IT {
|
||||
@BeforeAll
|
||||
public static void beforeClass() throws IOException {
|
||||
//Given
|
||||
// ElasticsearchClient elasticsearchHighLevelRestClient = ElasticsearchRestClientFactory.createElasticsearchHighLevelRestClient(
|
||||
// "http", embeddedElastic.getHost() + ":" + embeddedElastic.getMappedPort(9200), "", "");
|
||||
ElasticsearchClient elasticsearchHighLevelRestClient = ElasticsearchRestClientFactory.createElasticsearchHighLevelRestClient(
|
||||
"http", embeddedElastic.getHost() + ":" + embeddedElastic.getMappedPort(9200), "", "");
|
||||
|
||||
/* As of 2023-08-10, HAPI FHIR sets SubscriptionConstants.MAX_SUBSCRIPTION_RESULTS to 50000
|
||||
which is in excess of elastic's default max_result_window. If MAX_SUBSCRIPTION_RESULTS is changed
|
||||
to a value <= 10000, the following will no longer be necessary. - dotasek
|
||||
*/
|
||||
|
||||
/* elasticsearchHighLevelRestClient.indices().putTemplate(t->{
|
||||
elasticsearchHighLevelRestClient.indices().putTemplate(t->{
|
||||
t.name("hapi_fhir_template");
|
||||
t.indexPatterns("*");
|
||||
t.settings(new IndexSettings.Builder().maxResultWindow(50000).build());
|
||||
t.settings(new IndexSettings.Builder().maxNgramDiff(50).maxResultWindow(50000).build());
|
||||
return t;
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
@@ -151,6 +153,15 @@ class ElasticsearchLastNR4IT {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class TestConfig {
|
||||
@Bean
|
||||
public ElasticsearchClient elasticsearchClient() throws IOException {
|
||||
return ElasticsearchRestClientFactory.createElasticsearchHighLevelRestClient(
|
||||
"http", embeddedElastic.getHost() + ":" + embeddedElastic.getMappedPort(9200), "", "");
|
||||
}
|
||||
}
|
||||
|
||||
static class Initializer
|
||||
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
|
||||
|
||||
@@ -158,7 +169,7 @@ class ElasticsearchLastNR4IT {
|
||||
public void initialize(
|
||||
ConfigurableApplicationContext configurableApplicationContext) {
|
||||
// Since the port is dynamically generated, replace the URL with one that has the correct port
|
||||
TestPropertyValues.of("spring.elasticsearch.uris=" + embeddedElastic.getHost() +":" + embeddedElastic.getMappedPort(9200))
|
||||
TestPropertyValues.of("spring.elasticsearch.uris=http://" + embeddedElastic.getHost() + ":" + embeddedElastic.getMappedPort(9200))
|
||||
.applyTo(configurableApplicationContext.getEnvironment());
|
||||
TestPropertyValues.of("spring.jpa.properties.hibernate.search.backend.hosts=" + embeddedElastic.getHost() +":" + embeddedElastic.getMappedPort(9200))
|
||||
.applyTo(configurableApplicationContext.getEnvironment());
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package ca.uhn.fhir.jpa.starter;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.rest.client.api.IGenericClient;
|
||||
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
|
||||
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.elasticsearch.ElasticsearchContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
@Testcontainers
|
||||
@ActiveProfiles("test")
|
||||
@TestPropertySource(locations = "classpath:test-postgres-elasticsearch.yaml")
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class})
|
||||
class PostgresElasticsearchPatientIT {
|
||||
|
||||
@Container
|
||||
private static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine")
|
||||
.withDatabaseName("hapi")
|
||||
.withUsername("fhiruser")
|
||||
.withPassword("fhirpass");
|
||||
|
||||
@Container
|
||||
private static final ElasticsearchContainer ELASTICSEARCH = new ElasticsearchContainer(
|
||||
"docker.elastic.co/elasticsearch/elasticsearch:8.11.0"
|
||||
)
|
||||
.withEnv("xpack.security.enabled", "false")
|
||||
.withEnv("discovery.type", "single-node")
|
||||
.withEnv("ES_JAVA_OPTS", "-Xms512m -Xmx512m");
|
||||
|
||||
@DynamicPropertySource
|
||||
static void registerDatasourceProperties(DynamicPropertyRegistry registry) {
|
||||
// PostgreSQL configuration
|
||||
registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
|
||||
registry.add("spring.datasource.username", POSTGRES::getUsername);
|
||||
registry.add("spring.datasource.password", POSTGRES::getPassword);
|
||||
registry.add("spring.datasource.driver-class-name", POSTGRES::getDriverClassName);
|
||||
registry.add("spring.jpa.properties.hibernate.dialect", () -> "ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect");
|
||||
|
||||
// Elasticsearch configuration
|
||||
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;
|
||||
|
||||
private IGenericClient ourClient;
|
||||
private FhirContext ourCtx;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
ourCtx = FhirContext.forR4();
|
||||
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
|
||||
ourCtx.getRestfulClientFactory().setSocketTimeout((int) Duration.ofMinutes(20).toMillis());
|
||||
String ourServerBase = "http://localhost:" + port + "/fhir/";
|
||||
ourClient = ourCtx.newRestfulGenericClient(ourServerBase);
|
||||
ourClient.registerInterceptor(new LoggingInterceptor(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateAndSearchPatientByFamilyName() {
|
||||
String givenName = "Jane";
|
||||
String familyName = "Smith Doe";
|
||||
|
||||
Patient patient = new Patient();
|
||||
patient.addName().setFamily(familyName).addGiven(givenName);
|
||||
IIdType id = ourClient.create().resource(patient).execute().getId().toUnqualifiedVersionless();
|
||||
assertNotNull(id);
|
||||
|
||||
await().atMost(Duration.ofSeconds(30)).until(() -> searchByFamily(familyName).getTotal() == 1);
|
||||
|
||||
Bundle results = searchByFamily(familyName);
|
||||
assertEquals(1, results.getTotal());
|
||||
Patient found = (Patient) results.getEntry().get(0).getResource();
|
||||
assertEquals(familyName, found.getNameFirstRep().getFamily());
|
||||
assertEquals(id, found.getIdElement().toUnqualifiedVersionless());
|
||||
}
|
||||
|
||||
private Bundle searchByFamily(String family) {
|
||||
return ourClient
|
||||
.search()
|
||||
.forResource(Patient.class)
|
||||
.where(Patient.FAMILY.matches().value(family))
|
||||
.returnBundle(Bundle.class)
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package ca.uhn.fhir.jpa.starter;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.rest.client.api.IGenericClient;
|
||||
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
|
||||
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
@Testcontainers
|
||||
@ActiveProfiles("test")
|
||||
@TestPropertySource(locations = "classpath:test-postgres-lucene.yaml")
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class})
|
||||
class PostgresLucenePatientIT {
|
||||
|
||||
@Container
|
||||
private static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine")
|
||||
.withDatabaseName("hapi")
|
||||
.withUsername("fhiruser")
|
||||
.withPassword("fhirpass");
|
||||
|
||||
@DynamicPropertySource
|
||||
static void registerDatasourceProperties(DynamicPropertyRegistry registry) {
|
||||
registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
|
||||
registry.add("spring.datasource.username", POSTGRES::getUsername);
|
||||
registry.add("spring.datasource.password", POSTGRES::getPassword);
|
||||
registry.add("spring.datasource.driver-class-name", POSTGRES::getDriverClassName);
|
||||
registry.add("spring.jpa.properties.hibernate.dialect", () -> "ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect");
|
||||
}
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
private IGenericClient ourClient;
|
||||
private FhirContext ourCtx;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
ourCtx = FhirContext.forR4();
|
||||
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
|
||||
ourCtx.getRestfulClientFactory().setSocketTimeout((int) Duration.ofMinutes(20).toMillis());
|
||||
String ourServerBase = "http://localhost:" + port + "/fhir/";
|
||||
ourClient = ourCtx.newRestfulGenericClient(ourServerBase);
|
||||
ourClient.registerInterceptor(new LoggingInterceptor(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateAndSearchPatientByFamilyName() {
|
||||
String givenName = "Jane";
|
||||
String familyName = "Smith Doe";
|
||||
|
||||
Patient patient = new Patient();
|
||||
patient.addName().setFamily(familyName).addGiven(givenName);
|
||||
IIdType id = ourClient.create().resource(patient).execute().getId().toUnqualifiedVersionless();
|
||||
assertNotNull(id);
|
||||
|
||||
await().atMost(Duration.ofSeconds(30)).until(() -> searchByFamily(familyName).getTotal() == 1);
|
||||
|
||||
Bundle results = searchByFamily(familyName);
|
||||
assertEquals(1, results.getTotal());
|
||||
Patient found = (Patient) results.getEntry().get(0).getResource();
|
||||
assertEquals(familyName, found.getNameFirstRep().getFamily());
|
||||
assertEquals(id, found.getIdElement().toUnqualifiedVersionless());
|
||||
}
|
||||
|
||||
private Bundle searchByFamily(String family) {
|
||||
return ourClient
|
||||
.search()
|
||||
.forResource(Patient.class)
|
||||
.where(Patient.FAMILY.matches().value(family))
|
||||
.returnBundle(Bundle.class)
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user