Дата поста: 14-01-2013
Я хочу ещё раз закрепить для себя как пользоваться map-reduce в MongoDB. Закреплять буду на примере домашнего задания 3.4 из курса для MongoDB for DBAs. Текст задания в оригинале:
Use the built-in Map/Reduce functionality in MongoDB to answer the following question: of the zip codes in Pennsylvania, how many are closer to Pittsburgh and how many are closer to Philadelphia?The map function is already written for you.
Question: How many zip codes in PA are closer to philadelphia than to pittsburgh (given our map function’s implementation)? Use map/reduce to find the answer.
Текст задания по-русски:
Есть коллекция с документами такого вида:
{
"city" : "ACMAR",
"loc" : [
-86.51557,
33.584132
],
"pop" : 6055,
"state" : "AL",
"_id" : "35004"
}
Необходимо подсчитать количество городов в штате Пенсильвания, которые расположены ближе к Филадельфии, чем к Питсбургу. Даны координаты городов Питсбурга и Филадельфии:
Питсбург: [-80.064879, 40.612044];
Филадельфия: [-74.978052, 40.089738];
Предполагается что для решения задачи будет использоваться map/reduce. Что такое map-reduce? В mongo это всего-лишь один из способов агрегирования данных. Не могу точно сказать как это работает, могу только строить предположения. В тексте задания так же представлена функция map:
function map_closest() {
var pitt = [-80.064879, 40.612044];
var phil = [-74.978052, 40.089738];
function distance(a, b) {
var dx = a[0] - b[0];
var dy = a[1] - b[1];
return Math.sqrt(dx * dx + dy * dy);
}
if (distance(this.loc, pitt) < distance(this.loc, phil)) {
emit("pitt",1);
} else {
emit("phil",1);
}
}
По замыслу в этой функции данные должны разбиваться на группы и группироваться по некоторому ключу, чтобы потом подсчитывать в функции reduce. Функция map будет вызвана для всех элементов коллекции.
Забегу вперёд и сразу покажу как это выглядит в оболочке mongo вместе с ответом:
db.zips.mapReduce(map_closest, reduce_closest, {query: {state: 'PA'}, out: {inline: 1}})
{
"results" : [
{
"_id" : "phil",
"value" : 732
},
{
"_id" : "pitt",
"value" : 726
}
],
"timeMillis" : 62,
"counts" : {
"input" : 1458,
"emit" : 1458,
"reduce" : 19,
"output" : 2
},
"ok" : 1,
}
Опция out необходима для указания коллекции в котороую будет записан результат. Как я понял это обязательно начиная с версии 1.8. Я указал что хочу видеть результат на экране. Если забыть это сделать, то получится ошибка вроде этой:
assert failed : need to supply an optionsOrOutString
или этой:
“errmsg” : “exception: ‘out’ has to be a string or an object”
Опция query
фильтрует набор данных до того, как к ним будет применена функция map. Я отобрал здесь записи со штатом PA(Филадельфия).
Как работает map reduce в mongodb?
Для всех лементов коллекции вызывается функция map. На этом шаге данные разбиваются и группируются по ключу. Функция map должна вызывать метод emit
. Аргументами метода emit должны быть некие ключ и некоторое значение. В рассматриваемой задачи ключём является название города, а значение всегда будет равным единице. Вызов функции emit(“phil”, 1)
для города означает, что он ближе к Филадельфии нежели к Питсбургу.
Далее вероятно формируются объекты в виде ключа и списка значений. В нашей задаче они должны выглядеть как то так:
{phil: [1, 1, 1, 1, 1]}
{phit: [1, 1]}
Можно предположить что при каждом вызове функции emit
с ключём и значением – это значение добавляется в список значений для этого ключа. Вызов emit(‘phil’, 1)
– добавит элемент 1
в список к ключу phil
. Для phit
тоже самое.
На шаге reduce необходимо просуммировать сгруппированные на шаге map данные. Для этого я создал вот такую простую функцию reduce
:
function reduce_closest(key, values) {
var result = 0;
for(var i = 0; i < values.length; i++)
{
result += values[i];
}
return result;
}
Как я понял, в функции reduce обязательно нужно возвращать данные в том-же виде в каком они подаются ей на вход, имеется ввиду value(элемент списка values). То есть если элементы списка values – целые числа. возвращать надо целое число, если это объекты – возвращает надо такой же объект. Данные с выхода функции reduce попадают на вход другой функции reduce и так далее по цепочке.
В первый раз я прозевал этот момент и реализовал функцию reduce в одну строчку: return values.length
. Это оказалось не верно. Потом я ещё раз пересмотре лекцию и понял что это было ошибочным решением.