Compare commits

..

2 Commits

Author SHA1 Message Date
Philipinho 207c119792 fix collab server logging too 2026-01-29 00:41:00 +00:00
Philipinho 4683a85a8b - fix: set default Nest logger and bufferLogs to false for pino compatibility
- handle redis error event
2026-01-29 00:36:48 +00:00
2 changed files with 75 additions and 81 deletions
+77 -83
View File
@@ -30,110 +30,104 @@ export class SearchService {
const { query } = searchParams; const { query } = searchParams;
if (query.length < 1) { if (query.length < 1) {
return []; return;
} }
const searchQuery = tsquery(query.trim() + '*'); const searchQuery = tsquery(query.trim() + '*');
const limit = searchParams.limit || 25;
const offset = searchParams.offset || 0;
const includeSpace = !searchParams.shareId;
// Handle share search - resolve page IDs first let queryResults = this.db
let sharePageIds: string[] | null = null;
if (searchParams.shareId && !searchParams.spaceId && !opts.userId) {
const share = await this.shareRepo.findById(searchParams.shareId);
if (!share || share.workspaceId !== opts.workspaceId) {
return [];
}
if (share.includeSubPages) {
const pageList = await this.pageRepo.getPageAndDescendants(
share.pageId,
{ includeContent: false },
);
sharePageIds = pageList.map((page) => page.id);
} else {
sharePageIds = [share.pageId];
}
if (sharePageIds.length === 0) {
return [];
}
} else if (!searchParams.spaceId && !opts.userId) {
return [];
}
// CTE to get top N page IDs by rank (without expensive ts_headline)
// Join back to compute ts_headline only for those N rows
const tsQuery = sql<string>`to_tsquery('english', f_unaccent(${searchQuery}))`;
const queryResults = await this.db
.with('ranked_pages', (db) => {
let rankQuery = db
.selectFrom('pages') .selectFrom('pages')
.select(['id', sql<number>`ts_rank(tsv, ${tsQuery})`.as('rank')]) .select([
.where('tsv', '@@', tsQuery) 'id',
.where('deletedAt', 'is', null) 'slugId',
'title',
'icon',
'parentPageId',
'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',
),
])
.where(
'tsv',
'@@',
sql<string>`to_tsquery('english', f_unaccent(${searchQuery}))`,
)
.$if(Boolean(searchParams.creatorId), (qb) => .$if(Boolean(searchParams.creatorId), (qb) =>
qb.where('creatorId', '=', searchParams.creatorId), qb.where('creatorId', '=', searchParams.creatorId),
); )
.where('deletedAt', 'is', null)
.orderBy('rank', 'desc')
.limit(searchParams.limit | 25)
.offset(searchParams.offset || 0);
if (!searchParams.shareId) {
queryResults = queryResults.select((eb) => this.pageRepo.withSpace(eb));
}
if (searchParams.spaceId) { if (searchParams.spaceId) {
rankQuery = rankQuery.where('spaceId', '=', searchParams.spaceId); // search by spaceId
} else if (opts.userId) { queryResults = queryResults.where('spaceId', '=', searchParams.spaceId);
rankQuery = rankQuery } else if (opts.userId && !searchParams.spaceId) {
// only search spaces the user is a member of
queryResults = queryResults
.where( .where(
'spaceId', 'spaceId',
'in', 'in',
this.spaceMemberRepo.getUserSpaceIdsQuery(opts.userId), this.spaceMemberRepo.getUserSpaceIdsQuery(opts.userId),
) )
.where('workspaceId', '=', opts.workspaceId); .where('workspaceId', '=', opts.workspaceId);
} else if (sharePageIds) { } else if (searchParams.shareId && !searchParams.spaceId && !opts.userId) {
rankQuery = rankQuery // search in shares
.where('id', 'in', sharePageIds) const shareId = searchParams.shareId;
.where('workspaceId', '=', opts.workspaceId); const share = await this.shareRepo.findById(shareId);
if (!share || share.workspaceId !== opts.workspaceId) {
return [];
} }
return rankQuery.orderBy('rank', 'desc').limit(limit).offset(offset); const pageIdsToSearch = [];
}) if (share.includeSubPages) {
.selectFrom('ranked_pages') const pageList = await this.pageRepo.getPageAndDescendants(
.innerJoin('pages', 'pages.id', 'ranked_pages.id') share.pageId,
.select([ {
'pages.id', includeContent: false,
'pages.slugId', },
'pages.title', );
'pages.icon',
'pages.parentPageId',
'pages.creatorId',
'pages.createdAt',
'pages.updatedAt',
'ranked_pages.rank',
sql<string>`ts_headline('english', pages.text_content, ${tsQuery}, 'MinWords=9, MaxWords=10, MaxFragments=3')`.as(
'highlight',
),
])
.$if(includeSpace, (qb) =>
qb.innerJoin('spaces', 'spaces.id', 'pages.spaceId').select(
sql<{
id: string;
name: string;
slug: string;
}>`jsonb_build_object('id', spaces.id, 'name', spaces.name, 'slug', spaces.slug)`.as(
'space',
),
),
)
.orderBy('ranked_pages.rank', 'desc')
.execute();
return queryResults.map((result) => { pageIdsToSearch.push(...pageList.map((page) => page.id));
const mapped = result as unknown as SearchResponseDto; } else {
if (mapped.highlight) { pageIdsToSearch.push(share.pageId);
mapped.highlight = mapped.highlight }
if (pageIdsToSearch.length > 0) {
queryResults = queryResults
.where('id', 'in', pageIdsToSearch)
.where('workspaceId', '=', opts.workspaceId);
} else {
return [];
}
} else {
return [];
}
//@ts-ignore
queryResults = await queryResults.execute();
//@ts-ignore
const searchResults = queryResults.map((result: SearchResponseDto) => {
if (result.highlight) {
result.highlight = result.highlight
.replace(/\r\n|\r|\n/g, ' ') .replace(/\r\n|\r|\n/g, ' ')
.replace(/\s+/g, ' '); .replace(/\s+/g, ' ');
} }
return mapped; return result;
}); });
return searchResults;
} }
async searchSuggestions( async searchSuggestions(