Skip to main content

ahri_tre_app/service/
semantic_search.rs

1use super::catalogue_search::retain_matching_predicates;
2use super::*;
3use ahri_tre_protocol::{ProtocolVersion, PublicUuid, refs::*};
4fn app_variable_search_request(
5    request: &ahri_tre_protocol::dictionary::SearchVariablesRequest,
6    protocol_version: &ProtocolVersion,
7) -> Result<SearchVariablesRequest, AppError> {
8    let after = request
9        .decoded_cursor(protocol_version)
10        .map_err(search_validation_error)?
11        .map(|cursor| {
12            Ok::<_, AppError>(ahri_tre_core::VariableSearchCursorAnchor {
13                name: cursor.name,
14                domain_id: ahri_tre_types::DomainId(protocol_int_ref_id(
15                    "domain",
16                    cursor.domain.id,
17                )?),
18                variable_id: ahri_tre_types::VariableId(protocol_int_ref_id(
19                    "variable",
20                    cursor.variable.id,
21                )?),
22            })
23        })
24        .transpose()?;
25    Ok(SearchVariablesRequest {
26        kind: match &request.predicates {
27            ahri_tre_protocol::search::PredicateSet::AllOf(_) => {
28                ahri_tre_core::PredicateSetKind::AllOf
29            }
30            ahri_tre_protocol::search::PredicateSet::AnyOf(_) => {
31                ahri_tre_core::PredicateSetKind::AnyOf
32            }
33        },
34        predicates: request
35            .predicates
36            .predicates()
37            .iter()
38            .cloned()
39            .map(app_variable_search_predicate)
40            .collect::<Result<Vec<_>, _>>()?,
41        limit: request.page.limit,
42        after,
43    })
44}
45
46fn app_variable_search_predicate(
47    predicate: ahri_tre_protocol::dictionary::VariablePredicate,
48) -> Result<ahri_tre_core::VariableSearchPredicate, AppError> {
49    match predicate {
50        ahri_tre_protocol::dictionary::VariablePredicate::Text(text) => {
51            Ok(ahri_tre_core::VariableSearchPredicate::Text {
52                field: match text.field {
53                    ahri_tre_protocol::dictionary::VariableTextField::Name => {
54                        ahri_tre_core::VariableSearchTextField::Name
55                    }
56                    ahri_tre_protocol::dictionary::VariableTextField::Description => {
57                        ahri_tre_core::VariableSearchTextField::Description
58                    }
59                    ahri_tre_protocol::dictionary::VariableTextField::ValueType => {
60                        ahri_tre_core::VariableSearchTextField::ValueType
61                    }
62                    ahri_tre_protocol::dictionary::VariableTextField::VocabularyName => {
63                        ahri_tre_core::VariableSearchTextField::VocabularyName
64                    }
65                    ahri_tre_protocol::dictionary::VariableTextField::OntologyClass => {
66                        ahri_tre_core::VariableSearchTextField::OntologyClass
67                    }
68                },
69                mode: match text.mode {
70                    ahri_tre_protocol::search::TextMode::Exact => {
71                        ahri_tre_core::SearchTextMode::Exact
72                    }
73                    ahri_tre_protocol::search::TextMode::Prefix => {
74                        ahri_tre_core::SearchTextMode::Prefix
75                    }
76                    ahri_tre_protocol::search::TextMode::Contains => {
77                        ahri_tre_core::SearchTextMode::Contains
78                    }
79                },
80                value: text.value,
81            })
82        }
83        ahri_tre_protocol::dictionary::VariablePredicate::Domain { domain } => {
84            Ok(ahri_tre_core::VariableSearchPredicate::Domain(
85                app_variable_search_domain_selector(domain)?,
86            ))
87        }
88        ahri_tre_protocol::dictionary::VariablePredicate::ValueType { value_type } => Ok(
89            ahri_tre_core::VariableSearchPredicate::ValueType(value_type),
90        ),
91        ahri_tre_protocol::dictionary::VariablePredicate::Vocabulary { vocabulary } => {
92            Ok(ahri_tre_core::VariableSearchPredicate::Vocabulary(
93                ahri_tre_core::VariableSearchVocabularySelector::Id(ahri_tre_types::VocabularyId(
94                    protocol_int_ref_id("vocabulary", vocabulary.id)?,
95                )),
96            ))
97        }
98        ahri_tre_protocol::dictionary::VariablePredicate::KeyRole { key_role } => {
99            Ok(ahri_tre_core::VariableSearchPredicate::KeyRole(
100                app_variable_search_key_role(&key_role)?,
101            ))
102        }
103        ahri_tre_protocol::dictionary::VariablePredicate::Tag(tag) => Ok(
104            ahri_tre_core::VariableSearchPredicate::Tag(match tag.selector {
105                ahri_tre_protocol::refs::TagSelector::Id { tag } => {
106                    ahri_tre_core::VariableSearchTagSelector::Id(ahri_tre_types::TagId(
107                        protocol_int_ref_id("tag", tag.id)?,
108                    ))
109                }
110                ahri_tre_protocol::refs::TagSelector::Label { label, .. } => {
111                    ahri_tre_core::VariableSearchTagSelector::Name(
112                        ahri_tre_core::normalize_tags([label.as_str().to_string()])
113                            .into_iter()
114                            .next()
115                            .unwrap_or_else(|| label.as_str().to_string()),
116                    )
117                }
118            }),
119        ),
120    }
121}
122
123fn app_variable_search_domain_selector(
124    selector: ahri_tre_protocol::domain::DomainSelector,
125) -> Result<ahri_tre_core::VariableSearchDomainSelector, AppError> {
126    match selector {
127        ahri_tre_protocol::domain::DomainSelector::Id { domain } => {
128            Ok(ahri_tre_core::VariableSearchDomainSelector::Id(
129                ahri_tre_types::DomainId(protocol_int_ref_id("domain", domain.id)?),
130            ))
131        }
132        ahri_tre_protocol::domain::DomainSelector::Name { name } => Ok(
133            ahri_tre_core::VariableSearchDomainSelector::Name(name.into()),
134        ),
135    }
136}
137
138fn app_variable_search_key_role(value: &str) -> Result<ahri_tre_types::KeyRole, AppError> {
139    match value.trim().to_ascii_lowercase().as_str() {
140        "none" => Ok(ahri_tre_types::KeyRole::None),
141        "record" => Ok(ahri_tre_types::KeyRole::Record),
142        "external" => Ok(ahri_tre_types::KeyRole::External),
143        _ => Err(protocol_validation_error(
144            "variable key role is invalid",
145            "key_role must be one of none, record, or external",
146        )),
147    }
148}
149
150fn protocol_variable_search_cursor(
151    datastore_id: PublicUuid,
152    request: &ahri_tre_protocol::dictionary::SearchVariablesRequest,
153    protocol_version: &ProtocolVersion,
154    cursor: &ahri_tre_core::VariableSearchCursorAnchor,
155) -> Result<String, AppError> {
156    request
157        .encode_cursor(
158            protocol_version,
159            ahri_tre_protocol::dictionary::VariableSearchCursorAnchor {
160                domain: protocol_domain_ref(datastore_id, cursor.domain_id.0),
161                variable: VariableRef {
162                    datastore_id,
163                    kind: ahri_tre_protocol::refs::ObjectKind::Variable,
164                    id: protocol_int_ref("variable", cursor.variable_id.0),
165                },
166                name: cursor.name.clone(),
167            },
168        )
169        .map_err(search_validation_error)
170}
171
172fn app_entity_search_request(
173    request: &ahri_tre_protocol::model::SearchEntitiesRequest,
174    protocol_version: &ProtocolVersion,
175) -> Result<SearchEntitiesRequest, AppError> {
176    let after = request
177        .decoded_cursor(protocol_version)
178        .map_err(search_validation_error)?
179        .map(|cursor| {
180            Ok::<_, AppError>(ahri_tre_core::EntitySearchCursorAnchor {
181                name: cursor.name,
182                domain_id: ahri_tre_types::DomainId(protocol_int_ref_id(
183                    "domain",
184                    cursor.domain.id,
185                )?),
186                entity_id: ahri_tre_types::EntityId(protocol_int_ref_id(
187                    "entity",
188                    cursor.entity.id,
189                )?),
190            })
191        })
192        .transpose()?;
193    Ok(SearchEntitiesRequest {
194        kind: match &request.predicates {
195            ahri_tre_protocol::search::PredicateSet::AllOf(_) => {
196                ahri_tre_core::PredicateSetKind::AllOf
197            }
198            ahri_tre_protocol::search::PredicateSet::AnyOf(_) => {
199                ahri_tre_core::PredicateSetKind::AnyOf
200            }
201        },
202        predicates: request
203            .predicates
204            .predicates()
205            .iter()
206            .cloned()
207            .map(app_entity_search_predicate)
208            .collect::<Result<Vec<_>, _>>()?,
209        limit: request.page.limit,
210        after,
211    })
212}
213
214fn app_entity_search_predicate(
215    predicate: ahri_tre_protocol::model::EntityPredicate,
216) -> Result<ahri_tre_core::EntitySearchPredicate, AppError> {
217    match predicate {
218        ahri_tre_protocol::model::EntityPredicate::Text(text) => {
219            Ok(ahri_tre_core::EntitySearchPredicate::Text {
220                field: match text.field {
221                    ahri_tre_protocol::model::EntityTextField::Name => {
222                        ahri_tre_core::EntitySearchTextField::Name
223                    }
224                    ahri_tre_protocol::model::EntityTextField::Description => {
225                        ahri_tre_core::EntitySearchTextField::Description
226                    }
227                    ahri_tre_protocol::model::EntityTextField::OntologyClass => {
228                        ahri_tre_core::EntitySearchTextField::OntologyClass
229                    }
230                },
231                mode: match text.mode {
232                    ahri_tre_protocol::search::TextMode::Exact => {
233                        ahri_tre_core::SearchTextMode::Exact
234                    }
235                    ahri_tre_protocol::search::TextMode::Prefix => {
236                        ahri_tre_core::SearchTextMode::Prefix
237                    }
238                    ahri_tre_protocol::search::TextMode::Contains => {
239                        ahri_tre_core::SearchTextMode::Contains
240                    }
241                },
242                value: text.value,
243            })
244        }
245        ahri_tre_protocol::model::EntityPredicate::Domain { domain } => {
246            Ok(ahri_tre_core::EntitySearchPredicate::Domain(
247                app_entity_search_domain_selector(domain)?,
248            ))
249        }
250        ahri_tre_protocol::model::EntityPredicate::Tag(tag) => Ok(
251            ahri_tre_core::EntitySearchPredicate::Tag(match tag.selector {
252                ahri_tre_protocol::refs::TagSelector::Id { tag } => {
253                    ahri_tre_core::EntitySearchTagSelector::Id(ahri_tre_types::TagId(
254                        protocol_int_ref_id("tag", tag.id)?,
255                    ))
256                }
257                ahri_tre_protocol::refs::TagSelector::Label { label, .. } => {
258                    ahri_tre_core::EntitySearchTagSelector::Name(
259                        ahri_tre_core::normalize_tags([label.as_str().to_string()])
260                            .into_iter()
261                            .next()
262                            .unwrap_or_else(|| label.as_str().to_string()),
263                    )
264                }
265            }),
266        ),
267    }
268}
269
270fn app_entity_search_domain_selector(
271    selector: ahri_tre_protocol::domain::DomainSelector,
272) -> Result<ahri_tre_core::EntitySearchDomainSelector, AppError> {
273    match selector {
274        ahri_tre_protocol::domain::DomainSelector::Id { domain } => {
275            Ok(ahri_tre_core::EntitySearchDomainSelector::Id(
276                ahri_tre_types::DomainId(protocol_int_ref_id("domain", domain.id)?),
277            ))
278        }
279        ahri_tre_protocol::domain::DomainSelector::Name { name } => {
280            Ok(ahri_tre_core::EntitySearchDomainSelector::Name(name.into()))
281        }
282    }
283}
284
285fn protocol_entity_search_cursor(
286    datastore_id: PublicUuid,
287    request: &ahri_tre_protocol::model::SearchEntitiesRequest,
288    protocol_version: &ProtocolVersion,
289    cursor: &ahri_tre_core::EntitySearchCursorAnchor,
290) -> Result<String, AppError> {
291    request
292        .encode_cursor(
293            protocol_version,
294            ahri_tre_protocol::model::EntitySearchCursorAnchor {
295                domain: protocol_domain_ref(datastore_id, cursor.domain_id.0),
296                entity: protocol_entity_ref(datastore_id, cursor.entity_id.0),
297                name: cursor.name.clone(),
298            },
299        )
300        .map_err(search_validation_error)
301}
302
303fn app_relation_search_request(
304    request: &ahri_tre_protocol::model::SearchRelationsRequest,
305    protocol_version: &ProtocolVersion,
306) -> Result<SearchRelationsRequest, AppError> {
307    let after = request
308        .decoded_cursor(protocol_version)
309        .map_err(search_validation_error)?
310        .map(|cursor| {
311            Ok::<_, AppError>(ahri_tre_core::RelationSearchCursorAnchor {
312                name: cursor.name,
313                domain_id: ahri_tre_types::DomainId(protocol_int_ref_id(
314                    "domain",
315                    cursor.domain.id,
316                )?),
317                relation_id: ahri_tre_types::EntityRelationId(protocol_int_ref_id(
318                    "relation",
319                    cursor.relation.id,
320                )?),
321            })
322        })
323        .transpose()?;
324    Ok(SearchRelationsRequest {
325        kind: match &request.predicates {
326            ahri_tre_protocol::search::PredicateSet::AllOf(_) => {
327                ahri_tre_core::PredicateSetKind::AllOf
328            }
329            ahri_tre_protocol::search::PredicateSet::AnyOf(_) => {
330                ahri_tre_core::PredicateSetKind::AnyOf
331            }
332        },
333        predicates: request
334            .predicates
335            .predicates()
336            .iter()
337            .cloned()
338            .map(app_relation_search_predicate)
339            .collect::<Result<Vec<_>, _>>()?,
340        limit: request.page.limit,
341        after,
342    })
343}
344
345fn app_relation_search_predicate(
346    predicate: ahri_tre_protocol::model::RelationPredicate,
347) -> Result<ahri_tre_core::RelationSearchPredicate, AppError> {
348    match predicate {
349        ahri_tre_protocol::model::RelationPredicate::Text(text) => {
350            Ok(ahri_tre_core::RelationSearchPredicate::Text {
351                field: match text.field {
352                    ahri_tre_protocol::model::RelationTextField::Name => {
353                        ahri_tre_core::RelationSearchTextField::Name
354                    }
355                    ahri_tre_protocol::model::RelationTextField::Description => {
356                        ahri_tre_core::RelationSearchTextField::Description
357                    }
358                    ahri_tre_protocol::model::RelationTextField::SourceEntityName => {
359                        ahri_tre_core::RelationSearchTextField::SourceEntityName
360                    }
361                    ahri_tre_protocol::model::RelationTextField::TargetEntityName => {
362                        ahri_tre_core::RelationSearchTextField::TargetEntityName
363                    }
364                    ahri_tre_protocol::model::RelationTextField::OntologyClass => {
365                        ahri_tre_core::RelationSearchTextField::OntologyClass
366                    }
367                },
368                mode: match text.mode {
369                    ahri_tre_protocol::search::TextMode::Exact => {
370                        ahri_tre_core::SearchTextMode::Exact
371                    }
372                    ahri_tre_protocol::search::TextMode::Prefix => {
373                        ahri_tre_core::SearchTextMode::Prefix
374                    }
375                    ahri_tre_protocol::search::TextMode::Contains => {
376                        ahri_tre_core::SearchTextMode::Contains
377                    }
378                },
379                value: text.value,
380            })
381        }
382        ahri_tre_protocol::model::RelationPredicate::Domain { domain } => {
383            Ok(ahri_tre_core::RelationSearchPredicate::Domain(
384                app_relation_search_domain_selector(domain)?,
385            ))
386        }
387        ahri_tre_protocol::model::RelationPredicate::SourceEntity { entity } => {
388            Ok(ahri_tre_core::RelationSearchPredicate::SourceEntity(
389                app_relation_search_entity_selector(entity)?,
390            ))
391        }
392        ahri_tre_protocol::model::RelationPredicate::TargetEntity { entity } => {
393            Ok(ahri_tre_core::RelationSearchPredicate::TargetEntity(
394                app_relation_search_entity_selector(entity)?,
395            ))
396        }
397        ahri_tre_protocol::model::RelationPredicate::Tag(tag) => Ok(
398            ahri_tre_core::RelationSearchPredicate::Tag(match tag.selector {
399                ahri_tre_protocol::refs::TagSelector::Id { tag } => {
400                    ahri_tre_core::RelationSearchTagSelector::Id(ahri_tre_types::TagId(
401                        protocol_int_ref_id("tag", tag.id)?,
402                    ))
403                }
404                ahri_tre_protocol::refs::TagSelector::Label { label, .. } => {
405                    ahri_tre_core::RelationSearchTagSelector::Name(
406                        ahri_tre_core::normalize_tags([label.as_str().to_string()])
407                            .into_iter()
408                            .next()
409                            .unwrap_or_else(|| label.as_str().to_string()),
410                    )
411                }
412            }),
413        ),
414    }
415}
416
417fn app_relation_search_domain_selector(
418    selector: ahri_tre_protocol::domain::DomainSelector,
419) -> Result<ahri_tre_core::RelationSearchDomainSelector, AppError> {
420    match selector {
421        ahri_tre_protocol::domain::DomainSelector::Id { domain } => {
422            Ok(ahri_tre_core::RelationSearchDomainSelector::Id(
423                ahri_tre_types::DomainId(protocol_int_ref_id("domain", domain.id)?),
424            ))
425        }
426        ahri_tre_protocol::domain::DomainSelector::Name { name } => Ok(
427            ahri_tre_core::RelationSearchDomainSelector::Name(name.into()),
428        ),
429    }
430}
431
432fn app_relation_search_entity_selector(
433    selector: ahri_tre_protocol::model::EntitySelector,
434) -> Result<ahri_tre_core::RelationSearchEntitySelector, AppError> {
435    match selector {
436        ahri_tre_protocol::model::EntitySelector::Id { entity, .. } => {
437            Ok(ahri_tre_core::RelationSearchEntitySelector::Id(
438                ahri_tre_types::EntityId(protocol_int_ref_id("entity", entity.id)?),
439            ))
440        }
441        ahri_tre_protocol::model::EntitySelector::Name { domain, name } => {
442            Ok(ahri_tre_core::RelationSearchEntitySelector::Name {
443                domain: Some(app_relation_search_domain_selector(domain)?),
444                name: name.into(),
445            })
446        }
447    }
448}
449
450fn protocol_relation_search_cursor(
451    datastore_id: PublicUuid,
452    request: &ahri_tre_protocol::model::SearchRelationsRequest,
453    protocol_version: &ProtocolVersion,
454    cursor: &ahri_tre_core::RelationSearchCursorAnchor,
455) -> Result<String, AppError> {
456    request
457        .encode_cursor(
458            protocol_version,
459            ahri_tre_protocol::model::RelationSearchCursorAnchor {
460                domain: protocol_domain_ref(datastore_id, cursor.domain_id.0),
461                relation: protocol_relation_ref(datastore_id, cursor.relation_id.0),
462                name: cursor.name.clone(),
463            },
464        )
465        .map_err(search_validation_error)
466}
467
468fn protocol_int_ref_id(scope: &str, reference: PublicUuid) -> Result<i64, AppError> {
469    decode_scoped_integer_ref(scope, reference).ok_or_else(|| {
470        protocol_validation_error("selector ref is invalid", format!("expected {scope} ref"))
471    })
472}
473
474fn search_validation_error(error: ahri_tre_protocol::ProtocolError) -> AppError {
475    AppError::Validation(error.message)
476}
477fn protocol_validation_error(message: &str, detail: impl std::fmt::Display) -> AppError {
478    AppError::Validation(format!("{message}: {detail}"))
479}
480fn protocol_int_ref(scope: &str, id: i64) -> PublicUuid {
481    encode_scoped_integer_ref(scope, id)
482}
483fn protocol_domain_ref(datastore_id: PublicUuid, id: i64) -> ObjectRef {
484    ObjectRef {
485        datastore_id,
486        kind: ObjectKind::Domain,
487        id: protocol_int_ref("domain", id),
488    }
489}
490fn protocol_entity_ref(datastore_id: PublicUuid, id: i64) -> ObjectRef {
491    ObjectRef {
492        datastore_id,
493        kind: ObjectKind::Entity,
494        id: protocol_int_ref("entity", id),
495    }
496}
497fn protocol_relation_ref(datastore_id: PublicUuid, id: i64) -> ObjectRef {
498    ObjectRef {
499        datastore_id,
500        kind: ObjectKind::Relation,
501        id: protocol_int_ref("relation", id),
502    }
503}
504impl AppService {
505    pub async fn search_catalogue_variables(
506        &self,
507        mut request: ahri_tre_protocol::dictionary::SearchVariablesRequest,
508    ) -> Result<
509        (
510            Vec<ahri_tre_protocol::dictionary::VariableSummary>,
511            Option<String>,
512        ),
513        AppError,
514    > {
515        let datastore_id = self.catalogue_datastore_id()?;
516        let version = ProtocolVersion::current();
517        request
518            .validate(&version)
519            .map_err(search_validation_error)?;
520        if let Some(cursor) = request
521            .decoded_cursor(&version)
522            .map_err(search_validation_error)?
523        {
524            self.catalogue_reference_id(cursor.domain, ObjectKind::Domain)?;
525            self.catalogue_reference_id(cursor.variable, ObjectKind::Variable)?;
526        }
527        // Validate all supplied reference origins before looking up any predicate.
528        for predicate in request.predicates.predicates() {
529            match predicate {
530                ahri_tre_protocol::dictionary::VariablePredicate::Domain { domain } => {
531                    self.catalogue_domain_selector(domain.clone())?;
532                }
533                ahri_tre_protocol::dictionary::VariablePredicate::Tag(tag) => {
534                    if let TagSelector::Id { tag } = tag.selector {
535                        self.catalogue_reference_id(tag, ObjectKind::Tag)?;
536                    }
537                }
538                ahri_tre_protocol::dictionary::VariablePredicate::Vocabulary { vocabulary } => {
539                    self.catalogue_reference_id(*vocabulary, ObjectKind::Vocabulary)?;
540                }
541                _ => {}
542            }
543        }
544        let original = app_variable_search_request(&request, &version)?;
545        let mut resolved = request.clone();
546        resolved.page.cursor = None;
547        let mut unmatched = Vec::new();
548        for (index, predicate) in resolved.predicates.predicates_mut().iter_mut().enumerate() {
549            if let ahri_tre_protocol::dictionary::VariablePredicate::Domain { domain } = predicate {
550                if let Some(value) = self.canonical_catalogue_domain(domain.clone()).await? {
551                    *domain = value;
552                } else {
553                    unmatched.push(index);
554                }
555            }
556        }
557        if !retain_matching_predicates(&mut resolved.predicates, &unmatched) {
558            return Ok((Vec::new(), None));
559        }
560        let mut query = app_variable_search_request(&resolved, &version)?;
561        query.after = original.after;
562        let result = self.search_variables(query).await?;
563        let next_cursor = result
564            .next_cursor
565            .as_ref()
566            .map(|cursor| protocol_variable_search_cursor(datastore_id, &request, &version, cursor))
567            .transpose()?;
568        let values = result
569            .variables
570            .into_iter()
571            .map(|entry| {
572                let mut summary = crate::projections::semantic_variable(
573                    datastore_id,
574                    RegisteredVariable {
575                        variable: entry.variable,
576                        value_type: entry.value_type,
577                        vocabulary: entry.vocabulary,
578                        vocabulary_items: entry.vocabulary_items,
579                    },
580                )
581                .summary;
582                summary.tags = entry
583                    .tags
584                    .into_iter()
585                    .map(|tag| crate::projections::tag_summary(datastore_id, tag))
586                    .collect();
587                summary
588            })
589            .collect();
590        Ok((values, next_cursor))
591    }
592    pub async fn search_catalogue_entities(
593        &self,
594        mut request: ahri_tre_protocol::model::SearchEntitiesRequest,
595    ) -> Result<(Vec<ahri_tre_protocol::model::EntitySummary>, Option<String>), AppError> {
596        let datastore_id = self.catalogue_datastore_id()?;
597        let version = ProtocolVersion::current();
598        request
599            .validate(&version)
600            .map_err(search_validation_error)?;
601        if let Some(cursor) = request
602            .decoded_cursor(&version)
603            .map_err(search_validation_error)?
604        {
605            self.catalogue_reference_id(cursor.domain, ObjectKind::Domain)?;
606            self.catalogue_reference_id(cursor.entity, ObjectKind::Entity)?;
607        }
608        // Validate all supplied reference origins before looking up any predicate.
609        for predicate in request.predicates.predicates() {
610            match predicate {
611                ahri_tre_protocol::model::EntityPredicate::Domain { domain } => {
612                    self.catalogue_domain_selector(domain.clone())?;
613                }
614                ahri_tre_protocol::model::EntityPredicate::Tag(tag) => {
615                    if let TagSelector::Id { tag } = tag.selector {
616                        self.catalogue_reference_id(tag, ObjectKind::Tag)?;
617                    }
618                }
619                _ => {}
620            }
621        }
622        let original = app_entity_search_request(&request, &version)?;
623        let mut resolved = request.clone();
624        resolved.page.cursor = None;
625        let mut unmatched = Vec::new();
626        for (index, predicate) in resolved.predicates.predicates_mut().iter_mut().enumerate() {
627            if let ahri_tre_protocol::model::EntityPredicate::Domain { domain } = predicate {
628                if let Some(value) = self.canonical_catalogue_domain(domain.clone()).await? {
629                    *domain = value;
630                } else {
631                    unmatched.push(index);
632                }
633            }
634        }
635        if !retain_matching_predicates(&mut resolved.predicates, &unmatched) {
636            return Ok((Vec::new(), None));
637        }
638        let mut query = app_entity_search_request(&resolved, &version)?;
639        query.after = original.after;
640        let result = self.search_entities(query).await?;
641        let next_cursor = result
642            .next_cursor
643            .as_ref()
644            .map(|cursor| protocol_entity_search_cursor(datastore_id, &request, &version, cursor))
645            .transpose()?;
646        let values = result
647            .entities
648            .into_iter()
649            .map(|entry| {
650                let mut summary =
651                    crate::projections::semantic_entity(datastore_id, entry.entity).summary;
652                summary.tags = entry
653                    .tags
654                    .into_iter()
655                    .map(|tag| crate::projections::tag_summary(datastore_id, tag))
656                    .collect();
657                summary
658            })
659            .collect();
660        Ok((values, next_cursor))
661    }
662    pub async fn search_catalogue_relations(
663        &self,
664        mut request: ahri_tre_protocol::model::SearchRelationsRequest,
665    ) -> Result<
666        (
667            Vec<ahri_tre_protocol::model::RelationDefinitionSummary>,
668            Option<String>,
669        ),
670        AppError,
671    > {
672        let datastore_id = self.catalogue_datastore_id()?;
673        let version = ProtocolVersion::current();
674        request
675            .validate(&version)
676            .map_err(search_validation_error)?;
677        if let Some(cursor) = request
678            .decoded_cursor(&version)
679            .map_err(search_validation_error)?
680        {
681            self.catalogue_reference_id(cursor.domain, ObjectKind::Domain)?;
682            self.catalogue_reference_id(cursor.relation, ObjectKind::Relation)?;
683        }
684        // Validate all supplied reference origins before looking up any predicate.
685        for predicate in request.predicates.predicates() {
686            match predicate {
687                ahri_tre_protocol::model::RelationPredicate::Domain { domain } => {
688                    self.catalogue_domain_selector(domain.clone())?;
689                }
690                ahri_tre_protocol::model::RelationPredicate::Tag(tag) => {
691                    if let TagSelector::Id { tag } = tag.selector {
692                        self.catalogue_reference_id(tag, ObjectKind::Tag)?;
693                    }
694                }
695                ahri_tre_protocol::model::RelationPredicate::SourceEntity { entity }
696                | ahri_tre_protocol::model::RelationPredicate::TargetEntity { entity } => {
697                    if let ahri_tre_protocol::model::EntitySelector::Id { entity, domain } = entity
698                    {
699                        self.catalogue_reference_id(*entity, ObjectKind::Entity)?;
700                        if let Some(domain) = domain {
701                            self.catalogue_domain_selector(domain.clone())?;
702                        }
703                    }
704                }
705                _ => {}
706            }
707        }
708        let original = app_relation_search_request(&request, &version)?;
709        let mut resolved = request.clone();
710        resolved.page.cursor = None;
711        let mut unmatched = Vec::new();
712        for (index, predicate) in resolved.predicates.predicates_mut().iter_mut().enumerate() {
713            match predicate {
714                ahri_tre_protocol::model::RelationPredicate::Domain { domain } => {
715                    if let Some(value) = self.canonical_catalogue_domain(domain.clone()).await? {
716                        *domain = value;
717                    } else {
718                        unmatched.push(index);
719                    }
720                }
721                ahri_tre_protocol::model::RelationPredicate::SourceEntity { entity }
722                | ahri_tre_protocol::model::RelationPredicate::TargetEntity { entity } => {
723                    let selector = self.catalogue_entity_selector(entity.clone()).await?;
724                    if let Some(value) = self.resolve_entity_selector_optional(selector).await? {
725                        *entity = ahri_tre_protocol::model::EntitySelector::Id {
726                            entity: protocol_entity_ref(datastore_id, value.entity_id.0),
727                            domain: None,
728                        };
729                    } else {
730                        unmatched.push(index);
731                    }
732                }
733                _ => {}
734            }
735        }
736        if !retain_matching_predicates(&mut resolved.predicates, &unmatched) {
737            return Ok((Vec::new(), None));
738        }
739        let mut query = app_relation_search_request(&resolved, &version)?;
740        query.after = original.after;
741        let result = self.search_entity_relations(query).await?;
742        let next_cursor = result
743            .next_cursor
744            .as_ref()
745            .map(|cursor| protocol_relation_search_cursor(datastore_id, &request, &version, cursor))
746            .transpose()?;
747        let values = result
748            .relations
749            .into_iter()
750            .map(|entry| {
751                let names = BTreeMap::from([
752                    (
753                        entry.source_entity.entity_id.0,
754                        entry.source_entity.name.as_str().to_owned(),
755                    ),
756                    (
757                        entry.target_entity.entity_id.0,
758                        entry.target_entity.name.as_str().to_owned(),
759                    ),
760                ]);
761                let mut summary =
762                    crate::projections::semantic_relation(datastore_id, entry.relation, &names)
763                        .summary;
764                summary.tags = entry
765                    .tags
766                    .into_iter()
767                    .map(|tag| crate::projections::tag_summary(datastore_id, tag))
768                    .collect();
769                summary
770            })
771            .collect();
772        Ok((values, next_cursor))
773    }
774}