使用 MongoDB 的类似 Twitter 的应用程序

Posted

技术标签:

【中文标题】使用 MongoDB 的类似 Twitter 的应用程序【英文标题】:Twitter-like app using MongoDB 【发布时间】:2011-05-01 20:54:45 【问题描述】:

我正在制作一个使用经典“关注”机制的应用程序(Twitter 和网络上的许多其他应用程序都使用这种机制)。我正在使用 MongoDB。 不过,我的系统有一点不同:用户可以关注个用户。这意味着,如果您关注一个群组,您将自动关注属于该群组的所有用户。当然,用户可以属于多个组。

这是我想出的:

用户A跟随用户B时,用户B的ID被添加到用户A文档中的嵌入数组(称为following)中 为了取消关注,我从following 数组中删除了关注用户的ID

组的工作方式相同:当 用户 A 跟随 组 X 时,组 X 的 id 被添加到 following 数组中。 (我实际上添加了一个DBRef,所以我知道连接是针对用户还是组。)

当我必须检查 user A 是否跟随 group X 时,我只需在 user A' 中搜索组的 id s 跟随数组。

当我必须检查 user A 是否跟随 user B 时,事情变得有点棘手。每个用户的文档都有一个嵌入式数组,列出了用户所属的所有组。所以我使用$or 条件来检查用户A 是直接关注用户B 还是通过群组关注用户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,我确实这样做了并且它有效,但我很想避免这种情况以使事情变得更简单。也许我只需要跳出框框思考。或者,也许我两次尝试都采取了错误的方法。任何人都必须做类似的事情并提出更好的解决方案?

(这实际上是我的this older question 的后续。我决定发布一个新问题来更好地解释我的新情况;我希望这不是问题。)

【问题讨论】:

我的投票是使用地图将关注者列表写入临时集合 我听说 Map/Reduce 可能很慢,所以我不能在每次页面加载时都这样做。这意味着关注者列表不会是最新的,所以我宁愿避免这种解决方案...... 【参考方案1】:

您有两种可能的方式让用户关注另一个用户;直接或间接通过组,在这种情况下,用户直接关注该组。让我们从存储用户和组之间的这些直接关系开始:


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

现在,您希望能够快速找出用户 A 直接或间接关注的用户。为此,您可以对用户 A 所关注的组进行非规范化。假设组 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-reduce-finalize。如果组更新,您只需为关注该组的所有用户重新运行 map-reduce,订阅将再次保持最新。

映射减少

以下 map-reduce 函数将为单个用户生成订阅。

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-reduce,在本例中为 userA

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

几点说明:

在为该用户运行 map-reduce 之前,您应该删除该用户以前的订阅。 如果您更新一个组,您应该为所有关注该组的用户运行 map-reduce。

我应该注意到,这些 map-reduce 函数结果比我想象的要复杂,因为 MongoDB 不支持数组作为 reduce 函数的返回值。理论上,函数可以简单得多,但与 MongoDB 不兼容。但是,如果需要,可以使用这个更复杂的解决方案在一次调用中映射减少整个 users 集合。

【讨论】:

这听起来是个不错的解决方案,谢谢。分页问题仍然存在:我不能将 skip()/limit() 与嵌入文档一起使用。基本上,正如我在问题中所说,我需要列出用户关注的所有内容(就像 Twitter 所做的那样)。 @Brainfeeder:您可以将每个订阅作为文档存储在单独的集合中,以绕过跳过/限制限制。然后"userA" 将是我提到的每个订阅的ownerId,例如 ownerId: "userA", userId: "userB", origins: [ "!" ] . 正是我的想法。非常感谢! @Brainfeeder:这只是整个集合上的 map-reduce 的情况。但是您的 map-reduce 一次只能针对一个用户。您不是在减少整个 users 集合的映射,而是仅减少一个文档,所以它不应该很慢。我会用一个例子更新我的答案,看看你的另一个问题。 @Brainfeeder:为了解决这个问题,我必须引入一个对象来保存origins 数组,并使用concat() 函数在reduce 函数中合并这些值。

以上是关于使用 MongoDB 的类似 Twitter 的应用程序的主要内容,如果未能解决你的问题,请参考以下文章

是否使用 google/facebook/twitter 身份验证登录?还是授权?

你认为从 twitter 获得所有转推的最佳方式是啥

如何使用 django 制作类似 twitter 的主页?

mongodb 的唯一 ID

NodeJS/React:拍照,然后发送到 mongoDB 存储,稍后显示

Flutter Web App:类似 Twitter 的布局