1#[cfg(feature = "search-engine")]
2mod engine;
3#[cfg(feature = "search-engine")]
4mod indexing;
5
6use sofie_core::{Cast, ColumnSpec, SqlValue};
7use sofie_values::Search;
8
9use crate::condition::Kind;
10use crate::{Aggregate, Behavior, Condition, Field, Uses, Write};
11
12#[cfg(feature = "search-engine")]
13pub use engine::{Document, MATCHES_READ, SearchEngine, SearchIndex, install_search};
14#[cfg(feature = "search-engine")]
15pub(crate) use engine::{Installed, matched};
16#[cfg(feature = "search-engine")]
17pub use indexing::{REINDEX_TASK, Reindexing, SearchIndexes, index_searches, reindex};
18#[cfg(all(feature = "search-engine", feature = "retention"))]
19pub(crate) use indexing::{index_of, queue_forget};
20
21pub const SEARCH_COLUMN: &str = "search_text";
22
23pub type SearchedField = (&'static str, &'static [ColumnSpec], Search);
24
25pub struct Searched {
26 pub fields: &'static [SearchedField],
27}
28
29impl Behavior for Searched {
30 fn columns(&self) -> &'static [Field] {
31 const { &[Field::new(SEARCH_COLUMN, String::COLUMNS, false)] }
32 }
33
34 fn written(&self, write: &Write<'_>, values: &mut [SqlValue]) {
35 values[0] = SqlValue::Text(self.text(write));
36 }
37
38 fn searched(&self) -> bool {
39 true
40 }
41
42 #[cfg(feature = "search-engine")]
43 fn search_fields(&self) -> &'static [SearchedField] {
44 self.fields
45 }
46
47 #[cfg(feature = "search-engine")]
48 fn planned(&self, write: &Write<'_>, plan: &mut crate::Plan<'_>) -> sofie_core::Result<()> {
49 if plan.indexes_searches && indexing::changes_document(write) {
50 return indexing::queue(write, plan);
51 }
52 Ok(())
53 }
54}
55
56impl Searched {
57 fn text(&self, write: &Write<'_>) -> String {
58 search_text(self.fields, |column| write.value(column))
59 }
60}
61
62fn worded(fields: &[SearchedField]) -> impl Iterator<Item = (String, &ColumnSpec)> + '_ {
64 fields
65 .iter()
66 .filter(|(_, _, search)| matches!(search, Search::Text | Search::Exact))
67 .flat_map(|(name, columns, _)| {
68 columns
69 .iter()
70 .map(|column| (crate::sql::column_name(name, column), column))
71 })
72}
73
74#[cfg(feature = "search-engine")]
75pub(crate) fn text_columns(fields: &[SearchedField]) -> Vec<(String, sofie_core::SqlType)> {
76 worded(fields)
77 .map(|(column, spec)| (column, spec.ty))
78 .collect()
79}
80
81pub(crate) fn search_text<'a>(
82 fields: &[SearchedField],
83 value: impl Fn(&str) -> Option<&'a SqlValue>,
84) -> String {
85 let mut text = String::new();
86 for (column, _) in worded(fields) {
87 let word = match value(&column) {
88 Some(SqlValue::Text(value)) => value.clone(),
89 Some(SqlValue::Uuid(value)) => value.to_string(),
90 _ => continue,
91 };
92 if !text.is_empty() {
93 text.push('\n');
94 }
95 text.push_str(&word);
96 }
97 text
98}
99
100pub trait Searches: Aggregate {
101 #[must_use]
103 fn matching(words: &str) -> Condition<Self> {
104 Condition::new(Kind::TextSearch {
105 column: SEARCH_COLUMN.to_owned(),
106 words: words.to_owned(),
107 #[cfg(feature = "search-engine")]
108 index: (Self::MODULE, Self::NAME),
109 })
110 }
111}
112
113impl<A: Uses<Searched>> Searches for A {}