helios_persistence/core/search.rs
1//! Search provider traits.
2//!
3//! This module defines a hierarchy of search provider traits:
4//! - [`SearchProvider`] - Basic single-type search
5//! - [`MultiTypeSearchProvider`] - Search across multiple resource types
6//! - [`IncludeProvider`] - Support for _include
7//! - [`RevincludeProvider`] - Support for _revinclude
8//! - [`ChainedSearchProvider`] - Chained parameters and _has
9//! - [`TerminologySearchProvider`] - :above, :below, :in, :not-in
10//! - [`TextSearchProvider`] - Full-text search (_text, _content, :text)
11
12use async_trait::async_trait;
13
14use crate::error::StorageResult;
15use crate::tenant::TenantContext;
16use crate::types::{
17 IncludeDirective, Page, ReverseChainedParameter, SearchBundle, SearchQuery, StoredResource,
18};
19
20use super::storage::ResourceStorage;
21
22/// Result of a search operation.
23#[derive(Debug, Clone)]
24pub struct SearchResult {
25 /// The matching resources.
26 pub resources: Page<StoredResource>,
27
28 /// Included resources (from _include/_revinclude).
29 pub included: Vec<StoredResource>,
30
31 /// Total count of matches (if requested via _total).
32 pub total: Option<u64>,
33}
34
35impl SearchResult {
36 /// Creates a new search result.
37 pub fn new(resources: Page<StoredResource>) -> Self {
38 Self {
39 resources,
40 included: Vec::new(),
41 total: None,
42 }
43 }
44
45 /// Adds included resources.
46 pub fn with_included(mut self, included: Vec<StoredResource>) -> Self {
47 self.included = included;
48 self
49 }
50
51 /// Sets the total count.
52 pub fn with_total(mut self, total: u64) -> Self {
53 self.total = Some(total);
54 self
55 }
56
57 /// Returns the number of matching resources in this page.
58 pub fn len(&self) -> usize {
59 self.resources.len()
60 }
61
62 /// Returns true if there are no matching resources.
63 pub fn is_empty(&self) -> bool {
64 self.resources.is_empty()
65 }
66
67 /// Returns the cursor for the next page, if there is one.
68 pub fn next_cursor(&self) -> Option<&String> {
69 self.resources.page_info.next_cursor.as_ref()
70 }
71
72 /// Returns the cursor for the previous page, if there is one.
73 pub fn previous_cursor(&self) -> Option<&String> {
74 self.resources.page_info.previous_cursor.as_ref()
75 }
76
77 /// Returns whether there are more results after this page.
78 pub fn has_next(&self) -> bool {
79 self.resources.page_info.has_next
80 }
81
82 /// Returns whether there are results before this page.
83 pub fn has_previous(&self) -> bool {
84 self.resources.page_info.has_previous
85 }
86
87 /// Converts the result to a FHIR SearchBundle.
88 pub fn to_bundle(&self, base_url: &str, self_link: &str) -> SearchBundle {
89 use crate::types::{BundleEntry, SearchBundle};
90
91 let mut bundle = SearchBundle::new().with_self_link(self_link);
92
93 if let Some(total) = self.total {
94 bundle = bundle.with_total(total);
95 }
96
97 // Add next link if there's more data
98 if let Some(ref cursor) = self.resources.page_info.next_cursor {
99 bundle = bundle.with_next_link(format!("{}?_cursor={}", self_link, cursor));
100 }
101
102 // Add matching resources
103 for resource in &self.resources.items {
104 let full_url = format!("{}/{}", base_url, resource.url());
105 bundle = bundle.with_entry(BundleEntry::match_entry(
106 full_url,
107 resource.content().clone(),
108 ));
109 }
110
111 // Add included resources
112 for resource in &self.included {
113 let full_url = format!("{}/{}", base_url, resource.url());
114 bundle = bundle.with_entry(BundleEntry::include_entry(
115 full_url,
116 resource.content().clone(),
117 ));
118 }
119
120 bundle
121 }
122}
123
124/// Basic search provider for single resource type queries.
125///
126/// This trait provides search functionality for a single resource type,
127/// corresponding to the FHIR search interaction:
128/// `GET [base]/[type]?[parameters]`
129///
130/// # Example
131///
132/// ```ignore
133/// use helios_persistence::core::SearchProvider;
134/// use helios_persistence::types::{SearchQuery, SearchParameter, SearchParamType, SearchValue};
135///
136/// async fn search_patients<S: SearchProvider>(
137/// storage: &S,
138/// tenant: &TenantContext,
139/// ) -> Result<(), StorageError> {
140/// let query = SearchQuery::new("Patient")
141/// .with_parameter(SearchParameter {
142/// name: "name".to_string(),
143/// param_type: SearchParamType::String,
144/// modifier: None,
145/// values: vec![SearchValue::eq("Smith")],
146/// chain: vec![],
147/// components: vec![],
148/// })
149/// .with_count(20);
150///
151/// let result = storage.search(tenant, &query).await?;
152///
153/// for resource in result.resources.items {
154/// println!("Found: {}", resource.url());
155/// }
156///
157/// Ok(())
158/// }
159/// ```
160#[async_trait]
161pub trait SearchProvider: ResourceStorage {
162 /// Searches for resources matching the query.
163 ///
164 /// # Arguments
165 ///
166 /// * `tenant` - The tenant context for this operation
167 /// * `query` - The search query with parameters
168 ///
169 /// # Returns
170 ///
171 /// A search result with matching resources and pagination info.
172 ///
173 /// # Errors
174 ///
175 /// * `StorageError::Validation` - If the query contains invalid parameters
176 /// * `StorageError::Search` - If a search feature is not supported
177 /// * `StorageError::Tenant` - If the tenant doesn't have search permission
178 async fn search(
179 &self,
180 tenant: &TenantContext,
181 query: &SearchQuery,
182 ) -> StorageResult<SearchResult>;
183
184 /// Counts resources matching the query without returning them.
185 ///
186 /// This is more efficient than search when you only need the count.
187 async fn search_count(&self, tenant: &TenantContext, query: &SearchQuery)
188 -> StorageResult<u64>;
189}
190
191/// Search provider that supports searching across multiple resource types.
192///
193/// This extends [`SearchProvider`] to support system-level search:
194/// `GET [base]?[parameters]`
195#[async_trait]
196pub trait MultiTypeSearchProvider: SearchProvider {
197 /// Searches across multiple resource types.
198 ///
199 /// # Arguments
200 ///
201 /// * `tenant` - The tenant context for this operation
202 /// * `resource_types` - The resource types to search (empty = all types)
203 /// * `query` - The search query
204 ///
205 /// # Returns
206 ///
207 /// A search result with matching resources from all specified types.
208 async fn search_multi(
209 &self,
210 tenant: &TenantContext,
211 resource_types: &[&str],
212 query: &SearchQuery,
213 ) -> StorageResult<SearchResult>;
214}
215
216/// Search provider that supports _include.
217///
218/// _include adds referenced resources to the search results.
219#[async_trait]
220pub trait IncludeProvider: SearchProvider {
221 /// Resolves _include directives for search results.
222 ///
223 /// # Arguments
224 ///
225 /// * `tenant` - The tenant context for this operation
226 /// * `resources` - The primary search results
227 /// * `includes` - The include directives to resolve
228 ///
229 /// # Returns
230 ///
231 /// Resources referenced by the primary results according to the include directives.
232 async fn resolve_includes(
233 &self,
234 tenant: &TenantContext,
235 resources: &[StoredResource],
236 includes: &[IncludeDirective],
237 ) -> StorageResult<Vec<StoredResource>>;
238}
239
240/// Search provider that supports _revinclude.
241///
242/// _revinclude adds resources that reference the search results.
243#[async_trait]
244pub trait RevincludeProvider: SearchProvider {
245 /// Resolves _revinclude directives for search results.
246 ///
247 /// # Arguments
248 ///
249 /// * `tenant` - The tenant context for this operation
250 /// * `resources` - The primary search results
251 /// * `revincludes` - The revinclude directives to resolve
252 ///
253 /// # Returns
254 ///
255 /// Resources that reference the primary results according to the revinclude directives.
256 async fn resolve_revincludes(
257 &self,
258 tenant: &TenantContext,
259 resources: &[StoredResource],
260 revincludes: &[IncludeDirective],
261 ) -> StorageResult<Vec<StoredResource>>;
262}
263
264/// Search provider that supports chained parameters and _has.
265///
266/// Chained parameters search on referenced resources:
267/// `Observation?patient.name=Smith`
268///
269/// _has searches for resources referenced by other resources:
270/// `Patient?_has:Observation:patient:code=1234-5`
271#[async_trait]
272pub trait ChainedSearchProvider: SearchProvider {
273 /// Evaluates a chained search and returns matching resource IDs.
274 ///
275 /// This is used internally to resolve chains before the main search.
276 ///
277 /// # Arguments
278 ///
279 /// * `tenant` - The tenant context for this operation
280 /// * `base_type` - The base resource type being searched
281 /// * `chain` - The chain path (e.g., "patient.organization.name")
282 /// * `value` - The value to match
283 ///
284 /// # Returns
285 ///
286 /// IDs of base resources that match the chain condition.
287 async fn resolve_chain(
288 &self,
289 tenant: &TenantContext,
290 base_type: &str,
291 chain: &str,
292 value: &str,
293 ) -> StorageResult<Vec<String>>;
294
295 /// Evaluates a reverse chain (_has) and returns matching resource IDs.
296 ///
297 /// # Arguments
298 ///
299 /// * `tenant` - The tenant context for this operation
300 /// * `base_type` - The base resource type being searched
301 /// * `reverse_chain` - The reverse chain parameters
302 ///
303 /// # Returns
304 ///
305 /// IDs of base resources that are referenced by matching resources.
306 async fn resolve_reverse_chain(
307 &self,
308 tenant: &TenantContext,
309 base_type: &str,
310 reverse_chain: &ReverseChainedParameter,
311 ) -> StorageResult<Vec<String>>;
312}
313
314/// Search provider that supports terminology-aware modifiers.
315///
316/// These modifiers require integration with a terminology service:
317/// - `:above` - Match codes above in the hierarchy
318/// - `:below` - Match codes below in the hierarchy
319/// - `:in` - Match codes in a value set
320/// - `:not-in` - Match codes not in a value set
321#[async_trait]
322pub trait TerminologySearchProvider: SearchProvider {
323 /// Expands a value set and returns member codes.
324 ///
325 /// # Arguments
326 ///
327 /// * `value_set_url` - The canonical URL of the value set
328 ///
329 /// # Returns
330 ///
331 /// A list of (system, code) pairs in the value set.
332 async fn expand_value_set(&self, value_set_url: &str) -> StorageResult<Vec<(String, String)>>;
333
334 /// Gets codes above the given code in the hierarchy.
335 ///
336 /// # Arguments
337 ///
338 /// * `system` - The code system URL
339 /// * `code` - The code to find ancestors for
340 ///
341 /// # Returns
342 ///
343 /// Codes that are ancestors of the given code (including the code itself).
344 async fn codes_above(&self, system: &str, code: &str) -> StorageResult<Vec<String>>;
345
346 /// Gets codes below the given code in the hierarchy.
347 ///
348 /// # Arguments
349 ///
350 /// * `system` - The code system URL
351 /// * `code` - The code to find descendants for
352 ///
353 /// # Returns
354 ///
355 /// Codes that are descendants of the given code (including the code itself).
356 async fn codes_below(&self, system: &str, code: &str) -> StorageResult<Vec<String>>;
357}
358
359/// Search provider that supports full-text search.
360///
361/// Full-text search operations:
362/// - `_text` - Search in the narrative
363/// - `_content` - Search in the entire resource content
364/// - `:text` modifier - Full-text search on token parameters
365#[async_trait]
366pub trait TextSearchProvider: SearchProvider {
367 /// Performs a full-text search on resource narratives.
368 ///
369 /// # Arguments
370 ///
371 /// * `tenant` - The tenant context for this operation
372 /// * `resource_type` - The resource type to search
373 /// * `text` - The text to search for
374 /// * `pagination` - Pagination settings
375 ///
376 /// # Returns
377 ///
378 /// Resources with matching narrative text, ordered by relevance.
379 async fn search_text(
380 &self,
381 tenant: &TenantContext,
382 resource_type: &str,
383 text: &str,
384 pagination: &crate::types::Pagination,
385 ) -> StorageResult<SearchResult>;
386
387 /// Performs a full-text search on entire resource content.
388 ///
389 /// # Arguments
390 ///
391 /// * `tenant` - The tenant context for this operation
392 /// * `resource_type` - The resource type to search
393 /// * `content` - The content to search for
394 /// * `pagination` - Pagination settings
395 ///
396 /// # Returns
397 ///
398 /// Resources with matching content, ordered by relevance.
399 async fn search_content(
400 &self,
401 tenant: &TenantContext,
402 resource_type: &str,
403 content: &str,
404 pagination: &crate::types::Pagination,
405 ) -> StorageResult<SearchResult>;
406}
407
408/// Marker trait for search providers that support all advanced features.
409///
410/// This is a convenience trait that combines all search capabilities.
411pub trait FullSearchProvider:
412 SearchProvider
413 + MultiTypeSearchProvider
414 + IncludeProvider
415 + RevincludeProvider
416 + ChainedSearchProvider
417{
418}
419
420// Blanket implementation for types that implement all required traits
421impl<T> FullSearchProvider for T where
422 T: SearchProvider
423 + MultiTypeSearchProvider
424 + IncludeProvider
425 + RevincludeProvider
426 + ChainedSearchProvider
427{
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433 use crate::types::PageInfo;
434 use helios_fhir::FhirVersion;
435
436 #[test]
437 fn test_search_result_creation() {
438 let page = Page::new(Vec::new(), PageInfo::end());
439 let result = SearchResult::new(page);
440 assert!(result.included.is_empty());
441 assert!(result.total.is_none());
442 }
443
444 #[test]
445 fn test_search_result_with_included() {
446 let page = Page::new(Vec::new(), PageInfo::end());
447 let result = SearchResult::new(page)
448 .with_included(vec![StoredResource::new(
449 "Patient",
450 "123",
451 crate::tenant::TenantId::new("t1"),
452 serde_json::json!({}),
453 FhirVersion::default(),
454 )])
455 .with_total(100);
456
457 assert_eq!(result.included.len(), 1);
458 assert_eq!(result.total, Some(100));
459 }
460
461 #[test]
462 fn test_search_result_to_bundle() {
463 let resource = StoredResource::new(
464 "Patient",
465 "123",
466 crate::tenant::TenantId::new("t1"),
467 serde_json::json!({"resourceType": "Patient", "id": "123"}),
468 FhirVersion::default(),
469 );
470
471 let page = Page::new(vec![resource], PageInfo::end());
472 let result = SearchResult::new(page).with_total(1);
473
474 let bundle = result.to_bundle("http://example.com/fhir", "http://example.com/fhir/Patient");
475
476 assert_eq!(bundle.total, Some(1));
477 assert_eq!(bundle.entry.len(), 1);
478 }
479}