Лимит мангуста по ассоциации

У меня есть такая коллекция:

[
  { parent: 'a', d1: '1', d2: '2', d3: '3', w: 10 },
  { parent: 'a', d1: '1', d2: '2', d3: '3', w: 20 },
  { parent: 'a', d1: '1', d2: '2', d3: '3', w: 30 },
  { parent: 'a', d1: '1', d2: '2', d3: '3', w: 40 },
  { parent: 'a', d1: '1', d2: '2', d3: '3', w: 50 },
  { parent: 'a', d1: '1', d2: '2', d3: '3', w: 60 },
  { parent: 'b', d1: '1', d2: '2', d3: '3', w: 10 },
  { parent: 'b', d1: '1', d2: '2', d3: '3', w: 13 },
  { parent: 'b', d1: '1', d2: '2', d3: '3', w: 14 },
  { parent: 'b', d1: '1', d2: '2', d3: '3', w: 15 },
  { parent: 'c', d1: '1', d2: '2', d3: '3', w: 10 },
  { parent: 'c', d1: '1', d2: '2', d3: '3', w: 100 },
  { parent: 'c', d1: '1', d2: '2', d3: '3', w: 200 },
  { parent: 'c', d1: '1', d2: '2', d3: '3', w: 300 }
]

Учитывая запрос с соответствующими родительскими идентификаторами, ['b','c'], мне нужно получить первые 3 результата для каждого родителя, надеюсь, отсортированные по DESC по w:

[
  { parent: 'b', d1: '1', d2: '2', d3: '3', w: 15 },
  { parent: 'b', d1: '1', d2: '2', d3: '3', w: 14 },
  { parent: 'b', d1: '1', d2: '2', d3: '3', w: 13 },
  { parent: 'c', d1: '1', d2: '2', d3: '3', w: 300 },
  { parent: 'c', d1: '1', d2: '2', d3: '3', w: 200 },
  { parent: 'c', d1: '1', d2: '2', d3: '3', w: 100 }
]

Использование .find() и .limit() вернет первые N результатов в целом, а не первые N для каждого parent. Используя .aggregate(), я понял, как агрегировать по parent, но я не мог понять, как $limit по родителю или как вернуть все документы как {parent: 'b', items: [{..}, {..}] }, а не только данные группы. Я могу получить либо parent, который у меня уже был, либо, может быть, parent и массив на каком-то поле с помощью $push, но это все равно не годится.

Наконец, я также попробовал .mapReduce, но это кажется излишним, не придется ли мне использовать emit(this.project, this); для части агрегации? как бы я даже $ лимит на это? рукой? Это довольно малодокументировано.

В любом случае, какое-то направление, куда идти, было бы здорово здесь. Я использую mongoose@latest.


person bevacqua    schedule 09.01.2015    source источник
comment
jira.mongodb.org/browse/SERVER-9377   -  person lante    schedule 09.01.2015


Ответы (2)


Как указано , этого, к сожалению, нельзя достичь с помощью существующей структуры агрегирования MongoDB. в настоящее время, и правильно, как вы упомянули, map-reduce было бы излишним.

Но есть альтернативные подходы к этому:

Подход А:

  • Сохраните переменную, обозначающую уровень иерархии на основе поля w или поля, по которому вы хотите отсортировать набор результатов. Как только вы добавите переменную в каждый документ во время вставки.
  • Ваши документы будут содержать новое поле с именем level, которое содержит массив из одного значения. Мы обсудим, почему это должен быть массив, а не простое поле.

Вставить скрипты:

db.collection.insert([
  { parent: 'a', d1: '1', d2: '2', d3: '3', w: 10,level:[6] },
  { parent: 'a', d1: '1', d2: '2', d3: '3', w: 20,level:[5] },
  { parent: 'a', d1: '1', d2: '2', d3: '3', w: 30,level:[4] },
  { parent: 'a', d1: '1', d2: '2', d3: '3', w: 40,level:[3] },
  { parent: 'a', d1: '1', d2: '2', d3: '3', w: 50,level:[2] },
  { parent: 'a', d1: '1', d2: '2', d3: '3', w: 60,level:[1] },
  { parent: 'b', d1: '1', d2: '2', d3: '3', w: 10,level:[4] },
  { parent: 'b', d1: '1', d2: '2', d3: '3', w: 13,level:[3] },
  { parent: 'b', d1: '1', d2: '2', d3: '3', w: 14,level:[2] },
  { parent: 'b', d1: '1', d2: '2', d3: '3', w: 15,level:[1] },
  { parent: 'c', d1: '1', d2: '2', d3: '3', w: 10,level:[4] },
  { parent: 'c', d1: '1', d2: '2', d3: '3', w: 100,level:[3] },
  { parent: 'c', d1: '1', d2: '2', d3: '3', w: 200,level:[2] },
  { parent: 'c', d1: '1', d2: '2', d3: '3', w: 300,level:[1] }
])

Предположим, вы хотите получить лучшие 3 результаты на основе порядка сортировки поля w для каждого родителя. Вы можете легко агрегировать, как показано ниже:

var levels = [1,2,3];  // indicating the records in the range that we need to pick up,
                       // from each parent. 
  • Сопоставьте всех родителей, которые a или b.
  • Отсортируйте записи по полю w.
  • Сгруппируйте по parent. После группировки все документы родителя становятся вложенными документами сгруппированной записи, что позволяет применять $redact этап.
  • Теперь примените этап $redact для редактирования тех поддокументов, уровень которых не является подмножеством уровней, которые мы ищем. Мы сохранили level как массив, потому что к нему проще применить оператор $setIsSubset. В противном случае нам потребовалось бы $in, которое не поддерживается внутри выражения $cond.

Код:

Model.aggregate(
{$match:{"parent":{$in:["a","b"]}}},
{$sort:{"w":-1}},
{$group:{"_id":"$parent",
         "rec":{$push:"$$ROOT"}}},
{$redact:{$cond:[{$setIsSubset:[{$ifNull:["$levels",[1]]},
                               inp]},
                 "$$DESCEND","$$PRUNE"]}},
,function(err,resp){
 // handle response
})

Полученный результат идеален, как мы и хотели: (показана только группа b, чтобы она была короче)

{
        "_id" : "b",
        "rec" : [
                {
                        "_id" : ObjectId("54b030a3e4eae97f395e5e89"),
                        "parent" : "b",
                        "d1" : "1",
                        "d2" : "2",
                        "d3" : "3",
                        "w" : 15,
                        "level" : [
                                1
                        ]
                },
                {
                        "_id" : ObjectId("54b030a3e4eae97f395e5e88"),
                        "parent" : "b",
                        "d1" : "1",
                        "d2" : "2",
                        "d3" : "3",
                        "w" : 14,
                        "level" : [
                                2
                        ]
                },
                {
                        "_id" : ObjectId("54b030a3e4eae97f395e5e87"),
                        "parent" : "b",
                        "d1" : "1",
                        "d2" : "2",
                        "d3" : "3",
                        "w" : 13,
                        "level" : [
                                3
                        ]
                }
        ]
}

Подход Б:

Редактирование поддокументов выполняется на стороне клиента:

var result = db.collection.aggregate([
{$match:{"parent":{$in:["a","b"]}}},
{$sort:{"w":-1}},
{$group:{"_id":"$parent","rec":{$push:"$$ROOT"}}}
]).map(function(doc){
    doc.rec.splice(0,3);
    return doc;
})

Что довольно медленнее, так как все записи для каждого родителя будут возвращены MongoDB. Выбор за вами, в зависимости от того, что подходит для вашего приложения.

person BatScream    schedule 09.01.2015
comment
w на самом деле дата, а не просто уровень иерархии - person bevacqua; 09.01.2015
comment
Это не имеет значения. «уровень» — это просто поле, которое помогает идентифицировать лучшие записи. Когда вы вставляете документ, вам нужно убедиться, что он установлен в соответствии с полем, по которому вы хотите отсортировать. - person BatScream; 09.01.2015

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

Пример кода, основанный на моем первоначальном вопросе:

var _ = require('lodash');
var limited = require('limited');
var D = require('./models/D');

function getLastDsByParent (ids, done) {
  var options = {
    model: D,
    field: 'parent',
    query: { parent : { $in: ids } },
    limit: 3,
    sort: { w: -1 }
  };
  limited(options, find);

  function find (err, result) {
    if (err) {
      done(err); return;
    }

    D
      .find({ _id: { $in: _.flatten(result, 'documents') } })
      .lean()
      .exec(done);
  }
}
person bevacqua    schedule 09.01.2015
comment
Этот подход не отсекает верхние n записи для родителя на сервере базы данных, а связанный ответ имеет слишком много этапов и требует больших накладных расходов, что может привести к очень низкой производительности, если ваш набор данных большой. Если вы хотите получить n лучших записей, вам придется выполнить n групповых и n этапов раскрутки, не говоря уже о количестве промежуточных этапов проекта. - person BatScream; 09.01.2015
comment
Он нарезает верхние n записи, это в основном связанный ответ, превращенный в модуль. Меня, конечно, беспокоят накладные расходы. Я думаю, что я должен измерить это - person bevacqua; 09.01.2015
comment
Если бы я пошел с вашим первым подходом, мне пришлось бы изменять каждый документ для parent при каждой вставке. - person bevacqua; 10.01.2015
comment
Да, я согласен, вам потребуется массовое обновление. Таким образом, вам нужно выбрать лучшее решение с учетом вашего набора данных и кода приложения (что потребует некоторой доработки), если вы выберете первый подход. - person BatScream; 10.01.2015