MongoDB'nin $ in şartı garanti emri veriyor mu


Yanıtlar:


80

Belirtildiği gibi, bir $ in yan tümcesi dizisindeki bağımsız değişkenlerin sırası, belgelerin alınma sırasını yansıtmaz. Bu, tabii ki doğal sıra veya gösterildiği gibi seçilen endeks sırasına göre olacaktır.

Bu sırayı korumanız gerekiyorsa, temelde iki seçeneğiniz vardır.

O halde sen değerlerine uyan edildi diyelim _idgeçirilen olacak bir dizi ile belgelerde $inolduğu gibi [ 4, 2, 8 ].

Aggregate kullanarak yaklaşım


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

])

Böylece genişletilmiş form bu olacaktır. Burada temelde olan şey, tıpkı değerler dizisinin $insize aktarılması gibi, değerleri $condtest etmek ve uygun bir ağırlık atamak için "iç içe geçmiş" bir ifade oluşturmasıdır. Bu "ağırlık" değeri dizideki öğelerin sırasını yansıttığından, sonuçlarınızı gereken sırada almak için bu değeri bir sıralama aşamasına geçirebilirsiniz.

Tabii ki aslında ardışık düzen ifadesini kodda "oluşturursunuz", buna çok benzer:

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 kullanarak yaklaşım


Tabii ki tüm bunlar duyarlılıklarınız için ağır görünüyorsa, aynı şeyi mapReduce kullanarak da yapabilirsiniz, bu daha basit görünen ancak muhtemelen biraz daha yavaş çalışacaktır.

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

Ve bu temelde, emilen "anahtar" değerlerinin girdi dizisinde nasıl meydana geldiklerine ilişkin "dizin sırası" nda olmasına dayanır.


Dolayısıyla bunlar, esasen, bir giriş listesinin sırasını, $inbu listeyi belirli bir sırayla zaten bulundurduğunuz bir koşulda sürdürme yollarınızdır .


2
Mükemmel cevap. İhtiyaç duyanlar için, bir kahve reçetesi versiyonu burada
Lawrence Jones

1
@NeilLunn Agrega kullanarak yaklaşımı denedim, ancak id'leri ve ağırlığı aldım. Mesajları (nesneyi) nasıl alacağınızı biliyor musunuz?
Juanjo Lainez Reche

1
@NeilLunn Aslında yaptım (burada stackoverflow.com/questions/27525235/… ) Ama sorumu göndermeden önce bunu kontrol etsem de tek yorum buradaydı. Bana yardım edebilir misin? Teşekkür ederim!
Juanjo Lainez Reche

1
bunun eski olduğunu biliyorum, ancak inputs.indexOf () 'un this._id ile eşleşmemesinin nedeni hata ayıklamak için çok zaman harcadım. Yalnızca nesne kimliğinin değerini döndürüyorsanız, şu sözdizimini seçmeniz gerekebilir: obj.map = function () {for (var i = 0; i <inputs.length; i ++) {if (this. _id.equals (inputs [i])) {var order = i; }} emit (order, {doc: this}); };
NoobSter

1
Tüm orijinal alanların da olmasını istiyorsanız, "$ proje" yerine "$ addFields" kullanabilirsiniz
Jodo

40

Yalnızca MongoDB sürümü> = 3.4 için geçerli olan Toplama sorgusunu kullanmanın başka bir yolu -

Kredi bu güzel blog gönderisine gidiyor .

Bu siparişte getirilecek örnek belgeler -

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

Sorgu -

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

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

Gönderiden kullanılan bu toplama operatörlerini açıklayan başka bir alıntı -

"$ AddFields" aşaması 3.4 sürümünde yenidir ve mevcut tüm diğer alanları bilmeden mevcut belgelere yeni alanlar "$ projelendirmenize" olanak tanır. Yeni "$ indexOfArray" ifadesi, belirli bir dizideki belirli öğenin konumunu döndürür.

Temel olarak addFieldsoperatör order, onu bulduğunda her belgeye yeni bir alan ekler ve bu orderalan, sağladığımız dizimizin orijinal sırasını temsil eder. Sonra belgeleri bu alana göre sıralarız.


Sıra dizisini sorguda değişken olarak saklamanın bir yolu var mı, bu nedenle dizi büyükse aynı dizinin bu büyük sorgusunu iki kez yapmayız?
Ethan SK

27

Kullanmak istemiyorsanız aggregate, başka bir çözüm kullanmak findve ardından belge sonuçlarını istemci tarafında sıralamaktır.array#sort :

Eğer $indeğerler numaraları gibi ilkel türleri aşağıdaki gibi bir yaklaşımı kullanabilirsiniz:

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

Eğer $indeğerler temel olmayan türleri gibiObjectId s gibi başka yaklaşım gereklidir indexOf, bu durumda, referans ile karşılaştırır.

Node.js 4.x + kullanıyorsanız , işlevini şu şekilde değiştirerek bunu kullanabilir Array#findIndexve ObjectID#equalsişleyebilirsiniz sort:

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

Veya alt çizgi / lodash ile herhangi bir Node.js sürümünde 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); });
});

Eşit işlev, bir id özelliğini id 'döndür a.equals (id);' ile karşılaştıracağını, a'nın bu model için döndürülen tüm özellikleri tutmasına neden olacağını nasıl bilir?
lboyel

1
@lboyel O kadar zeki olduğunu kastetmemiştim :-), ama işe yaradı çünkü Document#equalsdoktorun _idalanıyla karşılaştırmak için Mongoose'u kullanıyordu . _idKarşılaştırmayı açık hale getirmek için güncellendi . Sorduğunuz için teşekkürler.
JohnnyHK

6

JonnyHK'nin çözümüne benzer şekilde find, istemcinizden (istemciniz JavaScript'teyse) döndürülen belgeleri , EcmaScript 2015'teki mapve Array.prototype.findişlevinin bir kombinasyonu ile yeniden sıralayabilirsiniz :

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

});

Birkaç not:

  • Yukarıdaki kod Mongo Node sürücüsünü kullanıyor, değil Mongoose'u
  • idArrayBir dizidirObjectId
  • Bu yöntemin performansını sıralamaya göre test etmedim, ancak döndürülen her öğeyi değiştirmeniz gerekiyorsa (ki bu oldukça yaygındır) map, kodunuzu basitleştirmek için geri aramada bunu yapabilirsiniz .

İç kısım finddizinin her bir öğesi için diziyi (dıştan map) geçtiği için çalışma süresi O (n * n ) 'dir. Bir arama tablosu kullanan bir O (n) çözümü olduğu için bu korkunç derecede verimsizdir.
Curran

5

Bu sorunun Mongoose JS çerçevesi ile ilgili olduğunu biliyorum, ancak çoğaltılmış olan geneldir, bu yüzden burada bir Python (PyMongo) çözümü göndermenin iyi olacağını umuyorum.

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

5

Mongo diziyi döndürdükten sonra sonucu sıralamanın kolay bir yolu, anahtar olarak id'li bir nesne yapmak ve ardından doğru sıralı bir dizi döndürmek için verilen _id'lerin üzerine eşlemektir.

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
Bu tam olarak ihtiyacım olan şeyi yapıyor ve en iyi yorumdan çok daha basit.
dyarbrough

@dyarbrough bu çözüm yalnızca tüm belgeleri (sınırlama veya atlama olmaksızın) getiren sorgular için çalışır. En üstteki yorum daha karmaşıktır ancak her senaryoda işe yarar.
marian2js

3

Her zaman? Asla. Sıra her zaman aynıdır: tanımsız (muhtemelen belgelerin saklandığı fiziksel sıra). Sen sıralamazsan.


$naturalnormal olarak fizikselden ziyade mantıklı olan sipariş
Sammaye

1

Bunun eski bir iş parçacığı olduğunu biliyorum, ancak dizideki Id'nin değerini döndürüyorsanız, bu sözdizimini seçmeniz gerekebilir. Bir mongo ObjectId formatıyla eşleşecek indexOf değerini alamadığım için.

  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 ()' sarmalayıcısını dahil etmeden nasıl dönüştürülür - yalnızca Değer?


0

Siparişi $ veya fıkra ile garanti edebilirsiniz.

Onun $or: [ _ids.map(_id => ({_id}))]yerine kullanın.


2
$orGeçici çözüm işe yaramadı v2.6 beri .
JohnnyHK

0

Bu, sonuçlar Mongo'dan alındıktan sonraki bir kod çözümüdür. Dizini depolamak için bir harita kullanmak ve ardından değerleri değiştirmek.

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++
        }
    }
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.