Compare commits

...

2 Commits

Author SHA1 Message Date
Philipinho b87bef0016 optimize search query and ts_headline usage 2026-01-30 00:18:30 +00:00
Philip Okugbe 60501de992 fix: missing logs on OnApplicationBootstrap hook (#1882)
* - fix: set default Nest logger and bufferLogs to false for pino compatibility
- handle redis error event

* fix collab server logging too
2026-01-29 09:25:23 +00:00
6 changed files with 96 additions and 78 deletions
@@ -79,6 +79,8 @@ export class RedisSyncExtension<TCE extends CustomEvents> implements Extension {
this.customEvents = (customEvents as any) ?? ({} as any as CustomEvents);
this.sub.subscribe(this.msgChannel, `${this.msgChannel}:${this.serverId}`);
this.sub.on('messageBuffer', this.handleRedisMessage);
this.pub.on('error', () => {});
this.sub.on('error', () => {});
}
private getKey(documentName: string) {
return `${this.lockPrefix}:${documentName}`;
@@ -19,7 +19,8 @@ async function bootstrap() {
},
}),
{
bufferLogs: true,
logger: false,
bufferLogs: false,
},
);
+80 -74
View File
@@ -30,104 +30,110 @@ export class SearchService {
const { query } = searchParams;
if (query.length < 1) {
return;
return [];
}
const searchQuery = tsquery(query.trim() + '*');
const limit = searchParams.limit || 25;
const offset = searchParams.offset || 0;
const includeSpace = !searchParams.shareId;
let queryResults = this.db
.selectFrom('pages')
.select([
'id',
'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) =>
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) {
// search by spaceId
queryResults = queryResults.where('spaceId', '=', searchParams.spaceId);
} else if (opts.userId && !searchParams.spaceId) {
// only search spaces the user is a member of
queryResults = queryResults
.where(
'spaceId',
'in',
this.spaceMemberRepo.getUserSpaceIdsQuery(opts.userId),
)
.where('workspaceId', '=', opts.workspaceId);
} else if (searchParams.shareId && !searchParams.spaceId && !opts.userId) {
// search in shares
const shareId = searchParams.shareId;
const share = await this.shareRepo.findById(shareId);
// Handle share search - resolve page IDs first
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 [];
}
const pageIdsToSearch = [];
if (share.includeSubPages) {
const pageList = await this.pageRepo.getPageAndDescendants(
share.pageId,
{
includeContent: false,
},
{ includeContent: false },
);
pageIdsToSearch.push(...pageList.map((page) => page.id));
sharePageIds = pageList.map((page) => page.id);
} else {
pageIdsToSearch.push(share.pageId);
sharePageIds = [share.pageId];
}
if (pageIdsToSearch.length > 0) {
queryResults = queryResults
.where('id', 'in', pageIdsToSearch)
.where('workspaceId', '=', opts.workspaceId);
} else {
if (sharePageIds.length === 0) {
return [];
}
} else {
} else if (!searchParams.spaceId && !opts.userId) {
return [];
}
//@ts-ignore
queryResults = await queryResults.execute();
// 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}))`;
//@ts-ignore
const searchResults = queryResults.map((result: SearchResponseDto) => {
if (result.highlight) {
result.highlight = result.highlight
const queryResults = await this.db
.with('ranked_pages', (db) => {
let rankQuery = db
.selectFrom('pages')
.select(['id', sql<number>`ts_rank(tsv, ${tsQuery})`.as('rank')])
.where('tsv', '@@', tsQuery)
.where('deletedAt', 'is', null)
.$if(Boolean(searchParams.creatorId), (qb) =>
qb.where('creatorId', '=', searchParams.creatorId),
);
if (searchParams.spaceId) {
rankQuery = rankQuery.where('spaceId', '=', searchParams.spaceId);
} else if (opts.userId) {
rankQuery = rankQuery
.where(
'spaceId',
'in',
this.spaceMemberRepo.getUserSpaceIdsQuery(opts.userId),
)
.where('workspaceId', '=', opts.workspaceId);
} else if (sharePageIds) {
rankQuery = rankQuery
.where('id', 'in', sharePageIds)
.where('workspaceId', '=', opts.workspaceId);
}
return rankQuery.orderBy('rank', 'desc').limit(limit).offset(offset);
})
.selectFrom('ranked_pages')
.innerJoin('pages', 'pages.id', 'ranked_pages.id')
.select([
'pages.id',
'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) => {
const mapped = result as unknown as SearchResponseDto;
if (mapped.highlight) {
mapped.highlight = mapped.highlight
.replace(/\r\n|\r|\n/g, ' ')
.replace(/\s+/g, ' ');
}
return result;
return mapped;
});
return searchResults;
}
async searchSuggestions(
+8 -2
View File
@@ -24,7 +24,11 @@ async function bootstrap() {
}),
{
rawBody: true,
bufferLogs: true,
// disable Nest logger so pino handles all logs
// bufferLogs must be false else pino will fail
// to log OnApplicationBootstrap logs
logger: false,
bufferLogs: false,
},
);
@@ -101,7 +105,9 @@ async function bootstrap() {
const port = process.env.PORT || 3000;
await app.listen(port, '0.0.0.0', () => {
logger.log(`Listening on http://127.0.0.1:${port} / ${process.env.APP_URL}`);
logger.log(
`Listening on http://127.0.0.1:${port} / ${process.env.APP_URL}`,
);
});
}
@@ -23,6 +23,9 @@ export class WsRedisIoAdapter extends IoAdapter {
const pubClient = new Redis(process.env.REDIS_URL, options);
const subClient = new Redis(process.env.REDIS_URL, options);
pubClient.on('error', (err) => () => {});
subClient.on('error', (err) => () => {});
this.adapterConstructor = createAdapter(pubClient, subClient);
}