perf(search): clamp search page size to bound per-request memory

The main search endpoint took `limit: params.limit.unwrap_or(100)` with no
ceiling, and that value flows straight into the SQL LIMIT of
search_files_paginated / search_files_in_subtree. A client passing
?limit=<huge> would make Postgres return that many rows into memory and into
the result cache. The suggestions endpoint already clamps (.min(20)); search
did not.

Cap at MAX_SEARCH_LIMIT (500). total_count still reflects the full match set
(COUNT(*) OVER()), so deeper results stay reachable via offset.

https://claude.ai/code/session_01UtfkS3nZF1vrF5jNAps6wV
This commit is contained in:
Claude
2026-06-09 14:59:01 +00:00
parent 8dc528f403
commit 175a908e99
@@ -21,6 +21,14 @@ use std::sync::Arc;
* formatting) is performed server-side. These handlers are thin HTTP
* adapters that delegate to the SearchUseCase.
*/
/// Hard cap on the search page size. The default is 100; without a ceiling a
/// client could pass `?limit=<huge>`, which flows straight into the SQL `LIMIT`
/// and would pull that many rows into memory (and into the result cache). 500
/// is a generous page for a search UI — `total_count` still reflects the full
/// match set, so deeper results stay reachable via `offset`. Mirrors the
/// suggestions endpoint, which already clamps with `.min(20)`.
const MAX_SEARCH_LIMIT: usize = 500;
pub struct SearchHandler;
impl SearchHandler {
@@ -62,7 +70,7 @@ impl SearchHandler {
max_size: params.max_size,
folder_id: params.folder_id,
recursive: params.recursive.unwrap_or(true),
limit: params.limit.unwrap_or(100),
limit: params.limit.unwrap_or(100).min(MAX_SEARCH_LIMIT),
offset: params.offset.unwrap_or(0),
sort_by: params.sort_by.unwrap_or_else(|| "relevance".to_string()),
};