mapreduce с сортировкой во внутреннем документе mongodb

#javascript #mongodb #mapreduce #aggregation-framework

#javascript #mongodb #mapreduce #агрегация-фреймворк

Вопрос:

У меня есть небольшой вопрос по map-reduce с mongodb. У меня есть следующая структура документа

 {
   "_id": "ffc74819-c844-4d61-8657-b6ab09617271",
   "value": {
     "mid_tag": {
       "0": {
         "0": "Prakash Javadekar",
         "1": "Shastri Bhawan",
         "2": "Prime Minister's Office (PMO)",
         "3": "Narendra Modi"
      },
       "1": {
         "0": "explosion",
         "1": "GAIL",
         "2": "Andhra Pradesh",
         "3": "N Chandrababu Naidu"
      },
       "2": {
         "0": "Prime Minister",
         "1": "Narendra Modi",
         "2": "Bharatiya Janata Party (BJP)",
         "3": "Government"
      }
    },
     "total": 3
  }
}
  

когда я выполняю свой код map reduce для этой коллекции документов, я хочу указать total в качестве поля сортировки в этой команде

 db.ana_mid_big.mapReduce(map, reduce, 
        {
            out: "analysis_result",
            sort: {"value.total": -1}
        }
);
  

Но, похоже, это не работает. Как я могу указать ключ, который является вложенным для сортировки? Пожалуйста, помогите.

———————— РЕДАКТИРОВАТЬ ———————————

согласно комментариям, я публикую всю свою проблему здесь. Я начал с коллекции, содержащей чуть более 3,5 млн документов (это всего лишь старый снимок текущего документа, который уже пересек 5,5 млн), который выглядит следующим образом

 {
   "_id": ObjectId("53b394d6f9c747e33d19234d"),
   "autoUid": "ffc74819-c844-4d61-8657-b6ab09617271"
   "createDate": ISODate("2014-07-02T05:12:54.171Z"),
   "account_details": {
     "tag_cloud": {
       "0": "FIFA World Cup 2014",
       "1": "Brazil",
       "2": "Football",
       "3": "Argentina",
       "4": "Belgium"
    }
  }
}
  

Итак, может быть много документов с одинаковым автоидентификацией, но с разными (или частично одинаковыми или даже одинаковыми) тегами_cloud.

Я написал следующее map-reduce для создания промежуточной коллекции, которая выглядит как та, что приведена в начале вопроса. Итак, очевидно, что эта коллекция всех тегов clouds принадлежит одному человеку в одном документе. Для достижения этого я использовал MR-код, который выглядит следующим образом

 var map = function(){

  final_val = {
        tag_cloud: this.account_details.tag_cloud,
        total: 1
  };
  emit(this.autoUid, final_val)
}

var reduce = function(key, values){
  var fv = {
        mid_tags: [],
        total: 0
  }
  try{
    for (i in values){
      fv.mid_tags.push(values[i].tag_cloud);
      fv.total = fv.total   1;
    }
  }catch(e){
    fv.mid_tags.push(values)
    fv.total = fv.total   1;
  }
  return fv;
}

db.my_orig_collection.mapReduce(map, reduce, 
        {
            out: "analysis_mid",
            sort: {createDate: -1}
    }
);
  

Здесь возникает проблема номер 1. когда у кого-то есть более одной записи, она подчиняется функции reduce. Но когда у кого-то есть только один, вместо того, чтобы называть его «mid_tag», он сохраняет имя «tag_cloud». Я понимаю, что есть какая-то проблема с кодом сокращения, но не могу найти какая.

Теперь я хочу достичь конечного результата, который выглядит как

 {"_id": "ffc74819-c844-4d61-8657-b6ab09617271",
"value": {
    "tags": {
        "Prakash Javadekar": 1,
        "Shastri Bhawan": 1,
        "Prime Minister's Office (PMO)": 1,
        "Narendra Modi": 2,
        "explosion": 1,
        "GAIL": 1,
        "Andhra Pradesh": 1,
        "N Chandrababu Naidu": 1,
        "Prime Minister": 1,
        "Bharatiya Janata Party (BJP)": 1,
        "Government": 1
    }
}
  

Который, наконец, является одним документом для каждого пользователя, представляющим плотность тегов, которую они использовали. Код MR, который я пытаюсь использовать (еще не тестировался), выглядит следующим образом—

 var map = function(){
  var val = {};
  if ("mid_tags" in this.value){
    for (i in this.value.mid_tags){
        for (j in this.value.mid_tags[i]){
            k = this.value.mid_tags[i][j].trim();
            if (!(k in val)){
                val[k] = 1;
            }else{
                val[k] = val[k]   1;
            }
        }
    }
    var final_val = {
        tag: val,
        total: this.value.total
    }
    emit(this._id, final_val);
  }else if("tag_cloud" in this.value){
    for (i in this.value.tag_cloud){
        k = this.value.tag_cloud[i].trim();
        if (!(k in val)){
            val[k] = 1;
        }else{
            val[k] = val[k]   1;
        }
    }
    var final_val = {
        tag: val,
        total: this.value.total
    }
    emit(this._id, final_val);  
  }
}
var reduce = function(key, values){
    return values;
}

db.analysis_mid.mapReduce(map, reduce, 
        {
            out: "analysis_result"
        }
);
  

Этот последний фрагмент кода еще не протестирован. Это все, что я хочу сделать. Пожалуйста, помогите

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

1. MapReduce выполняет сортировку только по значениям «ключа». Вы не можете отсортировать выходные данные без «вывода» в другую коллекцию (как у вас есть), а затем выполнить следующую .find() операцию над этой коллекцией с .sort() модификатором. Возможно, вам следует показать, «что вы на самом деле пытаетесь сделать» в операции «MapReduce». Это фактически оставляет ваш вопрос открытым для интерпретации «лучших способов сделать это». В настоящее время, хотя ответ таков, вы не можете сортировать выходные данные с помощью MapReduce.

2. Ага. Пожалуйста, укажите вашу карту и сократите функции.

3. Более конкретно, обратите внимание, что sort опция для mapReduce() применяется только к входным документам, а не к результирующим значениям.

Ответ №1:

Похоже, что отображается ваш фон PHP. Структуры данных, которые вы представляете, не отображают массивы в типичной нотации JSON, однако в вашем коде MapReduce отмечены вызовы «push», что, по крайней мере, в вашем «промежуточном документе» значения на самом деле являются массивами. Похоже, вы «обозначили» их одинаково, поэтому кажется разумным предположить, что они есть.

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

 {
   "_id": ObjectId("53b394d6f9c747e33d19234d"),
   "autoUid": "ffc74819-c844-4d61-8657-b6ab09617271"
   "createDate": ISODate("2014-07-02T05:12:54.171Z"),
   "account_details": {
     "tag_cloud": [
       "FIFA World Cup 2014",
       "Brazil",
       "Football",
       "Argentina",
       "Belgium"
     ]
   }
}
  

С подобными документами или если вы измените их, чтобы они были похожими на эти, то вашим подходящим инструментом для этого является платформа агрегации. Это работает в машинном коде и не требует интерпретации JavaScript, следовательно, это намного быстрее.

Оператор агрегирования для получения конечного результата выглядит следующим образом:

 db.collection.aggregate([

    // Unwind the array to "de-normalize"
    { "$unwind": "$account_details.tag_cloud" },

    // Group by "autoUid" and "tag", summing totals
    { "$group": {
        "_id": {
            "autoUid": "$autoUid",
            "tag": "$account_details.tag_cloud"                
        },
        "total": { "$sum": 1 }
    }},

    // Sort the results to largest count per user
    { "$sort": { "_id.autoUid": 1, "total": -1 }

    // Group to a single user with an array of "tags" if you must
    { "$group": {
        "_id": "$_id.autoUid",
        "tags": { 
            "$push": {
                "tag": "$_id.tag",
                "total": "$total"
            }
        }
    }}
])
  

Немного другой результат, но гораздо проще в обработке и намного быстрее:

 {
    "_id": "ffc74819-c844-4d61-8657-b6ab09617271",
    "tags": [
        { "tag": "Narendra Modi", "total": 2 },
        { "tag": "Prakash Javadekar", "total": 1 },
        { "tag": "Shastri Bhawan", "total": 1 },
        { "tag": "Prime Minister's Office (PMO)", "total": 1 },  
        { "tag": "explosion", "total": 1 },
        { "tag": "GAIL", "total":  1 },
        { "tag": "Andhra Pradesh", "total": 1 },
        { "tag": "N Chandrababu Naidu", "total": 1 },
        { "tag": "Prime Minister", "total": 1 },
        { "tag": "Bharatiya Janata Party (BJP)", "total": 1 },
        { "tag": "Government", "total": 1 }
    ]
}
  

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

Тем не менее, на сегодняшний день это лучший вариант. Узнайте, как использовать фреймворк агрегации. Если ваш «вывод» по-прежнему будет «большим» (более 16 МБ), попробуйте рассмотреть возможность перехода на MongoDB 2.6 или выше. Агрегированные инструкции могут создавать «курсор», который можно повторять, а не извлекать все результаты сразу. Также существует $out оператор, который может создавать коллекцию точно так же, как это делает MapReduce.


Если ваши данные на самом деле находятся в формате, подобном «хэшу» вложенных документов, как вы указываете в своей записи этого ( которая соответствует соглашению PHP о «дампе» для массивов), то вам нужно использовать MapReduce, поскольку платформа агрегации не может обрабатывать «хэш-ключи» так, как они представлены. Не лучшая структура, и вам следует изменить ее, если это так.

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

 db.collection.mapReduce(
    function() {

        var tag_cloud = this.account_details.tag_cloud; 
        var obj = {};

        for ( var k in tag_cloud ) {
            obj[tag_cloud[k]] = 1; 
        }

        emit( this.autoUid, obj );

    },
    function(key,values) {

        var reduced = {};

        // Combine keys and totals
        values.forEach(function(value) {
            for ( var k in value ) {
                if (!reduced.hasOwnProperty(k))
                    reduced[k] = 0;
                reduced[k]  = value[k];
            }
        });

        return reduced;
    },
    { 
        "out": { "inline": 1 }, 
        "finalize": function(key,value) {

            var output = [];

            // Mapped to array for output
            for ( var k in value ) {
                output.push({
                    "tag": k,
                    "total": value[k]
                });                    
            }

            // Even sorted just the same
            return output.sort(function(a,b) {
                return ( a.total < b.total ) ? -1 : ( a.total > b.total ) ? 1 : 0;
            });

        }
    }
)
  

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

 db.collection.mapReduce(
    function() {

        var tag_cloud = this.account_details.tag_cloud; 
        var obj = {};

        tag_cloud.forEach(function(tag) {
            obj[tag] = 1; 
        });

        emit( this.autoUid, obj );

    },
    function(key,values) {

        var reduced = {};

        // Combine keys and totals
        values.forEach(function(value) {
            for ( var k in value ) {
                if (!reduced.hasOwnProperty(k))
                    reduced[k] = 0;
                reduced[k]  = value[k];
            }
        });

        return reduced;
    },
    { 
        "out": { "replace": "newcollection" },
        "finalize": function(key,value) {

            var output = [];

            // Mapped to array for output
            for ( var k in value ) {
                output.push({
                    "tag": k,
                    "total": value[k]
                });                    
            }

            // Even sorted just the same
            return output.sort(function(a,b) {
                return ( a.total < b.total ) ? -1 : ( a.total > b.total ) ? 1 : 0;
            });

        }
    }
)
  

По сути, все следует тем же принципам, чтобы получить конечный результат:

  1. Отменить нормализацию до комбинации «пользователь» и «тег» с «пользователь» и ключом группировки
  2. Объедините результаты для каждого пользователя с общим количеством значений «тегов».

В данном подходе MapReduce, помимо того, что он более чистый, чем тот, который вы, казалось, пытались использовать, другим важным моментом, который следует учитывать, является то, что редуктору необходимо «выводить» точно такой же «ввод», который поступает от картографа. Причина на самом деле хорошо документирована, поскольку «reducer» на самом деле может вызываться несколько раз, в основном «уменьшая снова» вывод, который уже прошел обработку reduce.

Обычно именно так MapReduce работает с «большими входными данными», где для данного «ключа» имеется множество значений, а «редуктор» обрабатывает только очень много из них за один раз. Например, редуктор может фактически принимать только 30 или около того документов, отправленных с одним и тем же ключом, сокращать два набора из этих 30 до 2 документов, а затем, наконец, сводить к одному выводу для одного ключа.


Конечный результат здесь такой же, как и в другом выводе, показанном выше, с той разницей, что в MapReduce все находится под ключом «значение», поскольку именно так это и работает.

Итак, есть пара способов сделать это в зависимости от ваших данных. По возможности старайтесь придерживаться платформы aggregation framework, поскольку она намного быстрее, а современные версии могут потреблять и выводить ровно столько данных, сколько вы можете загрузить в MapReduce.

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

1. Такой красиво написанный проницательный ответ. Я рад это прочитать. На самом деле это разрешило множество недоразумений в моем сознании относительно map-reduce и фреймворка агрегации. Большое вам спасибо

2. Просто добавлю, что формат, который я вставил сюда, был взят из rockmongo и, таким образом, выглядел примерно так. На самом деле это []