From 957015ecac832773b7aeff9f99f0ee45c7df0594 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 2 Jul 2026 19:34:39 -0600 Subject: [PATCH 1/5] perf: optimize model aggregation pipelines and remove dead ones - LivechatRooms.getAnalyticsBetweenDate/getAnalyticsMetricsBetweenDateWithMessages: count messages inside the $lookup sub-pipeline instead of $unwind+$group (conversation-totalizers -39% median; preserves msgs:1 for zero-message rooms) - Messages.findAllNumberOfTransferredRooms: group by rid before the room $lookup, one join per distinct room projecting only departmentId (3.18x) - LivechatInquiry: widen {status} index to {status, department} so getDistinctQueuedDepartments becomes a covered scan (120k -> 0 docs examined per queue-worker cycle); allowDiskUse on getCurrentSortedQueueAsync - Rooms.getSubscribedRoomIdsWithoutE2EKeys: filter subscriptions inside the $lookup, avoiding full-membership arrays that can overflow the 16MB doc limit - Sessions: trim session:'$$ROOT' to used fields in hourly engagement pipelines, add allowDiskUse, fix logintAt index typo - Users: drop redundant second $group in getTotalOfRegisteredUsersByDate; replace $unwind+$group regroup with $project+$map in findAgentsWithDepartments - Remove dead methods: LivechatRooms.findAllNumberOfTransferredRooms, Rooms.findChannelsByTypesWithNumberOfMessagesBetweenDate (+builder), Messages.findOneByFederationIdAndUsernameOnReactions, Sessions.getActiveUsersBetweenDates, LivechatDepartmentAgents.findAgentsByAgentIdAndBusinessHourId (CE+EE files) All rewritten pipelines verified to return identical output against seeded data. --- .changeset/model-aggregations-perf.md | 7 + .../server/models/LivechatDepartmentAgents.ts | 7 - .../models/raw/LivechatDepartmentAgents.ts | 27 --- apps/meteor/ee/server/models/startup.ts | 1 - .../models/ILivechatDepartmentAgentsModel.ts | 1 - .../src/models/ILivechatRoomsModel.ts | 2 - .../src/models/IMessagesModel.ts | 2 - .../model-typings/src/models/IRoomsModel.ts | 9 - .../src/models/ISessionsModel.ts | 1 - .../src/models/LivechatDepartmentAgents.ts | 4 - packages/models/src/models/LivechatInquiry.ts | 2 + packages/models/src/models/LivechatRooms.ts | 212 ++---------------- packages/models/src/models/Messages.ts | 61 ++--- packages/models/src/models/Rooms.ts | 160 ++----------- packages/models/src/models/Sessions.ts | 35 +-- packages/models/src/models/Users.ts | 35 ++- 16 files changed, 83 insertions(+), 483 deletions(-) create mode 100644 .changeset/model-aggregations-perf.md delete mode 100644 apps/meteor/ee/server/models/LivechatDepartmentAgents.ts delete mode 100644 apps/meteor/ee/server/models/raw/LivechatDepartmentAgents.ts diff --git a/.changeset/model-aggregations-perf.md b/.changeset/model-aggregations-perf.md new file mode 100644 index 0000000000000..49b11b3235c1e --- /dev/null +++ b/.changeset/model-aggregations-perf.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/meteor': patch +'@rocket.chat/models': patch +'@rocket.chat/model-typings': patch +--- + +Improved loading time of the Omnichannel Analytics dashboards and Engagement Dashboard "users by time of day" charts on workspaces with large message/session volumes, and sped up encrypted-room key distribution when a user resets their E2EE keys. diff --git a/apps/meteor/ee/server/models/LivechatDepartmentAgents.ts b/apps/meteor/ee/server/models/LivechatDepartmentAgents.ts deleted file mode 100644 index 4365a750b0109..0000000000000 --- a/apps/meteor/ee/server/models/LivechatDepartmentAgents.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { registerModel } from '@rocket.chat/models'; - -import { LivechatDepartmentAgents } from './raw/LivechatDepartmentAgents'; -import { trashCollection } from '../../../server/database/trash'; -import { db } from '../../../server/database/utils'; - -registerModel('ILivechatDepartmentAgentsModel', new LivechatDepartmentAgents(db, trashCollection)); diff --git a/apps/meteor/ee/server/models/raw/LivechatDepartmentAgents.ts b/apps/meteor/ee/server/models/raw/LivechatDepartmentAgents.ts deleted file mode 100644 index 64174f03fe2ce..0000000000000 --- a/apps/meteor/ee/server/models/raw/LivechatDepartmentAgents.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { ILivechatDepartmentAgents } from '@rocket.chat/core-typings'; -import { LivechatDepartmentAgentsRaw } from '@rocket.chat/models'; - -export class LivechatDepartmentAgents extends LivechatDepartmentAgentsRaw { - override findAgentsByAgentIdAndBusinessHourId(agentId: string, businessHourId: string): Promise { - const match = { - $match: { agentId }, - }; - const lookup = { - $lookup: { - from: 'rocketchat_livechat_department', - localField: 'departmentId', - foreignField: '_id', - as: 'departments', - }, - }; - const unwind = { - $unwind: { - path: '$departments', - preserveNullAndEmptyArrays: true, - }, - }; - const withBusinessHourId = { $match: { 'departments.businessHourId': businessHourId } }; - const project = { $project: { departments: 0 } }; - return this.col.aggregate([match, lookup, unwind, withBusinessHourId, project]).toArray(); - } -} diff --git a/apps/meteor/ee/server/models/startup.ts b/apps/meteor/ee/server/models/startup.ts index a3e1c6175f5cc..c8d13b38fca55 100644 --- a/apps/meteor/ee/server/models/startup.ts +++ b/apps/meteor/ee/server/models/startup.ts @@ -17,5 +17,4 @@ void License.onLicense('livechat-enterprise', () => { import('./LivechatInquiry'); import('./LivechatDepartment'); import('./Users'); - import('./LivechatDepartmentAgents'); }); diff --git a/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts b/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts index 69c1d7ce63f4f..881ff9f45121c 100644 --- a/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts +++ b/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts @@ -40,7 +40,6 @@ export interface ILivechatDepartmentAgentsModel extends IBaseModel>; findByDepartmentIds(departmentIds: string[], options?: Record): FindCursor; - findAgentsByAgentIdAndBusinessHourId(_agentId: string, _businessHourId: string): Promise; setDepartmentEnabledByDepartmentId(departmentId: string, departmentEnabled: boolean): Promise; removeByDepartmentId(departmentId: string): Promise; findByDepartmentId(departmentId: string, options?: FindOptions): FindCursor; diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index d159e5307aa41..29b55bef24bec 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -53,8 +53,6 @@ export interface ILivechatRoomsModel extends IBaseModel { findAllServiceTime(params: Period & WithDepartment & WithOnlyCount & WithOptions): any; - findAllNumberOfTransferredRooms(params: Period & WithDepartment & WithOptions): any; - countAllOpenChatsBetweenDate(params: Period & WithDepartment): any; countAllClosedChatsBetweenDate(params: Period & WithDepartment): any; diff --git a/packages/model-typings/src/models/IMessagesModel.ts b/packages/model-typings/src/models/IMessagesModel.ts index 0d16750f012c8..ac467f3037684 100644 --- a/packages/model-typings/src/models/IMessagesModel.ts +++ b/packages/model-typings/src/models/IMessagesModel.ts @@ -90,8 +90,6 @@ export interface IMessagesModel extends IBaseModel { unsetFederationReactionEventId(federationEventId: string, _id: string, reaction: string): Promise; - findOneByFederationIdAndUsernameOnReactions(federationEventId: string, username: string): Promise; - findOneByFederationId(federationEventId: string): Promise; findLatestFederationThreadMessageByTmid(tmid: string, messageId: IMessage['_id']): Promise; diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index 39ce7d7ae3379..0be8e765a110a 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -123,15 +123,6 @@ export interface IRoomsModel extends IBaseModel { setTeamDefaultById(rid: IRoom['_id'], teamDefault: NonNullable, options?: UpdateOptions): Promise; - findChannelsByTypesWithNumberOfMessagesBetweenDate(params: { - types: Array; - start: number; - end: number; - startOfLastWeek: number; - endOfLastWeek: number; - options?: any; - }): AggregationCursor; - findOneByName(name: NonNullable, options?: FindOptions): Promise; findDefaultRoomsForTeam(teamId: any): FindCursor; diff --git a/packages/model-typings/src/models/ISessionsModel.ts b/packages/model-typings/src/models/ISessionsModel.ts index 654658a209b26..dd3102aae45c2 100644 --- a/packages/model-typings/src/models/ISessionsModel.ts +++ b/packages/model-typings/src/models/ISessionsModel.ts @@ -51,7 +51,6 @@ export interface ISessionsModel extends IBaseModel { count?: number; }): Promise<{ sessions: Array; count: number; offset: number; total: number }>; - getActiveUsersBetweenDates({ start, end }: DestructuredRange): Promise; findLastLoginByIp(ip: string): Promise; findOneBySessionId(sessionId: string): Promise; findOneBySessionIdAndUserId(sessionId: string, userId: string): Promise; diff --git a/packages/models/src/models/LivechatDepartmentAgents.ts b/packages/models/src/models/LivechatDepartmentAgents.ts index a0e15e6f1df3f..b95496285efdf 100644 --- a/packages/models/src/models/LivechatDepartmentAgents.ts +++ b/packages/models/src/models/LivechatDepartmentAgents.ts @@ -116,10 +116,6 @@ export class LivechatDepartmentAgentsRaw extends BaseRaw { - return []; - } - setDepartmentEnabledByDepartmentId(departmentId: string, departmentEnabled: boolean): Promise { return this.updateMany({ departmentId }, { $set: { departmentEnabled } }); } diff --git a/packages/models/src/models/LivechatInquiry.ts b/packages/models/src/models/LivechatInquiry.ts index f3b24ae1d2cae..d7fca6555cdc0 100644 --- a/packages/models/src/models/LivechatInquiry.ts +++ b/packages/models/src/models/LivechatInquiry.ts @@ -63,6 +63,7 @@ export class LivechatInquiryRaw extends BaseRaw implemen { key: { status: 1, + department: 1, }, }, { @@ -284,6 +285,7 @@ export class LivechatInquiryRaw extends BaseRaw implemen return this.col .aggregate & { position: number }>(filter, { readPreference: readSecondaryPreferred(), + allowDiskUse: true, }) .toArray(); } diff --git a/packages/models/src/models/LivechatRooms.ts b/packages/models/src/models/LivechatRooms.ts index b03445ff44bf7..663894a6a8868 100644 --- a/packages/models/src/models/LivechatRooms.ts +++ b/packages/models/src/models/LivechatRooms.ts @@ -592,135 +592,6 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return this.col.aggregate(params, { readPreference: readSecondaryPreferred() }); } - findAllNumberOfTransferredRooms({ - start, - end, - departmentId, - options = {}, - }: { - start: Date; - end: Date; - departmentId?: string; - options?: { offset?: number; count?: number; sort?: { [k: string]: number } }; - }) { - const match: Document = { - $match: { - t: 'l', - ts: { $gte: new Date(start), $lte: new Date(end) }, - }, - }; - const departmentsLookup = { - $lookup: { - from: 'rocketchat_livechat_department', - localField: 'departmentId', - foreignField: '_id', - as: 'departments', - }, - }; - const departmentsUnwind = { - $unwind: { - path: '$departments', - preserveNullAndEmptyArrays: true, - }, - }; - const departmentsGroup = { - $group: { - _id: { - _id: null, - departmentId: '$departments._id', - name: '$departments.name', - }, - rooms: { $push: '$$ROOT' }, - }, - }; - const departmentsProject = { - $project: { - _id: '$_id.departmentId', - name: '$_id.name', - rooms: 1, - }, - }; - const roomsUnwind = { - $unwind: { - path: '$rooms', - preserveNullAndEmptyArrays: true, - }, - }; - const messagesLookup = { - $lookup: { - from: 'rocketchat_message', - localField: 'rooms._id', - foreignField: 'rid', - as: 'messages', - }, - }; - const messagesProject = { - $project: { - _id: 1, - name: 1, - messages: { - $filter: { - input: '$messages', - as: 'message', - cond: { - $and: [{ $eq: ['$$message.t', 'livechat_transfer_history'] }], - }, - }, - }, - }, - }; - const transferProject = { - $project: { - name: 1, - transfers: { $size: { $ifNull: ['$messages', []] } }, - }, - }; - const transferGroup = { - $group: { - _id: { - departmentId: '$_id', - name: '$name', - }, - numberOfTransferredRooms: { $sum: '$transfers' }, - }, - }; - const presentationProject = { - $project: { - _id: { $ifNull: ['$_id.departmentId', null] }, - name: { $ifNull: ['$_id.name', null] }, - numberOfTransferredRooms: 1, - }, - }; - const firstParams: Document[] = [match, departmentsLookup, departmentsUnwind]; - if (departmentId && departmentId !== 'undefined') { - firstParams.push({ - $match: { - 'departments._id': departmentId, - }, - }); - } - const sort = { $sort: options.sort || { name: 1 } }; - const params: Document[] = [ - ...firstParams, - departmentsGroup, - departmentsProject, - roomsUnwind, - messagesLookup, - messagesProject, - transferProject, - transferGroup, - presentationProject, - sort, - ]; - if (options.offset) { - params.push({ $skip: options.offset }); - } - if (options.count) { - params.push({ $limit: options.count }); - } - return this.col.aggregate(params, { allowDiskUse: true, readPreference: readSecondaryPreferred() }).toArray(); - } - countAllOpenChatsBetweenDate({ start, end, departmentId }: { start: Date; end: Date; departmentId?: string }) { const query: Filter = { 't': 'l', @@ -2167,12 +2038,11 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive ...extraMatchers, }, }, - { $addFields: { roomId: '$_id' } }, { $lookup: { from: 'rocketchat_message', // mongo doesn't like _id as variable name here :( - let: { roomId: '$roomId' }, + let: { roomId: '$_id' }, pipeline: [ { $match: { @@ -2190,40 +2060,21 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive }, }, }, + { $count: 'total' }, ], as: 'messages', }, }, - { - $unwind: { - path: '$messages', - preserveNullAndEmptyArrays: true, - }, - }, - { - $group: { - _id: { - _id: '$_id', - ts: '$ts', - departmentId: '$departmentId', - open: '$open', - servedBy: '$servedBy', - metrics: '$metrics', - }, - messagesCount: { - $sum: 1, - }, - }, - }, { $project: { - _id: '$_id._id', - ts: '$_id.ts', - departmentId: '$_id.departmentId', - open: '$_id.open', - servedBy: '$_id.servedBy', - metrics: '$_id.metrics', - msgs: '$messagesCount', + _id: 1, + ts: 1, + departmentId: 1, + open: 1, + servedBy: 1, + metrics: 1, + // rooms without matching messages have always reported msgs: 1 (the room doc itself survived the old $unwind+$group count) + msgs: { $ifNull: [{ $arrayElemAt: ['$messages.total', 0] }, 1] }, }, }, ], @@ -2244,12 +2095,11 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive ...(departmentId && departmentId !== 'undefined' && { departmentId }), }, }, - { $addFields: { roomId: '$_id' } }, { $lookup: { from: 'rocketchat_message', // mongo doesn't like _id as variable name here :( - let: { roomId: '$roomId' }, + let: { roomId: '$_id' }, pipeline: [ { $match: { @@ -2266,42 +2116,22 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive }, }, }, + { $count: 'total' }, ], as: 'messages', }, }, - { - $unwind: { - path: '$messages', - preserveNullAndEmptyArrays: true, - }, - }, - { - $group: { - _id: { - _id: '$_id', - ts: '$ts', - departmentId: '$departmentId', - open: '$open', - servedBy: '$servedBy', - metrics: '$metrics', - onHold: '$onHold', - }, - messagesCount: { - $sum: 1, - }, - }, - }, { $project: { - _id: '$_id._id', - ts: '$_id.ts', - departmentId: '$_id.departmentId', - open: '$_id.open', - servedBy: '$_id.servedBy', - metrics: '$_id.metrics', - msgs: '$messagesCount', - onHold: '$_id.onHold', + _id: 1, + ts: 1, + departmentId: 1, + open: 1, + servedBy: 1, + metrics: 1, + onHold: 1, + // rooms without matching messages have always reported msgs: 1 (the room doc itself survived the old $unwind+$group count) + msgs: { $ifNull: [{ $arrayElemAt: ['$messages.total', 0] }, 1] }, }, }, ], diff --git a/packages/models/src/models/Messages.ts b/packages/models/src/models/Messages.ts index e4d3f1f4885bb..ba4b6a357f7d7 100644 --- a/packages/models/src/models/Messages.ts +++ b/packages/models/src/models/Messages.ts @@ -192,27 +192,28 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { ts: { $gte: new Date(start), $lte: new Date(end) }, }, }; + const groupByRoom = { + $group: { + _id: '$rid', + transfers: { $sum: 1 }, + }, + }; const lookup = { $lookup: { from: 'rocketchat_room', - localField: 'rid', + localField: '_id', foreignField: '_id', + pipeline: [...(departmentId ? [{ $match: { departmentId } }] : []), { $project: { departmentId: 1 } }], as: 'room', }, }; - const unwind = { - $unwind: { - path: '$room', - preserveNullAndEmptyArrays: true, - }, - }; const group = { $group: { _id: { _id: null, - departmentId: '$room.departmentId', + departmentId: { $arrayElemAt: ['$room.departmentId', 0] }, }, - numberOfTransferredRooms: { $sum: 1 }, + numberOfTransferredRooms: { $sum: '$transfers' }, }, }; const project = { @@ -221,11 +222,12 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { numberOfTransferredRooms: 1, }, }; - const firstParams: Exclude['aggregate']>[0], undefined> = [match, lookup, unwind]; + const firstParams: Exclude['aggregate']>[0], undefined> = [match, groupByRoom, lookup]; if (departmentId) { + // rooms outside the department produce an empty lookup, same as the old post-$unwind department $match firstParams.push({ $match: { - 'room.departmentId': departmentId, + 'room.0': { $exists: true }, }, }); } @@ -559,43 +561,6 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { ); } - async findOneByFederationIdAndUsernameOnReactions(federationEventId: string, username: string): Promise { - return ( - await this.col - .aggregate( - [ - { - $match: { - t: { $ne: 'rm' }, - }, - }, - { - $project: { - document: '$$ROOT', - reactions: { $objectToArray: '$reactions' }, - }, - }, - { - $unwind: { - path: '$reactions', - }, - }, - { - $match: { - $and: [ - { 'reactions.v.usernames': { $in: [username] } }, - { [`reactions.v.federationReactionEventIds.${federationEventId}`]: username }, - ], - }, - }, - { $replaceRoot: { newRoot: '$document' } }, - ], - { readPreference: readSecondaryPreferred() }, - ) - .toArray() - )[0] as IMessage; - } - removeByRoomId(roomId: string): Promise { return this.deleteMany({ rid: roomId }); } diff --git a/packages/models/src/models/Rooms.ts b/packages/models/src/models/Rooms.ts index c930207f79b13..b2126632cf4db 100644 --- a/packages/models/src/models/Rooms.ts +++ b/packages/models/src/models/Rooms.ts @@ -9,7 +9,7 @@ import type { IUser, RocketChatRecordDeleted, } from '@rocket.chat/core-typings'; -import type { FindPaginated, IRoomsModel, IChannelsWithNumberOfMessagesBetweenDate } from '@rocket.chat/model-typings'; +import type { FindPaginated, IRoomsModel } from '@rocket.chat/model-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import type { AggregationCursor, @@ -483,144 +483,6 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.updateOne({ _id: rid }, { $set: { teamDefault } }, options); } - getChannelsWithNumberOfMessagesBetweenDateQuery({ - types, - start, - end, - startOfLastWeek, - endOfLastWeek, - options, - }: { - types: Array; - start: number; - end: number; - startOfLastWeek: number; - endOfLastWeek: number; - options?: any; - }) { - const typeMatch = { - $match: { - t: { $in: types }, - }, - }; - const lookup = { - $lookup: { - from: 'rocketchat_analytics', - localField: '_id', - foreignField: 'room._id', - as: 'messages', - }, - }; - const messagesProject = { - $project: { - room: '$$ROOT', - messages: { - $filter: { - input: '$messages', - as: 'message', - cond: { - $and: [{ $gte: ['$$message.date', start] }, { $lte: ['$$message.date', end] }], - }, - }, - }, - lastWeekMessages: { - $filter: { - input: '$messages', - as: 'message', - cond: { - $and: [{ $gte: ['$$message.date', startOfLastWeek] }, { $lte: ['$$message.date', endOfLastWeek] }], - }, - }, - }, - }, - }; - const messagesUnwind = { - $unwind: { - path: '$messages', - preserveNullAndEmptyArrays: true, - }, - }; - const messagesGroup = { - $group: { - _id: { - _id: '$room._id', - }, - room: { $first: '$room' }, - messages: { $sum: '$messages.messages' }, - lastWeekMessages: { $first: '$lastWeekMessages' }, - }, - }; - const lastWeekMessagesUnwind = { - $unwind: { - path: '$lastWeekMessages', - preserveNullAndEmptyArrays: true, - }, - }; - const lastWeekMessagesGroup = { - $group: { - _id: { - _id: '$room._id', - }, - room: { $first: '$room' }, - messages: { $first: '$messages' }, - lastWeekMessages: { $sum: '$lastWeekMessages.messages' }, - }, - }; - const presentationProject = { - $project: { - _id: 0, - room: { - _id: '$_id._id', - name: { $ifNull: ['$room.name', '$room.fname'] }, - ts: '$room.ts', - t: '$room.t', - _updatedAt: '$room._updatedAt', - usernames: '$room.usernames', - }, - messages: '$messages', - lastWeekMessages: '$lastWeekMessages', - diffFromLastWeek: { $subtract: ['$messages', '$lastWeekMessages'] }, - }, - }; - const firstParams = [typeMatch, lookup, messagesProject, messagesUnwind, messagesGroup]; - const lastParams = [lastWeekMessagesUnwind, lastWeekMessagesGroup, presentationProject]; - - const sort = { $sort: options?.sort || { messages: -1 } }; - const sortAndPaginationParams: Exclude['aggregate']>[0], undefined> = [sort]; - - if (options?.offset) { - sortAndPaginationParams.push({ $skip: options.offset }); - } - - if (options?.count) { - sortAndPaginationParams.push({ $limit: options.count }); - } - const params: Exclude['aggregate']>[0], undefined> = [...firstParams]; - - if (options?.sort) { - params.push(...lastParams, ...sortAndPaginationParams); - } else { - params.push(...sortAndPaginationParams, ...lastParams, sort); - } - - return params; - } - - findChannelsByTypesWithNumberOfMessagesBetweenDate(params: { - types: Array; - start: number; - end: number; - startOfLastWeek: number; - endOfLastWeek: number; - options?: any; - }): AggregationCursor { - const aggregationParams = this.getChannelsWithNumberOfMessagesBetweenDateQuery(params); - return this.col.aggregate(aggregationParams, { - allowDiskUse: true, - readPreference: readSecondaryPreferred(), - }); - } - findOneByNameOrFname(name: NonNullable, options: FindOptions = {}): Promise { const query = { $or: [ @@ -2131,18 +1993,24 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { from: 'rocketchat_subscription', localField: '_id', foreignField: 'rid', + pipeline: [ + { + $match: { + 'u._id': uid, + 'E2EKey': { + $exists: false, + }, + }, + }, + { $limit: 1 }, + { $project: { _id: 1 } }, + ], as: 'subs', }, }, - { - $unwind: '$subs', - }, { $match: { - 'subs.u._id': uid, - 'subs.E2EKey': { - $exists: false, - }, + 'subs.0': { $exists: true }, }, }, { diff --git a/packages/models/src/models/Sessions.ts b/packages/models/src/models/Sessions.ts index 3763bf38b457a..01629d851b5d3 100644 --- a/packages/models/src/models/Sessions.ts +++ b/packages/models/src/models/Sessions.ts @@ -987,31 +987,13 @@ export class SessionsRaw extends BaseRaw implements ISessionsModel { { key: { sessionId: 1, instanceId: 1, year: 1, month: 1, day: 1 } }, { key: { _computedAt: 1 }, expireAfterSeconds: 60 * 60 * 24 * 45 }, { - key: { 'loginToken': 1, 'logoutAt': 1, 'userId': 1, 'device.name': 1, 'device.os.name': 1, 'logintAt': -1 }, + key: { 'loginToken': 1, 'logoutAt': 1, 'userId': 1, 'device.name': 1, 'device.os.name': 1, 'loginAt': -1 }, partialFilterExpression: { loginToken: { $exists: true } }, background: true, }, ]; } - async getActiveUsersBetweenDates({ start, end }: DestructuredRange): Promise { - return this.col - .aggregate([ - { - $match: { - ...matchBasedOnDate(start, end), - type: 'user_daily', - }, - }, - { - $group: { - _id: '$userId', - }, - }, - ]) - .toArray(); - } - async findLastLoginByIp(ip: string): Promise { return this.findOne( { @@ -1120,7 +1102,7 @@ export class SessionsRaw extends BaseRaw implements ISessionsModel { range: { $range: [0, 24, groupSize], }, - session: '$$ROOT', + session: { loginAt: '$loginAt', closedAt: '$closedAt', userId: '$userId' }, }, }; const unwind = { @@ -1143,7 +1125,7 @@ export class SessionsRaw extends BaseRaw implements ISessionsModel { .aggregate<{ hour: number; users: number; - }>([match, rangeProject, unwind, groups.listGroup, groups.countGroup, presentationProject, sort]) + }>([match, rangeProject, unwind, groups.listGroup, groups.countGroup, presentationProject, sort], { allowDiskUse: true }) .toArray(); } @@ -1211,7 +1193,14 @@ export class SessionsRaw extends BaseRaw implements ISessionsModel { range: { $range: [{ $hour: '$loginAt' }, { $sum: [{ $ifNull: [{ $hour: '$closedAt' }, 23] }, 1] }], }, - session: '$$ROOT', + session: { + loginAt: '$loginAt', + closedAt: '$closedAt', + userId: '$userId', + day: '$day', + month: '$month', + year: '$year', + }, }, }; const unwind = { @@ -1245,7 +1234,7 @@ export class SessionsRaw extends BaseRaw implements ISessionsModel { month: number; year: number; users: number; - }>([match, rangeProject, unwind, groups.listGroup, groups.countGroup, presentationProject, sort]) + }>([match, rangeProject, unwind, groups.listGroup, groups.countGroup, presentationProject, sort], { allowDiskUse: true }) .toArray(); } diff --git a/packages/models/src/models/Users.ts b/packages/models/src/models/Users.ts index 6a84f20530369..35328761ca4ef 100644 --- a/packages/models/src/models/Users.ts +++ b/packages/models/src/models/Users.ts @@ -272,21 +272,20 @@ export class UsersRaw extends BaseRaw> implements IU }, }, { - $unwind: { - path: '$departments', - preserveNullAndEmptyArrays: true, - }, - }, - { - $group: { - _id: '$_id', - username: { $first: '$username' }, - status: { $first: '$status' }, - statusLivechat: { $first: '$statusLivechat' }, - name: { $first: '$name' }, - emails: { $first: '$emails' }, - livechat: { $first: '$livechat' }, - departments: { $push: '$departments.departmentId' }, + $project: { + username: 1, + status: 1, + statusLivechat: 1, + name: 1, + emails: 1, + livechat: 1, + departments: { + $map: { + input: '$departments', + as: 'department', + in: '$$department.departmentId', + }, + }, }, }, { @@ -1042,12 +1041,6 @@ export class UsersRaw extends BaseRaw> implements IU users: { $sum: 1 }, }, }, - { - $group: { - _id: '$_id', - users: { $sum: '$users' }, - }, - }, { $project: { _id: 0, From 05d5c6a92dbd4d54e5eef48b540accf0abb21c99 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 2 Jul 2026 20:23:17 -0600 Subject: [PATCH 2/5] perf: drop no-op allowDiskUse additions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rocket.Chat requires MongoDB >=7.0, where allowDiskUseByDefault is true (since 6.0) and memory-exceeding stages spill to disk automatically — the explicit flags added no behavior on supported deployments. --- packages/models/src/models/LivechatInquiry.ts | 1 - packages/models/src/models/Sessions.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/models/src/models/LivechatInquiry.ts b/packages/models/src/models/LivechatInquiry.ts index d7fca6555cdc0..2061b70eb037e 100644 --- a/packages/models/src/models/LivechatInquiry.ts +++ b/packages/models/src/models/LivechatInquiry.ts @@ -285,7 +285,6 @@ export class LivechatInquiryRaw extends BaseRaw implemen return this.col .aggregate & { position: number }>(filter, { readPreference: readSecondaryPreferred(), - allowDiskUse: true, }) .toArray(); } diff --git a/packages/models/src/models/Sessions.ts b/packages/models/src/models/Sessions.ts index 01629d851b5d3..940eb520fa4c5 100644 --- a/packages/models/src/models/Sessions.ts +++ b/packages/models/src/models/Sessions.ts @@ -1125,7 +1125,7 @@ export class SessionsRaw extends BaseRaw implements ISessionsModel { .aggregate<{ hour: number; users: number; - }>([match, rangeProject, unwind, groups.listGroup, groups.countGroup, presentationProject, sort], { allowDiskUse: true }) + }>([match, rangeProject, unwind, groups.listGroup, groups.countGroup, presentationProject, sort]) .toArray(); } @@ -1234,7 +1234,7 @@ export class SessionsRaw extends BaseRaw implements ISessionsModel { month: number; year: number; users: number; - }>([match, rangeProject, unwind, groups.listGroup, groups.countGroup, presentationProject, sort], { allowDiskUse: true }) + }>([match, rangeProject, unwind, groups.listGroup, groups.countGroup, presentationProject, sort]) .toArray(); } From ea4101aa5a768d726c06c0b220bf12ebe8341877 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 2 Jul 2026 20:24:27 -0600 Subject: [PATCH 3/5] chore: align changeset wording with measured results --- .changeset/model-aggregations-perf.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/model-aggregations-perf.md b/.changeset/model-aggregations-perf.md index 49b11b3235c1e..4069cb06bf22f 100644 --- a/.changeset/model-aggregations-perf.md +++ b/.changeset/model-aggregations-perf.md @@ -4,4 +4,4 @@ '@rocket.chat/model-typings': patch --- -Improved loading time of the Omnichannel Analytics dashboards and Engagement Dashboard "users by time of day" charts on workspaces with large message/session volumes, and sped up encrypted-room key distribution when a user resets their E2EE keys. +Improved loading time of the Omnichannel Analytics dashboards (conversation totalizers and the transferred-chats department report) on workspaces with large message volumes, reduced the database work done by the Omnichannel queue worker on busy queues, and fixed a failure that could prevent E2EE key redistribution in encrypted rooms with very large memberships. From 83eba621f138d251ae190f7e056658ede94bb509 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 2 Jul 2026 20:38:56 -0600 Subject: [PATCH 4/5] perf: revert Sessions $$ROOT trim, keep index typo fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The trimmed projection measured a consistent ~5% regression (128.5 -> 135.9ms at 40k sessions, 756 -> 795ms at 200k; non-overlapping ranges) — building the sub-object per document costs more than the smaller $unwind copies save, and there is no failure mode to protect against. Only the logintAt -> loginAt index typo fix remains. --- packages/models/src/models/Sessions.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/models/src/models/Sessions.ts b/packages/models/src/models/Sessions.ts index 940eb520fa4c5..2bcd5f5e32899 100644 --- a/packages/models/src/models/Sessions.ts +++ b/packages/models/src/models/Sessions.ts @@ -1102,7 +1102,7 @@ export class SessionsRaw extends BaseRaw implements ISessionsModel { range: { $range: [0, 24, groupSize], }, - session: { loginAt: '$loginAt', closedAt: '$closedAt', userId: '$userId' }, + session: '$$ROOT', }, }; const unwind = { @@ -1193,14 +1193,7 @@ export class SessionsRaw extends BaseRaw implements ISessionsModel { range: { $range: [{ $hour: '$loginAt' }, { $sum: [{ $ifNull: [{ $hour: '$closedAt' }, 23] }, 1] }], }, - session: { - loginAt: '$loginAt', - closedAt: '$closedAt', - userId: '$userId', - day: '$day', - month: '$month', - year: '$year', - }, + session: '$$ROOT', }, }; const unwind = { From 6630944a5a51e63cb4ed4acb1dde3f41a672cfdd Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 2 Jul 2026 20:44:47 -0600 Subject: [PATCH 5/5] perf: revert Users pipeline micro-cleanups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The redundant second $group in getTotalOfRegisteredUsersByDate operates on ~#days documents and the findAgentsWithDepartments regroup on a small admin listing — no measurable perf or memory gain, so they don't belong in this branch. --- packages/models/src/models/Users.ts | 35 +++++++++++++++++------------ 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/packages/models/src/models/Users.ts b/packages/models/src/models/Users.ts index 35328761ca4ef..6a84f20530369 100644 --- a/packages/models/src/models/Users.ts +++ b/packages/models/src/models/Users.ts @@ -272,20 +272,21 @@ export class UsersRaw extends BaseRaw> implements IU }, }, { - $project: { - username: 1, - status: 1, - statusLivechat: 1, - name: 1, - emails: 1, - livechat: 1, - departments: { - $map: { - input: '$departments', - as: 'department', - in: '$$department.departmentId', - }, - }, + $unwind: { + path: '$departments', + preserveNullAndEmptyArrays: true, + }, + }, + { + $group: { + _id: '$_id', + username: { $first: '$username' }, + status: { $first: '$status' }, + statusLivechat: { $first: '$statusLivechat' }, + name: { $first: '$name' }, + emails: { $first: '$emails' }, + livechat: { $first: '$livechat' }, + departments: { $push: '$departments.departmentId' }, }, }, { @@ -1041,6 +1042,12 @@ export class UsersRaw extends BaseRaw> implements IU users: { $sum: 1 }, }, }, + { + $group: { + _id: '$_id', + users: { $sum: '$users' }, + }, + }, { $project: { _id: 0,