Skip to content

Использование MapReduce в MongoDB

25/05/2010

Ещё одна полезная штука в MongoDB — возможность делать агрегатные запросы с помощью MapReduce. Но в отличии от CouchDB, здесь map-reduce используется только когда это явно необходимо. Сейчас я попробую воспользоваться этой фичей.

Все дальнейшие действия будут проводиться в консоли. Любой консольной команде можно найти соответствие в коннекторе Node.js (если Вы здесь за этим).

Итак, сначала нам нужна тестовая база, которую мы и будем опрашивать. Продолжая предыдущую статью с игрой в «танчики» — пусть в этой базе хранятся результаты матчей в формате «имя первого игрока, очки первого игрока, имя второго игрока, очки второго игрока»:

var record = {
    'player1':'abc',
    'player2':'def',
    'score1':10,
    'score2':50
}

Теперь заполняем базу — 1000 значений хватит для начала. Игроки фиксированные, очки генерируются случайно.

for (var i = 0; i<1000; i++) db.rounds.insert({'player1':'kze', 'player2':'vls', 'score1':Math.random()*100, 'score2':Math.random()*100});

База готова, мы можем посмотреть её содержимое с помощью db.scores.find(). Теперь нам нужны две функции — Map и Reduce. Map преобразует каждую запись в исходной таблице и выдаёт её вместе с ключом, Reduce работает с преобразованными записями одного ключа.

Предположим, нам надо получить среднее количество очков первого игрока, но только в тех матчах где первый игрок победил. В фазе Map мы просто выдаём очки первого игрока в случае если его результат выше чем у второго:

m = function() {if (this.score1 > this.score2) emit(this.player1, this.score1); }

(для удобства я объявляю функции отдельно). Теперь в функцию Reduce придёт набор этих чисел с ключом «kze» (мы указали его первым в emit). Нам надо просто вычислить и вернуть среднее значение:

r = function (k, vals) {var sum = 0;for(var i in vals) sum += vals[i];return (sum / vals.length); }

Вот так. Теперь можно использовать db.rounds.mapReduce(m, r), который вернёт ссылку на коллекцию. Чтобы сразу увидеть что получилось, сделаем find():

db.rounds.mapReduce(m,r).find();

Результат (примерный):

{ "_id" : "kze", "value" : 68.44162140613429 }

Можно теперь сделать запрос более общим — пусть возвращает статистику по обоим игрокам сразу. Для этого нам надо изменить только функцию map:

m = function() { if (this.score1 > this.score2) { emit(this.player1, this.score1) } else { if (this.score2 > this.score1) emit(this.player2, this.score2)}; }

Снова запускаем db.rounds.mapReduce(m, r). Результат должен быть примерно таким:

{ "_id" : "kze", "value" : 68.44162140613429 }
{ "_id" : "vls", "value" : 68.99777604410806 }

Но каждый раз запускать mapReduce — дело не очень правильное. Всё таки его преимущество как раз в том чтобы не пересчитывать промежуточные результаты, когда это не нужно. Сохраним ссылку на результаты:

var victories = db.rounds.mapReduce(m,r)
victories.find()

Вот так. Теперь можно делать запросы к victories, не пересоздавая таблицы промежуточных результатов.

Для разнообразия набросим ещё тысячу записей в таблицу rounds, с немного другими игроками:

for (var i = 0; i<1000; i++) db.rounds.insert({'player1':'kze', 'player2':'max', 'score1':Math.random()*100, 'score2':Math.random()*100});

Вот теперь нам придётся сделать mapReduce() заново, чтобы получить новые результаты. Ради интереса я накинул ещё 28 тысяч записей в коллекцию — тот же самый mapReduce-запрос отрабатывает практически мгновенно, вставка шла дольше.

UPD: код в Node.js

// Здесь объявляем базу данных и sys

db.open(function(err, db) {
    db.collection('rounds', function(err, collection) {
        var name_letter = 'v';
        var map = function() {
            if (this.score1 > this.score2) {
                emit(this.player1, this.score1);
            } else {
                if (this.score2 > this.score1) emit(this.player2, this.score2)
            }
        };

        var reduce = function (k, vals) {var sum = 0;for(var i in vals) sum += vals[i];return (sum / vals.length); };

        collection.mapReduce(map, reduce, function(err, result) {
            if (err) {
                sys.puts('Err: ' + JSON.stringify(err));
            } else {
                result.findOne({'_id':'kze'}, function(err, scores) {
                    if (err) {
                       sys.puts('Err in results');
                    }
                    sys.puts('Kaze scores: ' + JSON.stringify(scores));
                });
                sys.puts('Mapreduce done');
            }

        });
    });

});

Ссылки по теме

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: