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, 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 pub result: FlattenedItemResult,
34
35 pub data: SearchResultData,
37
38 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 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 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 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 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 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 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 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 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 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}