docbox_core/document_box/
search_document_box.rs1use 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 pub result: FlattenedItemResult,
35
36 pub data: SearchResultData,
38
39 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 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 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 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 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 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 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 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 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 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}