MongoDB Map-Reduce: escolha da chave

Introdução

As chaves usadas para o padrão Map-Reduce podem ser um destes três tipos:

  • valor único: inteiro, flutuante, string …
  • objeto: Date Object, NumberLong Object …
  • conjunto de dados (documento): {índice a: 123, índice b: nova data (2015,0,1)}

Este tutorial mostra três exemplos para cada tipo para resolver o seguinte problema.

O problema

Vamos considerar o documento abaixo j4

>db.j4.find()
{ "_id" : 1, "value" : { "timeline" : ISODate("2015-07-01T01:23:03Z") } }
{ "_id" : 2, "value" : { "timeline" : ISODate("2015-07-01T05:00:00Z") } }
{ "_id" : 3, "value" : { "timeline" : ISODate("2015-07-03T13:02:14Z") } }
{ "_id" : 4, "value" : { "timeline" : ISODate("2015-07-03T20:10:06Z") } }
{ "_id" : 5, "value" : { "timeline" : ISODate("2015-07-03T21:03:07Z") } }
{ "_id" : 6, "value" : { } }

Aqui está o problema: conte os elementos de j4 agrupados por aaaa-mm-dd .

A saída deve ser semelhante a esta:

date : 2015-07-01, total : 2
date
: 2015-07-03, total : 3

Solução 1: digite como string de data

Uma data pode ser tratada como string se não houver necessidade de fazer uma consulta complexa sobre ela. <br>
Esta solução transforma um objeto de data em uma string com preenchimento e usa a string como chave.

var map_use_pad = function() { 

if (this.value.timeline == null) return;

// for this pad function, thanks to https://stackoverflow.com/users/182668/pointy
var pad = function pad(n, width, z) {
z
= z || '0';
n
= n + '';
return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
};

var d = this.value.timeline;
var key = d.getFullYear() + "-" + pad(d.getMonth() + 1, 2, 0) + "-" + pad(d.getDate(), 2, 0);

emit
(key, {"tot":1});
};

var reduce_with_pad = function (key, values) {

var reduce = {"tot" : 0};

/* if date as object is needed, use this instead */
// var reduce = {"date_as_object" : new Date(key + "T00:00:00.000Z"), "tot" : 0};

values
.forEach(function(value){
reduce
.tot += value.tot;
});

return reduce;
}

Agora é possível usar a função de redução de mapa sobre o documento j4 da seguinte maneira:

>db.j4.mapReduce(map_use_pad, reduce_with_pad, {out : {reduce: 'out_pad' }});
{
"result" : "out_pad",
"timeMillis" : 81,
"counts" : {
"input" : 6,
"emit" : 5,
"reduce" : 2,
"output" : 2
},
"ok" : 1
}

Vamos dar uma olhada no resultado:

> db.out_pad.find()
{ "_id" : "2015-07-01", "value" : { "tot" : 2 } }
{ "_id" : "2015-07-03", "value" : { "tot" : 3 } }

Solução 2: chave como objeto de data

Uma vez resolvido o problema e gerado o documento, pode ser necessário operar consultas sobre a data (_id) como o seguinte (pseudo-código):

db.out_pad.find({_id > "2015-01-01" AND _id < "2015-07-02"})       W R O N G!!!

Contanto que _id seja uma string, apenas expressões regulares complexas podem ser usadas para fazer o pseudocódigo.

Esta segunda solução resolve o problema: a chave (_id) é um objeto de data.

var map_use_object = function() { 

if (this.value.timeline == null) return;

var d = this.value.timeline;
var key = new Date( Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) );

emit
(key, {"tot":1});
};

var reduce_with_object = function (key, values) {

var reduce = {"tot" : 0};

values
.forEach(function(value){
reduce
.tot += value.tot;
});

return reduce;
}

Uma vez gerado o resultado usando …

db.j4.mapReduce(map_use_object, reduce_with_object, {out : {reduce: 'out_object' }});

… é possível realizar a consulta esperada:

>db.out_object.find
(
{
_id
:{
$gt
: new Date("2015-01-01T00:00:00Z"),
$lt
: new Date("2015-07-02T00:00:00Z")
}
}
)
{ "_id" : ISODate("2015-07-01T00:00:00Z"), "value" : { "tot" : 2 } }

Solução 3: chave como documento

A maneira mais fácil de dividir uma data em um grupo exclusivo de chaves é algo assim:

chave = {ano: aaaa, mês: mm, dia: dd}

Esta chave permite a construção fácil de certos tipos de consultas.

var map_use_set = function() { 

if (this.value.timeline == null) return;

var d = this.value.timeline;
var key = { year:d.getFullYear(), month:d.getMonth()+1, day:d.getDate() };

emit
(key, {"tot":1});
};

var reduce_with_set = function (key, values) {

var reduce = {"tot" : 0};

values
.forEach(function(value){
reduce
.tot += value.tot;
});

return reduce;
}

Uma vez gerado o resultado usando …

db.j4.mapReduce(map_use_set, reduce_with_set, {out : {reduce: 'out_set' }});

… é possível saber o total para cada primeiro dia de 2015 com uma simples consulta:

> db.out_set.find({"_id.year":2015, "_id.day" : 1})
{ "_id" : { "year" : 2015, "month" : 7, "day" : 1 }, "value" : { "tot" : 2 } }

Conclusão

A escolha da chave de redução de mapa tem uma consequência importante do tipo de consultas que estão disponíveis depois que os dados são gerados. É a natureza das consultas que impõe o tipo de chave a ser usada.