Гарантирует ли предложение MongoDB $ in

Ответы:

78

Как уже отмечалось, порядок аргументов в массиве предложения $ in не отражает порядок получения документов. Это, конечно, будет естественный порядок или выбранный порядок индекса, как показано.

Если вам нужно сохранить этот порядок, у вас есть два варианта.

Итак, предположим, что вы сравнивали значения _idв своих документах с массивом, который будет передан в $inas [ 4, 2, 8 ].

Подход с использованием агрегата


var list = [ 4, 2, 8 ];

db.collection.aggregate([

    // Match the selected documents by "_id"
    { "$match": {
        "_id": { "$in": [ 4, 2, 8 ] },
    },

    // Project a "weight" to each document
    { "$project": {
        "weight": { "$cond": [
            { "$eq": [ "$_id", 4  ] },
            1,
            { "$cond": [
                { "$eq": [ "$_id", 2 ] },
                2,
                3
            ]}
        ]}
    }},

    // Sort the results
    { "$sort": { "weight": 1 } }

])

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

Конечно, вы на самом деле «строите» оператор конвейера в коде, примерно так:

var list = [ 4, 2, 8 ];

var stack = [];

for (var i = list.length - 1; i > 0; i--) {

    var rec = {
        "$cond": [
            { "$eq": [ "$_id", list[i-1] ] },
            i
        ]
    };

    if ( stack.length == 0 ) {
        rec["$cond"].push( i+1 );
    } else {
        var lval = stack.pop();
        rec["$cond"].push( lval );
    }

    stack.push( rec );

}

var pipeline = [
    { "$match": { "_id": { "$in": list } }},
    { "$project": { "weight": stack[0] }},
    { "$sort": { "weight": 1 } }
];

db.collection.aggregate( pipeline );

Подход с использованием mapReduce


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

var list = [ 4, 2, 8 ];

db.collection.mapReduce(
    function () {
        var order = inputs.indexOf(this._id);
        emit( order, { doc: this } );
    },
    function() {},
    { 
        "out": { "inline": 1 },
        "query": { "_id": { "$in": list } },
        "scope": { "inputs": list } ,
        "finalize": function (key, value) {
            return value.doc;
        }
    }
)

И это в основном зависит от того, что излучаемые «ключевые» значения находятся в «порядковом индексе» того, как они встречаются во входном массиве.


Таким образом, это, по сути, ваши способы поддержания порядка входного списка до $inсостояния, при котором у вас уже есть этот список в определенном порядке.

Нил Ланн
источник
2
Отличный ответ. Для тех, кому это нужно, версия кофе-скрипта здесь
Лоуренс Джонс
1
@NeilLunn Я пробовал подход с использованием агрегата, но получил идентификатор и вес. Вы знаете, как получить сообщения (объект)?
Хуанхо Лайнес Рече
1
@NeilLunn На самом деле я это сделал (это здесь stackoverflow.com/questions/27525235/… ) Но единственный комментарий имел в виду здесь, хотя я проверил это перед тем, как опубликовать свой вопрос. Вы можете мне помочь? Спасибо!
Juanjo Lainez Reche
1
знаю, что это устарело, но я потратил много времени на отладку, почему inputs.indexOf () не соответствовал this._id. Если вы просто возвращаете значение идентификатора объекта, возможно, вам придется выбрать следующий синтаксис: obj.map = function () {for (var i = 0; i <inputs.length; i ++) {if (this. _id.equals (входы [i])) {var order = i; }} emit (заказ, {документ: это}); };
NoobSter
1
Вы можете использовать «$ addFields» вместо «$ проекта» , если вы хотите , чтобы все исходные поля тоже
Jodo
39

Другой способ использования запроса агрегации применим только для MongoDB версии> = 3.4 -

Благодарность за это хорошее сообщение в блоге .

Примеры документов, которые нужно получить в этом порядке -

var order = [ "David", "Charlie", "Tess" ];

Запрос -

var query = [
             {$match: {name: {$in: order}}},
             {$addFields: {"__order": {$indexOfArray: [order, "$name" ]}}},
             {$sort: {"__order": 1}}
            ];

var result = db.users.aggregate(query);

Еще одна цитата из сообщения, объясняющего эти используемые операторы агрегации -

Этап «$ addFields» появился в версии 3.4 и позволяет «$ project» добавлять новые поля в существующие документы, не зная всех остальных существующих полей. Новое выражение «$ indexOfArray» возвращает позицию определенного элемента в данном массиве.

Обычно addFieldsоператор добавляет новое orderполе к каждому документу, когда он его находит, и это orderполе представляет исходный порядок предоставленного нами массива. Затем мы просто сортируем документы по этому полю.

Джйотман Сингх
источник
есть ли способ сохранить массив заказов в качестве переменной в запросе, чтобы у нас не было этого массивного запроса одного и того же массива дважды, если массив большой?
Итан СК
24

Если вы не хотите использовать aggregate, другое решение - использовать, findа затем отсортировать результаты документа на стороне клиента, используяarray#sort :

Если $inзначения являются примитивными типами, например числами, вы можете использовать такой подход:

var ids = [4, 2, 8, 1, 9, 3, 5, 6];
MyModel.find({ _id: { $in: ids } }).exec(function(err, docs) {
    docs.sort(function(a, b) {
        // Sort docs by the order of their _id values in ids.
        return ids.indexOf(a._id) - ids.indexOf(b._id);
    });
});

Если $inзначения не являются примитивными типами, такими как ObjectIds, indexOfв этом случае требуется другой подход, сравниваемый по ссылке.

Если вы используете Node.js 4.x +, вы можете использовать Array#findIndexи, ObjectID#equalsчтобы справиться с этим, изменив sortфункцию на:

docs.sort((a, b) => ids.findIndex(id => a._id.equals(id)) - 
                    ids.findIndex(id => b._id.equals(id)));

Или с любой версией Node.js с подчеркиванием / lodash findIndex:

docs.sort(function (a, b) {
    return _.findIndex(ids, function (id) { return a._id.equals(id); }) -
           _.findIndex(ids, function (id) { return b._id.equals(id); });
});
JohnnyHK
источник
откуда функция equal знает, что нужно сравнивать свойство id с id 'return a.equals (id);', потому что a содержит все свойства, возвращаемые для этой модели?
lboyel
1
@lboyel Я не имел в виду, что это так умно :-), но это сработало, потому что он использовал Mongoose Document#equalsдля сравнения с _idполем документа. Обновлено, чтобы сделать _idсравнение явным. Спасибо за вопрос.
JohnnyHK
6

Подобно решению JonnyHK , вы можете изменить порядок документов, возвращаемых findв вашем клиенте (если ваш клиент использует JavaScript), с помощью комбинации mapи Array.prototype.findфункции в EcmaScript 2015:

Collection.find({ _id: { $in: idArray } }).toArray(function(err, res) {

    var orderedResults = idArray.map(function(id) {
        return res.find(function(document) {
            return document._id.equals(id);
        });
    });

});

Пара замечаний:

  • В приведенном выше коде используется драйвер Mongo Node, а не Mongoose.
  • Это idArrayмассивObjectId
  • Я не тестировал производительность этого метода по сравнению с сортировкой, но если вам нужно манипулировать каждым возвращаемым элементом (что довольно часто), вы можете сделать это в mapобратном вызове, чтобы упростить свой код.
tebs1200
источник
5

Простой способ упорядочить результат после того, как mongo вернет массив, - создать объект с идентификатором в качестве ключей, а затем сопоставить данные _id, чтобы вернуть массив, который правильно упорядочен.

async function batchUsers(Users, keys) {
  const unorderedUsers = await Users.find({_id: {$in: keys}}).toArray()
  let obj = {}
  unorderedUsers.forEach(x => obj[x._id]=x)
  const ordered = keys.map(key => obj[key])
  return ordered
}
Арне Йенссен
источник
1
Это именно то, что мне нужно, и это намного проще, чем верхний комментарий.
dyarbrough
@dyarbrough это решение работает только для запросов, которые извлекают все документы (без ограничений и пропусков). Верхний комментарий более сложен, но работает для любого сценария.
marian2js
4

Я знаю, что этот вопрос связан с фреймворком Mongoose JS, но дублированный вариант является общим, поэтому я надеюсь, что публикация решения Python (PyMongo) здесь подойдет.

things = list(db.things.find({'_id': {'$in': id_array}}))
things.sort(key=lambda thing: id_array.index(thing['_id']))
# things are now sorted according to id_array order
Денис Голомазов
источник
3

Всегда? Никогда. Порядок всегда один и тот же: не определен (вероятно, физический порядок, в котором хранятся документы). Если только вы его не отсортируете.

причудливый
источник
$naturalпорядок обычно логический, а не физический
Sammaye 02
1

Я знаю, что это старый поток, но если вы просто возвращаете значение Id в массиве, вам, возможно, придется выбрать этот синтаксис. Поскольку я не мог получить значение indexOf, соответствующее формату mongo ObjectId.

  obj.map = function() {
    for(var i = 0; i < inputs.length; i++){
      if(this._id.equals(inputs[i])) {
        var order = i;
      }
    }
    emit(order, {doc: this});
  };

Как преобразовать mongo ObjectId .toString без включения оболочки ObjectId () - только значение?

NoobSter
источник
0

Вы можете гарантировать заказ с помощью пункта $ или.

Так что используйте $or: [ _ids.map(_id => ({_id}))]вместо этого.

fakenickels
источник
2
$orОбходной путь не работал с тех пор v2.6 .
JohnnyHK
0

Это кодовое решение после получения результатов из Mongo. Использование карты для хранения индекса с последующей заменой значений.

catDetails := make([]CategoryDetail, 0)
err = sess.DB(mdb).C("category").
    Find(bson.M{
    "_id":       bson.M{"$in": path},
    "is_active": 1,
    "name":      bson.M{"$ne": ""},
    "url.path":  bson.M{"$exists": true, "$ne": ""},
}).
    Select(
    bson.M{
        "is_active": 1,
        "name":      1,
        "url.path":  1,
    }).All(&catDetails)

if err != nil{
    return 
}
categoryOrderMap := make(map[int]int)

for index, v := range catDetails {
    categoryOrderMap[v.Id] = index
}

counter := 0
for i := 0; counter < len(categoryOrderMap); i++ {
    if catId := int(path[i].(float64)); catId > 0 {
        fmt.Println("cat", catId)
        if swapIndex, exists := categoryOrderMap[catId]; exists {
            if counter != swapIndex {
                catDetails[swapIndex], catDetails[counter] = catDetails[counter], catDetails[swapIndex]
                categoryOrderMap[catId] = counter
                categoryOrderMap[catDetails[swapIndex].Id] = swapIndex
            }
            counter++
        }
    }
}
Prateek
источник