diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index 0bc70d37f9..b159a91944 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -1,4 +1,3 @@ -import { Brackets } from "typeorm"; import { Notes } from "@/models/index.js"; import { Note } from "@/models/entities/note.js"; import define from "@/server/api/define.js"; @@ -7,6 +6,7 @@ import { generateVisibilityQuery } from "@/server/api/common/generate-visibility import { generateMutedUserQuery } from "@/server/api/common/generate-muted-user-query.js"; import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js"; import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; +import type { SelectQueryBuilder } from "typeorm"; export const meta = { tags: ["notes"], @@ -69,91 +69,123 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery( - Notes.createQueryBuilder("note"), - ps.sinceId, - ps.untilId, - ps.sinceDate ?? undefined, - ps.untilDate ?? undefined, - ); + async function search( + modifier?: (query: SelectQueryBuilder<Note>) => void, + ): Promise<Note[]> { + const query = makePaginationQuery( + Notes.createQueryBuilder("note"), + ps.sinceId, + ps.untilId, + ps.sinceDate ?? undefined, + ps.untilDate ?? undefined, + ); + modifier?.(query); - if (ps.userId != null) { - query.andWhere("note.userId = :userId", { userId: ps.userId }); + if (ps.userId != null) { + query.andWhere("note.userId = :userId", { userId: ps.userId }); + } + + if (ps.channelId != null) { + query.andWhere("note.channelId = :channelId", { + channelId: ps.channelId, + }); + } + + query.innerJoinAndSelect("note.user", "user"); + + // "from: me": search all (public, home, followers, specified) my posts + // otherwise: search public indexable posts only + if (ps.userId == null || ps.userId !== me?.id) { + query + .andWhere("note.visibility = 'public'") + .andWhere("user.isIndexable = TRUE"); + } + + if (ps.userId != null) { + query.andWhere("note.userId = :userId", { userId: ps.userId }); + } + + if (ps.host === null) { + query.andWhere("note.userHost IS NULL"); + } + if (ps.host != null) { + query.andWhere("note.userHost = :userHost", { userHost: ps.host }); + } + + if (ps.withFiles === true) { + query.andWhere("note.fileIds != '{}'"); + } + + query + .leftJoinAndSelect("user.avatar", "avatar") + .leftJoinAndSelect("user.banner", "banner") + .leftJoinAndSelect("note.reply", "reply") + .leftJoinAndSelect("note.renote", "renote") + .leftJoinAndSelect("reply.user", "replyUser") + .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") + .leftJoinAndSelect("replyUser.banner", "replyUserBanner") + .leftJoinAndSelect("renote.user", "renoteUser") + .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") + .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); + + generateVisibilityQuery(query, me); + if (me) generateMutedUserQuery(query, me); + if (me) generateBlockedUserQuery(query, me); + + return await query.take(ps.limit).getMany(); } - if (ps.channelId != null) { - query.andWhere("note.channelId = :channelId", { - channelId: ps.channelId, - }); - } + let notes: Note[]; if (ps.query != null) { const q = sqlLikeEscape(ps.query); if (ps.searchCwAndAlt) { - query.andWhere( - new Brackets((qb) => { - qb.where("note.text &@~ :q", { q }) - .orWhere("note.cw &@~ :q", { q }) - .orWhere( - `EXISTS ( - SELECT FROM "drive_file" - WHERE - comment &@~ :q - AND - drive_file."id" = ANY(note."fileIds") - )`, - { q }, - ); - }), - ); + // Whether we should return latest notes first + const isDescendingOrder = + (ps.sinceId == null || ps.untilId != null) && + (ps.sinceId != null || + ps.untilId != null || + ps.sinceDate == null || + ps.untilDate != null); + + const compare = isDescendingOrder + ? (lhs: Note, rhs: Note) => + Math.sign(rhs.createdAt.getTime() - lhs.createdAt.getTime()) + : (lhs: Note, rhs: Note) => + Math.sign(lhs.createdAt.getTime() - rhs.createdAt.getTime()); + + notes = [ + ...new Map( + ( + await Promise.all([ + search((query) => { + query.andWhere("note.text &@~ :q", { q }); + }), + search((query) => { + query.andWhere("note.cw &@~ :q", { q }); + }), + search((query) => { + query + .andWhere("drive_file.comment &@~ :q", { q }) + .innerJoin("note.files", "drive_file"); + }), + ]) + ) + .flatMap((e) => e) + .map((note) => [note.id, note]), + ).values(), + ] + .sort(compare) + .slice(0, ps.limit); } else { - query.andWhere("note.text &@~ :q", { q }); + notes = await search((query) => { + query.andWhere("note.text &@~ :q", { q }); + }); } + } else { + notes = await search(); } - query.innerJoinAndSelect("note.user", "user"); - - // "from: me": search all (public, home, followers, specified) my posts - // otherwise: search public indexable posts only - if (ps.userId == null || ps.userId !== me?.id) { - query - .andWhere("note.visibility = 'public'") - .andWhere("user.isIndexable = TRUE"); - } - - if (ps.userId != null) { - query.andWhere("note.userId = :userId", { userId: ps.userId }); - } - - if (ps.host === null) { - query.andWhere("note.userHost IS NULL"); - } - if (ps.host != null) { - query.andWhere("note.userHost = :userHost", { userHost: ps.host }); - } - - if (ps.withFiles === true) { - query.andWhere("note.fileIds != '{}'"); - } - - query - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") - .leftJoinAndSelect("note.reply", "reply") - .leftJoinAndSelect("note.renote", "renote") - .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); - - generateVisibilityQuery(query, me); - if (me) generateMutedUserQuery(query, me); - if (me) generateBlockedUserQuery(query, me); - - const notes: Note[] = await query.take(ps.limit).getMany(); - return await Notes.packMany(notes, me); });