perf (backend): improved post search with CW/alt text

Co-authored-by: naskya <m@naskya.net>
This commit is contained in:
sup39 2024-03-17 22:37:58 +09:00 committed by naskya
parent 2220d5c56e
commit cca63b7286
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C

View file

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