Как улучшить или избежать цикла поиска / извлечения в публикации метеора?
TL;DR:
Чат - это одна коллекция. ChatMess другой, который имеет сообщения, ссылающиеся на _id чата. Как получить последние сообщения из списка чатов с наименьшим количеством вычислений? Здесь цикл поиска / извлечения в цикле слишком тяжелый и длинный.
У меня есть эта публикация, которая используется для возврата курсора пользователю:
- Чаты, в которых он принимает участие (из коллекции чатов)
- Последнее сообщение от каждого сеанса чата, указанного в первом курсоре (из коллекции ChatMess)
В настоящее время логика заключается в:
- Получить список сеансов чата из профиля пользователя
- Найдите сеансы чата и пройдитесь по ним
- В цикле я нахожу одно последнее сообщение из этого сеанса чата и сохраняю его _id в массиве. Кроме того, я храню всех остальных пользователей _ids.
- Затем я нахожу сообщения, которые _id соответствуют сообщениям в моем массиве.
Вот моя главная проблема:
Нет ли более быстрого способа получить последние сообщения от каждого сеанса чата? С помощью этого алгоритма я легко достигаю 8000 мс времени отклика, что является слишком трудоемким временем вычислений, так как большая часть этого времени затрачивается на поиск / извлечение _id сообщений чата (cf связанный экран от Kadira).
Meteor.publish("publishNewChat", function() {
this.unblock();
// we get a list of chat _id
let chatIdList = _get_all_the_user_chats_ids(this.userId);
if (!chatList)
return ;
// get the chat sessions objects
let chats_cursor = Modules.both.queryGet({
type : 'chat',
method : 'find',
query : { _id: { $in: chatIdList } },
projection : { sort: { _id: 1 }, limit : 1000 }
});
let array_of_fetched_chats = chats_cursor.fetch();
let chat_ids = [];
// and here we loop through the chat documents in order to get the last message that's been attached to each of them
array_of_fetched_chats.forEach(function(e) {
let lastMess = Modules.both.queryGet({
type : 'chatMess',
method : 'findOne',
query : { chatId: e._id },
projection : { sort: { date: -1 } }
});
if (lastMess)
chat_ids.push(lastMess._id);
});
return ([
chats_cursor,
Modules.both.queryGet({
type : 'chatMess',
method : 'find',
query : { _id: { $in: chat_ids } },
projection : { sort: { date: -1 }, limit: 1000 }
})
]);
});
Наконец, это также добавляет задержку ко всем моим следующим запросам DDP. В настоящее время я использую this.unblock(), чтобы избежать этого, но я бы предпочел не использовать его здесь.
К вашему сведению, у меня есть еще одна публикация, которая обновляется каждый раз, когда клиент меняет свой текущий активный сеанс чата: на клиенте при маршрутизации в новый чат добавьте его _id в реактивный массив, который обновляет мою подписку getChatMess, чтобы получать на клиенте сообщения из каждого чата, который посетил пользователь, поскольку он подключился. Очевидно, что цель состоит в том, чтобы избавить сервер от отправки каждого сообщения из каждого сеанса чата, который посетил пользователь за свою жизнь.
К сожалению, мне не хватает идей, чтобы улучшить этот алгоритм без нарушения всей логики чата:S. У тебя есть идеи? Как бы вы сделали?
Спасибо вам.
РЕДАКТИРОВАТЬ: вот экран от Кадира, который ясно показывает проблему:
2 ответа
Вот решение, которое я разработал:
Meteor.publish("publishNewChat", function() {
this.unblock();
let user = Modules.both.queryGet({
type : 'users',
method : 'findOne',
query : { _id: this.userId },
projection : { fields: { "profile.chat": true } }
});
let thisUserschats = tryReach(user, "profile", "chat").value;
if (!thisUserschats)
return ;
thisUserschats = thisUserschats.map(function(e) { return (e.chatId); });
let chats = Modules.both.queryGet({
type : 'chat',
method : 'find',
query : { _id: { $in: thisUserschats } },
projection : { sort : { _id: 1 },
limit : 1000
}
});
let chatArray = chats.fetch(),
uids = cmid = [];
let messages_id_list = [],
i = chatArray.length;
let _parallelQuery = index => {
Meteor.setTimeout(function () {
let tmp = Modules.both.queryGet({
type : 'chatMess',
method : 'find',
query : { chatId: chatArray[index]._id },
projection: { limit: 1, sort: { date: -1 } }
});
tmp.forEach(doc => {
messages_id_list.push((doc && doc._id) ? doc._id : null);
});
}, 1);
}
while (--i >= 0)
_parallelQuery(i);
let cursors = {
chats : chats,
chatMessages : null
}
let interval = Meteor.setInterval(function () {
if (messages_id_list.length === chatArray.length)
{
Meteor.clearInterval(interval);
cursors.chatMessages = Modules.both.queryGet({
type : 'chatMess',
method : 'find',
query : { _id: { $in: messages_id_list } },
projection : { sort: { date: -1 }, limit: 1000 }
});
cursors.chats.observeChanges({
// ...
});
cursors.chatMessages.observeChanges({
// ...
});
self.ready();
self.onStop(() => subHandle.stop(); );
}
}, 10);
});
Я использовал асинхронную функцию с Meteor.setTimeout для распараллеливания запросов и сохранения индекса, ссылающегося на чат _id для поиска. Затем, когда запрос завершен, я добавляю последнее сообщение в массив. С Meteor.setInterval я проверяю длину массива, чтобы знать, когда все запросы выполнены. Затем, поскольку я больше не могу возвращать курсоры, я использую API низкого уровня публикации Meteor для управления публикацией документов.
К вашему сведению: в первой попытке я использовал 'findOne' в своих _parallelQueries, что делило мое время вычислений на 2/3. Но потом, благодаря другу, я попробовал функцию cursor.foreach(), которая позволила мне снова разделить время вычислений на 2!
В производстве тесты позволили мне перейти от времени отклика 7/8 секунды к среднему времени отклика 1,6 секунды:)
Надеюсь, это будет полезно для вас, люди!:)
Рассматривали ли вы использование пакета reywood/publishComposite? С помощью этого пакета вы можете публиковать связанные данные одним и тем же методом, не прибегая к куче логики для публикации правильных данных.
Приведенный ниже код должен помочь вам начать:
Meteor.publishComposite("publishNewChat", function() {
return [{
find:function(){
return Users.find({ _id: this.userId },{fields:{"profile.chat":1}});
},
children:[{
find:function(user){ //this function is passed each user returned from the cursor above.
return UserChats.find({userId:user._id},{fields:{blah:1,blah:1}}); //find the user chats using whatever query
},
children:[
//if there are any children of user chats that you need to publish, do so here...
{
find:function(userchat){
return Chats.find({_id:userchat.chatId})
},
children:[
{
find:function(chat){
return ChatMess.find({chatId:chat._id},{ sort: { date: -1 } });
},
children:[
{
find:function(chatMess){
var uids = _.without(chatMess.participants, this.userId);
return Users.find({_id:{$in:uids}});
}
}
]
}
]
}
]
},
]
}]
Это опубликует курсоры для всех документов, связанных с каждым из родительских документов. Это довольно быстро, я использую этот пакет на рабочей платформе с большим трафиком и большими наборами данных без проблем. На клиенте вы можете запросить документы, как обычно, чтобы получить те, которые вам нужно отобразить.
Что-то вроде:
Users.findOne({_id:Meteor.userId()});
UserChats.find({userId:Meteor.userId()});
etc...