Después de experimentar un poco, descubrí que es posible crear una función de clasificación basada en MapReduce, suponiendo que el conjunto de resultados puede caber en el tamaño máximo del documento.
Por ejemplo, supongamos que tengo una colección como esta:
{ player: "joe", points: 1000, foo: 10, bar: 20, bang: "some text" }
{ player: "susan", points: 2000, foo: 10, bar: 20, bang: "some text" }
{ player: "joe", points: 1500, foo: 10, bar: 20, bang: "some text" }
{ player: "ben", points: 500, foo: 10, bar: 20, bang: "some text" }
...
Puedo realizar el equivalente aproximado de un DENSE_RANK así:
var m = function() {
++g_counter;
if ((this.player == "joe") && (g_scores.length != g_fake_limit)) {
g_scores.push({
player: this.player,
points: this.points,
foo: this.foo,
bar: this.bar,
bang: this.bang,
rank: g_counter
});
}
if (g_counter == g_final)
{
emit(this._id, g_counter);
}
}}
var r = function (k, v) { }
var f = function(k, v) { return g_scores; }
var test_mapreduce = function (limit) {
var total_scores = db.scores.count();
return db.scores.mapReduce(m, r, {
out: { inline: 1 },
sort: { points: -1 },
finalize: f,
limit: total_scores,
verbose: true,
scope: {
g_counter: 0,
g_final: total_scores,
g_fake_limit: limit,
g_scores:[]
}
}).results[0].value;
}
A modo de comparación, aquí está el enfoque "ingenuo" mencionado en otra parte:
var test_naive = function(limit) {
var cursor = db.scores.find({player: "joe"}).limit(limit).sort({points: -1});
var scores = [];
cursor.forEach(function(score) {
score.rank = db.scores.count({points: {"$gt": score.points}}) + 1;
scores.push(score);
});
return scores;
}
Comparé ambos enfoques en una sola instancia de MongoDB 1.8.2 usando el siguiente código:
var rand = function(max) {
return Math.floor(Math.random() * max);
}
var create_score = function() {
var names = ["joe", "ben", "susan", "kevin", "lucy"]
return { player: names[rand(names.length)], points: rand(1000000), foo: 10, bar: 20, bang: "some kind of example text"};
}
var init_collection = function(total_records) {
db.scores.drop();
for (var i = 0; i != total_records; ++i) {
db.scores.insert(create_score());
}
db.scores.createIndex({points: -1})
}
var benchmark = function(test, count, limit) {
init_collection(count);
var durations = [];
for (var i = 0; i != 5; ++i) {
var start = new Date;
result = test(limit)
var stop = new Date;
durations.push(stop - start);
}
db.scores.drop();
return durations;
}
Si bien MapReduce fue más rápido de lo que esperaba, el enfoque ingenuo lo dejó fuera del agua para tamaños de colección más grandes, especialmente una vez que el caché se calentó:
> benchmark(test_naive, 1000, 50);
[ 22, 16, 17, 16, 17 ]
> benchmark(test_mapreduce, 1000, 50);
[ 16, 15, 14, 11, 14 ]
>
> benchmark(test_naive, 10000, 50);
[ 56, 16, 17, 16, 17 ]
> benchmark(test_mapreduce, 10000, 50);
[ 154, 109, 116, 109, 109 ]
>
> benchmark(test_naive, 100000, 50);
[ 492, 15, 18, 17, 16 ]
> benchmark(test_mapreduce, 100000, 50);
[ 1595, 1071, 1099, 1108, 1070 ]
>
> benchmark(test_naive, 1000000, 50);
[ 6600, 16, 15, 16, 24 ]
> benchmark(test_mapreduce, 1000000, 50);
[ 17405, 10725, 10768, 10779, 11113 ]
Entonces, por ahora, parece que el enfoque ingenuo es el camino a seguir, aunque me interesará ver si la historia cambia más adelante este año a medida que el equipo de MongoDB continúa mejorando el rendimiento de MapReduce.