使用猫鼬的Mongo自定义排序策略

Mongo Custom Sort Strategy using mongoose

本文关键字:Mongo 自定义 排序策略      更新时间:2023-09-26

首先:我使用的是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 }
});

我的收藏已经有50万份文件了。

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

 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之前都能很好地工作,之后查询变得越来越慢。我怎么能做到这一点
重新设计
投影的错误使用我在做什么?

谢谢你抽出时间!

您可以在更新时完成所有这些操作,然后您可以对排名进行索引,并使用范围查询来实现分页。比使用$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颗"星星",但我会让你来解决。

这基本上就是您需要为解决方案做的事情。尝试使用聚合框架在大型集合上动态执行此操作不会为您的应用程序体验带来良好的性能。因此,这里有一些建议可以帮助你更有效地维护结果的顺序。