optimize folder search: SQL-level filtering, user isolation, no in-memory filter; batch cascade trigger

This commit is contained in:
Dionisio
2026-02-24 17:15:36 +01:00
parent 6aa38d0d24
commit 28966ce28e
12 changed files with 361 additions and 131 deletions
+4 -1
View File
@@ -94,7 +94,10 @@ pub trait FolderUseCase: Send + Sync + 'static {
#[async_trait]
pub trait SearchUseCase: Send + Sync + 'static {
/// Performs a full search based on the specified criteria.
async fn search(&self, criteria: SearchCriteriaDto) -> Result<SearchResultsDto, DomainError>;
///
/// `user_id` identifies the authenticated user so that SQL queries filter
/// by owner and the result cache is isolated per tenant.
async fn search(&self, criteria: SearchCriteriaDto, user_id: &str) -> Result<SearchResultsDto, DomainError>;
/// Returns quick suggestions for autocomplete (lightweight, fast).
async fn suggest(
+21 -40
View File
@@ -272,12 +272,9 @@ impl SearchUseCase for SearchService {
* - Human-readable size formatting
* - Pagination
*/
async fn search(&self, criteria: SearchCriteriaDto) -> Result<SearchResultsDto> {
async fn search(&self, criteria: SearchCriteriaDto, user_id: &str) -> Result<SearchResultsDto> {
let start = Instant::now();
// TODO: Get user ID from the authentication context
let user_id = "default-user";
// Try to get from cache
let cache_key = Self::create_cache_key(&criteria, user_id);
if let Some(cached_results) = self.get_from_cache(cache_key).await {
@@ -302,27 +299,19 @@ impl SearchUseCase for SearchService {
.map(|f| Self::enrich_file(f, query))
.collect();
// Get folders for this folder (non-recursive)
// Get folders for this folder (non-recursive, filtered in SQL)
let folders = self
.folder_repository
.list_folders(criteria.folder_id.as_deref())
.search_folders(
criteria.folder_id.as_deref(),
criteria.name_contains.as_deref(),
user_id,
false,
)
.await?;
// Filter folders if name criteria present
let filtered_folders: Vec<FolderDto> = if let Some(name_query) = &criteria.name_contains
{
let query_lower = name_query.to_lowercase();
folders
.into_iter()
.map(FolderDto::from)
.filter(|f| {
let folder_name_lower = f.name.to_lowercase();
folder_name_lower.contains(&query_lower)
})
.collect()
} else {
folders.into_iter().map(FolderDto::from).collect()
};
let filtered_folders: Vec<FolderDto> =
folders.into_iter().map(FolderDto::from).collect();
// For folders, apply sorting and pagination in memory (usually fewer folders)
let mut enriched_folders: Vec<SearchFolderResultDto> = filtered_folders
@@ -397,24 +386,16 @@ impl SearchUseCase for SearchService {
.search_files_in_subtree(criteria.folder_id.as_deref(), &criteria, user_id)
.await?;
// Get descendant folders (ltree-based when folder_id is specified)
let found_folders: Vec<Folder> = if let Some(ref fid) = criteria.folder_id {
self.folder_repository
.list_descendant_folders(fid, criteria.name_contains.as_deref(), user_id)
.await?
} else {
// No folder scope → search all user folders
let all_folders = self.folder_repository.list_folders(None).await?;
if let Some(ref name_query) = criteria.name_contains {
let q = name_query.to_lowercase();
all_folders
.into_iter()
.filter(|f| f.name().to_lowercase().contains(&q))
.collect()
} else {
all_folders
}
};
// Get folders (SQL-filtered, user-scoped, recursive when applicable)
let found_folders: Vec<Folder> = self
.folder_repository
.search_folders(
criteria.folder_id.as_deref(),
criteria.name_contains.as_deref(),
user_id,
true,
)
.await?;
// ── Convert to DTOs and enrich with server-computed metadata ──
let file_dtos: Vec<FileDto> = found_files.into_iter().map(FileDto::from).collect();
@@ -515,7 +496,7 @@ impl SearchService {
#[async_trait]
impl SearchUseCase for SearchServiceStub {
async fn search(&self, _criteria: SearchCriteriaDto) -> Result<SearchResultsDto> {
async fn search(&self, _criteria: SearchCriteriaDto, _user_id: &str) -> Result<SearchResultsDto> {
Ok(SearchResultsDto::empty())
}