GrabDuck

MongoDB Aggregation Framework — простой и быстрый способ обработки массивов данных ...

:

Aggregation Framework MongoDB даёт возможность расчёта агрегированных данных без использования Map-Reduce. Хотя Map-Reduce и является мощным инструментом, он медленно справляется с обработкой больших объёмов данных. В этой статье я хотел бы сравнить фреймворк Map-Reduce с MongoDB  Aggregation Framework и показать существенные преимущества использования последнего.

MongoDB в сравнении с Map-Reduce

Aggregation Framework MongoDB даёт возможность работать с агрегированными данными. Ниже перечислены основные отличия Aggregation Framework от Map-Reduce:

  • декларативный синтаксис — нет необходимости писать код на JavaScript;
  • описание применяемых цепочек операций;
  • вычисление выражений;
  • более высокая производительность, так как фреймворк реализован на C++, а не на JavaScript;
  • наличие проекций возвращаемых данных, а значит возможность добавления вычисляемых полей, вложенных объектов и проч.

Концепция фреймворка

Aggregation Framework работает по тому же принципу, что и оператор “GROUP BY” в SQL. Его две основные концепции — конвейеры и выражения. Конвейеры — это операторы, которые обрабатывают поток документов. А выражения возвращают обработанные документы после проведения вычислений над исходными документами. Вот некоторые конвейеры:

  • $match — использует уточняющий предикат, так же как и collection.find({});
  • $project — позволяет менять форму результата, включая рассчитанные значения, суб-объекты и проч.;
  • $unwind — разделяет элементы массива и добавляет его (их?) в выходной документ;
  • $sort — сортирует документы;
  • $limit — указывает максимальное число документов, которые будут возвращены;
  • $skip — пропускает указанное число документов.
  • $group — aggregates items into buckets defined by a key;  группирует данные по определенным ключам (полям)

Использование MongoDB в Node.JS: наш практический опыт

MongoDB имеет драйверы для многих языков программирования и платформ, включая Node.JS. Чтобы установить драйвер для Node.JS, наберите выполните команду:

npm install mongodb

Все функции MongoDB доступны в драйвере. Задача была агрегировать (сгруппировать) большую коллекцию данных из трёх полей, чтобы составить статистический отчёт. Коллекция (база) содержала около 500 тысяч (полумиллиона) записей со статистикой просмотра веб-страниц. Формат каждого документа следующий:

{
 "_id" : ObjectId("50890388e4b04b876d9cebf1"),
 "userId" : "someId",
 "url" : "http://some.url",
 "backendTime" : 835,
 "domProcessingTime" : 14,
 "pageRenderingTime" : 419,
 "totalLoadTime" : 1273,
 "ip" : "0.0.0.0",
 "browser" : "Firefox",
 "version" : "16",
 "OS" : "Linux",
 "pageSize" : 907,
 "images" : 1,
 "styleSheets" : 4,
 "screenResolution" : "1920;1080",
 "country" : "RU",
 "time" : NumberLong("1351156616195")
 }

Было необходимо сгруппировать данные по времени, IP-адресу и URL. Первый вариант решения этой задачи был реализован с использованием Map-Reduce:

var map = function() {
 var d = new Date(parseFloat(this.time));
 var currTimeSlice = getCurrTimeSlice(d, frequency);
 var key = {time: currTimeSlice, url: this.url, ip: this.ip};
 emit(key, {count: 1});
 };
var reduce = function(key, values) {
 var sum = 0;
 values.forEach(function(value) {
 sum += value.count;
 });
 return {time: key.time, url: key.url, ip: key.ip, count: sum};
 };
var scope = {
 frequency : frequency,
 getCurrTimeSlice: new Code(getCurrTimeSlice.toString()),
 getHour : new Code(getHour.toString()),
 getDay: new Code(getDay.toString()),
 getWeek : new Code(getWeek.toString()),
 getMonth : new Code(getMonth.toString())
 };
db.collection('beacons', function(err, collection) {
 collection.mapReduce(map, reduce, {out: {inline: 1}, query: filter, scope : scope},
 function(err, items) {
 // logic
 });
 });

Обработка 500 тысяч (полумиллиона) записей заняла одну минуту. Это было досадной проблемой (это было слишком медленно), и мы решили переключиться на Aggregation Framework MongoDB 2.1. Новая версия реализации механизма агрегирования представлена ниже:

db.collection('beacons', function(err, collection) {
 collection.aggregate([
 {$match : filter},
 {$group : {
 '_id' : {'time' :'$time', 'url' : '$url', 'ip' : '$ip'},
 'count' : {$sum : 1}
 }}],
 function (err, items){
 // logic
 });
 });

В этом коде мы использовали два конвейера: $match и $group. $match фильтрует требуемые записи, а $group объединяет их в три поля: time, URL и IP. Эти поля были установлены как ключ, так как мы явно указали поле ‘_id’, а выражение $sum вычисляет количество записей с тем же ключом. Выходные данные имеют следующий вид:

[
 {
 '_id' : {'time' : 1111111111, 'ip' : '0.0.0.0', 'url' : 'http://some.url'},
 'count' : 38
 },
 {
 '_id' : {'time' : 1111111112, 'ip' : '0.0.0.0', 'url' : 'http://some.url'},
 'count' : 38
 },
 ...
]

Результат

Использование Aggregation Framework значительно улучшило производительность обработки данных. Теперь 500 тысяч (полмиллиона) записей обрабатываются за 3-4 секунды. Таким образом, MongoDB Aggregation Framevork — мощный, простой и лёгкий инструмент, который действительно позволяет улучшить производительность расчётов над агрегированными данными без использования Map-Reduce.