Агрегирование выборки в MongoDB + PHP

MongoDB — очередное веяние моды в веб-разработке, когда хранение данных заранее планируется таким огромным, что необходимо их распределение на несколько серверов с помощью шардирования. Поизучаем же непростой синтаксис агрегирования данных в mongo + php

Уникальное поле

Итак допустим есть коллекция объектов с повторяющимися полями, а нам хочется вытянуть только уникальные поля. В MySQL это делает DISTINCT(поле), здесь же надо запустить комманду которая вернёт только поле person_id.

$aObjects = $Mongo->command(array(
    'distinct'=>'my_userlog_collection',
    'key'=>'person_id',
    'query'=>array(
        '$or'=>array(
            array('person_id' => new MongoRegex('/'.$sSearch.'.*/i')),
            array('firstname' => new MongoRegex('/.*'.$sSearch.'.*/i')),
            array('lastname' => new MongoRegex('/.*'.$sSearch.'.*/i')),
        )
    ),
));

Как можно заметить одного лишь поля порой недостаточно — зачастую хочется сделать GROUP BY, т.е. повторяющиеся по заданным полям ряды склеиваются (а в случае различия других полей берётся первый из подходящих). В mongo такую пост-обработку результатов производят запускаемые пользовательские функции map, reduce и finalize. Я сначала было подумал что в адаптер Mongo надо передавать имя php-функции и даже начал смотреть синтаксис анонимных функций, но потом оказалось что передаются вовсе не функции а банально текст с javascript-функцией

Нанести на карту и уменьшить

Функция map() это своего рода SELECT — надо вызвать функцию emit() у которой первый параметр — ключ, а второй — собственно нужные поля. Конечно можно вместо полей поставить this, по аналогии со звёздочкой в SQL, но мы же знаем что это плохая практика. Но я как плохой, возьму все данные.

Теперь когда mongos, будет получать от mongod данные, он будет группировать их по первому ключу и передавать в reduce() функцию. Очень много примеров зачем-то делают там циклы и переменные.. Мне достаточно взять самый первый подходящий объект.

В коде выше я для полноты картины добавил регулярный поиск для имитации LIKE %, так что тут не буду повторяться. Поскольку возвращаемый адаптером результат вовсе не список, а какая-то левая статистика, то приходится делать что-то типа под-запроса. Вот тут уже реальные данные, но они в формате array (id, value), что помоему тоже извращение, а поскольку в map я сделал SELECT *, то тут я для наглядности показываю ещё и пост-фильтрацию. В итоге что-бы сгруппировать по person_id надо создать такого монстра..

$aMyCollectionResult = $Mongo->command(array(
    "mapreduce" => 'my_userlog_collection',
    "map"       => new MongoCode("function() { emit(this.person_id,this); }"), //SELECT
    "reduce" => new MongoCode("function(k, vals) { return vals[0]; }"), //GROUP
    "query" => array() //сами поставьте сюда WHERE
));
$aList = $Mongo->oMongo->selectCollection($aMyCollectionResult['result'])->find(
    array(),array("value.person_id",'value.first_name','value.last_name','value.country_code')
); //
print_r(iterator_to_array($aList)); //распечатаем что-бы посмотреть