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