Предоставляет ли заказ MongoDB $в порядке гарантии

При использовании предложения MongoDB $in, порядок возвращаемых документов всегда соответствует порядку аргумента массива?

Ответ 1

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

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

Итак, скажем, что вы сопоставляли значения _id в своих документах с массивом, который будет передан в $in как [ 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

Другой способ использования запроса агрегации применим только для MongoDB verion> = 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 представляет исходный порядок нашего массива, который мы предоставили. Затем мы просто сортируем документы на основе этого поля.

Ответ 3

Если вы не хотите использовать 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 не являются примитивными типами, такими как ObjectId s, требуется другой подход, поскольку в этом случае 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); });
});

Ответ 4

Как и решение 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, чтобы упростить код.

Ответ 5

Простой способ упорядочить результат после того, как mongo возвращает массив, должен сделать объект с id как ключи, а затем отобразить на заданный _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
}

Ответ 6

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

Ответ 7

Я знаю, что этот вопрос связан с картой Mongoose JS, но duplicated one является общим, поэтому я надеюсь, что публикация решения 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

Ответ 8

Я знаю, что это старый поток, но если вы просто возвращаете значение 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() - только значение?

Ответ 9

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

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

Ответ 10

Это решение кода после того, как результаты получены из 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++
        }
    }
}