diff --git a/server/search/queries.ts b/server/search/queries.ts index bc61290092..fc7adfc3ad 100644 --- a/server/search/queries.ts +++ b/server/search/queries.ts @@ -18,3 +18,43 @@ export const getSearchUsers = async (searchString: string, limit = 5) => { limit, }); }; + +/** + * Batch-search for multiple author names in a single query, returning a map + * from each searched name to its matching users (up to `limit` per name). + */ +export const batchSearchUsers = async (names: string[], limit = 5) => { + const nonEmpty = names.filter((n) => n.length > 0); + if (nonEmpty.length === 0) { + return new Map(); + } + + const users = await User.findAll({ + where: { + [Op.or]: nonEmpty.flatMap((name) => [ + { fullName: { [Op.iLike]: `%${name}%` } }, + { slug: { [Op.iLike]: `%${name}%` } }, + { email: { [Op.iLike]: name } }, + ]), + }, + attributes: ['id', 'slug', 'fullName', 'initials', 'avatar'], + }); + + const resultMap = new Map(nonEmpty.map((name) => [name, []])); + const allUsers = users.map((u) => u.toJSON()); + + for (const name of nonEmpty) { + const lowerName = name.toLowerCase(); + const matches = allUsers + .filter( + (u: any) => + u.fullName?.toLowerCase().includes(lowerName) || + u.slug?.toLowerCase().includes(lowerName) || + u.email?.toLowerCase() === lowerName, + ) + .slice(0, limit); + resultMap.set(name, matches); + } + + return resultMap; +}; diff --git a/workers/queue.ts b/workers/queue.ts index 08fce82f6f..975b50175c 100644 --- a/workers/queue.ts +++ b/workers/queue.ts @@ -19,6 +19,7 @@ let currentWorkerThreads = 0; /** Nice to be able to run certain tasks longer than the default timeout */ const customTimeouts = { archive: 14_400, // 4 hours + import: 300, // 5 minutes } satisfies Partial>; if (env.NODE_ENV === 'production') { diff --git a/workers/tasks/import/metadata.ts b/workers/tasks/import/metadata.ts index 2f9e736d2d..a15c1214f8 100644 --- a/workers/tasks/import/metadata.ts +++ b/workers/tasks/import/metadata.ts @@ -3,7 +3,7 @@ import type { Falsy } from 'types'; import { metaValueToJsonSerializable, metaValueToString } from '@pubpub/prosemirror-pandoc'; import unidecode from 'unidecode'; -import { getSearchUsers } from 'server/search/queries'; +import { batchSearchUsers } from 'server/search/queries'; import { isValidDate } from 'utils/dates'; const getAuthorsArray = (author) => { @@ -28,15 +28,19 @@ const getAttributions = async (author) => { if (author) { const authorsArray = getAuthorsArray(author); const authorEntries = authorsArray.map(metaValueToJsonSerializable) as any[]; - const attributions = await Promise.all( - authorEntries.map(async (authorEntry: string) => { - if (typeof authorEntry === 'string') { - const users = await getSearchUsers(authorEntry); - return { name: authorEntry, users: users.map((user) => user.toJSON()) }; - } - return authorEntry; - }), + const stringEntries = authorEntries.filter( + (entry): entry is string => typeof entry === 'string', ); + const usersByName = + stringEntries.length > 0 + ? await batchSearchUsers(stringEntries) + : new Map(); + const attributions = authorEntries.map((authorEntry) => { + if (typeof authorEntry === 'string') { + return { name: authorEntry, users: usersByName.get(authorEntry) ?? [] }; + } + return authorEntry; + }); return attributions; } return null;