Как улучшить или избежать цикла поиска / извлечения в публикации метеора?

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...
Другие вопросы по тегам