Apakah perintah penjaminan klausa $ in MongoDB

Jawaban:

80

Sebagaimana dicatat, urutan argumen dalam larik klausa $ in tidak mencerminkan urutan bagaimana dokumen diambil. Itu tentu saja akan menjadi urutan alami atau dengan urutan indeks yang dipilih seperti yang ditunjukkan.

Jika Anda perlu mempertahankan tatanan ini, pada dasarnya Anda memiliki dua opsi.

Jadi katakanlah Anda mencocokkan nilai _iddalam dokumen Anda dengan array yang akan diteruskan ke $inas [ 4, 2, 8 ].

Pendekatan menggunakan Agregat


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 } }

])

Jadi itu akan menjadi bentuk yang diperluas. Apa yang pada dasarnya terjadi di sini adalah bahwa sama seperti larik nilai yang diteruskan kepada $inAnda, Anda juga membuat pernyataan "bersarang" $conduntuk menguji nilai dan menetapkan bobot yang sesuai. Karena nilai "bobot" tersebut mencerminkan urutan elemen dalam larik, Anda kemudian dapat meneruskan nilai tersebut ke tahap pengurutan untuk mendapatkan hasil dalam urutan yang diperlukan.

Tentu saja Anda benar-benar "membangun" pernyataan pipeline dalam kode, seperti ini:

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 );

Pendekatan menggunakan mapReduce


Tentu saja jika semua itu tampak lumayan untuk kepekaan Anda, maka Anda dapat melakukan hal yang sama menggunakan mapReduce, yang terlihat lebih sederhana tetapi kemungkinan akan berjalan agak lebih lambat.

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;
        }
    }
)

Dan itu pada dasarnya bergantung pada nilai "kunci" yang dipancarkan berada dalam "urutan indeks" bagaimana mereka muncul dalam larik masukan.


Jadi pada dasarnya itu adalah cara Anda mempertahankan urutan daftar input ke $inkondisi di mana Anda sudah memiliki daftar itu dalam urutan yang ditentukan.

Neil Lunn
sumber
2
Jawaban yang bagus. Bagi mereka yang membutuhkannya, versi coffeescript di sini
Lawrence Jones
1
@NeilLunn Saya mencoba pendekatan menggunakan agregat, tetapi saya mendapatkan id dan bobotnya. Apakah Anda tahu cara mengambil kiriman (objek)?
Juanjo Lainez Reche
1
@NeilLunn Sebenarnya saya melakukannya (ada di sini stackoverflow.com/questions/27525235/… ) Tetapi satu-satunya komentar merujuk di sini, meskipun saya memeriksa ini sebelum memposting pertanyaan saya. Bisakah kamu membantuku di sana? Terima kasih!
Juanjo Lainez Reche
1
tahu ini sudah tua, tetapi saya membuang banyak waktu untuk men-debug mengapa inputs.indexOf () tidak cocok dengan this._id. Jika Anda baru saja mengembalikan nilai ID objek, Anda mungkin harus memilih sintaks ini: obj.map = function () {for (var i = 0; i <inputs.length; i ++) {if (this. _id.equals (input [i])) {var order = i; }} emit (pesan, {doc: this}); };
NoobSter
1
Anda dapat menggunakan "$ addFields" daripada "$ project" jika Anda ingin memiliki semua field asli juga
Jodo
40

Cara lain menggunakan kueri Agregasi hanya berlaku untuk MongoDB verion> = 3.4 -

Penghargaan untuk kebaikan ini entri blog yang .

Contoh dokumen yang akan diambil dalam pesanan ini -

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

Kueri -

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

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

Kutipan lain dari posting yang menjelaskan operator agregasi ini digunakan -

Tahap "$ addFields" baru di 3.4 dan ini memungkinkan Anda untuk "$ project" bidang baru ke dokumen yang ada tanpa mengetahui semua bidang lain yang ada. Ekspresi "$ indexOfArray" baru mengembalikan posisi elemen tertentu dalam larik tertentu.

Pada dasarnya addFieldsoperator menambahkan orderbidang baru ke setiap dokumen ketika ditemukan dan orderbidang ini mewakili urutan asli dari larik yang kami sediakan. Kemudian kami cukup mengurutkan dokumen berdasarkan bidang ini.

Jyotman Singh
sumber
apakah ada cara untuk menyimpan array order sebagai variabel dalam kueri sehingga kami tidak memiliki kueri besar ini dari array yang sama dua kali jika array besar?
Ethan SK
27

Jika Anda tidak ingin menggunakan aggregate, solusi lain adalah menggunakan finddan kemudian mengurutkan hasil dokumen sisi klien menggunakan array#sort:

Jika $innilainya adalah tipe primitif seperti angka, Anda dapat menggunakan pendekatan seperti:

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);
    });
});

Jika $innilainya adalah tipe non-primitif seperti ObjectIds, pendekatan lain diperlukan sebagai indexOfperbandingan dengan referensi dalam kasus tersebut.

Jika Anda menggunakan Node.js 4.x +, Anda dapat menggunakan Array#findIndexdan ObjectID#equalsmenangani ini dengan mengubah sortfungsinya menjadi:

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

Atau dengan versi Node.js apa pun, dengan garis bawah / 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
sumber
bagaimana fungsi yang sama tahu untuk membandingkan properti id dengan id 'return a.equals (id);', menyebabkan a menahan semua properti yang dikembalikan untuk model itu?
lboyel
1
@lboyel Saya tidak bermaksud begitu pintar :-), tapi itu berhasil karena menggunakan Mongoose Document#equalsuntuk membandingkan dengan bidang dokumen _id. Diperbarui untuk membuat _idperbandingan menjadi eksplisit. Terima kasih untuk bertanya.
JohnnyHK
6

Mirip dengan solusi JonnyHK , Anda dapat menyusun ulang dokumen yang dikembalikan dari findklien Anda (jika klien Anda menggunakan JavaScript) dengan kombinasi mapdan Array.prototype.findfungsi di 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);
        });
    });

});

Beberapa catatan:

  • Kode di atas menggunakan driver Mongo Node dan bukan Mongoose
  • Ini idArrayadalah larikObjectId
  • Saya belum menguji kinerja metode ini vs semacamnya, tetapi jika Anda perlu memanipulasi setiap item yang dikembalikan (yang cukup umum), Anda dapat melakukannya di mapcallback untuk menyederhanakan kode Anda.
tebs1200.dll
sumber
Waktu berjalan adalah O (n * n), karena bagian dalam findmelintasi larik untuk setiap elemen larik (dari luar map). Ini sangat tidak efisien, karena ada solusi O (n) menggunakan tabel pencarian.
Curran
5

Saya tahu pertanyaan ini terkait dengan kerangka JS Mongoose, tetapi yang digandakan bersifat umum, jadi saya harap memposting solusi Python (PyMongo) baik-baik saja di sini.

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
Dennis Golomazov
sumber
5

Cara mudah untuk mengurutkan hasil setelah mongo mengembalikan larik adalah dengan membuat objek dengan id sebagai kunci dan kemudian memetakan di atas _id yang diberikan untuk mengembalikan larik yang diurutkan dengan benar.

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
}
Arne Jenssen
sumber
1
Ini persis seperti yang saya butuhkan dan jauh lebih sederhana daripada komentar teratas.
dyarbrough
@dyarbrough solusi ini hanya berfungsi untuk kueri yang mengambil semua dokumen (tanpa batas atau lewati). Komentar teratas lebih kompleks tetapi berfungsi untuk setiap skenario.
marian2js
3

Selalu? Tidak pernah. Urutannya selalu sama: tidak ditentukan (mungkin urutan fisik tempat dokumen disimpan). Kecuali Anda mengurutkannya.

aneh
sumber
$naturalmemesan secara normal yang lebih logis daripada fisik
Sammaye
1

Saya tahu ini adalah utas lama, tetapi jika Anda baru saja mengembalikan nilai Id dalam larik, Anda mungkin harus memilih sintaks ini. Karena saya tidak bisa mendapatkan nilai indexOf untuk mencocokkan dengan format ObjectId mongo.

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

Bagaimana cara mengonversi ObjectId .toString mongo tanpa menyertakan pembungkus 'ObjectId ()' - hanya Nilai?

NoobSter
sumber
0

Anda dapat menjamin pesanan dengan $ atau klausul.

Jadi gunakan $or: [ _ids.map(_id => ({_id}))]saja.

fakenickels
sumber
2
The $orsolusi tidak bekerja sejak v2.6 .
JohnnyHK
0

Ini adalah solusi kode setelah hasil diambil dari Mongo. Menggunakan peta untuk menyimpan indeks dan kemudian menukar nilai.

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
sumber