Merge branch 'master' into rel_8_7-tracking
This commit is contained in:
@@ -129,6 +129,7 @@ public class AppProperties {
|
||||
private Boolean mark_resources_for_reindexing_upon_search_parameter_change = true;
|
||||
private Integer reindex_thread_count = null;
|
||||
private Integer expunge_thread_count = null;
|
||||
private Elasticsearch elasticsearch = null;
|
||||
|
||||
public List<String> getCustomInterceptorClasses() {
|
||||
return custom_interceptor_classes;
|
||||
@@ -847,6 +848,14 @@ public class AppProperties {
|
||||
this.store_meta_source_information = store_meta_source_information;
|
||||
}
|
||||
|
||||
public Elasticsearch getElasticsearch() {
|
||||
return elasticsearch;
|
||||
}
|
||||
|
||||
public void setElasticsearch(Elasticsearch elasticsearch) {
|
||||
this.elasticsearch = elasticsearch;
|
||||
}
|
||||
|
||||
public static class Cors {
|
||||
private Boolean allow_Credentials = true;
|
||||
private List<String> allowed_origin = List.of("*");
|
||||
@@ -1196,4 +1205,17 @@ public class AppProperties {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Elasticsearch {
|
||||
|
||||
private String index_prefix = "";
|
||||
|
||||
public String getIndex_prefix() {
|
||||
return index_prefix;
|
||||
}
|
||||
|
||||
public void setIndex_prefix(String index_prefix) {
|
||||
this.index_prefix = index_prefix;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,6 +296,12 @@ public class FhirServerConfigCommon {
|
||||
appProperties.getExpunge_thread_count());
|
||||
}
|
||||
|
||||
// Determine index prefix from configuration
|
||||
if (appProperties.getElasticsearch() != null) {
|
||||
String indexPrefix = appProperties.getElasticsearch().getIndex_prefix();
|
||||
jpaStorageSettings.setHSearchIndexPrefix(indexPrefix != null ? indexPrefix : "");
|
||||
}
|
||||
|
||||
return jpaStorageSettings;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.dao.TolerantJsonParser;
|
||||
import ca.uhn.fhir.jpa.search.lastn.ElasticsearchSvcImpl;
|
||||
import ca.uhn.fhir.jpa.search.lastn.IElasticsearchSvc;
|
||||
import ca.uhn.fhir.jpa.search.lastn.json.ObservationJson;
|
||||
import ca.uhn.fhir.jpa.starter.AppProperties;
|
||||
import ca.uhn.fhir.parser.IParser;
|
||||
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
@@ -33,8 +34,8 @@ import java.util.stream.Collectors;
|
||||
public class ElasticsearchBootSvcImpl implements IElasticsearchSvc {
|
||||
|
||||
// Index Constants
|
||||
public static final String OBSERVATION_INDEX = "observation_index";
|
||||
public static final String OBSERVATION_CODE_INDEX = "code_index";
|
||||
public static final String OBSERVATION_INDEX_BASE_NAME = "observation_index";
|
||||
public static final String OBSERVATION_CODE_INDEX_BASE_NAME = "code_index";
|
||||
public static final String OBSERVATION_INDEX_SCHEMA_FILE = "ObservationIndexSchema.json";
|
||||
public static final String OBSERVATION_CODE_INDEX_SCHEMA_FILE = "ObservationCodeIndexSchema.json";
|
||||
|
||||
@@ -53,11 +54,26 @@ public class ElasticsearchBootSvcImpl implements IElasticsearchSvc {
|
||||
|
||||
private final FhirContext myContext;
|
||||
|
||||
public ElasticsearchBootSvcImpl(ElasticsearchClient client, FhirContext fhirContext) {
|
||||
// Prefixed index names
|
||||
private String observationIndexName = OBSERVATION_INDEX_BASE_NAME;
|
||||
private String observationCodeIndexName = OBSERVATION_CODE_INDEX_BASE_NAME;
|
||||
|
||||
public ElasticsearchBootSvcImpl(ElasticsearchClient client, FhirContext fhirContext, AppProperties appProperties) {
|
||||
|
||||
myContext = fhirContext;
|
||||
myRestHighLevelClient = client;
|
||||
|
||||
// Determine index prefix from configuration
|
||||
if (appProperties.getElasticsearch() != null) {
|
||||
String indexPrefix = appProperties.getElasticsearch().getIndex_prefix();
|
||||
if (indexPrefix != null
|
||||
&& !sanitizeElasticsearchIndexName(indexPrefix).isEmpty()) {
|
||||
// Set prefixed index names
|
||||
this.observationIndexName = indexPrefix + "-" + OBSERVATION_INDEX_BASE_NAME;
|
||||
this.observationCodeIndexName = indexPrefix + "-" + OBSERVATION_CODE_INDEX_BASE_NAME;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
createObservationIndexIfMissing();
|
||||
createObservationCodeIndexIfMissing();
|
||||
@@ -66,6 +82,34 @@ public class ElasticsearchBootSvcImpl implements IElasticsearchSvc {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a string to be a valid Elasticsearch index name.
|
||||
* <p>
|
||||
* Elasticsearch index name requirements:
|
||||
* - Must be lowercase
|
||||
* - Can only contain: lowercase letters, numbers, hyphens (-), and underscores (_)
|
||||
* - Cannot start with: -, _, or +
|
||||
* - Cannot exceed 255 characters
|
||||
* <p>
|
||||
* This method performs the following transformations:
|
||||
* 1. Converts to lowercase
|
||||
* 2. Replaces any invalid characters with underscores
|
||||
* 3. Removes leading -, _, or + characters
|
||||
* 4. Truncates to 255 characters if necessary
|
||||
* 5. Trims any remaining whitespace
|
||||
*
|
||||
* @param name the string to sanitize
|
||||
* @return a valid Elasticsearch index name
|
||||
*/
|
||||
private String sanitizeElasticsearchIndexName(String name) {
|
||||
String cleaned = name.toLowerCase().replaceAll("[^a-z0-9\\-_]", "_");
|
||||
cleaned = cleaned.replaceAll("^[\\-_.]+", "");
|
||||
if (cleaned.length() > 255) {
|
||||
cleaned = cleaned.substring(0, 255);
|
||||
}
|
||||
return cleaned.trim();
|
||||
}
|
||||
|
||||
private String getIndexSchema(String theSchemaFileName) throws IOException {
|
||||
InputStreamReader input =
|
||||
new InputStreamReader(ElasticsearchSvcImpl.class.getResourceAsStream(theSchemaFileName));
|
||||
@@ -80,21 +124,21 @@ public class ElasticsearchBootSvcImpl implements IElasticsearchSvc {
|
||||
}
|
||||
|
||||
private void createObservationIndexIfMissing() throws IOException {
|
||||
if (indexExists(OBSERVATION_INDEX)) {
|
||||
if (indexExists(observationIndexName)) {
|
||||
return;
|
||||
}
|
||||
String observationMapping = getIndexSchema(OBSERVATION_INDEX_SCHEMA_FILE);
|
||||
if (!createIndex(OBSERVATION_INDEX, observationMapping)) {
|
||||
if (!createIndex(observationIndexName, observationMapping)) {
|
||||
throw new RuntimeException(Msg.code(1176) + "Failed to create observation index");
|
||||
}
|
||||
}
|
||||
|
||||
private void createObservationCodeIndexIfMissing() throws IOException {
|
||||
if (indexExists(OBSERVATION_CODE_INDEX)) {
|
||||
if (indexExists(observationCodeIndexName)) {
|
||||
return;
|
||||
}
|
||||
String observationCodeMapping = getIndexSchema(OBSERVATION_CODE_INDEX_SCHEMA_FILE);
|
||||
if (!createIndex(OBSERVATION_CODE_INDEX, observationCodeMapping)) {
|
||||
if (!createIndex(observationCodeIndexName, observationCodeMapping)) {
|
||||
throw new RuntimeException(Msg.code(1177) + "Failed to create observation code index");
|
||||
}
|
||||
}
|
||||
@@ -147,7 +191,7 @@ public class ElasticsearchBootSvcImpl implements IElasticsearchSvc {
|
||||
.map(v -> FieldValue.of(v))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return SearchRequest.of(sr -> sr.index(OBSERVATION_INDEX)
|
||||
return SearchRequest.of(sr -> sr.index(observationIndexName)
|
||||
.query(qb -> qb.bool(bb -> bb.must(bbm -> {
|
||||
bbm.terms(terms ->
|
||||
terms.field(OBSERVATION_IDENTIFIER_FIELD_NAME).terms(termsb -> termsb.value(values)));
|
||||
@@ -160,4 +204,20 @@ public class ElasticsearchBootSvcImpl implements IElasticsearchSvc {
|
||||
public void refreshIndex(String theIndexName) throws IOException {
|
||||
myRestHighLevelClient.indices().refresh(fn -> fn.index(theIndexName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the observation index name (with prefix if configured)
|
||||
* @return the observation index name
|
||||
*/
|
||||
public String getObservationIndexName() {
|
||||
return observationIndexName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the observation code index name (with prefix if configured)
|
||||
* @return the observation code index name
|
||||
*/
|
||||
public String getObservationCodeIndexName() {
|
||||
return observationCodeIndexName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package ca.uhn.fhir.jpa.starter.elastic;
|
||||
|
||||
import co.elastic.clients.elasticsearch.ElasticsearchClient;
|
||||
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
|
||||
import co.elastic.clients.transport.rest_client.RestClientTransport;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.auth.AuthScope;
|
||||
import org.apache.http.auth.UsernamePasswordCredentials;
|
||||
import org.apache.http.impl.client.BasicCredentialsProvider;
|
||||
import org.elasticsearch.client.RestClient;
|
||||
import org.elasticsearch.client.RestClientBuilder;
|
||||
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Custom Elasticsearch configuration that creates the ElasticsearchClient bean
|
||||
* without the sniffer. This is used when the default Spring Boot autoconfiguration
|
||||
* is excluded.
|
||||
*/
|
||||
@Configuration
|
||||
@Conditional(ElasticConfigCondition.class)
|
||||
public class ElasticsearchConfig {
|
||||
|
||||
@Bean
|
||||
public RestClient elasticsearchRestClient(ElasticsearchProperties properties) {
|
||||
List<String> uris = properties.getUris();
|
||||
|
||||
HttpHost[] hosts = uris.stream()
|
||||
.map(URI::create)
|
||||
.map(uri -> new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()))
|
||||
.toArray(HttpHost[]::new);
|
||||
|
||||
RestClientBuilder builder = RestClient.builder(hosts);
|
||||
|
||||
// Configure authentication if credentials are provided
|
||||
if (properties.getUsername() != null && properties.getPassword() != null) {
|
||||
BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
|
||||
credentialsProvider.setCredentials(
|
||||
AuthScope.ANY, new UsernamePasswordCredentials(properties.getUsername(), properties.getPassword()));
|
||||
|
||||
builder.setHttpClientConfigCallback(
|
||||
httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider));
|
||||
}
|
||||
|
||||
// Configure connection and socket timeouts if needed
|
||||
builder.setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder
|
||||
.setConnectTimeout(
|
||||
properties.getConnectionTimeout() != null
|
||||
? (int) properties.getConnectionTimeout().toMillis()
|
||||
: 5000)
|
||||
.setSocketTimeout(
|
||||
properties.getSocketTimeout() != null
|
||||
? (int) properties.getSocketTimeout().toMillis()
|
||||
: 60000));
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ElasticsearchClient elasticsearchClient(RestClient restClient) {
|
||||
RestClientTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
|
||||
return new ElasticsearchClient(transport);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user