使用mongoose的Mongo自定义排序策略

时间:2014-06-08 12:55:21

标签: javascript node.js mongodb mongoose aggregation-framework

首先:我使用Mongo 2.6和Mongoose 3.8.8

我有以下架构:

var Link = new Schema({

  title: { type: String, trim: true },
  owner: { id: { type: Schema.ObjectId }, name: { type: String } },
  url:   { type: String, default: '', trim: true},
  stars: { users: [ { name: { type: String }, _id: {type: Schema.ObjectId} }] },
  createdAt: { type: Date, default: Date.now }

});

我的收藏品已有500k文件。

我需要的是使用自定义策略对文档进行排序。我最初的解决方案是使用聚合框架。

 var today = new Date();
 //fx = (TodayDay * TodayYear) - ( DocumentCreatedDay * DocumentCreatedYear)
 var relevance = { $subtract: [
    { $multiply: [ { $dayOfYear: today },  { $year: today } ]  },
    { $multiply: [ { $dayOfYear: '$createdAt' }, { $year: '$createdAt' } ]  }
   ]}


 var projection = {
    _id: 1,
    url: 1,
    title: 1,
    createdAt: 1,
    thumbnail: 1,
    stars: { $size: '$stars.users'}
    ranking: { $multiply: [ relevance, { $size: '$stars.users' } ] }
  }

var sort = {
    $sort: { ranking: 1, stars: 1 }
  }

var page = 1;
var limit = { $limit: 40 }
var skip = { $skip: ( 40 * (page - 1) ) }
var project = { $project: projection }

Link.aggregate([project, sort, limit, skip]).exec(resultCallback);

它可以很好地工作直到100k,之后查询变得缓慢而缓慢。 我怎么能做到这一点?
重新设计?
错误使用投影我在做什么?

谢谢你的时间!

1 个答案:

答案 0 :(得分:2)

您可以在更新时执行所有这些操作,然后您可以实际索引排名和使用范围查询以实现您的分页。比使用 $skip $limit 要好得多,这对任何形式都是大数据的坏消息。您应该能够找到许多来源,确认跳过和限制是一种不好的分页做法。

这里唯一的问题是因为你不能使用.update()类型的语句来实际引用另一个字段的现有值,所以你必须小心更新时的并发问题。这需要“滚动”一些自定义锁定处理,您可以使用.findOneAndUpdate()方法:

Link.findOneAndUpdate(
    { "_id": docId, "locked": false },
    { "locked": true },
    function(err,doc) {

        if ( doc.locked.true ) {
            // then update your document

            // I would just use the epoch date difference per day
            var relevance = (
               ( Date.now.valueOf() - ( Date.now().valueOf() % 1000 * 60 * 60 * 24) )
             - ( doc.createdAt.valueOf() - ( doc.createdAt.valueOf() % 1000 * 60 * 60 * 24 ))
            );

            var update = { "$set": { "locked": false } };

            if ( actionAdd ) {
              update["$push"] = { "stars.users": star };
              update["$set"]["score"] = relevance * ( doc.stars.users.length +1 );
            } else {
              update["$pull"] = { "stars.users": star };
              update["$set"]["score"] = relevance * ( doc.stars.users.length -1 );
            }

            // Then update
            Link.findOneAndUpdate(
                { "_id": doc._id, "locked":  update,function(err,newDoc) {

               // possibly check that new "locked" is false, but really
               // that should be okay
            });

        } else {
          // some mechanism to retry "n" times at interval 
          // or report that you cannot update
        }

    }

)

这样的想法是,您只能获取一个“锁定”状态等于false的文档才能实际更新,而第一个“更新”操作只是将该值设置为true这样在完成之前没有其他操作可以更新文档。

根据代码注释,您可能希望尝试执行此操作而不是仅仅尝试更新失败,因为可能有另外一个操作在数组中添加或减去。

然后,根据当前更新的“模式”,如果要添加到数组或从中取出项目,只需更改要发出的更新语句以执行任一操作并设置相应的“得分”值在你的文件中。

然后,更新将把“锁定”状态设置为false,检查当前状态是否为true是有意义的,尽管此时确实应该没问题。但这为你提供了一些能够引发例外的空间。

管理一般更新情况,但您仍然有问题在这里整理您的“排名”订单,因为跳过和限制仍然不是您想要的性能。这可能最好通过定期更新另一个字段来处理,您可以将其用于确定的“范围”查询,但您可能只想关注设定页面范围内最“相关”的分数范围,而不是更新整个集合。

更新需要定期更新,因为如果您尝试更改单个更新中多个文档的“排名”顺序,则会出现并发问题。因此,您需要确保此过程不会与其他此类更新重叠。

作为最后一点,请考虑您的“得分”计算,因为您真正想要的是最新且“最受欢迎”的内容。目前的计算有一些缺陷,例如在同一天和0“星星”,但我会留给你解决。

这基本上是您需要为解决方案做的事情。尝试使用聚合框架在大型集合上动态执行此操作不会为您的应用程序体验产生有利的性能。因此,您可以采取一些措施来更有效地维护结果的顺序。