La clause $ in de MongoDB garantit-elle l'ordre

Réponses:

78

Comme indiqué, l'ordre des arguments dans le tableau d'une clause $ in ne reflète pas l'ordre dans lequel les documents sont récupérés. Ce sera bien sûr l'ordre naturel ou par l'ordre d'index sélectionné comme indiqué.

Si vous devez conserver cet ordre, vous avez essentiellement deux options.

Supposons donc que vous compariez les valeurs de _iddans vos documents avec un tableau qui va être transmis à $inas [ 4, 2, 8 ].

Approche utilisant Aggregate


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

])

Ce serait donc le formulaire élargi. Ce qui se passe essentiellement ici, c'est que tout comme le tableau de valeurs vous est transmis, $inconstruisez également une $condinstruction "imbriquée" pour tester les valeurs et attribuer un poids approprié. Comme cette valeur de «poids» reflète l'ordre des éléments dans le tableau, vous pouvez ensuite passer cette valeur à une étape de tri afin d'obtenir vos résultats dans l'ordre requis.

Bien sûr, vous "construisez" l'instruction pipeline dans le code, un peu comme ceci:

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

Approche à l'aide de mapReduce


Bien sûr, si tout cela semble trop important pour votre sensibilité, vous pouvez faire la même chose en utilisant mapReduce, qui semble plus simple mais fonctionnera probablement un peu plus lentement.

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

Et cela repose essentiellement sur le fait que les valeurs «clés» émises sont dans «l'ordre d'index» de la façon dont elles se produisent dans le tableau d'entrée.


Donc, ce sont essentiellement vos façons de maintenir l'ordre d'une liste d'entrée à une $incondition où vous avez déjà cette liste dans un ordre déterminé.

Neil Lunn
la source
2
Très bonne réponse. Pour ceux qui en ont besoin, une version coffeescript ici
Lawrence Jones
1
@NeilLunn J'ai essayé l'approche utilisant l'agrégat, mais j'obtiens l'identifiant et le poids. Savez-vous comment récupérer les messages (objet)?
Juanjo Lainez Reche
1
@NeilLunn je l'ai fait en fait (c'est ici stackoverflow.com/questions/27525235/… ) Mais le seul commentaire faisait référence ici, même si j'ai vérifié cela avant de poster ma question. Pouvez-vous m'aider là-bas? Merci!
Juanjo Lainez Reche
1
je sais que c'est vieux, mais j'ai perdu beaucoup de temps à déboguer pourquoi inputs.indexOf () ne correspondait pas à this._id. Si vous ne renvoyez que la valeur de l'identifiant de l'objet, vous devrez peut-être opter pour cette syntaxe: obj.map = function () {for (var i = 0; i <inputs.length; i ++) {if (this. _id.equals (entrées [i])) {var order = i; }} emit (ordre, {doc: this}); };
NoobSter
1
vous pouvez utiliser "$ addFields" au lieu de "$ project" si vous voulez avoir tous les champs originaux aussi
Jodo
39

Une autre façon d'utiliser la requête d'agrégation applicable uniquement pour la version MongoDB> = 3.4 -

Le mérite revient à ce joli article de blog .

Exemples de documents à récupérer dans cet ordre -

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

La requête -

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

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

Une autre citation du post expliquant ces opérateurs d'agrégation utilisés -

L'étape "$ addFields" est nouvelle dans la version 3.4 et vous permet de "$ projeter" de nouveaux champs vers des documents existants sans connaître tous les autres champs existants. La nouvelle expression "$ indexOfArray" renvoie la position d'un élément particulier dans un tableau donné.

En gros, l' addFieldsopérateur ajoute un nouveau orderchamp à chaque document lorsqu'il le trouve et ce orderchamp représente l'ordre d'origine de notre tableau que nous avons fourni. Ensuite, nous trions simplement les documents en fonction de ce champ.

Jyotman Singh
la source
existe-t-il un moyen de stocker le tableau de commande en tant que variable dans la requête afin que nous n'ayons pas cette requête massive du même tableau deux fois si le tableau est grand?
Ethan SK
24

Si vous ne souhaitez pas utiliser aggregate, une autre solution consiste à utiliser find, puis à trier les résultats de la documentation côté client en utilisant array#sort:

Si les $invaleurs sont des types primitifs comme les nombres, vous pouvez utiliser une approche comme:

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

Si les $invaleurs sont des types non primitifs tels que ObjectIds, une autre approche est nécessaire pour indexOfcomparer par référence dans ce cas.

Si vous utilisez Node.js 4.x +, vous pouvez utiliser Array#findIndexet ObjectID#equalsgérer cela en modifiant la sortfonction en:

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

Ou avec n'importe quelle version de Node.js, avec des traits de soulignement / 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
la source
comment la fonction equal sait-elle comparer une propriété id à id 'return a.equals (id);', car a détient toutes les propriétés retournées pour ce modèle?
lboyel
1
@lboyel Je ne voulais pas que ce soit aussi intelligent :-), mais cela a fonctionné parce qu'il utilisait Mongoose Document#equalspour comparer avec le _idchamp du doc . Mis à jour pour rendre la _idcomparaison explicite. Merci d'avoir posé la question.
JohnnyHK
6

Semblable à la solution de JonnyHK , vous pouvez réorganiser les documents renvoyés par findvotre client (si votre client est en JavaScript) avec une combinaison de mapet la Array.prototype.findfonction dans 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);
        });
    });

});

Quelques notes:

  • Le code ci-dessus utilise le pilote Mongo Node et non Mongoose
  • Le idArrayest un tableau deObjectId
  • Je n'ai pas testé les performances de cette méthode par rapport au tri, mais si vous devez manipuler chaque élément retourné (ce qui est assez courant), vous pouvez le faire dans le maprappel pour simplifier votre code.
tebs1200
la source
5

Un moyen simple de classer le résultat après que mongo a renvoyé le tableau consiste à créer un objet avec id comme clés, puis à mapper les _id donnés pour renvoyer un tableau correctement ordonné.

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
la source
1
Cela fait exactement ce dont j'ai besoin et est beaucoup plus simple que le commentaire du haut.
dyarbrough
@dyarbrough cette solution ne fonctionne que pour les requêtes qui récupèrent tous les documents (sans limite ni saut). Le commentaire du haut est plus complexe mais fonctionne pour chaque scénario.
marian2js
4

Je sais que cette question est liée au framework Mongoose JS, mais la duplication est générique, donc j'espère que publier une solution Python (PyMongo) est bien ici.

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
la source
3

Toujours? Jamais. L'ordre est toujours le même: indéfini (probablement l'ordre physique dans lequel les documents sont stockés). Sauf si vous le triez.

bizarre
la source
$naturalcommande normalement, ce qui est logique plutôt que physique
Sammaye
1

Je sais que c'est un ancien thread, mais si vous ne renvoyez que la valeur de l'Id dans le tableau, vous devrez peut-être opter pour cette syntaxe. Comme je ne pouvais pas sembler obtenir la valeur indexOf pour correspondre avec un format 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});
  };

Comment convertir mongo ObjectId .toString sans inclure le wrapper 'ObjectId ()' - juste la valeur?

NoobSter
la source
0

Vous pouvez garantir la commande avec $ ou clause.

Alors utilisez $or: [ _ids.map(_id => ({_id}))]plutôt.

fakenickels
la source
2
La $orsolution de contournement n'a pas fonctionné depuis la v2.6 .
JohnnyHK le
0

Il s'agit d'une solution de code une fois les résultats récupérés à partir de Mongo. Utilisation d'une carte pour stocker l'index, puis permutation des valeurs.

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
la source