Twitter-подобное приложение с использованием MongoDB - PullRequest
13 голосов
/ 28 октября 2010

Я создаю приложение, которое использует классический механизм «Follow» (тот, который используется Twitter и многими другими приложениями в Интернете).Я использую MongoDB.В моей системе есть различие: пользователь может следить за группами пользователей.Это означает, что если вы будете следовать за группой, вы автоматически будете следить за всеми пользователями, которые являются членами этой группы.Конечно, пользователи могут принадлежать к нескольким группам.

Вот что я придумал:

  • , когда пользователь A следует пользователь B , идентификатор пользователя B добавляется во встроенный массив (называемый following) в документе пользователя A
  • , чтобы отписаться, я удаляю идентификатор отслеживаемого пользователя из массива following
  • группы работают одинаково: когда пользователь A следует group X , идентификатор группы X добавляется в массив following.(Я на самом деле добавляю DBRef, поэтому я знаю, если соединение с пользователем или группой.)

  • , когда мне нужно проверить, следует ли user A группа X , я просто ищу идентификатор группы в следующем массиве пользователя A .

  • , когда мне нужно проверить, если пользователя A следует пользователь B , все становится немного сложнее.Документ каждого пользователя имеет встроенный массив, в котором перечислены все группы, к которым принадлежит пользователь.Поэтому я использую условие $or, чтобы проверить, следует ли пользователь A или следует за пользователем B напрямую или через группу.Вот так:

    db.users.find({'$or':{'following.ref.$id':$user_id,'following.ref.$ref','users'},{'following.ref.$id':{'$in':$group_ids},'following.ref.$ref':'groups'}}})

Это отлично работает, но я думаю, у меня есть несколько проблем.Например, как мне показать список подписчиков для конкретного пользователя, включая нумерацию страниц?Я не могу использовать skip () и limit () во встроенном документе.

Я мог бы изменить дизайн и использовать коллекцию userfollow, которая выполняла бы ту же работу, что и встроенный документ following.Проблема с этим подходом, который я попробовал, состоит в том, что с условием $or, которое я использовал ранее, пользователи, следующие за двумя группами, содержащими одного и того же пользователя, будут перечислены дважды.Чтобы избежать этого, я мог бы использовать group или MapReduce, что я и сделал, и это работает, но я бы хотел избежать этого, чтобы все было проще.Может быть, мне просто нужно думать нестандартно.Или, может быть, я ошибся с обеими попытками.Кто-нибудь уже должен был сделать подобное и придумал лучшее решение?

(Это на самом деле продолжение этого старого вопроса моего. Я решил опубликовать новый вопрос, чтобы лучше объяснить свою новую ситуацию; надеюсь, это не проблема.)

1 Ответ

15 голосов
/ 28 октября 2010

У вас есть два возможных способа, которыми пользователь может следовать за другим пользователем; либо напрямую, либо косвенно через группу, и в этом случае пользователь напрямую следует за группой. Давайте начнем с хранения этих прямых отношений между пользователями и группами:

{
  _id: "userA",
  followingUsers: [ "userB", "userC" ],
  followingGroups: [ "groupX", "groupY" ]
}

Теперь вам нужно будет быстро выяснить, за какими пользователями следует пользователь А, прямо или косвенно. Чтобы достичь этого, вы можете денормализовать группы, за которыми следует пользователь А. Допустим, группы X и Y определены следующим образом:

{
  _id: "groupX",
  members: [ "userC", "userD" ]
},
{
  _id: "groupY",
  members: [ "userD", "userE" ]
}

На основе этих групп и прямых связей, которые имеет пользователь A, вы можете создавать подписки между пользователями. Источник (и) подписки хранятся вместе с каждой подпиской. Для примера данных подписки будут выглядеть так:

// abusing exclamation mark to indicate a direct relation
{ ownerId: "userA", userId: "userB", origins: [ "!" ] },
{ ownerId: "userA", userId: "userC", origins: [ "!", "groupX" ] },
{ ownerId: "userA", userId: "userD", origins: [ "groupX", "groupY" ] },
{ ownerId: "userA", userId: "userE", origins: [ "groupY" ] }

Вы можете создать эти подписки довольно легко, используя вызов map-redu-finalize для отдельного пользователя. Если группа обновляется, вам нужно только повторно запустить map-limit для всех пользователей, которые следуют за группой, и подписки снова будут обновлены.

Карта-свертка

Следующие функции уменьшения карты будут генерировать подписки для одного пользователя.

map = function () {
  ownerId = this._id;

  this.followingUsers.forEach(function (userId) {
    emit({ ownerId: ownerId, userId: userId } , { origins: [ "!" ] });
  });

  this.followingGroups.forEach(function (groupId) {
    group = db.groups.findOne({ _id: groupId });

    group.members.forEach(function (userId) {
      emit({ ownerId: ownerId, userId: userId } , { origins: [ group._id ] });
    });
  });
}

reduce = function (key, values) {
  origins = [];

  values.forEach(function (value) {
    origins = origins.concat(value.origins);
  });

  return { origins: origins };
}

finalize = function (key, value) {
  db.subscriptions.update(key, { $set: { origins: value.origins }}, true);
}

Затем можно запустить map-reduction для одного пользователя, указав запрос, в данном случае для userA.

db.users.mapReduce(map, reduce, { finalize: finalize, query: { _id: "userA" }})

Несколько заметок:

  • Вы должны удалить предыдущие подписки пользователя, прежде чем запускать map-lower для этого пользователя.
  • Если вы обновляете группу, вы должны запустить map-lower для всех пользователей, которые следуют за группой.

Я должен отметить, что эти функции сокращения карты оказались на более сложными, чем я имел в виду , потому что MongoDB не поддерживает массивы как возвращаемые значения функций сокращения. Теоретически, функции могут быть намного проще, но не совместимы с MongoDB. Однако это более сложное решение можно использовать для отображения-сокращения всей коллекции users за один вызов, если вам когда-либо понадобится.

...