【问题标题】:Mongo Custom Sort Strategy using mongoose使用猫鼬的Mongo自定义排序策略
【发布时间】:2014-07-29 04:09:01
【问题描述】:

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

感谢您的宝贵时间!

【问题讨论】:

  • 您是否考虑过将计算得出的 ranking 字段添加到您的架构和文档中,然后对其进行索引,而不是每次都即时计算?
  • @JohnnyHK 感谢您抽出宝贵时间。我希望我可以预先计算排名,但我不能。排名字段取决于动态计算的星数。我说清楚了吗,我的意思是,你明白我的意思吗?
  • 这个想法是,每当您更新文档时,您还会重新计算其 ranking 值并将其与文档的其余部分一起存储。

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


【解决方案1】:

您可以在更新时执行所有这些操作,然后您可以实际索引排名并使用范围查询来实现分页。比使用 $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,这样就没有其他操作可以更新文档,直到完成为止。

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

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

更新当然会将“锁定”状态设置为false,检查当前状态是否不是true 是有意义的,尽管此时它确实应该没问题。但这为您提供了引发异常的空间。

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

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-09-18
    • 2019-09-16
    • 2012-02-19
    • 2018-01-21
    • 2019-08-19
    相关资源
    最近更新 更多