feat: add unaccent support for accent-insensitive search (#1402)

- Add PostgreSQL unaccent and pg_trgm extensions
- Create immutable f_unaccent wrapper function for performance
- Update all search queries to use f_unaccent for accent-insensitive matching
- Add 1MB limit to tsvector content to prevent errors on large documents
- Update full-text search trigger to use f_unaccent
- Fix MultiSelect client-side filtering to show server results properly
This commit is contained in:
Philip Okugbe
2025-07-29 22:47:13 +01:00
committed by GitHub
parent f90c5a636b
commit 5da92a538a
10 changed files with 154 additions and 64 deletions
+37 -9
View File
@@ -44,12 +44,18 @@ export class SearchService {
'creatorId',
'createdAt',
'updatedAt',
sql<number>`ts_rank(tsv, to_tsquery(${searchQuery}))`.as('rank'),
sql<string>`ts_headline('english', text_content, to_tsquery(${searchQuery}),'MinWords=9, MaxWords=10, MaxFragments=3')`.as(
sql<number>`ts_rank(tsv, to_tsquery('english', f_unaccent(${searchQuery})))`.as(
'rank',
),
sql<string>`ts_headline('english', text_content, to_tsquery('english', f_unaccent(${searchQuery})),'MinWords=9, MaxWords=10, MaxFragments=3')`.as(
'highlight',
),
])
.where('tsv', '@@', sql<string>`to_tsquery(${searchQuery})`)
.where(
'tsv',
'@@',
sql<string>`to_tsquery('english', f_unaccent(${searchQuery}))`,
)
.$if(Boolean(searchParams.creatorId), (qb) =>
qb.where('creatorId', '=', searchParams.creatorId),
)
@@ -138,21 +144,37 @@ export class SearchService {
const query = suggestion.query.toLowerCase().trim();
if (suggestion.includeUsers) {
users = await this.db
const userQuery = this.db
.selectFrom('users')
.select(['id', 'name', 'email', 'avatarUrl'])
.where((eb) => eb(sql`LOWER(users.name)`, 'like', `%${query}%`))
.where('workspaceId', '=', workspaceId)
.where('deletedAt', 'is', null)
.limit(limit)
.execute();
.where((eb) =>
eb.or([
eb(
sql`LOWER(f_unaccent(users.name))`,
'like',
sql`LOWER(f_unaccent(${`%${query}%`}))`,
),
eb(sql`users.email`, 'ilike', sql`f_unaccent(${`%${query}%`})`),
]),
)
.limit(limit);
users = await userQuery.execute();
}
if (suggestion.includeGroups) {
groups = await this.db
.selectFrom('groups')
.select(['id', 'name', 'description'])
.where((eb) => eb(sql`LOWER(groups.name)`, 'like', `%${query}%`))
.where((eb) =>
eb(
sql`LOWER(f_unaccent(groups.name))`,
'like',
sql`LOWER(f_unaccent(${`%${query}%`}))`,
),
)
.where('workspaceId', '=', workspaceId)
.limit(limit)
.execute();
@@ -162,7 +184,13 @@ export class SearchService {
let pageSearch = this.db
.selectFrom('pages')
.select(['id', 'slugId', 'title', 'icon', 'spaceId'])
.where((eb) => eb(sql`LOWER(pages.title)`, 'like', `%${query}%`))
.where((eb) =>
eb(
sql`LOWER(f_unaccent(pages.title))`,
'like',
sql`LOWER(f_unaccent(${`%${query}%`}))`,
),
)
.where('workspaceId', '=', workspaceId)
.limit(limit);