Skip to main content

docbox_core/document_box/
search_document_box.rs

1use docbox_database::{
2    DbErr, DbPool, DbResult,
3    models::{
4        document_box::DocumentBoxScopeRaw,
5        file::{File, FileId, FileWithExtra},
6        folder::{Folder, FolderId, FolderWithExtra},
7        link::{Link, LinkId, LinkWithExtra},
8        shared::{DocboxInputPair, FolderPathSegment},
9    },
10};
11use docbox_search::{
12    SearchError, TenantSearchIndex,
13    models::{
14        AdminSearchRequest, FlattenedItemResult, SearchIndexType, SearchRequest, SearchResultData,
15    },
16};
17use std::collections::HashMap;
18use thiserror::Error;
19
20#[derive(Debug, Error)]
21pub enum SearchDocumentBoxError {
22    #[error(transparent)]
23    Database(#[from] DbErr),
24
25    #[error("document box missing root folder")]
26    MissingRoot,
27
28    #[error(transparent)]
29    QueryIndex(SearchError),
30}
31
32pub struct ResolvedSearchResult {
33    /// Result from opensearch
34    pub result: FlattenedItemResult,
35
36    /// Resolve result from database
37    pub data: SearchResultData,
38
39    /// Path to the item
40    pub path: Vec<FolderPathSegment>,
41}
42
43pub struct DocumentBoxSearchResults {
44    pub results: Vec<ResolvedSearchResult>,
45    pub total_hits: u64,
46}
47
48pub async fn search_document_box(
49    db: &DbPool,
50    search: &TenantSearchIndex,
51    scope: DocumentBoxScopeRaw,
52    request: SearchRequest,
53) -> Result<DocumentBoxSearchResults, SearchDocumentBoxError> {
54    // When searching within a specific folder resolve all allowed folder ID's
55    let search_folder_ids = match request.folder_id {
56        Some(folder_id) => {
57            let folder = Folder::find_by_id(db, &scope, folder_id)
58                .await
59                .inspect_err(|error| tracing::error!(?error, "failed to query root folder"))?
60                .ok_or_else(|| {
61                    tracing::error!("failed to find folder");
62                    SearchDocumentBoxError::MissingRoot
63                })?;
64
65            let folder_children = folder
66                .tree_all_children(db)
67                .await
68                .inspect_err(|error| tracing::error!(?error, "failed to query folder children"))?;
69
70            Some(folder_children)
71        }
72        None => None,
73    };
74
75    tracing::debug!(?search_folder_ids, "searching within folders");
76
77    // Query search engine
78    let results = search
79        .search_index(std::slice::from_ref(&scope), request, search_folder_ids)
80        .await
81        .map_err(|error| {
82            tracing::error!(?error, "failed to query search index");
83            SearchDocumentBoxError::QueryIndex(error)
84        })?;
85
86    let total_hits = results.total_hits;
87    let results = resolve_search_results_same_scope(db, results.results, scope).await?;
88
89    Ok(DocumentBoxSearchResults {
90        results,
91        total_hits,
92    })
93}
94
95pub async fn search_document_boxes_admin(
96    db: &DbPool,
97    search: &TenantSearchIndex,
98    request: AdminSearchRequest,
99) -> Result<DocumentBoxSearchResults, SearchDocumentBoxError> {
100    // Query search engine
101    let results = search
102        .search_index(&request.scopes, request.request, None)
103        .await
104        .map_err(|error| {
105            tracing::error!(?error, "failed to query search index");
106            SearchDocumentBoxError::QueryIndex(error)
107        })?;
108
109    let total_hits = results.total_hits;
110    let results = resolve_search_results_mixed_scopes(db, results.results).await?;
111
112    Ok(DocumentBoxSearchResults {
113        results,
114        total_hits,
115    })
116}
117
118pub async fn resolve_search_results_same_scope(
119    db: &DbPool,
120    results: Vec<FlattenedItemResult>,
121    scope: DocumentBoxScopeRaw,
122) -> DbResult<Vec<ResolvedSearchResult>> {
123    // Collect the IDs to lookup
124    let mut file_ids = Vec::new();
125    let mut folder_ids = Vec::new();
126    let mut link_ids = Vec::new();
127    for hit in &results {
128        match hit.item_ty {
129            SearchIndexType::File => file_ids.push(hit.item_id),
130            SearchIndexType::Folder => folder_ids.push(hit.item_id),
131            SearchIndexType::Link => link_ids.push(hit.item_id),
132        }
133    }
134
135    // Resolve the results from the database
136    let files = File::resolve_with_extra(db, &scope, file_ids);
137    let folders = Folder::resolve_with_extra(db, &scope, folder_ids);
138    let links = Link::resolve_with_extra(db, &scope, link_ids);
139    let (files, folders, links) = tokio::try_join!(files, folders, links)?;
140
141    // Create maps to take the results from
142    let mut files: HashMap<FileId, (FileWithExtra, Vec<FolderPathSegment>)> = files
143        .into_iter()
144        .map(|item| (item.data.file.id, (item.data, item.full_path)))
145        .collect();
146
147    let mut folders: HashMap<FolderId, (FolderWithExtra, Vec<FolderPathSegment>)> = folders
148        .into_iter()
149        .map(|item| (item.data.folder.id, (item.data, item.full_path)))
150        .collect();
151
152    let mut links: HashMap<LinkId, (LinkWithExtra, Vec<FolderPathSegment>)> = links
153        .into_iter()
154        .map(|item| (item.data.link.id, (item.data, item.full_path)))
155        .collect();
156
157    Ok(results
158        .into_iter()
159        .filter_map(|result| {
160            let (result, data, path) = match result.item_ty {
161                SearchIndexType::File => {
162                    let file = files.remove(&result.item_id);
163                    file.map(|(file, full_path)| (result, SearchResultData::File(file), full_path))
164                }
165                SearchIndexType::Folder => {
166                    let folder = folders.remove(&result.item_id);
167                    folder.map(|(folder, full_path)| {
168                        (result, SearchResultData::Folder(folder), full_path)
169                    })
170                }
171                SearchIndexType::Link => {
172                    let link = links.remove(&result.item_id);
173                    link.map(|(link, full_path)| (result, SearchResultData::Link(link), full_path))
174                }
175            }?;
176
177            Some(ResolvedSearchResult { result, data, path })
178        })
179        .collect())
180}
181
182pub async fn resolve_search_results_mixed_scopes(
183    db: &DbPool,
184    results: Vec<FlattenedItemResult>,
185) -> DbResult<Vec<ResolvedSearchResult>> {
186    // Collect the IDs to lookup
187    let mut file_ids = Vec::new();
188    let mut folder_ids = Vec::new();
189    let mut link_ids = Vec::new();
190
191    for hit in &results {
192        match hit.item_ty {
193            SearchIndexType::File => {
194                file_ids.push(DocboxInputPair::new(&hit.document_box, hit.item_id))
195            }
196            SearchIndexType::Folder => {
197                folder_ids.push(DocboxInputPair::new(&hit.document_box, hit.item_id))
198            }
199            SearchIndexType::Link => {
200                link_ids.push(DocboxInputPair::new(&hit.document_box, hit.item_id))
201            }
202        }
203    }
204
205    // Resolve the results from the database
206    let files = File::resolve_with_extra_mixed_scopes(db, file_ids);
207    let folders = Folder::resolve_with_extra_mixed_scopes(db, folder_ids);
208    let links = Link::resolve_with_extra_mixed_scopes(db, link_ids);
209    let (files, folders, links) = tokio::try_join!(files, folders, links)?;
210
211    // Create maps to take the results from
212    let mut files: HashMap<(DocumentBoxScopeRaw, FileId), (FileWithExtra, Vec<FolderPathSegment>)> =
213        files
214            .into_iter()
215            .map(|item| {
216                (
217                    (item.document_box, item.data.file.id),
218                    (item.data, item.full_path),
219                )
220            })
221            .collect();
222
223    let mut folders: HashMap<
224        (DocumentBoxScopeRaw, FolderId),
225        (FolderWithExtra, Vec<FolderPathSegment>),
226    > = folders
227        .into_iter()
228        .map(|item| {
229            (
230                (item.data.folder.document_box.clone(), item.data.folder.id),
231                (item.data, item.full_path),
232            )
233        })
234        .collect();
235
236    let mut links: HashMap<(DocumentBoxScopeRaw, LinkId), (LinkWithExtra, Vec<FolderPathSegment>)> =
237        links
238            .into_iter()
239            .map(|item| {
240                (
241                    (item.document_box, item.data.link.id),
242                    (item.data, item.full_path),
243                )
244            })
245            .collect();
246
247    Ok(results
248        .into_iter()
249        .filter_map(|result| {
250            let (result, data, path) = match result.item_ty {
251                SearchIndexType::File => {
252                    let file = files.remove(&(result.document_box.clone(), result.item_id));
253                    file.map(|(file, full_path)| (result, SearchResultData::File(file), full_path))
254                }
255                SearchIndexType::Folder => {
256                    let folder = folders.remove(&(result.document_box.clone(), result.item_id));
257                    folder.map(|(folder, full_path)| {
258                        (result, SearchResultData::Folder(folder), full_path)
259                    })
260                }
261                SearchIndexType::Link => {
262                    let link = links.remove(&(result.document_box.clone(), result.item_id));
263                    link.map(|(link, full_path)| (result, SearchResultData::Link(link), full_path))
264                }
265            }?;
266
267            Some(ResolvedSearchResult { result, data, path })
268        })
269        .collect())
270}