La clausola $ in di MongoDB garantisce l'ordine


Risposte:


80

Come notato, l'ordine degli argomenti nella matrice di una clausola $ in non riflette l'ordine di come i documenti vengono recuperati. Quello ovviamente sarà l'ordine naturale o dall'ordine dell'indice selezionato come mostrato.

Se hai bisogno di preservare questo ordine, hai fondamentalmente due opzioni.

Quindi diciamo che stavi facendo corrispondere i valori di _idnei tuoi documenti con un array che verrà passato a $inas [ 4, 2, 8 ].

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

])

Quindi quella sarebbe la forma espansa. Quello che fondamentalmente accade qui è che così come viene passato l'array di valori, $incostruisci anche un "annidato"$cond un'istruzione per testare i valori e assegnare un peso appropriato. Poiché il valore di "peso" riflette l'ordine degli elementi nell'array, è quindi possibile passare tale valore a una fase di ordinamento per ottenere i risultati nell'ordine richiesto.

Ovviamente in realtà "costruisci" l'istruzione pipeline nel codice, in modo molto simile a questo:

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

Approccio usando mapReduce


Ovviamente se tutto questo sembra pesante per la tua sensibilità, puoi fare la stessa cosa usando mapReduce, che sembra più semplice ma probabilmente funzionerà un po 'più lentamente.

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

E questo fondamentalmente si basa sul fatto che i valori "chiave" emessi siano nell '"ordine di indice" di come si presentano nell'array di input.


Quindi questi sono essenzialmente i tuoi modi per mantenere l'ordine di un elenco di input a una $incondizione in cui hai già quell'elenco in un determinato ordine.


2
Bella risposta. Per coloro che ne hanno bisogno, una versione coffeescript qui
Lawrence Jones,

1
@ NeilLunn Ho provato l'approccio utilizzando aggregato, ma ottengo gli ID e il peso. Sai come recuperare i post (oggetto)?
Juanjo Lainez Reche

1
@ NeilLunn l'ho fatto in realtà (è qui stackoverflow.com/questions/27525235/… ) Ma l'unico commento si riferiva qui, anche se l'ho controllato prima di postare la mia domanda. Puoi aiutarmi lì? Grazie!
Juanjo Lainez Reche

1
so che questo è vecchio, ma ho perso un sacco di tempo nel debug del motivo per cui inputs.indexOf () non corrispondeva a this._id. Se stai solo restituendo il valore dell'ID dell'oggetto, potresti dover optare per questa sintassi: obj.map = function () {for (var i = 0; i <inputs.length; i ++) {if (this. _id.equals (input [i])) {var order = i; }} emit (order, {doc: this}); };
NoobSter

1
puoi usare "$ addFields" invece di "$ project" se vuoi avere anche tutti i campi originali
Jodo

40

Un altro modo di utilizzare la query di aggregazione applicabile solo per MongoDB versione> = 3.4 -

Il merito va a questo bel post sul blog .

Documenti di esempio da recuperare in questo ordine:

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

La domanda -

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

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

Un'altra citazione dal post che spiega questi operatori di aggregazione utilizzati -

Lo stadio "$ addFields" è nuovo in 3.4 e ti permette di "$ proiettare" nuovi campi su documenti esistenti senza conoscere tutti gli altri campi esistenti. La nuova espressione "$ indexOfArray" restituisce la posizione di un particolare elemento in un dato array.

Fondamentalmente l' addFieldsoperatore aggiunge un nuovo ordercampo a ogni documento quando lo trova e questo ordercampo rappresenta l'ordine originale del nostro array che abbiamo fornito. Quindi ordiniamo semplicemente i documenti in base a questo campo.


c'è un modo per memorizzare l'array order come variabile nella query in modo da non avere questa query massiccia dello stesso array due volte se l'array è grande?
Ethan SK,

27

Se non si desidera utilizzare aggregate, un'altra soluzione è utilizzare finde quindi ordinare i risultati del documento lato client utilizzando array#sort:

Se i $invalori sono tipi primitivi come i numeri, puoi utilizzare un approccio come:

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

Se i $invalori sono tipi non primitivi come ObjectIds, indexOfin questo caso è necessario un altro approccio per i confronti per riferimento.

Se stai usando Node.js 4.x +, puoi usare Array#findIndexe ObjectID#equalsper gestirlo cambiando la sortfunzione in:

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

O con qualsiasi versione di Node.js, con underscore / 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); });
});

come fa la funzione equal a confrontare una proprietà id con id 'return a.equals (id);', perché a contiene tutte le proprietà restituite per quel modello?
lboyel

1
@lboyel Non intendevo che fosse così intelligente :-), ma ha funzionato perché stava usando Mongoose's Document#equalsper confrontare con il _idcampo del doc . Aggiornato per rendere _idesplicito il confronto. Grazie per avermelo chiesto.
JohnnyHK

6

Simile alla soluzione di JonnyHK , puoi riordinare i documenti restituiti dal findtuo client (se il tuo client è in JavaScript) con una combinazione di mape la Array.prototype.findfunzione in 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);
        });
    });

});

Un paio di note:

  • Il codice precedente utilizza il driver Mongo Node e non Mongoose
  • Il idArrayè un array diObjectId
  • Non ho testato le prestazioni di questo metodo rispetto all'ordinamento, ma se devi manipolare ogni elemento restituito (che è abbastanza comune) puoi farlo nel mapcallback per semplificare il tuo codice.

Il tempo di esecuzione è O (n * n), poiché l'interno findattraversa l'array per ogni elemento dell'array (dall'esterno map). Questo è orribilmente inefficiente, poiché esiste una soluzione O (n) che utilizza una tabella di ricerca.
curran

5

So che questa domanda è correlata al framework Mongoose JS, ma quella duplicata è generica, quindi spero che pubblicare una soluzione Python (PyMongo) vada bene qui.

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

Un modo semplice per ordinare il risultato dopo che mongo ha restituito l'array è creare un oggetto con id come chiavi e quindi mappare gli _id forniti per restituire un array ordinato correttamente.

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
Questo fa esattamente ciò di cui ho bisogno ed è molto più semplice del commento in alto.
dyarbrough

@dyarbrough questa soluzione funziona solo per le query che recuperano tutti i documenti (senza limiti o ignorati). Il commento in alto è più complesso ma funziona per ogni scenario.
marian2js

3

Sempre? Mai. L'ordine è sempre lo stesso: indefinito (probabilmente l'ordine fisico in cui sono archiviati i documenti). A meno che tu non lo risolva.


$naturalordine normalmente che è logico piuttosto che fisico
Sammaye


0

Puoi garantire l'ordine con $ o clausola.

Quindi usa $or: [ _ids.map(_id => ({_id}))]invece.


2
La $orsoluzione alternativa non ha funzionato dalla v2.6 .
JohnnyHK

0

Questa è una soluzione di codice dopo che i risultati sono stati recuperati da Mongo. Utilizzo di una mappa per memorizzare l'indice e quindi scambiare i valori.

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++
        }
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.