Merge pull request #154 from DioCrafts/claude/optimize-performance-FbFK2

Add GIN trigram indexes for substring search performance
This commit is contained in:
Dionisio Pozo
2026-03-02 02:30:47 +01:00
committed by GitHub
4 changed files with 108 additions and 37 deletions
@@ -532,10 +532,10 @@ impl FileReadPort for FileBlobReadRepository {
}
if let Some(name) = &criteria.name_contains
&& !name.is_empty()
&& name.len() >= 3
{
bind_idx += 1;
conditions.push(format!("LOWER(fi.name) LIKE ${bind_idx}"));
conditions.push(format!("fi.name ILIKE ${bind_idx}"));
}
let where_clause = conditions.join(" AND ");
@@ -578,9 +578,9 @@ impl FileReadPort for FileBlobReadRepository {
query = query.bind(fid);
}
if let Some(name) = &criteria.name_contains
&& !name.is_empty()
&& name.len() >= 3
{
query = query.bind(format!("%{}%", name.to_lowercase()));
query = query.bind(format!("%{}%", name));
}
query = query.bind(limit).bind(offset);
@@ -652,10 +652,10 @@ impl FileReadPort for FileBlobReadRepository {
);
if let Some(name) = &criteria.name_contains
&& !name.is_empty()
&& name.len() >= 3
{
bind_idx += 1;
conditions.push(format!("LOWER(fi.name) LIKE ${bind_idx}"));
conditions.push(format!("fi.name ILIKE ${bind_idx}"));
}
if let Some(types) = &criteria.file_types
&& !types.is_empty()
@@ -737,9 +737,9 @@ impl FileReadPort for FileBlobReadRepository {
.bind(root_id);
if let Some(name) = &criteria.name_contains
&& !name.is_empty()
&& name.len() >= 3
{
query = query.bind(format!("%{}%", name.to_lowercase()));
query = query.bind(format!("%{}%", name));
}
if let Some(types) = &criteria.file_types
&& !types.is_empty()
@@ -807,9 +807,8 @@ impl FileReadPort for FileBlobReadRepository {
query: &str,
limit: usize,
) -> Result<Vec<File>, DomainError> {
let pattern = format!("%{}%", query.to_lowercase());
let pattern = format!("%{}%", query);
let limit_i64 = limit as i64;
let query_lower = query.to_lowercase();
let rows: Vec<(
String,
@@ -833,10 +832,10 @@ impl FileReadPort for FileBlobReadRepository {
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
WHERE fi.folder_id = $1::uuid
AND NOT fi.is_trashed
AND LOWER(fi.name) LIKE $2
AND fi.name ILIKE $2
ORDER BY CASE
WHEN LOWER(fi.name) = $3 THEN 0
WHEN LOWER(fi.name) LIKE $3 || '%' THEN 1
WHEN fi.name ILIKE $3 THEN 0
WHEN fi.name ILIKE $3 || '%' THEN 1
ELSE 2
END,
fi.name
@@ -845,7 +844,7 @@ impl FileReadPort for FileBlobReadRepository {
)
.bind(fid)
.bind(&pattern)
.bind(&query_lower)
.bind(query)
.bind(limit_i64)
.fetch_all(self.pool.as_ref())
.await
@@ -861,10 +860,10 @@ impl FileReadPort for FileBlobReadRepository {
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
WHERE fi.folder_id IS NULL
AND NOT fi.is_trashed
AND LOWER(fi.name) LIKE $1
AND fi.name ILIKE $1
ORDER BY CASE
WHEN LOWER(fi.name) = $2 THEN 0
WHEN LOWER(fi.name) LIKE $2 || '%' THEN 1
WHEN fi.name ILIKE $2 THEN 0
WHEN fi.name ILIKE $2 || '%' THEN 1
ELSE 2
END,
fi.name
@@ -872,7 +871,7 @@ impl FileReadPort for FileBlobReadRepository {
"#,
)
.bind(&pattern)
.bind(&query_lower)
.bind(query)
.bind(limit_i64)
.fetch_all(self.pool.as_ref())
.await
@@ -720,15 +720,16 @@ impl FolderRepository for FolderDbRepository {
.await;
}
// Build optional name filter
// Build optional name filter — use ILIKE (case-insensitive) so the
// GIN trigram index idx_folders_name_trgm is used instead of a seq scan.
let (name_clause, name_pattern) = match name_contains {
Some(name) if !name.is_empty() => (
Some(name) if name.len() >= 3 => (
if recursive {
" AND LOWER(fo.name) LIKE $2"
" AND fo.name ILIKE $2"
} else {
" AND LOWER(fo.name) LIKE $3"
" AND fo.name ILIKE $3"
},
Some(format!("%{}%", name.to_lowercase())),
Some(format!("%{}%", name)),
),
_ => ("", None),
};
@@ -794,7 +795,7 @@ impl FolderRepository for FolderDbRepository {
} else {
// Root folders: parent_id IS NULL, reindex params ($1=user_id, $2=pattern)
let name_clause_root = match name_contains {
Some(name) if !name.is_empty() => " AND LOWER(fo.name) LIKE $2",
Some(name) if name.len() >= 3 => " AND fo.name ILIKE $2",
_ => "",
};
format!(
@@ -866,9 +867,9 @@ impl FolderRepository for FolderDbRepository {
user_id: &str,
) -> Result<Vec<Folder>, DomainError> {
let (where_extra, name_pattern) = match name_contains {
Some(name) if !name.is_empty() => (
" AND LOWER(fo.name) LIKE $3",
Some(format!("%{}%", name.to_lowercase())),
Some(name) if name.len() >= 3 => (
" AND fo.name ILIKE $3",
Some(format!("%{}%", name)),
),
_ => ("", None),
};
@@ -924,8 +925,7 @@ impl FolderRepository for FolderDbRepository {
query: &str,
limit: usize,
) -> Result<Vec<Folder>, DomainError> {
let pattern = format!("%{}%", query.to_lowercase());
let query_lower = query.to_lowercase();
let pattern = format!("%{}%", query);
let limit_i64 = limit as i64;
let rows: Vec<(String, String, String, Option<String>, String, i64, i64)> =
@@ -938,10 +938,10 @@ impl FolderRepository for FolderDbRepository {
FROM storage.folders
WHERE parent_id = $1::uuid
AND NOT is_trashed
AND LOWER(name) LIKE $2
AND name ILIKE $2
ORDER BY CASE
WHEN LOWER(name) = $3 THEN 0
WHEN LOWER(name) LIKE $3 || '%' THEN 1
WHEN name ILIKE $3 THEN 0
WHEN name ILIKE $3 || '%' THEN 1
ELSE 2
END,
name
@@ -950,7 +950,7 @@ impl FolderRepository for FolderDbRepository {
)
.bind(pid)
.bind(&pattern)
.bind(&query_lower)
.bind(query)
.bind(limit_i64)
.fetch_all(self.pool())
.await
@@ -963,10 +963,10 @@ impl FolderRepository for FolderDbRepository {
FROM storage.folders
WHERE parent_id IS NULL
AND NOT is_trashed
AND LOWER(name) LIKE $1
AND name ILIKE $1
ORDER BY CASE
WHEN LOWER(name) = $2 THEN 0
WHEN LOWER(name) LIKE $2 || '%' THEN 1
WHEN name ILIKE $2 THEN 0
WHEN name ILIKE $2 || '%' THEN 1
ELSE 2
END,
name
@@ -974,7 +974,7 @@ impl FolderRepository for FolderDbRepository {
"#,
)
.bind(&pattern)
.bind(&query_lower)
.bind(query)
.bind(limit_i64)
.fetch_all(self.pool())
.await