简介
您的目标需要考虑一下何时记录事件,因为您将事件结构化到给定的时间段聚合中。显而易见的一点是,您所代表的单个文档实际上可以代表最终汇总结果中要在“多个”时间段内报告的事件。
分析结果表明这是一个超出aggregation framework 范围的问题,因为要查找的时间段。某些事件需要在可以分组的范围之外“生成”,您应该能够看到。
为了做到这一点,你需要mapReduce。这具有通过 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
}
}
)
映射和归约
本质上,“映射器”函数查看当前记录,将每个开/关时间四舍五入到小时,然后计算出事件发生在哪个六小时期间的开始时间。
使用这些新的日期值,将启动一个循环以获取开始的“开启”时间,并在该期间在单个元素数组中发出当前“灯”打开的事件,如下所述。每个循环将开始时间增加六个小时,直到达到“熄灯”结束时间。
这些出现在 reducer 函数中,它需要与它返回的相同的预期输入,因此灯阵列在值对象内的周期内打开。它在与这些值对象列表相同的键下处理发出的数据。
首先迭代要归约的值列表,然后查看可能来自之前的归约通道的内部灯光数组,并将其中的每一个处理成一个独特的灯光结果数组。只需在结果数组中查找当前光照值并将其推送到不存在的那个数组即可。
注意“前一次传递”,好像您不熟悉 mapReduce 的工作原理,那么您应该了解 reducer 函数本身发出的结果可能无法通过处理“所有”可能的值来实现一次通过“关键”。它可以并且通常只处理键的发出数据的“子集”,因此将采用与从映射器发出数据相同的方式将“缩减”结果作为输入。
这就是为什么 mapper 和 reducer 都需要输出具有相同结构的数据的原因,因为 reducer 本身也可以从之前已缩减的数据中获取输入。这就是 mapReduce 处理发出大量相同键值的大型数据集的方式。它通常以“块”的形式处理,而不是一次全部处理。
结束减少归结为时段内打开的灯的列表,每个时段的开始和结束都作为发出的键。像这样:
{
"_id": {
"start": ISODate("2015-01-01T06:00:00Z"),
"end": ISODate("2015-01-01T12:00:00Z")
},
{
"result": {
"lights_on": [ "light_1", "light_2" ]
}
}
},
“_id”、“result”结构只是所有 mapReduce 输出如何输出的属性,但所需的值都在那里。
查询
现在这里还有一个关于查询选择的注释,需要考虑到灯可能已经在当天开始之前的某个日期通过其集合条目“打开”。同样的道理,它也可以在报告当前日期之后“关闭”,并且实际上可能具有null 值或文档中没有“关闭”键,具体取决于您的数据存储方式以及实际观察的日期。
该逻辑从要报告的当天开始创建一些所需的计算,并考虑该日期之前和之后的六小时期间,并列出查询条件:
{
"on": { "$gte": yesterday, "$lt": tomorrow },
"$or": [
{ "off": { "$gte:" today, "$lt": nextday } },
{ "off": null },
{ "off": { "$exists": false } }
]
}
那里的基本选择器使用$gte和$lt的范围运算符来分别在他们正在测试的值的字段上查找大于或等于和小于的值,以便查找数据在合适的范围内。
在$or 条件内,考虑了“off”值的各种可能性。要么它属于范围标准,要么具有null 值,或者可能通过$exists 运算符在文档中根本没有键。这取决于$or 内的这些条件的要求,在尚未关闭灯的情况下,您实际上如何表示“关闭”,但这些将是合理的假设。
与所有 MongoDB 查询一样,除非另有说明,否则所有条件都是隐含的“AND”条件。
这仍然有些缺陷,具体取决于灯可能会打开多长时间。但是这些变量都是有意在外部列出的,以便根据您的需求进行调整,并考虑到在报告日期之前或之后获取的预期持续时间。
创建空时间序列
这里的另一个注意事项是,数据本身可能没有任何事件显示在给定时间段内点亮。出于这个原因,在 mapper 函数中嵌入了一个简单的方法,可以查看我们是否处于结果的第一次迭代中。
仅在第一次时,会发出一组可能的周期键,其中包括一个空数组,用于在每个周期中打开的灯。这使得报告还可以显示那些根本没有灯亮的时段,因为这被插入到发送到减速器和输出的数据中。
您可能会对此方法有所不同,因为它仍然依赖于某些满足查询条件的数据才能输出任何内容。因此,为了迎合没有数据记录或不符合标准的真正“空白日”,最好创建一个所有键的外部哈希表,所有键都显示灯的空结果。然后将 mapReduce 操作的结果“合并”到那些预先存在的键中以生成报告。
总结
这里有许多关于日期的计算,并且不知道实际的最终语言实现,我只是单独声明任何在实际 mapReduce 操作外部起作用的东西。所以任何看起来像重复的东西都是为了这个意图而做的,使逻辑语言的那部分独立。大多数编程语言都支持根据使用的方法操作日期的功能。
所有语言特定的输入都作为选项块传入,此处显示为 mapReduce 方法的最后一个参数。值得注意的是,查询的参数化值都是从要报告的日期计算出来的。然后是“范围”,它是一种传递值的方式,可以被 mapReduce 操作中的函数使用。
考虑到这些因素,mapper 和 reducer 的 JavaScript 代码保持不变,因为这是方法所期望的输入。过程中的任何变量都由范围和查询结果提供,以便在不更改代码的情况下获得结果。
因此,主要是因为“灯亮”的持续时间可以跨越要报告的不同时期,这成为聚合框架不适合做的事情。它无法执行获得结果所需的“循环”和“数据发送”,因此我们为什么要使用 mapReduce 来代替此任务。
也就是说,很好的问题。我不知道您是否已经考虑过如何在此处获得结果的概念,但至少现在有一个指南可供处理类似问题的人使用。