MongoDB、MapReduce和排序

11

可能我有点超出能力范围,因为我仍在学习 MongoDB 的各个方面,但我还是要试试。

目前,我正在开发一个工具来搜索/筛选数据集,根据任意数据点(例如流行度)排序,然后按 id 进行分组。我唯一看到的方法就是通过 Mongo 的 MapReduce 功能来实现。

我无法使用 .group(),因为我处理的键超过了 10,000 个,并且我需要对数据集进行排序。

我的 MapReduce 代码完全正常,除了一件事:排序。排序根本就不起作用。

db.runCommand({
  'mapreduce': 'products',
  'map': function() {
    emit({
      product_id: this.product_id,
      popularity: this.popularity
    }, 1);
  },
  'reduce': function(key, values) {
    var sum = 0;
    values.forEach(function(v) {
      sum += v;
    });

    return sum;  
  },
  'query': {category_id: 20},
  'out': {inline: 1},
  'sort': {popularity: -1}
});

我已经在受欢迎程度数据点上有一个降序索引,所以它肯定不起作用是因为缺少那个:

{ 
  "v" : 1, 
  "key" : { "popularity" : -1 }, 
  "ns" : "app.products", 
  "name" : "popularity_-1" 
}

我就是想不通为什么它不想排序。

与其内联结果集,我无法将其输出到另一个集合,然后在该集合上运行.find().sort({popularity: -1}),因为这个功能的工作方式。


1
你考虑过使用2.2版本和聚合框架吗? - Asya Kamsky
1
目前内联Map/Reduce不支持排序(请参见SERVER-3973以观看/投票此功能)。 内联Map/Reduce(或聚合查询)还受到最大文档大小的限制(在MongoDB 2.0中当前为16Mb)。 有什么问题阻止输出到可以按排序查询的临时集合? 另一个选项是在客户端应用程序中对结果进行排序,例如使用usort() - Stennie
@Aysa:我有看过,但2.2版本还没有准备好用于生产。 - Jon Ursenbach
@Stennie: 看起来是这样。阻止我将其输出到临时集合的问题在于我不能让此功能为每个进入应用程序的请求创建一个新集合。Mongo文档似乎也不像我现在可以创建“临时”集合。当然,在完成后我可以删除它,但那似乎不是一个好的解决方案。而且我一点也不担心16MB的文档大小限制。 - Jon Ursenbach
@JonUrsenbach:本周发布了MongoDB 2.2的第二个候选版本(rc1,紧随7月中旬的rc0)。我预计正式版本不会太远,值得测试。 - Stennie
2个回答

15

首先,Mongo的map/reduce不是为了像CouchDB一样用作查询工具而设计的,而是设计成后台任务运行的工具。我在工作中使用它来分析流量数据。

然而,你做错的地方是将sort()应用于输入,但这是无用的,因为当map()阶段完成时,中间文档会按每个key排序。因为你的键是一个文档,所以它按 product_id,popularity排序。

这就是我生成数据集的方法

function generate_dummy_data() {
    for (i=2; i < 1000000; i++) { 
        db.foobar.save({
          _id: i, 
         category_id: parseInt(Math.random() * 30), 
         popularity:    parseInt(Math.random() * 50)
        }) 
    }
}

这是我的Map/Reduce任务:

var data = db.runCommand({
  'mapreduce': 'foobar',
  'map': function() {
    emit({
      sorting: this.popularity * -1,
      product_id: this._id,
      popularity: this.popularity,
    }, 1);
  },
  'reduce': function(key, values) {
    var sum = 0;
    values.forEach(function(v) {
      sum += v;
    });

    return sum;  
  },
  'query': {category_id: 20},
  'out': {inline: 1},
});

以下是最终结果(过长,无法在此处粘贴):

http://cesarodas.com/results.txt

这样做是有效的,因为现在我们按 sorting、product_id、popularity 进行排序。你可以根据自己的需要调整排序方式,但请记住,最终的排序是按照 key 进行的,而不管输入的排序方式如何。

总之,正如我之前所说的,应避免使用 Map/Reduce 进行查询,因为它是为后台处理而设计的。如果是我的话,我会设计我的数据,使得可以使用简单的查询来访问它,在这种情况下,通常需要进行复杂的插入/更新操作以实现简单的查询(这就是我对 MongoDB 的看法)。


不错的解决方法..我没有意识到即使没有明确的sort设置,发射键也会保持排序顺序 :). 不知道这是否是一个意外的副作用; Map/Reduce文档实际上提到sort键是“用于优化,例如按发射键排序以减少减少”。 - Stennie
1
这不是Mongo的功能,而是应用于算法的Map/Reduce功能,它必须按键排序(因此适用于CouchDB、Hadoop等)。我通过观看Cloudera的一些视频学习到了这一点。就我所看到的,Mongo的sort仅用于对输入进行排序,而不是输出。 - crodas
这个运行得非常好。不确定是否可以重新构建这些数据,所以看起来我只能使用 M/R,直到2.2稳定并且准备投入生产,并使用新的聚合框架重新审视。再次感谢,crodas。 - Jon Ursenbach
@crodas 实际上,对输入进行排序可能有助于 MR 性能 这篇文章讨论了这个问题,我在我的分析数据库中进行了测试,确实有帮助(也许不像文章所述的那样可以提高 6 倍)。干杯。 - Lucas Lazaro

10

如原问题中所讨论:

  • 目前使用内联输出的 Map/Reduce 无法使用显式的 sort 键(参见SERVER-3973)。可能的解决方法包括依赖发出的键顺序(请参阅 @crodas 的回答);将输出到集合并使用排序顺序查询该集合;或在您的应用程序中使用类似于 usort() 的东西对结果进行排序。

  • 原帖发布人喜欢内联结果,而不是创建/删除临时集合。

  • MongoDB 2.2 中的 聚合框架(目前为生产版本)将提供一个适当的解决方案。

以下是一个类似于原始 Map/Reduce 查询的示例,但改为使用聚合框架:

db.products.aggregate(
  { $match: { category_id: 20 }},
  { $group : {
     _id : "$product_id",
     'popularity' : { $sum : "$popularity" },
  }},
  { $sort: { 'popularity': -1 }}
)

...以及样本输出:

{
    "result" : [
        {
            "_id" : 50,
            "popularity" : 139
        },
        {
            "_id" : 150,
            "popularity" : 99
        },
        {
            "_id" : 123,
            "popularity" : 55
        }
    ],
    "ok" : 1
}

现在先采用Crodas的解决方案,等2.2版本准备好了再考虑聚合框架。再次感谢。 - Jon Ursenbach

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接