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