From f27e4033a6599203ba37efe01edbd988699b4068 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 20 Mar 2021 13:54:59 +0900
Subject: [PATCH] perf(server): Reduce database query

Related: #6813
---
 src/models/repositories/note.ts               | 86 +++++++++++++++++--
 src/models/repositories/notification.ts       | 45 +++++++++-
 src/models/repositories/user.ts               | 40 +++++++--
 src/server/api/endpoints/antennas/notes.ts    |  4 +
 src/server/api/endpoints/channels/timeline.ts |  4 +
 src/server/api/endpoints/clips/notes.ts       |  4 +
 src/server/api/endpoints/i/notifications.ts   |  6 +-
 src/server/api/endpoints/notes.ts             |  6 +-
 src/server/api/endpoints/notes/children.ts    |  6 +-
 src/server/api/endpoints/notes/featured.ts    |  6 +-
 .../api/endpoints/notes/global-timeline.ts    |  6 +-
 .../api/endpoints/notes/hybrid-timeline.ts    |  4 +
 .../api/endpoints/notes/local-timeline.ts     |  6 +-
 src/server/api/endpoints/notes/mentions.ts    |  6 +-
 src/server/api/endpoints/notes/renotes.ts     |  6 +-
 src/server/api/endpoints/notes/replies.ts     |  6 +-
 .../api/endpoints/notes/search-by-tag.ts      |  6 +-
 src/server/api/endpoints/notes/search.ts      |  6 +-
 src/server/api/endpoints/notes/timeline.ts    |  4 +
 .../api/endpoints/notes/user-list-timeline.ts |  4 +
 src/server/api/endpoints/users/notes.ts       |  6 +-
 21 files changed, 240 insertions(+), 27 deletions(-)

diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts
index 43caaf94b2..1fcedbd56f 100644
--- a/src/models/repositories/note.ts
+++ b/src/models/repositories/note.ts
@@ -85,6 +85,7 @@ export class NoteRepository extends Repository<Note> {
 			detail?: boolean;
 			skipHide?: boolean;
 			_hint_?: {
+				emojis: Emoji[] | null;
 				myReactions: Map<Note['id'], NoteReaction | null>;
 			};
 		}
@@ -141,11 +142,43 @@ export class NoteRepository extends Repository<Note> {
 		 * @param reactionNames Note等にリアクションされたカスタム絵文字名 (:は含めない)
 		 */
 		async function populateEmojis(emojiNames: string[], noteUserHost: string | null, reactionNames: string[]) {
+			const customReactions = reactionNames?.map(x => decodeReaction(x)).filter(x => x.name);
+
 			let all = [] as {
 				name: string,
 				url: string
 			}[];
 
+			// 与えられたhintだけで十分(=新たにクエリする必要がない)かどうかを表すフラグ
+			let enough = true;
+			if (options?._hint_?.emojis) {
+				for (const name of emojiNames) {
+					const matched = options._hint_.emojis.find(x => x.name === name && x.host === noteUserHost);
+					if (matched) {
+						all.push({
+							name: matched.name,
+							url: matched.url,
+						});
+					} else {
+						enough = false;
+					}
+				}
+				for (const customReaction of customReactions) {
+					const matched = options._hint_.emojis.find(x => x.name === customReaction.name && x.host === customReaction.host);
+					if (matched) {
+						all.push({
+							name: `${matched.name}@${matched.host || '.'}`,	// @host付きでローカルは.
+							url: matched.url,
+						});
+					} else {
+						enough = false;
+					}
+				}
+			} else {
+				enough = false;
+			}
+			if (enough) return all;
+
 			// カスタム絵文字
 			if (emojiNames?.length > 0) {
 				const tmp = await Emojis.find({
@@ -164,8 +197,6 @@ export class NoteRepository extends Repository<Note> {
 				all = concat([all, tmp]);
 			}
 
-			const customReactions = reactionNames?.map(x => decodeReaction(x)).filter(x => x.name);
-
 			if (customReactions?.length > 0) {
 				const where = [] as {}[];
 
@@ -230,7 +261,12 @@ export class NoteRepository extends Repository<Note> {
 			id: note.id,
 			createdAt: note.createdAt.toISOString(),
 			userId: note.userId,
-			user: Users.pack(note.user || note.userId, meId),
+			user: Users.pack(note.user || note.userId, meId, {
+				detail: false,
+				_hint_: {
+					emojis: options?._hint_?.emojis || null
+				}
+			}),
 			text: text,
 			cw: note.cw,
 			visibility: note.visibility,
@@ -258,12 +294,12 @@ export class NoteRepository extends Repository<Note> {
 			_prId_: (note as any)._prId_ || undefined,
 
 			...(opts.detail ? {
-				reply: note.replyId ? this.pack(note.replyId, meId, {
+				reply: note.replyId ? this.pack(note.reply || note.replyId, meId, {
 					detail: false,
 					_hint_: options?._hint_
 				}) : undefined,
 
-				renote: note.renoteId ? this.pack(note.renoteId, meId, {
+				renote: note.renoteId ? this.pack(note.renote || note.renoteId, meId, {
 					detail: true,
 					_hint_: options?._hint_
 				}) : undefined,
@@ -314,10 +350,48 @@ export class NoteRepository extends Repository<Note> {
 			}
 		}
 
+		// TODO: ここら辺の処理をaggregateEmojisみたいな関数に切り出したい
+		let emojisWhere: any[] = [];
+		for (const note of notes) {
+			if (typeof note !== 'object') continue;
+			emojisWhere.push({
+				name: In(note.emojis),
+				host: note.userHost
+			});
+			if (note.renote) {
+				emojisWhere.push({
+					name: In(note.renote.emojis),
+					host: note.renote.userHost
+				});
+				if (note.renote.user) {
+					emojisWhere.push({
+						name: In(note.renote.user.emojis),
+						host: note.renote.userHost
+					});
+				}
+			}
+			const customReactions = Object.keys(note.reactions).map(x => decodeReaction(x)).filter(x => x.name);
+			emojisWhere = emojisWhere.concat(customReactions.map(x => ({
+				name: x.name,
+				host: x.host
+			})));
+			if (note.user) {
+				emojisWhere.push({
+					name: In(note.user.emojis),
+					host: note.userHost
+				});
+			}
+		}
+		const emojis = emojisWhere.length > 0 ? await Emojis.find({
+			where: emojisWhere,
+			select: ['name', 'host', 'url']
+		}) : null;
+
 		return await Promise.all(notes.map(n => this.pack(n, me, {
 			...options,
 			_hint_: {
-				myReactions: myReactionsMap
+				myReactions: myReactionsMap,
+				emojis: emojis
 			}
 		})));
 	}
diff --git a/src/models/repositories/notification.ts b/src/models/repositories/notification.ts
index a9fe5e7b4e..1027155873 100644
--- a/src/models/repositories/notification.ts
+++ b/src/models/repositories/notification.ts
@@ -1,11 +1,13 @@
 import { EntityRepository, In, Repository } from 'typeorm';
-import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '..';
+import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions, Emojis } from '..';
 import { Notification } from '../entities/notification';
 import { awaitAll } from '../../prelude/await-all';
 import { SchemaType } from '../../misc/schema';
 import { Note } from '../entities/note';
 import { NoteReaction } from '../entities/note-reaction';
 import { User } from '../entities/user';
+import { decodeReaction } from '../../misc/reaction-lib';
+import { Emoji } from '../entities/emoji';
 
 export type PackedNotification = SchemaType<typeof packedNotificationSchema>;
 
@@ -15,6 +17,7 @@ export class NotificationRepository extends Repository<Notification> {
 		src: Notification['id'] | Notification,
 		options: {
 			_hintForEachNotes_: {
+				emojis: Emoji[] | null;
 				myReactions: Map<Note['id'], NoteReaction | null>;
 			};
 		}
@@ -98,9 +101,47 @@ export class NotificationRepository extends Repository<Notification> {
 			myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) || null);
 		}
 
+		// TODO: ここら辺の処理をaggregateEmojisみたいな関数に切り出したい
+		let emojisWhere: any[] = [];
+		for (const note of notes) {
+			if (typeof note !== 'object') continue;
+			emojisWhere.push({
+				name: In(note.emojis),
+				host: note.userHost
+			});
+			if (note.renote) {
+				emojisWhere.push({
+					name: In(note.renote.emojis),
+					host: note.renote.userHost
+				});
+				if (note.renote.user) {
+					emojisWhere.push({
+						name: In(note.renote.user.emojis),
+						host: note.renote.userHost
+					});
+				}
+			}
+			const customReactions = Object.keys(note.reactions).map(x => decodeReaction(x)).filter(x => x.name);
+			emojisWhere = emojisWhere.concat(customReactions.map(x => ({
+				name: x.name,
+				host: x.host
+			})));
+			if (note.user) {
+				emojisWhere.push({
+					name: In(note.user.emojis),
+					host: note.userHost
+				});
+			}
+		}
+		const emojis = emojisWhere.length > 0 ? await Emojis.find({
+			where: emojisWhere,
+			select: ['name', 'host', 'url']
+		}) : null;
+
 		return await Promise.all(notifications.map(x => this.pack(x, {
 			_hintForEachNotes_: {
-				myReactions: myReactionsMap
+				myReactions: myReactionsMap,
+				emojis: emojis,
 			}
 		})));
 	}
diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts
index 3a6ab48c5f..19b0e54239 100644
--- a/src/models/repositories/user.ts
+++ b/src/models/repositories/user.ts
@@ -5,6 +5,7 @@ import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMes
 import config from '../../config';
 import { SchemaType } from '../../misc/schema';
 import { awaitAll } from '../../prelude/await-all';
+import { Emoji } from '../entities/emoji';
 
 export type PackedUser = SchemaType<typeof packedUserSchema>;
 
@@ -149,6 +150,9 @@ export class UserRepository extends Repository<User> {
 		options?: {
 			detail?: boolean,
 			includeSecrets?: boolean,
+			_hint_?: {
+				emojis: Emoji[] | null;
+			};
 		}
 	): Promise<PackedUser> {
 		const opts = Object.assign({
@@ -166,6 +170,34 @@ export class UserRepository extends Repository<User> {
 		}) : [];
 		const profile = opts.detail ? await UserProfiles.findOneOrFail(user.id) : null;
 
+		let emojis: Emoji[] = [];
+		if (user.emojis.length > 0) {
+			// 与えられたhintだけで十分(=新たにクエリする必要がない)かどうかを表すフラグ
+			let enough = true;
+			if (options?._hint_?.emojis) {
+				for (const name of user.emojis) {
+					const matched = options._hint_.emojis.find(x => x.name === name && x.host === user.host);
+					if (matched) {
+						emojis.push(matched);
+					} else {
+						enough = false;
+					}
+				}
+			} else {
+				enough = false;
+			}
+
+			if (!enough) {
+				emojis = await Emojis.find({
+					where: {
+						name: In(user.emojis),
+						host: user.host
+					},
+					select: ['name', 'host', 'url', 'aliases']
+				});
+			}
+		}
+
 		const falsy = opts.detail ? false : undefined;
 
 		const packed = {
@@ -190,13 +222,7 @@ export class UserRepository extends Repository<User> {
 			} : undefined) : undefined,
 
 			// カスタム絵文字添付
-			emojis: user.emojis.length > 0 ? Emojis.find({
-				where: {
-					name: In(user.emojis),
-					host: user.host
-				},
-				select: ['name', 'host', 'url', 'aliases']
-			}) : [],
+			emojis: emojis,
 
 			...(opts.detail ? {
 				url: profile!.url,
diff --git a/src/server/api/endpoints/antennas/notes.ts b/src/server/api/endpoints/antennas/notes.ts
index 750fc080cf..2ea3e43745 100644
--- a/src/server/api/endpoints/antennas/notes.ts
+++ b/src/server/api/endpoints/antennas/notes.ts
@@ -74,6 +74,10 @@ export default define(meta, async (ps, user) => {
 	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
 		.andWhere(`note.id IN (${ antennaQuery.getQuery() })`)
 		.leftJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser')
 		.setParameters(antennaQuery.getParameters());
 
 	generateVisibilityQuery(query, user);
diff --git a/src/server/api/endpoints/channels/timeline.ts b/src/server/api/endpoints/channels/timeline.ts
index acb34f124d..292f21b63d 100644
--- a/src/server/api/endpoints/channels/timeline.ts
+++ b/src/server/api/endpoints/channels/timeline.ts
@@ -88,6 +88,10 @@ export default define(meta, async (ps, user) => {
 	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
 		.andWhere('note.channelId = :channelId', { channelId: channel.id })
 		.leftJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser')
 		.leftJoinAndSelect('note.channel', 'channel');
 	//#endregion
 
diff --git a/src/server/api/endpoints/clips/notes.ts b/src/server/api/endpoints/clips/notes.ts
index 6a507e2036..86d1e507f4 100644
--- a/src/server/api/endpoints/clips/notes.ts
+++ b/src/server/api/endpoints/clips/notes.ts
@@ -72,6 +72,10 @@ export default define(meta, async (ps, user) => {
 	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
 		.andWhere(`note.id IN (${ clipQuery.getQuery() })`)
 		.leftJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser')
 		.setParameters(clipQuery.getParameters());
 
 	if (user) {
diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts
index af49549d37..812a4bd1dd 100644
--- a/src/server/api/endpoints/i/notifications.ts
+++ b/src/server/api/endpoints/i/notifications.ts
@@ -87,7 +87,11 @@ export default define(meta, async (ps, user) => {
 		.andWhere(`notification.notifieeId = :meId`, { meId: user.id })
 		.leftJoinAndSelect('notification.notifier', 'notifier')
 		.leftJoinAndSelect('notification.note', 'note')
-		.leftJoinAndSelect('note.user', 'user');
+		.leftJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
 
 	query.andWhere(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`);
 	query.setParameters(mutingQuery.getParameters());
diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts
index fab8455d78..708ce38c0e 100644
--- a/src/server/api/endpoints/notes.ts
+++ b/src/server/api/endpoints/notes.ts
@@ -76,7 +76,11 @@ export default define(meta, async (ps) => {
 	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
 		.andWhere(`note.visibility = 'public'`)
 		.andWhere(`note.localOnly = FALSE`)
-		.leftJoinAndSelect('note.user', 'user');
+		.leftJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
 
 	if (ps.local) {
 		query.andWhere('note.userHost IS NULL');
diff --git a/src/server/api/endpoints/notes/children.ts b/src/server/api/endpoints/notes/children.ts
index 0875e0f240..a8b239e445 100644
--- a/src/server/api/endpoints/notes/children.ts
+++ b/src/server/api/endpoints/notes/children.ts
@@ -64,7 +64,11 @@ export default define(meta, async (ps, user) => {
 				}));
 			}));
 		}))
-		.leftJoinAndSelect('note.user', 'user');
+		.leftJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
 
 	generateVisibilityQuery(query, user);
 	if (user) generateMutedUserQuery(query, user);
diff --git a/src/server/api/endpoints/notes/featured.ts b/src/server/api/endpoints/notes/featured.ts
index 4dda7d0edb..6c416b1c04 100644
--- a/src/server/api/endpoints/notes/featured.ts
+++ b/src/server/api/endpoints/notes/featured.ts
@@ -49,7 +49,11 @@ export default define(meta, async (ps, user) => {
 		.andWhere(`note.score > 0`)
 		.andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) })
 		.andWhere(`note.visibility = 'public'`)
-		.leftJoinAndSelect('note.user', 'user');
+		.leftJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
 
 	if (user) generateMutedUserQuery(query, user);
 
diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts
index 6d99f1fdbc..64c2f5851c 100644
--- a/src/server/api/endpoints/notes/global-timeline.ts
+++ b/src/server/api/endpoints/notes/global-timeline.ts
@@ -81,7 +81,11 @@ export default define(meta, async (ps, user) => {
 			ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
 		.andWhere('note.visibility = \'public\'')
 		.andWhere('note.channelId IS NULL')
-		.leftJoinAndSelect('note.user', 'user');
+		.leftJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
 
 	generateRepliesQuery(query, user);
 	if (user) generateMutedUserQuery(query, user);
diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts
index 2b91b8c67b..c01e170aa9 100644
--- a/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -130,6 +130,10 @@ export default define(meta, async (ps, user) => {
 				.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
 		}))
 		.leftJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser')
 		.setParameters(followingQuery.getParameters());
 
 	generateChannelQuery(query, user);
diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts
index 51e35e6241..31abb26882 100644
--- a/src/server/api/endpoints/notes/local-timeline.ts
+++ b/src/server/api/endpoints/notes/local-timeline.ts
@@ -98,7 +98,11 @@ export default define(meta, async (ps, user) => {
 	const query = makePaginationQuery(Notes.createQueryBuilder('note'),
 			ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
 		.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
-		.leftJoinAndSelect('note.user', 'user');
+		.leftJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
 
 	generateChannelQuery(query, user);
 	generateRepliesQuery(query, user);
diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts
index 1e3014bd46..83e890d9ad 100644
--- a/src/server/api/endpoints/notes/mentions.ts
+++ b/src/server/api/endpoints/notes/mentions.ts
@@ -63,7 +63,11 @@ export default define(meta, async (ps, user) => {
 			.where(`:meId = ANY(note.mentions)`, { meId: user.id })
 			.orWhere(`:meId = ANY(note.visibleUserIds)`, { meId: user.id });
 		}))
-		.leftJoinAndSelect('note.user', 'user');
+		.leftJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
 
 	generateVisibilityQuery(query, user);
 	generateMutedUserQuery(query, user);
diff --git a/src/server/api/endpoints/notes/renotes.ts b/src/server/api/endpoints/notes/renotes.ts
index 31c24f294a..f528197220 100644
--- a/src/server/api/endpoints/notes/renotes.ts
+++ b/src/server/api/endpoints/notes/renotes.ts
@@ -68,7 +68,11 @@ export default define(meta, async (ps, user) => {
 
 	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
 		.andWhere(`note.renoteId = :renoteId`, { renoteId: note.id })
-		.leftJoinAndSelect('note.user', 'user');
+		.leftJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
 
 	generateVisibilityQuery(query, user);
 	if (user) generateMutedUserQuery(query, user);
diff --git a/src/server/api/endpoints/notes/replies.ts b/src/server/api/endpoints/notes/replies.ts
index 9fad74c78e..5ac663d77a 100644
--- a/src/server/api/endpoints/notes/replies.ts
+++ b/src/server/api/endpoints/notes/replies.ts
@@ -59,7 +59,11 @@ export const meta = {
 export default define(meta, async (ps, user) => {
 	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
 		.andWhere('note.replyId = :replyId', { replyId: ps.noteId })
-		.leftJoinAndSelect('note.user', 'user');
+		.leftJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
 
 	generateVisibilityQuery(query, user);
 	if (user) generateMutedUserQuery(query, user);
diff --git a/src/server/api/endpoints/notes/search-by-tag.ts b/src/server/api/endpoints/notes/search-by-tag.ts
index e0f7f4d62c..d878f379c3 100644
--- a/src/server/api/endpoints/notes/search-by-tag.ts
+++ b/src/server/api/endpoints/notes/search-by-tag.ts
@@ -95,7 +95,11 @@ export const meta = {
 
 export default define(meta, async (ps, me) => {
 	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
-		.leftJoinAndSelect('note.user', 'user');
+		.leftJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
 
 	generateVisibilityQuery(query, me);
 	if (me) generateMutedUserQuery(query, me);
diff --git a/src/server/api/endpoints/notes/search.ts b/src/server/api/endpoints/notes/search.ts
index 1aca056299..ae0e9242a6 100644
--- a/src/server/api/endpoints/notes/search.ts
+++ b/src/server/api/endpoints/notes/search.ts
@@ -79,7 +79,11 @@ export default define(meta, async (ps, me) => {
 
 		query
 			.andWhere('note.text ILIKE :q', { q: `%${ps.query}%` })
-			.leftJoinAndSelect('note.user', 'user');
+			.leftJoinAndSelect('note.user', 'user')
+			.leftJoinAndSelect('note.reply', 'reply')
+			.leftJoinAndSelect('note.renote', 'renote')
+			.leftJoinAndSelect('reply.user', 'replyUser')
+			.leftJoinAndSelect('renote.user', 'renoteUser');
 
 		generateVisibilityQuery(query, me);
 		if (me) generateMutedUserQuery(query, me);
diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts
index f09f3d1733..c91bea0c1f 100644
--- a/src/server/api/endpoints/notes/timeline.ts
+++ b/src/server/api/endpoints/notes/timeline.ts
@@ -123,6 +123,10 @@ export default define(meta, async (ps, user) => {
 			if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`);
 		}))
 		.leftJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser')
 		.setParameters(followingQuery.getParameters());
 
 	generateChannelQuery(query, user);
diff --git a/src/server/api/endpoints/notes/user-list-timeline.ts b/src/server/api/endpoints/notes/user-list-timeline.ts
index b0ff499d95..040f017fd5 100644
--- a/src/server/api/endpoints/notes/user-list-timeline.ts
+++ b/src/server/api/endpoints/notes/user-list-timeline.ts
@@ -131,6 +131,10 @@ export default define(meta, async (ps, user) => {
 	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
 		.andWhere(`note.userId IN (${ listQuery.getQuery() })`)
 		.leftJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser')
 		.setParameters(listQuery.getParameters());
 
 	generateVisibilityQuery(query, user);
diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts
index 33e3ecb03f..91d1d7f4dd 100644
--- a/src/server/api/endpoints/users/notes.ts
+++ b/src/server/api/endpoints/users/notes.ts
@@ -131,7 +131,11 @@ export default define(meta, async (ps, me) => {
 	//#region Construct query
 	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
 		.andWhere('note.userId = :userId', { userId: user.id })
-		.leftJoinAndSelect('note.user', 'user');
+		.leftJoinAndSelect('note.user', 'user')
+		.leftJoinAndSelect('note.reply', 'reply')
+		.leftJoinAndSelect('note.renote', 'renote')
+		.leftJoinAndSelect('reply.user', 'replyUser')
+		.leftJoinAndSelect('renote.user', 'renoteUser');
 
 	generateVisibilityQuery(query, me);
 	if (me) generateMutedUserQuery(query, me, user);