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