Garantiert die $ in-Klausel von MongoDB die Bestellung?

Antworten:

78

Wie bereits erwähnt, spiegelt die Reihenfolge der Argumente im Array einer $ in-Klausel nicht die Reihenfolge wider, in der die Dokumente abgerufen werden. Dies ist natürlich die natürliche Reihenfolge oder die ausgewählte Indexreihenfolge, wie gezeigt.

Wenn Sie diese Reihenfolge beibehalten müssen, haben Sie grundsätzlich zwei Möglichkeiten.

Nehmen wir also an, Sie haben die Werte _idin Ihren Dokumenten mit einem Array abgeglichen, das an das $inas übergeben wird [ 4, 2, 8 ].

Ansatz mit Aggregat


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

])

Das wäre also die erweiterte Form. Grundsätzlich passiert hier, dass $inSie , sobald das Array von Werten an Sie übergeben wird, auch eine "verschachtelte" $condAnweisung erstellen , um die Werte zu testen und ein geeignetes Gewicht zuzuweisen. Da dieser "Gewicht" -Wert die Reihenfolge der Elemente im Array widerspiegelt, können Sie diesen Wert an eine Sortierstufe übergeben, um Ihre Ergebnisse in der erforderlichen Reihenfolge zu erhalten.

Natürlich "erstellen" Sie die Pipeline-Anweisung tatsächlich im Code, ähnlich wie folgt:

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

Vorgehensweise mit mapReduce


Wenn das alles für Ihre Sensibilität zu schwer zu sein scheint, können Sie dasselbe mit mapReduce tun, das einfacher aussieht, aber wahrscheinlich etwas langsamer läuft.

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

Und das hängt im Wesentlichen davon ab, dass die ausgegebenen "Schlüssel" -Werte in der "Indexreihenfolge" liegen, wie sie im Eingabearray auftreten.


Dies sind also im Wesentlichen Ihre Möglichkeiten, die Reihenfolge einer Eingabeliste in einem $inZustand zu halten, in dem Sie diese Liste bereits in einer bestimmten Reihenfolge haben.

Neil Lunn
quelle
2
Gute Antwort. Für diejenigen, die es brauchen, eine Coffeescript-Version hier
Lawrence Jones
1
@NeilLunn Ich habe den Ansatz mit Aggregat versucht, aber ich bekomme die IDs und das Gewicht. Wissen Sie, wie Sie die Beiträge (Objekt) abrufen können?
Juanjo Lainez Reche
1
@NeilLunn habe ich tatsächlich gemacht (es ist hier stackoverflow.com/questions/27525235/… ) Aber der einzige Kommentar bezog sich hier, obwohl ich dies überprüft habe, bevor ich meine Frage gestellt habe. Kannst du mir da helfen? Vielen Dank!
Juanjo Lainez Reche
1
Ich weiß, dass dies alt ist, aber ich habe viel Zeit damit verschwendet, zu debuggen, warum inputs.indexOf () nicht mit this._id übereinstimmte. Wenn Sie nur den Wert der Objekt-ID zurückgeben, müssen Sie möglicherweise folgende Syntax wählen: obj.map = function () {for (var i = 0; i <inputs.length; i ++) {if (this. _id.equals (Eingaben [i])) {var order = i; }} emit (order, {doc: this}); };
NoobSter
1
Sie können "$ addFields" anstelle von "$ project" verwenden, wenn Sie auch alle ursprünglichen Felder haben möchten
Jodo
38

Eine andere Möglichkeit, die Aggregationsabfrage zu verwenden, gilt nur für die MongoDB-Version> = 3.4 -

Der Kredit geht an diesen schönen Blog-Beitrag .

Beispieldokumente, die in dieser Reihenfolge abgerufen werden sollen -

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

Die Abfrage -

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

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

Ein weiteres Zitat aus dem Beitrag, in dem diese verwendeten Aggregationsoperatoren erläutert werden:

Die Phase "$ addFields" ist neu in 3.4 und ermöglicht es Ihnen, neue Felder in vorhandene Dokumente zu "projizieren", ohne alle anderen vorhandenen Felder zu kennen. Der neue Ausdruck "$ indexOfArray" gibt die Position eines bestimmten Elements in einem bestimmten Array zurück.

Grundsätzlich addFieldsfügt der Operator orderjedem Dokument ein neues Feld hinzu, wenn er es findet, und dieses orderFeld repräsentiert die ursprüngliche Reihenfolge unseres von uns bereitgestellten Arrays. Dann sortieren wir einfach die Dokumente anhand dieses Feldes.

Jyotman Singh
quelle
Gibt es eine Möglichkeit, das Ordnungsarray als Variable in der Abfrage zu speichern, damit wir diese massive Abfrage desselben Arrays nicht zweimal haben, wenn das Array groß ist?
Ethan SK
24

Wenn Sie nicht verwenden möchten aggregate, besteht eine andere Lösung darin, finddie Dokumentergebnisse clientseitig zu verwenden und anschließend zu sortieren array#sort:

Wenn die $inWerte primitive Typen wie Zahlen sind, können Sie einen Ansatz wie den folgenden verwenden:

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

Wenn es sich bei den $inWerten um nicht-primitive Typen wie ObjectIds handelt, ist indexOfin diesem Fall ein anderer Ansatz erforderlich, der durch Bezugnahme verglichen wird.

Wenn Sie Node.js 4.x + verwenden, können Sie dies verwenden Array#findIndexund ObjectID#equalshandhaben, indem Sie die sortFunktion in Folgendes ändern :

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

Oder mit einer beliebigen Node.js-Version mit Unterstrich / lodashs 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
quelle
Woher weiß die Gleichheitsfunktion, dass eine ID-Eigenschaft mit der ID 'return a.equals (id);' verglichen werden kann, da a alle für dieses Modell zurückgegebenen Eigenschaften enthält?
Lboyel
1
@lboyel Ich wollte nicht, dass es so klug ist :-), aber das hat funktioniert, weil es Mongoose's verwendet Document#equals, um es mit dem _idFeld des Docs zu vergleichen . Aktualisiert, um den _idVergleich explizit zu machen . Danke für die Frage.
JohnnyHK
6

Ähnlich wie bei der Lösung von JonnyHK können Sie die von findIhrem Client zurückgegebenen Dokumente (wenn Ihr Client JavaScript verwendet) mit einer Kombination aus mapund der Array.prototype.findFunktion in EcmaScript 2015 neu anordnen:

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

});

Ein paar Anmerkungen:

  • Der obige Code verwendet den Mongo Node-Treiber und nicht Mongoose
  • Das idArrayist ein Array vonObjectId
  • Ich habe die Leistung dieser Methode im Vergleich zur Sortierung nicht getestet, aber wenn Sie jedes zurückgegebene Element bearbeiten müssen (was ziemlich häufig vorkommt), können Sie dies im mapRückruf tun , um Ihren Code zu vereinfachen.
tebs1200
quelle
5

Eine einfache Möglichkeit, das Ergebnis zu ordnen, nachdem Mongo das Array zurückgegeben hat, besteht darin, ein Objekt mit der ID als Schlüssel zu erstellen und dann die angegebenen _id's zuzuordnen, um ein Array zurückzugeben, das korrekt geordnet ist.

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
quelle
1
Dies macht genau das, was ich brauche und ist viel einfacher als der oberste Kommentar.
Dyarbrough
@dyarbrough Diese Lösung funktioniert nur bei Abfragen, bei denen alle Dokumente abgerufen werden (ohne Einschränkung oder Überspringen). Der oberste Kommentar ist komplexer, funktioniert aber für jedes Szenario.
marian2js
3

Immer? Noch nie. Die Reihenfolge ist immer dieselbe: undefiniert (wahrscheinlich die physische Reihenfolge, in der Dokumente gespeichert werden). Es sei denn, Sie sortieren es.

verrückt
quelle
$naturalNormalerweise bestellen, was eher logisch als physisch ist
Sammaye
3

Ich weiß, dass diese Frage mit dem Mongoose JS-Framework zusammenhängt, aber das duplizierte ist generisch. Daher hoffe ich, dass das Posten einer Python-Lösung (PyMongo) hier in Ordnung ist.

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
quelle
1

Ich weiß, dass dies ein alter Thread ist, aber wenn Sie nur den Wert der ID im Array zurückgeben, müssen Sie sich möglicherweise für diese Syntax entscheiden. Da ich scheinbar keinen indexOf-Wert bekommen konnte, der mit einem mongo ObjectId-Format übereinstimmt.

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

Wie konvertiere ich mongo ObjectId .toString ohne den Wrapper 'ObjectId ()' - nur den Wert?

NoobSter
quelle
0

Sie können die Bestellung mit $ oder Klausel garantieren.

Verwenden Sie $or: [ _ids.map(_id => ({_id}))]stattdessen.

Fakenickels
quelle
2
Die $orProblemumgehung hat seit Version 2.6 nicht mehr funktioniert .
JohnnyHK
0

Dies ist eine Codelösung, nachdem die Ergebnisse aus Mongo abgerufen wurden. Verwenden einer Karte zum Speichern des Index und anschließendes Austauschen von Werten.

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
quelle