From cca63b728699374953bec59c8908cdb8604e72b7 Mon Sep 17 00:00:00 2001
From: sup39 <dev@sup39.dev>
Date: Sun, 17 Mar 2024 22:37:58 +0900
Subject: [PATCH] perf (backend): improved post search with CW/alt text

Co-authored-by: naskya <m@naskya.net>
---
 .../src/server/api/endpoints/notes/search.ts  | 182 ++++++++++--------
 1 file changed, 107 insertions(+), 75 deletions(-)

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);
 });