Skip to main content

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}