Общее·количество·просмотров·страницы

Java Dev Notes - разработка на Java (а также на JavaScript/Python/Flex и др), факты, события из АйТи

пятница, 14 октября 2011 г.

MapReduce и MongoDB

Для обработки данных в коллекциях в MongoDB есть замечательный инструмент - map-reduce. Посмотрим, как он работает.

Я это делал на реальном примере по одному проекту - нужно обработать данные по пользовательским покупкам, которые хранятся в монге. Нас интересует два поля: ID пользователя и сумма покупки. В качестве ID пользователя выступает GUID.

Нужно сделать следующий отчет: показать пользователей с наибольшим количеством потраченных денег.

Если бы мы были в SQL, то табличка с данными о покупках была бы следующие:

CREATE TABLE sales(
  guid varchar(32) NOT NULL PRIMARY KEY,
  price float NOT NULL
);
Запрос, который выводит данный отчет, был бы следующий:
SELECT guid, sum(price) AS summa FROM sales 
GROUP BY guid
ORDER BY summa DESC;
В монге данные хранятся в коллекции stat, вид данных следующий:
> db.stat.findOne();
{ 
  "_id" : ObjectId("4e4c73eb41e5c790a7391848"), 
  "guid" : "346fbb3968c9453d9dc8f8ebe0fa6763", 
  "item" : "Apple MacBook Air MC968LL/A 11.6-Inch", 
  "price" : 78.64, 
  "retailerId" : "COST", 
  "ip" : "0:0:0:0:0:0:0:1", 
  "datetime" : "2011-07-28 10:12:38", 
  "timezone" : "-1", 
  "createDate" : "Thu Aug 18 2011 06:07:39 GMT+0400 (MSK)", 
}
Что ж, приступим в мап-редьюсу...
function salesMap() {
    emit(this.guid, {price: this.price });
}
 
function salesReduce(key, values) {
    var result = { 
        count: 0,
        summa: 0.0
    };
    values.forEach(function(v){
        if (v.price) {
            result.summa += v.price;
            result.count++;
        }
    });
    return result;
}
db.userrep.drop();
db.stat.mapReduce(salesMap,salesReduce,{out:'userrep', verbose: true});
db.userrep.find();
Функция emit(key, value), которую мы использовали в функции salesMap, подает на вход редьюсу пару ключ - значение. Ключом мы выбрали GUID, т.к. по нему идет группировка, а в значение кладем цену товара. Функция salesReduce получает на вход набор объектов, сгруппированных по ключу.

Внутри функции все тривиально:
1) создаем объект с результатами, пока что содержащий нулевые значения var result = { count: 0, summa: 0.0 };
2) пробегаемся по коллекции и заполняем результат
3) возвращаем результат.
4) PROFIT!!!
Вид результатов следующий:

> db.userrep.find().limit(5)
{ "_id" : "01dc299862094450ac232d384d883a5f", "value" : { "count" : 2, "summa" : 245.19 } }
{ "_id" : "0a1afef3b8b27942d9a8d02903ca2c28", "value" : { "count" : 1, "summa" : 30.43 } }
{ "_id" : "0c9d6a05458313b85706548c290991e9", "value" : { "count" : 0, "summa" : 0 } }
{ "_id" : "0f4e595202884408ae4c4c6306d764f9", "value" : { "count" : 1, "summa" : 88.43 } }
{ "_id" : "17a57229a9c14b2cb46337a3d196051c", "value" : { "count" : 1, "summa" : 49.54 } }
Какая жалость - данные не отсортированы.

Монго не умеет сортировать коллекцию данным из составных объектов, т.е. мы не можем написать что-то вроде:

db.userrep.find().sort({value.summa: -1});
Поэтому придется немножко извратиться:
db.userrep.find().forEach(function(v) {
    var s = v.value.summa;
    var c = v.value.count;
    var id = v._id;
    db.userrep.update({_id: id},{$set:{summa:s, count: c}}, true, true);
});
Мы просто скопировали поля summa, count из объекта value в объект-контейнер коллекции. Теперь данные выглядят так:
> db.userrep.find().limit(5);
{ "_id" : "01dc299862094450ac232d384d883a5f", "count" : 2, "summa" : 245.19, "value" : { "count" : 2, "summa" : 245.19 } }
{ "_id" : "0a1afef3b8b27942d9a8d02903ca2c28", "count" : 1, "summa" : 30.43, "value" : { "count" : 1, "summa" : 30.43 } }
{ "_id" : "0c9d6a05458313b85706548c290991e9", "count" : 0, "summa" : 0, "value" : { "count" : 0, "summa" : 0 } }
{ "_id" : "0f4e595202884408ae4c4c6306d764f9", "count" : 1, "summa" : 88.43, "value" : { "count" : 1, "summa" : 88.43 } }
{ "_id" : "17a57229a9c14b2cb46337a3d196051c", "count" : 1, "summa" : 49.54, "value" : { "count" : 1, "summa" : 49.54 } }
Теперь, наконец, можно получить отсортированные данные:
> db.userrep.find().sort({summa:-1}).limit(5);
{ "_id" : "cb1332aed05d48fd9c51d0c5be584156", "count" : 4, "summa" : 496.72999999999996, "value" : { "count" : 4, "summa" : 496.72999999999996 } }
{ "_id" : "ade1efffe9d3fb116f33c320bf9b447e", "count" : 3, "summa" : 412.45000000000005, "value" : { "count" : 3, "summa" : 412.45000000000005 } }
{ "_id" : "ef98c2c430c4486394bc2b85ec5b1a77", "count" : 3, "summa" : 408.5, "value" : { "count" : 3, "summa" : 408.5 } }
{ "_id" : "5295f9b927c59dacdd3fb20d8173a19b", "count" : 4, "summa" : 367.08, "value" : { "count" : 4, "summa" : 367.08 } }
{ "_id" : "01dc299862094450ac232d384d883a5f", "count" : 2, "summa" : 245.19, "value" : { "count" : 2, "summa" : 245.19 } }

Комментариев нет:

Отправить комментарий

Постоянные читатели