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::search_parameter_cardinality;
12use crate::{
13 IndexOutcome, IndexResource, ParameterLevel, ResolvedParameter, SearchEngine, SearchOptions,
14 SearchParameterResolve, SearchReturn,
15 elastic_search::is_mapped_search_parameter_type,
16 indexing_conversion::{self, InsertableIndex},
17 memory::R4_SEARCH_PARAMETERS_INDEX,
18 pg_search::schema::{SchemaRegistry, generate_schemas},
19};
20
21mod indexing;
22pub mod keys;
23pub mod migration;
24pub mod schema;
25mod search;
26pub mod search_parameter_resolver;
27
28#[derive(OperationOutcomeError, Debug)]
29pub enum PgSearchError {
30 #[fatal(
31 code = "exception",
32 diagnostic = "Failed to evaluate fhirpath expression."
33 )]
34 FHIRPathError(#[from] haste_fhirpath::FHIRPathError),
35 #[fatal(
36 code = "exception",
37 diagnostic = "PG search does not support the fhir method: '{arg0:?}'"
38 )]
39 UnsupportedFHIRMethod(FHIRMethod),
40 #[fatal(code = "exception", diagnostic = "PG search database error.")]
41 SqlxError(String),
42}
43
44impl From<sqlx::Error> for PgSearchError {
45 fn from(e: sqlx::Error) -> Self {
46 tracing::error!(error = %e, "PG search database error");
47 PgSearchError::SqlxError(e.to_string())
48 }
49}
50
51#[derive(Clone)]
54pub struct PgSearchEngine<SearchParameterResolver: SearchParameterResolve + 'static> {
55 parameter_resolver: Arc<SearchParameterResolver>,
56 fp_engine: Arc<FPEngine>,
57 pool: Pool<Postgres>,
58 schema_registry: Arc<SchemaRegistry>,
60}
61
62static R4_SCHEMA_REGISTRY: LazyLock<Arc<SchemaRegistry>> = LazyLock::new(|| {
64 Arc::new(generate_schemas(
65 SupportedFHIRVersions::R4,
66 &R4_SEARCH_PARAMETERS_INDEX.all_parameters(),
67 ))
68});
69
70impl<Resolver: SearchParameterResolve + 'static> PgSearchEngine<Resolver> {
71 pub fn new(
72 parameter_resolver: Arc<Resolver>,
73 fp_engine: Arc<FPEngine>,
74 pool: Pool<Postgres>,
75 ) -> Self {
76 PgSearchEngine {
77 parameter_resolver,
78 fp_engine,
79 pool,
80 schema_registry: R4_SCHEMA_REGISTRY.clone(),
81 }
82 }
83
84 pub async fn is_connected(&self) -> Result<(), OperationOutcomeError> {
90 sqlx::query("SELECT 1")
91 .execute(&self.pool)
92 .await
93 .map(|_| ())
94 .map_err(|e| {
95 tracing::error!(error = %e, "PG search database is not reachable");
96 OperationOutcomeError::fatal(
97 IssueType::exception(),
98 "Search database is not reachable.".to_string(),
99 )
100 })
101 }
102}
103
104pub async fn create_pg_search_pool(
110 database_url: &str,
111 max_connections: u32,
112) -> Result<Pool<Postgres>, OperationOutcomeError> {
113 PgPoolOptions::new()
114 .max_connections(max_connections)
115 .connect(database_url)
116 .await
117 .map_err(|e| {
118 OperationOutcomeError::fatal(
119 IssueType::exception(),
120 format!("Failed to create PG search database pool: {e}"),
121 )
122 })
123}
124
125#[derive(Default)]
127pub(crate) struct ResourceSearchIndex {
128 pub system_entries: Vec<(String, InsertableIndex)>,
131 pub dynamic_entries: Vec<(String, InsertableIndex)>,
133}
134
135pub(crate) async fn resource_to_search_index(
138 fp_engine: Arc<FPEngine>,
139 parameters: &[ResolvedParameter],
140 resource: &Resource,
141 resource_type: &str,
142) -> Result<ResourceSearchIndex, OperationOutcomeError> {
143 let mut index = ResourceSearchIndex::default();
144
145 for (param, expression, url) in parameters.iter().filter_map(indexable) {
146 let evaluated = fp_engine
147 .evaluate(expression, vec![resource])
148 .await
149 .map_err(|e| {
150 tracing::error!(
151 "Failed to evaluate FHIRPath expression: '{}' for resource.",
152 expression,
153 );
154 PgSearchError::from(e)
155 })?;
156
157 let insertable =
158 indexing_conversion::to_insertable_index(param, &evaluated.iter().collect::<Vec<_>>())?;
159
160 match param.level {
163 ParameterLevel::System
164 if search_parameter_cardinality::is_single_valued(url, resource_type) =>
165 {
166 if let Some(code) = param.search_parameter.code.value.as_ref() {
167 index.system_entries.push((code.clone(), insertable));
168 }
169 }
170 ParameterLevel::System | ParameterLevel::Project => {
171 index.dynamic_entries.push((url.clone(), insertable));
172 }
173 }
174 }
175
176 Ok(index)
177}
178
179fn indexable(param: &ResolvedParameter) -> Option<(&ResolvedParameter, &String, &String)> {
182 let search_parameter = ¶m.search_parameter;
183 let expression = search_parameter.expression.as_ref()?.value.as_ref()?;
184 let url = search_parameter.url.value.as_ref()?;
185
186 is_mapped_search_parameter_type(&search_parameter.type_).then_some((param, expression, url))
187}
188
189impl<SearchParameterResolver: SearchParameterResolve> SearchEngine
190 for PgSearchEngine<SearchParameterResolver>
191{
192 async fn search(
193 &self,
194 _fhir_version: &SupportedFHIRVersions,
195 tenant: &TenantId,
196 project: &ProjectId,
197 search_request: &SearchRequest,
198 options: Option<SearchOptions>,
199 ) -> Result<SearchReturn, OperationOutcomeError> {
200 search::execute_search(
201 &self.pool,
202 self.parameter_resolver.clone(),
203 &self.schema_registry,
204 tenant,
205 project,
206 search_request,
207 options.as_ref(),
208 )
209 .await
210 }
211
212 async fn index(
213 &self,
214 _fhir_version: SupportedFHIRVersions,
215 resources: Vec<IndexResource>,
216 ) -> Result<IndexOutcome, OperationOutcomeError> {
217 crate::project_parameters::index_search_parameters_first(
219 self.parameter_resolver.as_ref(),
220 resources,
221 |resources, _searchable| {
222 indexing::index_resources(
223 &self.pool,
224 &self.parameter_resolver,
225 &self.schema_registry,
226 self.fp_engine.clone(),
227 resources,
228 )
229 },
230 )
231 .await
232 }
233
234 async fn migrate(
235 &self,
236 _fhir_version: &SupportedFHIRVersions,
237 ) -> Result<(), OperationOutcomeError> {
238 migration::run_migration(&self.pool, &self.schema_registry).await
239 }
240}