perf(#12): push suggest() filtering + LIMIT to SQL

Replace the in-memory approach (list_files → filter in Rust) with
SQL-level ILIKE + relevance ORDER BY + LIMIT.

- Add suggest_files_by_name() to FileReadPort (default impl for stubs)
- Add suggest_folders_by_name() to FolderRepository (default impl)
- Implement efficient SQL in FileBlobReadRepository & FolderDbRepository
  with LOWER(name) LIKE pattern, 3-tier relevance sorting, and LIMIT
- Rewrite SearchService::suggest() to call new methods in parallel

Before: 50K files → ~10 MB transferred, 52K string comparisons
After:  50K files → ~20 rows transferred, index-assisted scan
This commit is contained in:
Dionisio
2026-02-23 23:36:38 +01:00
parent ab0c4476b3
commit 44113eabdc
5 changed files with 240 additions and 39 deletions
@@ -120,4 +120,27 @@ pub trait FolderRepository: Send + Sync + 'static {
let _ = (folder_id, name_contains, user_id);
Ok(Vec::new())
}
/// Return up to `limit` folders whose name contains `query` (case-insensitive).
///
/// Results are ordered by relevance (exact > starts-with > contains) for
/// autocomplete suggestions.
///
/// The default implementation falls back to `list_folders` + in-memory
/// filter so that stubs and mocks compile without changes.
async fn suggest_folders_by_name(
&self,
parent_id: Option<&str>,
query: &str,
limit: usize,
) -> Result<Vec<Folder>, DomainError> {
let all = self.list_folders(parent_id).await?;
let q = query.to_lowercase();
let mut matched: Vec<Folder> = all
.into_iter()
.filter(|f| f.name().to_lowercase().contains(&q))
.collect();
matched.truncate(limit);
Ok(matched)
}
}