haste_fhir_search/pg_search/
mod.rs1use std::sync::{Arc, LazyLock};
2
3use haste_fhir_client::request::SearchRequest;
4use haste_fhir_model::r4::generated::{resources::Resource, terminology::IssueType};
5use haste_fhir_operation_error::{OperationOutcomeError, derive::OperationOutcomeError};
6use haste_fhirpath::FPEngine;
7use haste_jwt::{ProjectId, TenantId};
8use haste_repository::types::{FHIRMethod, SupportedFHIRVersions};
9use sqlx::{Pool, Postgres, postgres::PgPoolOptions};
10
11use crate::{
12 IndexOutcome, IndexResource, ParameterLevel, ResolvedParameter, SearchEngine, SearchOptions,
13 SearchParameterResolve, SearchReturn,
14 elastic_search::is_mapped_search_parameter_type,
15 indexing_conversion::{self, InsertableIndex},
16 memory::R4_SEARCH_PARAMETERS_INDEX,
17 pg_search::schema::{SchemaRegistry, generate_schemas},
18};
19
20mod indexing;
21pub mod migration;
22pub mod schema;
23mod search;
24pub mod search_parameter_resolver;
25
26#[derive(OperationOutcomeError, Debug)]
27pub enum PgSearchError {
28 #[fatal(
29 code = "exception",
30 diagnostic = "Failed to evaluate fhirpath expression."
31 )]
32 FHIRPathError(#[from] haste_fhirpath::FHIRPathError),
33 #[fatal(
34 code = "exception",
35 diagnostic = "PG search does not support the fhir method: '{arg0:?}'"
36 )]
37 UnsupportedFHIRMethod(FHIRMethod),
38 #[fatal(code = "exception", diagnostic = "PG search database error: '{arg0}'")]
39 SqlxError(String),
40}
41
42impl From<sqlx::Error> for PgSearchError {
43 fn from(e: sqlx::Error) -> Self {
44 PgSearchError::SqlxError(e.to_string())
45 }
46}
47
48#[derive(Clone)]
49pub struct PgSearchEngine<SearchParameterResolver: SearchParameterResolve + 'static> {
50 parameter_resolver: Arc<SearchParameterResolver>,
51 fp_engine: Arc<FPEngine>,
52 pool: Pool<Postgres>,
53 schema_registry: Arc<SchemaRegistry>,
57}
58
59static R4_SCHEMA_REGISTRY: LazyLock<Arc<SchemaRegistry>> = LazyLock::new(|| {
63 Arc::new(generate_schemas(
64 &R4_SEARCH_PARAMETERS_INDEX.all_parameters(),
65 ))
66});
67
68pub async fn create_pg_search_pool(
75 database_url: &str,
76 max_connections: u32,
77) -> Result<Pool<Postgres>, OperationOutcomeError> {
78 PgPoolOptions::new()
79 .max_connections(max_connections)
80 .connect(database_url)
81 .await
82 .map_err(|e| {
83 OperationOutcomeError::fatal(
84 IssueType::exception(),
85 format!("Failed to create PG search database pool: {e}"),
86 )
87 })
88}
89
90impl<SearchParameterResolver: SearchParameterResolve + 'static>
91 PgSearchEngine<SearchParameterResolver>
92{
93 pub fn new(
94 parameter_resolver: Arc<SearchParameterResolver>,
95 fp_engine: Arc<FPEngine>,
96 pool: Pool<Postgres>,
97 ) -> Self {
98 PgSearchEngine {
99 parameter_resolver,
100 fp_engine,
101 pool,
102 schema_registry: R4_SCHEMA_REGISTRY.clone(),
103 }
104 }
105}
106
107pub(crate) struct ResourceSearchIndex {
110 pub system_entries: Vec<(String, InsertableIndex)>,
114 pub dynamic_entries: Vec<(String, InsertableIndex)>,
117}
118
119pub(crate) async fn resource_to_search_index(
127 fp_engine: Arc<FPEngine>,
128 parameters: &[ResolvedParameter],
129 resource: &Resource,
130) -> Result<ResourceSearchIndex, OperationOutcomeError> {
131 let mut system_entries = Vec::new();
132 let mut dynamic_entries = Vec::new();
133
134 for param in parameters {
135 if let Some(expression) = param
136 .search_parameter
137 .expression
138 .as_ref()
139 .and_then(|e| e.value.as_ref())
140 && let Some(url) = param.search_parameter.url.value.as_ref()
141 {
142 if !is_mapped_search_parameter_type(¶m.search_parameter.type_) {
146 continue;
147 }
148
149 let result = fp_engine
150 .evaluate(expression, vec![resource])
151 .await
152 .map_err(PgSearchError::from);
153
154 if let Err(err) = result {
155 tracing::error!(
156 "Failed to evaluate FHIRPath expression: '{}' for resource.",
157 expression,
158 );
159 return Err(err.into());
160 }
161
162 let insertable = indexing_conversion::to_insertable_index(
163 param,
164 &result?.iter().collect::<Vec<_>>(),
165 )?;
166
167 match ¶m.level {
168 ParameterLevel::System => {
171 if let Some(code) = param.search_parameter.code.value.as_ref() {
172 system_entries.push((code.clone(), insertable));
173 }
174 }
175 ParameterLevel::Project => {
176 dynamic_entries.push((url.clone(), insertable));
177 }
178 }
179 }
180 }
181
182 Ok(ResourceSearchIndex {
183 system_entries,
184 dynamic_entries,
185 })
186}
187
188impl<SearchParameterResolver: SearchParameterResolve> SearchEngine
189 for PgSearchEngine<SearchParameterResolver>
190{
191 async fn search(
192 &self,
193 _fhir_version: &SupportedFHIRVersions,
194 tenant: &TenantId,
195 project: &ProjectId,
196 search_request: &SearchRequest,
197 options: Option<SearchOptions>,
198 ) -> Result<SearchReturn, OperationOutcomeError> {
199 search::execute_search(
200 &self.pool,
201 self.parameter_resolver.clone(),
202 &self.schema_registry,
203 tenant,
204 project,
205 search_request,
206 options.as_ref(),
207 )
208 .await
209 }
210
211 async fn index(
212 &self,
213 _fhir_version: SupportedFHIRVersions,
214 resources: Vec<IndexResource>,
215 ) -> Result<IndexOutcome, OperationOutcomeError> {
216 indexing::index_resources(
217 &self.pool,
218 &self.parameter_resolver,
219 &self.schema_registry,
220 self.fp_engine.clone(),
221 resources,
222 )
223 .await
224 }
225
226 async fn migrate(
227 &self,
228 _fhir_version: &SupportedFHIRVersions,
229 ) -> Result<(), OperationOutcomeError> {
230 migration::run_migration(&self.pool, &self.schema_registry).await
231 }
232}