MongoDb聚合 - 分裂成时间桶

时间:2015-07-29 09:32:17

标签: mongodb mapreduce time-series mongodb-query aggregation-framework

是否可以使用MongoDB聚合框架生成时间序列输出,其中任何被认为属于每个存储桶的源文档都被添加到该存储桶中?

说我的收藏看起来像这样:

/*light_1 on from 10AM to 1PM*/
{
    "_id" : "light_1",
    "on" : ISODate("2015-01-01T10:00:00Z"),
    "off" : ISODate("2015-01-01T13:00:00Z"),

},
/*light_2 on from 11AM to 7PM*/
{
    "_id" : "light_2",
    "on" : ISODate("2015-01-01T11:00:00Z"),
    "off" : ISODate("2015-01-01T19:00:00Z")
}

..我正在使用6小时的桶间隔来生成2015-01-01的报告。我希望我的结果看起来像:

    {
        "start"         : ISODate("2015-01-01T00:00:00Z"),
        "end"           : ISODate("2015-01-01T06:00:00Z"),
        "lights"        : []
    },
    {
        "start"         : ISODate("2015-01-01T06:00:00Z"),
        "end"           : ISODate("2015-01-01T12:00:00Z"),
        "lights_on"     : ["light_1", "light_2"]
    },
    {
        "start"         : ISODate("2015-01-01T12:00:00Z"),
        "end"           : ISODate("2015-01-01T18:00:00Z"),
        "lights_on"     : ["light_1", "light_2"]
    },
    {
        "start"         : ISODate("2015-01-01T18:00:00Z"),
        "end"           : ISODate("2015-01-02T00:00:00Z"),
        "lights_on"     : ["light_2"]
    }

一盏灯被视为' on'在一个范围内,如果它在' on'价值<水桶结束'和它的关闭'值> =桶'开始'

我知道我可以使用$ group和聚合日期运算符按开始或结束时间分组,但在这种情况下,它是一对一的映射。在这里,如果单个源文档跨越多个存储桶,则可以将其分成几个时间段。

报告范围和间隔跨度直到运行时才知道。

3 个答案:

答案 0 :(得分:5)

简介

这里的目标需要考虑一下在何时记录事件的考虑因素,因为它们将事件结构化为给定的时间段聚合。显而易见的一点是,您所代表的单个文档实际上可以表示要在"多个"中报告的事件。结束聚合结果的时间段。

由于要查找的时间段,分析结果是aggregation framework范围之外的问题。某些事件需要"生成"除了可以分组的东西之外,您应该能够看到它。

为了做到这一点" generataion",你需要mapReduce。这有'#34;流量控制"通过JavaScript,因为它的处理语言能够基本上确定开/关之间的时间是否超过一个周期,因此发出它在多个时期内发生的数据。

作为旁注,"灯"可能不是最适合_id,因为它可能在某一天可能多次打开/关闭。那么"实例"开/关可能更好。但是我只是在这里关注你的例子,所以为了转换它,然后只需用映射器代码中的_id替换引用任何实际字段代表灯的标识符。

但是代码:

// start date and next date for query ( should be external to main code )
var oneHour = ( 1000 * 60 * 60 ),
    sixHours = ( oneHour * 6 ),
    oneDay = ( oneHour * 24 ),
    today = new Date("2015-01-01"),               // your input
    tomorrow = new Date( today.valueOf() + oneDay ),
    yesterday = new Date( today.valueOf() - sixHours ),
    nextday = new Date( tomorrow.valueOf() + sixHours);

// main logic
db.collection.mapReduce(
    // mapper to emit data
    function() {
        // Constants and round date to hour
        var oneHour = ( 1000 * 60 * 60 )
            sixHours = ( oneHour * 6 )
            startPeriod = new Date( this.on.valueOf() 
              - ( this.on.valueOf() % oneHour )),
            endPeriod = new Date( this.off.valueOf()
              - ( this.off.valueOf() % oneHour ));

        // Hour to 6 hour period and convert to UTC timestamp
        startPeriod = startPeriod.setUTCHours( 
            Math.floor( startPeriod.getUTCHours() / 6) * 6 );
        endPeriod = endPeriod.setUTCHours( 
            Math.floor( endPeriod.getUTCHours() / 6) * 6 );

        // Init empty reults for each period only on first document processed
        if ( counter == 0 ) {
            for ( var x = startDay.valueOf(); x < endDay.valueOf(); x+= sixHours ) {
                emit(
                    { start: new Date(x), end: new Date(x + sixHours) },
                    { lights_on: [] }
                );
            }
        }

        // Emit for every period until turned off only within the day
        for ( var x = startPeriod; x <= endPeriod; x+= sixHours ) {
           if ( ( x >= startDay ) && ( x < endDay ) ) {
               emit(
                   { start: new Date(x), end: new Date(x + sixHours)  },
                   { lights_on: [this._id] }
               );
           }
        }
        counter++;
    },

    // reducer to keep all lights in one array per period
    function(key,values) {
        var result = { lights_on: [] };
        values.forEach(function(value) {
            value.lights_on.forEach(function(light){
                if ( result.lights_on.indexOf(light) == -1 )
                    result.lights_on.push(light);
            });
        });
        result.lights_on.sort();
        return result;
    },

    // options and query
    { 
        "out": { "inline": 1 },
        "query": {
            "on": { "$gte": yesterday, "$lt": tomorrow }, 
            "$or": [
                { "off": { "$gte:"  today, "$lt": nextday } },
                { "off": null },
                { "off": { "$exists": false } }
            ]
        },
        "scope": { 
            "startDay": today,
            "endDay": tomorrow,
            "counter": 0
        }
    }
)

映射和缩小

本质上,&#34;映射器&#34;函数查看当前记录,将每个开/关时间舍入为小时,然后计算出事件发生的六小时内的开始时间。

使用这些新的日期值,启动一个循环以启动&#34; on&#34;时间并发出当前&#34;灯&#34;在此期间打开,在单个元素数组中,如后面所述。每个循环将开始周期增加六个小时,直到结束&#34;点亮&#34;时间到了。

它们出现在reducer函数中,它需要返回相同的预期输入,因此在值对象内的句点中打开了灯光数组。它在与这些值对象列表相同的键下处理发出的数据。

首先迭代要减少的值列表,然后查看内部的灯光阵列,这些灯光可能来自之前的减少传递,并将每个光源处理成唯一光源的单个结果数组。简单地通过查找结果数组中的当前光值并推送到不存在的数组来完成。

注意&#34;之前的传递&#34;,就像你不熟悉mapReduce的工作方式一样,那么你应该理解reducer函数本身会发出一个结果,这个结果可能是处理过程中未能实现的。所有&#34; &#34;键&#34;的可能值一次通过。它可以并且通常只处理一个&#34;子集&#34;密钥的发出数据,因此将减少&#34;减少&#34;输入的结果与从映射器发出数据的方式相同。

这一点设计就是为什么mapper和reducer都需要输出具有相同结构的数据,因为reducer本身也可以从先前已经减少的数据中获得它的输入。这就是mapReduce处理发出大量相同键值的大型数据集的方式。它通常以&#34; chunks&#34;而不是一次全部。

结束减少归结为在此期间打开的灯光列表,每个周期的开始和结束都是发出的键。像这样:

    {
        "_id": {
            "start": ISODate("2015-01-01T06:00:00Z"),
            "end": ISODate("2015-01-01T12:00:00Z")
        },
        {
            "result": {
                "lights_on": [ "light_1", "light_2" ]
            }
        }
    },

那&#34; _id&#34;,&#34;结果&#34;结构只是所有mapReduce输出结果的一个属性,但所需的值都存在。

查询

现在还有一个关于查询选择的注释,需要考虑到灯光已经是&#34; on&#34;通过其收集条目在当天开始之前的日期。同样的事实是,它可以被关闭&#34;关闭&#34;在报告当前日期之后,实际上可能具有null值或没有&#34; off&#34;根据数据的存储方式以及实际观察的日期,文档中的键。

该逻辑从当天开始创建一些必要的计算报告,并考虑该日期之前和之后的六小时时段,并列出查询条件:

        {
            "on": { "$gte": yesterday, "$lt": tomorrow }, 
            "$or": [
                { "off": { "$gte:"  today, "$lt": nextday } },
                { "off": null },
                { "off": { "$exists": false } }
            ]
        }

那里的基本选择器使用$gte$lt的范围运算符来查找分别大于或等于和小于它们正在测试顺序值的字段的值找到合适范围内的数据。

$or条件下,&#34; off&#34;的各种可能性。价值被考虑。要么它属于范围标准,要么通过$exists运算符具有null值或者文档中根本没有关键字。这取决于你实际代表的方式&#34; off&#34;关于$or范围内那些条件的要求尚未关闭的情况,但这些是合理的假设。

与所有MongoDB查询一样,所有条件都是隐含的&#34; AND&#34;除非另有说明,否则必须遵守。

这仍然有些瑕疵,具体取决于可能需要打开灯的时间长短。但这些变量都是有意在外部列出的,以便根据您的需求进行调整,同时考虑在报告日期之前或之后获取的预期持续时间。

创建空时间序列

此处另一个注意事项是,数据本身很可能没有任何事件在给定时间段内显示灯亮。出于这个原因,mapper函数中嵌入了一个简单的方法,用于查看我们是否在第一次结果迭代中。

仅在第一次时,会发出一组可能的周期键,其中包含每个周期中打开的灯的空数组。这允许报告还显示那些根本没有亮灯的时段,因为它被插入到发送到减速器和输出的数据中。

您可能会对此方法有所不同,因为它仍然依赖于某些数据符合查询条件以输出任何内容。所以要迎合一个真正的空白日#34;在没有记录数据或符合标准的情况下,最好创建一个键的外部哈希表,所有键都显示灯的空结果。然后只是&#34;合并&#34;将mapReduce操作的结果导入那些预先存在的密钥以生成报告。

摘要

这里有很多关于日期的计算,并且没有意识到实际的结束语言实现我只是单独声明在实际mapReduce操作外部工作的任何东西。所以看起来像重复的任何东西都是针对那个意图完成的,使逻辑语言的那一部分独立。大多数编程语言都支持根据使用的方法操作日期的功能。

然后,所有语言特定的输入作为mapReduce方法的最后一个参数显示的选项块传入。值得注意的是,查询中包含的参数值都是根据要报告的日期计算得出的。然后是&#34; scope&#34;,这是一种传递mapReduce操作中函数可以使用的值的方法。

考虑到这些因素,mapper和reducer的JavaScript代码保持不变,因为这是该方法作为输入所期望的。该过程的任何变量都由范围和查询结果提供,以便在不更改该代码的情况下获得结果。

因此,主要是因为&#34;灯的持续时间在&#34;可以跨越不同时期进行报告,这将成为聚合框架不能设计的事情。它不能执行&#34;循环&#34;和&#34;数据发射&#34;这是获得结果所必需的,因此我们为什么要使用mapReduce来完成这项任务。

那说,好问题。我不知道你是否已经考虑过如何在这里实现结果的概念,但至少现在有一个指南可以解决类似问题。

答案 1 :(得分:2)

我原本误解了你的问题。假设我理解你现在需要什么,这看起来更像是map-reduce的工作。我不确定你是如何确定范围或间隔跨度的,所以我将制作这些常量,你可以正确地修改这部分代码。你可以这样做:

var mapReduceObj = {};

mapReduceObj.map = function() {
    var start = new Date("2015-01-01T00:00:00Z").getTime(),
    end = new Date("2015-01-02T00:00:00Z").getTime(),
    interval = 21600000;                     //6 hours in milliseconds

    var time = start;
    while(time < end) {
        var endtime = time + interval;
        if(this.on < endtime && this.off >= time) {
            emit({start : new Date(time), end : new Date(endtime)}, [this._id]);
            break;
        }

        time = endtime;
    }
};

mapReduceObj.reduce = function(times, light_ids) {
    var lightsArr = {lights : []};

    for(var i = 0; i < light_ids.length; i++) {
        lightsArr.lights.push(light_ids[i]);
    }

    return lightsArr;
};

结果将具有以下形式:

results :    {
    _id     :    {
        start   :   ISODate("2015-01-01T06:00:00Z"),
        end     :   ISODate("2015-01-01T12:00:00Z")
    },
    value   :    {
        lights  :    [
            "light_6",
            "light_7"
        ]
    },
    ...
}

~~~原帖答案~~~

这应该为您提供所需的确切格式。

db.lights.aggregate([
    { "$match": {
        "$and": [ 
            { on  : { $lt : ISODate("2015-01-01T06:00:00Z") } },
            { off : { $gte: ISODate("2015-01-01T12:00:00Z") } }
        ]
    }},
    { "$group": {
        _id         :   null,
        "lights_on" : {$push : "$_id"}
    }},
    { "$project": {
        _id     :    false,
        start   :    { $add : ISODate("2015-01-01T06:00:00Z") },
        end     :    { $add : ISODate("2015-01-01T12:00:00Z") },
        lights_on:   true
    }}
]);

首先,$match条件查找符合时间限制的所有文档。然后$group将_id字段(在本例中为light_n,其中n是整数)推入lights_on字段。可以使用$addToSet$push,因为_id字段是唯一的,但如果您使用的字段可能有重复项,则需要确定数组中的重复项是否可接受。最后,使用$project获取您想要的确切格式。

答案 2 :(得分:0)

一种方法是使用$ project的$cond运算符,并将每个“start”和“end”与原始集合中的“on”和“off”字段进行比较。使用MongoDB客户端遍历每个存储桶并执行以下操作:

db.lights.aggregate([
    { "$project": { 
       "present": { "$cond": [
           { "$and": [ 
               { "$lte": [ "$on", ISODate("2015-01-01T06:00:00Z") ] },
               { "$gte": [ "$off", ISODate("2015-01-01T12:00:00Z") ] }
           ]},
           1, 
       0
       ]}
   }}
]);

结果应如下所示:

{ "_id" : "light_1", "present" : 0 }
{ "_id" : "light_2", "present" : 0 }
{ "_id" : "light_3", "present" : 1 }

对于{"present":1}的所有文档,请将"_id"灯光集合添加到客户端的"lights_on"字段中。希望这会有所帮助。