¿La orden de garantía de $ in de MongoDB ordena

Al usar la cláusula $in MongoDB, ¿el orden de los documentos devueltos siempre corresponde al orden del argumento de la matriz?

Como se señaló, el orden de los argumentos en el conjunto de una cláusula $ in no refleja el orden de cómo se recuperan los documentos. Eso, por supuesto, será el orden natural o el orden de índice seleccionado como se muestra.

Si necesita conservar este orden, básicamente tiene dos opciones.

Entonces digamos que estaba haciendo coincidir los valores de _id en sus documentos con una matriz que se pasará a $in como [ 4, 2, 8 ] .

Enfoque usando Agregado


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

Entonces esa sería la forma expandida. Lo que básicamente ocurre aquí es que, al igual que la matriz de valores que se pasa a $in , también se construye una instrucción $cond “anidada” para probar los valores y asignarle un peso adecuado. Como ese valor de “peso” refleja el orden de los elementos en la matriz, puede pasar ese valor a una etapa de clasificación para obtener los resultados en el orden requerido.

Por supuesto que realmente “comstack” la sentencia de canalización en código, muy parecido a esto:

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

Enfoque usando mapReduce


Por supuesto, si todo eso parece tener una gran sensibilidad para ti, entonces puedes hacer lo mismo usando mapReduce, que parece más simple pero que probablemente se ejecutará un poco más lento.

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

Y eso básicamente depende de que los valores “clave” emitidos estén en el “orden de índice” de cómo ocurren en la matriz de entrada.


Entonces, esas son esencialmente sus formas de mantener el orden de una lista de entrada en una condición $in que ya tiene esa lista en un orden determinado.

Si no quiere usar aggregate , otra solución es usar find y luego ordenar los resultados de doc del lado del cliente usando array#sort :

Si los valores $in son tipos primitivos como números, puede usar un enfoque como:

 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 los valores $in son tipos no primitivos como ObjectId , se requiere otro enfoque ya que indexOf compara por referencia en ese caso.

Si está utilizando Node.js 4.x +, puede usar Array#findIndex y ObjectID#equals Array#findIndex para manejar esto cambiando la función de sort a:

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

O con cualquier versión de Node.js, con underscore / lodash’s 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); }); }); 

Otra forma de utilizar la consulta de agregación solo aplicable para mongoDB verion> 3.4

El crédito va a esta agradable publicación de blog .

Ejemplos de documentos que se deben obtener en este orden:

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

La consulta –

 var query = [ {$match: {name: {$in: order}}}, {$addFields: {"__order": {$indexOfArray: [order, "$name" ]}}}, {$sort: {"__order": 1}} ]; var result = db.users.aggregate(query); 

Otra cita de la publicación que explica estos operadores de agregación utilizados:

La etapa “$ addFields” es nueva en 3.4 y le permite “$ project” nuevos campos a documentos existentes sin conocer todos los demás campos existentes. La nueva expresión “$ indexOfArray” devuelve la posición de un elemento particular en una matriz determinada.

Básicamente, el operador addToSet agrega un nuevo campo de order a cada documento cuando lo encuentra y este campo de order representa el orden original de nuestra matriz que proporcionamos. Luego, simplemente ordenamos los documentos basados ​​en este campo.

Similar a la solución de JonnyHK , puede reordenar los documentos devueltos de find en su cliente (si su cliente está en JavaScript) con una combinación de map y la función Array.prototype.find en 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 par de notas:

  • El código anterior está utilizando el controlador Mongo Node y no Mongoose
  • idArray es una matriz de ObjectId
  • No he probado el rendimiento de este método en comparación con el género, pero si necesita manipular cada elemento devuelto (que es bastante común), puede hacerlo en la callback del map para simplificar su código.

¿Siempre? Nunca. El orden es siempre el mismo: indefinido (probablemente el orden físico en el que se almacenan los documentos). A menos que lo clasifiques.

Sé que esta pregunta está relacionada con Mongoose JS framework, pero el duplicado es genérico, así que espero publicar una solución de Python (PyMongo) está bien aquí.

 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 

Sé que este es un hilo antiguo, pero si solo está devolviendo el valor del Id en la matriz, es posible que tenga que optar por esta syntax. Como no pude conseguir el valor de indexOf para que coincida con un formato 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}); }; 

Cómo convertir mongo ObjectId .toString sin incluir el contenedor 'ObjectId ()' - ¿solo el valor?

Puede garantizar el pedido con $ o cláusula.

Entonces use $or: [ _ids.map(_id => ({_id}))] lugar.

Esta es una solución de código después de que los resultados se recuperan de Mongo. Usar un mapa para almacenar el índice y luego intercambiar valores.

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

Una manera fácil de ordenar el resultado después de que mongo devuelva la matriz es crear un objeto con id como claves y luego mapear sobre la _id dada para devolver una matriz ordenada correctamente.

 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 }