1use std::sync::Arc;
10
11use haste_fhir_model::r4::generated::terminology::IssueType;
12use haste_fhir_operation_error::OperationOutcomeError;
13use haste_fhirpath::FPEngine;
14use haste_repository::pg::PGConnection;
15use haste_repository::types::SearchIndexBackend;
16use serde::{Deserialize, Serialize};
17
18use crate::{
19 IndexOutcome, IndexResource, SearchEngine, SearchOptions, SearchReturn,
20 elastic_search::{
21 ElasticSearchEngine, create_es_client,
22 search_parameter_resolver::ElasticSearchParameterResolver,
23 },
24 pg_search::{
25 PgSearchEngine, create_pg_search_pool, search_parameter_resolver::PgSearchParameterResolver,
26 },
27};
28
29#[derive(Debug, Clone, Deserialize, Serialize)]
31#[serde(tag = "backend", rename_all = "snake_case")]
32pub enum SearchConfig {
33 Elasticsearch(ElasticsearchConfig),
34 Postgres(PostgresSearchConfig),
35}
36
37#[derive(Debug, Clone, Deserialize, Serialize)]
38pub struct ElasticsearchConfig {
39 pub url: String,
40 pub username: String,
41 pub password: String,
42 #[serde(default)]
47 pub prune_removed_search_parameters: bool,
48}
49
50#[derive(Debug, Clone, Deserialize, Serialize)]
51pub struct PostgresSearchConfig {
52 pub database_url: String,
53 pub max_connections: u32,
54}
55
56impl Default for ElasticsearchConfig {
57 fn default() -> Self {
58 Self {
59 url: "http://localhost:9200".to_string(),
60 username: "elastic".to_string(),
61 password: "elastic".to_string(),
62 prune_removed_search_parameters: false,
63 }
64 }
65}
66
67impl Default for PostgresSearchConfig {
68 fn default() -> Self {
69 Self {
70 database_url: "postgresql://postgres:postgres@127.0.0.1/haste_health_search"
71 .to_string(),
72 max_connections: 20,
73 }
74 }
75}
76
77impl Default for SearchConfig {
78 fn default() -> Self {
79 SearchConfig::Postgres(PostgresSearchConfig::default())
80 }
81}
82
83#[derive(Clone)]
90pub enum SearchEngineBackend {
91 Elasticsearch(ElasticSearchEngine<ElasticSearchParameterResolver<PGConnection>>),
92 Postgres(PgSearchEngine<PgSearchParameterResolver<PGConnection>>),
93}
94
95impl SearchEngineBackend {
96 pub async fn is_connected(&self) -> Result<(), OperationOutcomeError> {
103 match self {
104 SearchEngineBackend::Elasticsearch(engine) => {
105 engine.is_connected().await.map_err(|e| {
106 OperationOutcomeError::fatal(
107 IssueType::exception(),
108 format!("Elasticsearch is not reachable: {e:?}"),
109 )
110 })
111 }
112 SearchEngineBackend::Postgres(engine) => engine.is_connected().await,
113 }
114 }
115
116 #[must_use]
119 pub const fn name(&self) -> &'static str {
120 match self {
121 SearchEngineBackend::Elasticsearch(_) => "Elasticsearch",
122 SearchEngineBackend::Postgres(_) => "PostgreSQL search",
123 }
124 }
125
126 #[must_use]
129 pub const fn lock_kind(&self) -> SearchIndexBackend {
130 match self {
131 SearchEngineBackend::Elasticsearch(_) => SearchIndexBackend::Elasticsearch,
132 SearchEngineBackend::Postgres(_) => SearchIndexBackend::Postgres,
133 }
134 }
135}
136
137impl SearchEngine for SearchEngineBackend {
138 async fn search(
139 &self,
140 fhir_version: &haste_repository::types::SupportedFHIRVersions,
141 tenant: &haste_jwt::TenantId,
142 project: &haste_jwt::ProjectId,
143 search_request: &haste_fhir_client::request::SearchRequest,
144 options: Option<SearchOptions>,
145 ) -> Result<SearchReturn, OperationOutcomeError> {
146 match self {
147 SearchEngineBackend::Elasticsearch(e) => {
148 e.search(fhir_version, tenant, project, search_request, options)
149 .await
150 }
151 SearchEngineBackend::Postgres(e) => {
152 e.search(fhir_version, tenant, project, search_request, options)
153 .await
154 }
155 }
156 }
157
158 async fn index(
159 &self,
160 fhir_version: haste_repository::types::SupportedFHIRVersions,
161 resource: Vec<IndexResource>,
162 ) -> Result<IndexOutcome, OperationOutcomeError> {
163 match self {
164 SearchEngineBackend::Elasticsearch(e) => e.index(fhir_version, resource).await,
165 SearchEngineBackend::Postgres(e) => e.index(fhir_version, resource).await,
166 }
167 }
168
169 async fn migrate(
170 &self,
171 fhir_version: &haste_repository::types::SupportedFHIRVersions,
172 ) -> Result<(), OperationOutcomeError> {
173 match self {
174 SearchEngineBackend::Elasticsearch(e) => e.migrate(fhir_version).await,
175 SearchEngineBackend::Postgres(e) => e.migrate(fhir_version).await,
176 }
177 }
178}
179
180pub async fn create_search_engine(
190 config: &SearchConfig,
191 repo: Arc<PGConnection>,
192 migrations: bool,
193) -> Result<Arc<SearchEngineBackend>, OperationOutcomeError> {
194 match config {
195 SearchConfig::Elasticsearch(es_config) => {
196 let client = create_es_client(
197 &es_config.url,
198 es_config.username.clone(),
199 es_config.password.clone(),
200 )?;
201
202 let engine = ElasticSearchEngine::new(
203 Arc::new(ElasticSearchParameterResolver::new(client.clone(), repo)),
204 Arc::new(FPEngine::new()),
205 client,
206 migrations && es_config.prune_removed_search_parameters,
207 );
208
209 Ok(Arc::new(SearchEngineBackend::Elasticsearch(engine)))
210 }
211 SearchConfig::Postgres(pg_config) => {
212 let pool =
213 create_pg_search_pool(&pg_config.database_url, pg_config.max_connections).await?;
214
215 let engine = PgSearchEngine::new(
216 Arc::new(PgSearchParameterResolver::new(pool.clone(), repo)),
217 Arc::new(FPEngine::new()),
218 pool,
219 );
220
221 Ok(Arc::new(SearchEngineBackend::Postgres(engine)))
222 }
223 }
224}