Skip to main content

sofie_db/
search.rs

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