Skip to main content

sofie_db/
search.rs

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
65/// Numbers and times are ranges a filter asks for, not words.
66fn 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    /// `"quoted words"` match as a phrase and `-word` excludes, where the database parses queries.
104    #[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 {}