From e44a0cd0e0f062914dcbaaa9bc2b75ad37fcafea Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Sat, 23 May 2026 03:22:29 +0100 Subject: [PATCH] feat(search): add titlesOnly option to searchPage for efficient title-only searches --- apps/server/src/core/search/search.service.ts | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/apps/server/src/core/search/search.service.ts b/apps/server/src/core/search/search.service.ts index 9883b2654..54d748329 100644 --- a/apps/server/src/core/search/search.service.ts +++ b/apps/server/src/core/search/search.service.ts @@ -27,6 +27,7 @@ export class SearchService { opts: { userId?: string; workspaceId: string; + titlesOnly?: boolean; }, ): Promise<{ items: SearchResponseDto[] }> { const { query } = searchParams; @@ -34,6 +35,12 @@ export class SearchService { if (query.length < 1) { return { items: [] }; } + + // Use ILIKE titles-only search if titlesOnly flag is set + if (opts.titlesOnly) { + return this.searchPageTitlesOnly(searchParams, opts); + } + const searchQuery = tsquery(query.trim() + '*'); let queryResults = this.db @@ -151,6 +158,71 @@ export class SearchService { return { items: searchResults }; } + private async searchPageTitlesOnly( + searchParams: SearchDTO, + opts: { + userId?: string; + workspaceId: string; + }, + ): Promise<{ items: SearchResponseDto[] }> { + const { query } = searchParams; + + let queryResults = this.db + .selectFrom('pages') + .select([ + 'id', + 'slugId', + 'title', + 'icon', + 'parentPageId', + 'creatorId', + 'createdAt', + 'updatedAt', + ]) + .where('title', 'ilike', `%${query}%`) + .where('deletedAt', 'is', null) + .orderBy('updatedAt', 'desc') + .limit(searchParams.limit || 10) + .offset(searchParams.offset || 0); + + if (searchParams.spaceId) { + // search by spaceId + queryResults = queryResults.where('spaceId', '=', searchParams.spaceId); + } else if (opts.userId) { + // only search spaces the user is a member of + queryResults = queryResults + .where( + 'spaceId', + 'in', + this.spaceMemberRepo.getUserSpaceIdsQuery(opts.userId), + ) + .where('workspaceId', '=', opts.workspaceId); + } else { + return { items: [] }; + } + + queryResults = queryResults.select((eb) => this.pageRepo.withSpace(eb)); + + //@ts-ignore + let results: any[] = await queryResults.execute(); + + // Filter results by page-level permissions (if user is authenticated) + if (opts.userId && results.length > 0) { + const pageIds = results.map((r: any) => r.id); + const accessibleIds = + await this.pagePermissionRepo.filterAccessiblePageIds({ + pageIds, + userId: opts.userId, + spaceId: searchParams.spaceId, + }); + const accessibleSet = new Set(accessibleIds); + results = results.filter((r: any) => accessibleSet.has(r.id)); + } + + //@ts-ignore + return { items: results }; + } + async searchSuggestions( suggestion: SearchSuggestionDTO, userId: string,