mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
optimize search query and ts_headline usage
This commit is contained in:
@@ -30,104 +30,110 @@ 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;
|
||||||
|
|
||||||
let queryResults = this.db
|
// Handle share search - resolve page IDs first
|
||||||
.selectFrom('pages')
|
let sharePageIds: string[] | null = null;
|
||||||
.select([
|
if (searchParams.shareId && !searchParams.spaceId && !opts.userId) {
|
||||||
'id',
|
const share = await this.shareRepo.findById(searchParams.shareId);
|
||||||
'slugId',
|
if (!share || share.workspaceId !== opts.workspaceId) {
|
||||||
'title',
|
return [];
|
||||||
'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 (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')
|
||||||
|
.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) {
|
if (searchParams.spaceId) {
|
||||||
// search by spaceId
|
rankQuery = rankQuery.where('spaceId', '=', searchParams.spaceId);
|
||||||
queryResults = queryResults.where('spaceId', '=', searchParams.spaceId);
|
} else if (opts.userId) {
|
||||||
} else if (opts.userId && !searchParams.spaceId) {
|
rankQuery = rankQuery
|
||||||
// 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 (searchParams.shareId && !searchParams.spaceId && !opts.userId) {
|
} else if (sharePageIds) {
|
||||||
// search in shares
|
rankQuery = rankQuery
|
||||||
const shareId = searchParams.shareId;
|
.where('id', 'in', sharePageIds)
|
||||||
const share = await this.shareRepo.findById(shareId);
|
|
||||||
if (!share || share.workspaceId !== opts.workspaceId) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageIdsToSearch = [];
|
|
||||||
if (share.includeSubPages) {
|
|
||||||
const pageList = await this.pageRepo.getPageAndDescendants(
|
|
||||||
share.pageId,
|
|
||||||
{
|
|
||||||
includeContent: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
pageIdsToSearch.push(...pageList.map((page) => page.id));
|
|
||||||
} else {
|
|
||||||
pageIdsToSearch.push(share.pageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pageIdsToSearch.length > 0) {
|
|
||||||
queryResults = queryResults
|
|
||||||
.where('id', 'in', pageIdsToSearch)
|
|
||||||
.where('workspaceId', '=', opts.workspaceId);
|
.where('workspaceId', '=', opts.workspaceId);
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//@ts-ignore
|
return rankQuery.orderBy('rank', 'desc').limit(limit).offset(offset);
|
||||||
queryResults = await queryResults.execute();
|
})
|
||||||
|
.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();
|
||||||
|
|
||||||
//@ts-ignore
|
return queryResults.map((result) => {
|
||||||
const searchResults = queryResults.map((result: SearchResponseDto) => {
|
const mapped = result as unknown as SearchResponseDto;
|
||||||
if (result.highlight) {
|
if (mapped.highlight) {
|
||||||
result.highlight = result.highlight
|
mapped.highlight = mapped.highlight
|
||||||
.replace(/\r\n|\r|\n/g, ' ')
|
.replace(/\r\n|\r|\n/g, ' ')
|
||||||
.replace(/\s+/g, ' ');
|
.replace(/\s+/g, ' ');
|
||||||
}
|
}
|
||||||
return result;
|
return mapped;
|
||||||
});
|
});
|
||||||
|
|
||||||
return searchResults;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchSuggestions(
|
async searchSuggestions(
|
||||||
|
|||||||
+1
-1
Submodule apps/server/src/ee updated: f858f127b5...256d1a54c4
Reference in New Issue
Block a user