mongo聚合计数字段中的唯一元素

时间:2014-07-28 19:39:12

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

我退回了以下表格的文件:

{ _id: '48nmqsyxmswpkkded2ac_331fabf34fcd3935',
  actions: { sales: { pixel: [Object] } },
  date: Sun Jul 27 2014 00:00:00 GMT-0400 (EDT),
  client: '48nmqsyxmswpkkded2ac',
  campaignId: null,
  domain: null,
  affiliate: '964540',
  subaffiliate: '11776-hlc-a-click-here' }

我想按客户分组,并且有四个字段用于显示在campaignId,domain,affiliate和subaffiliate中的唯一商品数量。理想情况下,我希望以下列形式返回文件:

{ client: '48nmqsyxmswpkkded2ac',
  affiliates: 45,
  subaffiliates: 51,
  campaignIds: 2,
  domains: 234 }

我正在尝试以下内容:

[       {
            $match:
            {
                date: { $gte:  dutil.getDateOnly(self.date), $lt: dutil.getDateOnly(new Date()) }
            } 
        },
        {
            $group:
            {
                _id: "$client",
                affiliate: { $addToSet : "$affiliate" },
                //subaffiliate: { $addToSet : "$subaffiliate" }
            },
        },

], 
    {
        allowDiskUse: true,
        cursor: { batchSize: 40000 }
    }

我可以在affiate.length之后获取唯一元素,但是当我取消关联子网络线时,此代码不起作用(没有文档返回),因为返回的66MB BSON对象大于Mongo的16MB限制。

@Neil Lunn这是现在的问题。我尝试将光标限制更改为40或10但错误相同。甚至返回光标并使用每个抛出相同的错误。我认为allowdiskuse选项会让mongo写入磁盘,因此没有16MB的限制。我错了吗?我现在的一些想法是

  1. async.forEach $匹配到每个客户
  2. 某种凌乱的条件使用$ cond和$ if为每个
  3. 创建一个计数

    这两种想法似乎都是次优和杂乱的。有什么建议? Mongo意味着巨大;我被16MB的限制所困扰并不具有讽刺意味:)

1 个答案:

答案 0 :(得分:2)

如果你真的在这里吹嘘BSON限制,让我们清楚你正在爆炸的是在你为每个分组文档创建的“集合”中,那么你真正拥有的数量远远超过你在预期成绩。至少在某些文件中是这样的。

真正的问题是,由于BSON限制,$addToSet不会为你削减它。现在要克服的一个大问题是,你真的没有另外一种方法可以在一次传递中累积所有字段数。特别是考虑到你需要将累积的文件保持在BSON限制之下。

底线表示单独的查询。然后基本上“聚合”那些结果。单独查询的意思是“有效大小”形式,它将获得每个不同字段值的计数。基本上就是这样:

[
    { "$group": {
        "_id": {
            "client": "$client",
            "afilliate": "$affiliate"
        }
    }},
    { "$group": {
        "_id": "$_id.client",
        "affiliates": { "$sum": 1 },
        "subaffiliates": { "$sum": 0 },
        "domains": { "$sum": 0 },
        "campaignids": { "$sum": 0 }
    }}
]

基本前提是返回特定字段的“计数”,主要是通过使用该字段作为“键”的一部分在初始$group中查找“不同”项。第二个分组获取每个键下现在不同的术语的计数。完整的符号似乎是人为的,但它确实消除了在以后的处理中测试“空”或不存在字段的需要。

再次捕获这种方法,是你需要“组合”所有查询的结果,以确定最终结果。如图所示的查询表单不可能违反文档的BSON限制。所以现在唯一真正的问题是以某种方式“合并”每个查询的结果并以有效的方式进行。

此处的实际方法取决于您的“结果文档数量”需要“光标”迭代的位置,或者可以在单个结果中处理,其中每个响应都在16MB BSON限制之下。

这里的两种方法是:

  1. 使用返回的游标,并且通常在服务器上创建另一个集合以发出进一步的聚合语句。

  2. 如果生成的“集合”低于16MB BSON限制,那么您可以查看结果在内存中的聚合,这可能是最快的解决方案,这是一个选项。

  3. 一般情况归结为像这样处理“光标”:

    var async = require('async'),
        pluralize = require('pluralize'),
        mongoose = require('mongoose'),
        Schema = mongoose.Schema;
    
    mongoose.connect('mongodb://localhost/test');
    
    var sampleSchema = new Schema({
      date: Date,
      client: String,
      campaignId: Schema.Types.Mixed,
      domain: Schema.Types.Mixed,
      affiliate: String,
      subaffiliate: String
    });
    
    var targetSchema = new Schema({},{ strict: false });
    
    var Sample = mongoose.model( 'Sample', sampleSchema, 'sample' );
    var Target = mongoose.model( 'Target', targetSchema, 'target' );
    
    function getFields() {
      return Object.keys( Sample.schema.paths ).filter(function(field) {
        var blacklist = ['_id','__v','date','client'];
       return blacklist.indexOf(field) == -1;
      })
    }
    
    
    function getPipe(field,fields) {
    
      var grp = { "client": "$client" };
      grp[field] = "$" + field;
    
      var pipe = [{ "$group": { "_id": grp }}];
    
      var obj = { "_id": "$_id.client" };
      fields.forEach(function(current) {
        var plural = pluralize( current );
    
        obj[plural] = {
          "$sum": (field == current) ? 1 : 0
        };
      });
    
      pipe.push({ "$group": obj });
      return pipe;
    
    }
    
    var fields = getFields();
    var tasks = {};
    
    fields.forEach(function(current) {
      var pipe = getPipe( current, fields );
      tasks[current] = function(callback) {
        var cursor = Sample.collection.aggregate(
          pipe,
          { cursor: { batchSize: 100 } }
        );
    
        var bulk = Target.collection.initializeOrderedBulkOp();
        var counter = 0;
    
        cursor.on("data",function(item) {
          var client  = item._id;
          delete item._id;
          item.client = client;
          console.log(item);
          bulk.insert(item);
          counter++;
    
          if ( counter % 1000 == 0 )
            bulk.execute(function(err,result) {
              if (err) throw err;
              bulk = Target.collection.initializeOrderdBulkOp();
            });
        });
    
        cursor.on("end",function() {
          if ( counter % 1000 != 0 )
            bulk.execute(function(err,result) {
              if (err) throw err;
              callback(null,counter);
            });
        });
    
      };
    });
    
    mongoose.connection.on("open",function(err,conn) {
    
      async.parallel(
        tasks,
        function( err, results ) {
          if (err) throw err;
    
          console.log( results );
    
          var obj = { "_id": "$client" };
          fields.forEach(function(field) {
            var plural = pluralize( field );
            obj[plural] = { "$sum": "$" + plural };
          });
    
          var pipe = [{ "$group": obj }];
    
          var cursor = Target.collection.aggregate(
            pipe,
            { cursor: { batchSize: 100 } }
          );
    
          // do something with the cursor, like pipe or other event process
    
        }
      );
    
    });
    

    或者如果结果允许那么在内存中:

    var sampleSchema = new Schema({
      date: Date,
      client: String,
      campaignId: Schema.Types.Mixed,
      domain: Schema.Types.Mixed,
      affiliate: String,
      subaffiliate: String
    });
    
    var Sample = mongoose.model( 'Sample', sampleSchema, 'sample' );
    
    function getFields() {
      return Object.keys( Sample.schema.paths ).filter(function(field) {
        var blacklist = ['_id','__v','date','client'];
       return blacklist.indexOf(field) == -1;
      })
    }
    
    
    function getPipe(field,fields) {
    
      var grp = { "client": "$client" };
      grp[field] = "$" + field;
    
      var pipe = [{ "$group": { "_id": grp }}];
    
      var obj = { "_id": "$_id.client" };
      fields.forEach(function(current) {
        var plural = pluralize( current );
    
        obj[plural] = {
          "$sum": (field == current) ? 1 : 0
        };
      });
    
      pipe.push({ "$group": obj });
      return pipe;
    
    }
    
    var fields = getFields();
    var tasks = {};
    
    fields.forEach(function(current) {
      var pipe = getPipe( current, fields );
      tasks[current] = function(callback) {
        Sample.collection.aggregate(
          pipe,
          function(err,result) {
            callback(err,result);
          }
        );
    
      };
    });
    
    mongoose.connection.on("open",function(err,conn) {
    
      async.waterfall(
        [
          function(callback) {
            async.parallel(
              tasks,
              function( err, results ) {
                callback(err,results);
              }
            );
          },
          function(results,callback) {
            async.concat(
              fields,
              function(item,callback) {
                callback(null,results[item]);
              },
              function(err,results) {
                callback(err,results);
              }
            );
          },
          function(results,callback) {
            var obj = {};
            results.forEach(function(item) {
              if ( !obj.hasOwnProperty(item._id) ) {
                var blank = {};
                fields.map(function(field) {
                  return pluralize(field);
                }).forEach(function(field) {
                  blank[field] = 0;
                });
                obj[item._id] = blank;
              } else {
                fields.map(function(field) {
                  return pluralize(field);
                }).forEach(function(field) {
                  obj[item._id][field] += item[field];
                });
              }
            });
            callback(null,obj);
          },
          function(results,callback) {
            var results = Object.keys( results ).map(function(id) {
              var obj = { _id: id };
              fields.map(function(field) {
                return pluralize( field );
              }).forEach(function(field) {
                obj[field] = results[id][field];
              });
              return obj;
            });
            callback(null,results);
          }
        ],
        function(err,results) {
          if (err) throw err;
          console.log( results );
        }
      );
    });
    

    在两者中都有一些mongoose库的使用,至少为被引用的集合定义“模式”。因此,为了确定要使用的字段的“列表”,有一点内省,但您可以使用标准数组来执行此操作。

    每个案例都会将"async.parallel"视为一种统一执行该服务器上的语句的方式,否则会“结合”结果。它实际上取决于结果集实际有多大,关于通过游标甚至$out规范处理结果是否更有效,然后使用类似.eval()之类的内容来保留所有内容那里作为服务器端操作。不是最好的选择,因为“.eval()”有自己的问题,你应该从提供的链接中读到。

    无论如何,结合结果的并行处理方法是我能想到的唯一真正的选择:

    1. $addToSet创建了太大的文档,甚至可以使用$size运算符进行缩减。所以问题已经发生了。

    2. 使分组更精细,首先说“单日”不会产生正确的“不同”计数。而不是在尝试任何导致与1相同问题的事情之前。

    3. 您不能“有条件地评估”跨文档的不同值。所有聚合操作一次只能处理一个文档,除非您故意将文档组合在一起,在这种情况下再次回到问题1.

    4. 您不能尝试一次对所有键进行分组,因为再次对多个字段的值进行分组会导致错误的不同计数,因为它们在某些组合中只是“不同”而不是单个“客户端”键

    5. 尝试使用内存选项,只要完整响应低于16MB,这将是最快的处理。