feat: add title-only search mode

Adds a titleOnly flag to page search: the Postgres driver matches
titles via trigram-indexed unaccented LIKE ranked by word_similarity,
and a Match filter in the search spotlight switches between everything
and title-only matching.
This commit is contained in:
Philipinho
2026-08-24 21:37:28 +01:00
parent 99288ff8ad
commit 5f270d0502
8 changed files with 111 additions and 12 deletions
@@ -30,6 +30,10 @@ export class SearchDTO {
@IsUUID('all', { each: true })
labelIds?: string[];
@IsOptional()
@IsBoolean()
titleOnly?: boolean;
@IsOptional()
@IsNumber()
limit?: number;
+32 -10
View File
@@ -36,6 +36,21 @@ export class SearchService {
}
const searchQuery = tsquery(query.trim() + '*');
const labelIds = [...new Set(searchParams.labelIds ?? [])];
const titleOnly = searchParams.titleOnly === true;
const titleQuery = query.trim();
const rankColumn = titleOnly
? sql<number>`word_similarity(lower(f_unaccent(${titleQuery})), lower(f_unaccent(pages.title)))`.as(
'rank',
)
: sql<number>`ts_rank(tsv, to_tsquery('english', f_unaccent(${searchQuery})))`.as(
'rank',
);
const highlightColumn = titleOnly
? sql<string>`''`.as('highlight')
: sql<string>`ts_headline('english', text_content, to_tsquery('english', f_unaccent(${searchQuery})),'MinWords=9, MaxWords=10, MaxFragments=3')`.as(
'highlight',
);
let queryResults = this.db
.selectFrom('pages')
@@ -48,17 +63,24 @@ export class SearchService {
'creatorId',
'createdAt',
'updatedAt',
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',
),
rankColumn,
highlightColumn,
])
.where(
'tsv',
'@@',
sql<string>`to_tsquery('english', f_unaccent(${searchQuery}))`,
.$if(!titleOnly, (qb) =>
qb.where(
'tsv',
'@@',
sql<string>`to_tsquery('english', f_unaccent(${searchQuery}))`,
),
)
.$if(titleOnly, (qb) =>
qb.where((eb) =>
eb(
sql`lower(f_unaccent(pages.title))`,
'like',
sql`lower(f_unaccent(${`%${titleQuery}%`}))`,
),
),
)
.$if(Boolean(searchParams.creatorId), (qb) =>
qb.where('creatorId', '=', searchParams.creatorId),
@@ -0,0 +1,11 @@
import { type Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`CREATE INDEX IF NOT EXISTS pages_title_trgm_idx ON pages USING gin (lower(f_unaccent(title)) gin_trgm_ops)`.execute(
db,
);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`DROP INDEX IF EXISTS pages_title_trgm_idx`.execute(db);
}