sofie_db/search/
engine.rs1use std::sync::Arc;
2
3use sofie_core::{Error, Result, SqlType, SqlValue, Uuid};
4
5use super::{Document, SearchIndex};
6use crate::condition::Kind;
7use crate::statement::Criteria;
8use crate::{BoxFuture, Db, Migrations, Read};
9
10pub const MATCHES_READ: usize = 1000;
12
13pub trait SearchEngine: Send + Sync + 'static {
14 fn matching<'a>(
16 &'a self,
17 index: &'a SearchIndex,
18 words: &'a str,
19 owner: Option<Uuid>,
20 ) -> BoxFuture<'a, Result<Vec<Uuid>>>;
21
22 fn put<'a>(
24 &'a self,
25 index: &'a SearchIndex,
26 documents: &'a [Document],
27 ) -> BoxFuture<'a, Result<()>>;
28
29 fn remove<'a>(&'a self, index: &'a SearchIndex, ids: &'a [Uuid]) -> BoxFuture<'a, Result<()>>;
30}
31
32impl SearchIndex {
33 #[must_use]
35 pub const fn new(
36 module: &'static str,
37 name: &'static str,
38 owner: Option<&'static str>,
39 ) -> Self {
40 Self {
41 module,
42 name,
43 owner,
44 hidden: None,
45 fields: &[],
46 }
47 }
48
49 #[must_use]
50 pub fn uid(&self) -> String {
51 format!("{}_{}", self.module, self.name)
52 }
53
54 #[must_use]
55 pub const fn owned(&self) -> bool {
56 self.owner.is_some()
57 }
58}
59
60pub(crate) struct Installed {
61 pub(crate) engine: Arc<dyn SearchEngine>,
62 pub(crate) indexes: Vec<SearchIndex>,
63}
64
65impl Installed {
66 pub(crate) fn of(db: &Db) -> Result<&Self> {
67 db.search.get().ok_or_else(|| {
68 Error::internal_message(
69 "no search engine is installed, so the database matches searches itself; set \
70 SOFIE_SEARCH=meilisearch",
71 )
72 })
73 }
74
75 pub(crate) fn find(&self, module: &str, name: &str) -> Option<&SearchIndex> {
76 self.indexes
77 .iter()
78 .find(|index| index.module == module && index.name == name)
79 }
80
81 pub(crate) fn index(&self, module: &str, name: &str) -> Result<&SearchIndex> {
82 self.find(module, name).ok_or_else(|| {
83 Error::internal_message(format!(
84 "`{module}.{name}` is searched, but no module's migrations list it among \
85 their aggregates; run `sofie gen`"
86 ))
87 })
88 }
89}
90
91pub fn install_search(db: &Db, engine: Arc<dyn SearchEngine>, migrations: &[Migrations]) {
93 db.search.get_or_init(|| Installed {
94 engine,
95 indexes: migrations
96 .iter()
97 .flat_map(|migrations| migrations.aggregates)
98 .filter_map(crate::AggregateTable::search_index)
99 .collect(),
100 });
101}
102
103pub(crate) async fn matched(mut criteria: Criteria, read: &Read) -> Result<Criteria> {
105 let Some(installed) = read.db.search.get() else {
106 return Ok(criteria);
107 };
108 let mut searches = Vec::new();
109 for condition in &mut criteria.conditions {
110 condition.searches(true, &mut searches);
111 }
112 for (search, own_records) in searches {
113 let Kind::TextSearch {
114 words,
115 index: (module, name),
116 ..
117 } = &*search
118 else {
119 continue;
120 };
121 let index = installed.index(module, name)?;
122 let owner = read.owner().filter(|_| own_records && index.owned());
123 let ids = if words.trim().is_empty() {
125 Vec::new()
126 } else {
127 installed.engine.matching(index, words, owner).await?
128 };
129 *search = Kind::In {
130 columns: vec!["id".to_owned()],
131 types: vec![SqlType::Uuid],
132 rows: ids.into_iter().map(|id| vec![SqlValue::Uuid(id)]).collect(),
133 };
134 }
135 Ok(criteria)
136}