Ao usar a $in
cláusula do MongoDB , a ordem dos documentos retornados sempre corresponde à ordem do argumento da matriz?
Ao usar a $in
cláusula do MongoDB , a ordem dos documentos retornados sempre corresponde à ordem do argumento da matriz?
Respostas:
Conforme observado, a ordem dos argumentos na matriz de uma cláusula $ in não reflete a ordem de como os documentos são recuperados. Essa, é claro, será a ordem natural ou pela ordem do índice selecionado, conforme mostrado.
Se você precisar preservar esse pedido, terá basicamente duas opções.
Então, digamos que você estava combinando os valores de _id
em seus documentos com um array que será passado para o $in
as [ 4, 2, 8 ]
.
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 } }
])
Então essa seria a forma expandida. O que basicamente acontece aqui é que, assim como a matriz de valores é passada para $in
você, você também constrói uma $cond
instrução "aninhada" para testar os valores e atribuir um peso apropriado. Como esse valor de "peso" reflete a ordem dos elementos na matriz, você pode então passar esse valor para um estágio de classificação para obter seus resultados na ordem necessária.
É claro que você realmente "constrói" a instrução do pipeline no código, assim:
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 );
Claro, se tudo isso parecer pesado para sua sensibilidade, você pode fazer a mesma coisa usando mapReduce, que parece mais simples, mas provavelmente será executado um pouco mais 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;
}
}
)
E isso basicamente depende dos valores de "chave" emitidos estarem na "ordem do índice" de como eles ocorrem na matriz de entrada.
Então, essas são essencialmente suas maneiras de manter a ordem de uma lista de entrada para uma $in
condição em que você já tem essa lista em uma determinada ordem.
Outra maneira de usar a consulta de agregação aplicável apenas para MongoDB versão> = 3.4 -
O crédito vai para esta bela postagem no blog .
Documentos de exemplo a serem buscados neste pedido -
var order = [ "David", "Charlie", "Tess" ];
A pergunta -
var query = [
{$match: {name: {$in: order}}},
{$addFields: {"__order": {$indexOfArray: [order, "$name" ]}}},
{$sort: {"__order": 1}}
];
var result = db.users.aggregate(query);
Outra citação do post explicando esses operadores de agregação usados -
O estágio "$ addFields" é novo no 3.4 e permite que você "projete" novos campos para documentos existentes sem conhecer todos os outros campos existentes. A nova expressão "$ indexOfArray" retorna a posição de um elemento específico em uma determinada matriz.
Basicamente, o addFields
operador anexa um novo order
campo a cada documento quando o encontra e esse order
campo representa a ordem original do nosso array que fornecemos. Em seguida, simplesmente classificamos os documentos com base neste campo.
Se você não quiser usar aggregate
, outra solução é usar find
e classificar os resultados do documento do lado do cliente usando array#sort
:
Se os $in
valores são tipos primitivos, como números, você pode usar uma abordagem 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);
});
});
Se os $in
valores forem tipos não primitivos como ObjectId
s, outra abordagem é necessária como indexOf
comparação por referência nesse caso.
Se você estiver usando o Node.js 4.x +, pode usar Array#findIndex
e ObjectID#equals
para lidar com isso alterando a sort
função para:
docs.sort((a, b) => ids.findIndex(id => a._id.equals(id)) -
ids.findIndex(id => b._id.equals(id)));
Ou com qualquer versão do Node.js, com sublinhado / 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); });
});
Document#equals
para comparar com o _id
campo do doc . Atualizado para tornar a _id
comparação explícita. Obrigado por perguntar.
Semelhante à solução JonnyHK , você pode reordenar os documentos retornados de find
seu cliente (se seu cliente estiver em JavaScript) com uma combinação de map
e a Array.prototype.find
função em 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);
});
});
});
Algumas notas:
idArray
é uma matriz deObjectId
map
retorno de chamada para simplificar seu código.Uma maneira fácil de ordenar o resultado após o mongo retornar o array é fazer um objeto com id como chaves e, em seguida, mapear os _id's fornecidos para retornar um array que esteja corretamente ordenado.
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
}
Eu sei que esta questão está relacionada ao framework JS Mongoose, mas o duplicado é genérico, então espero que postar uma solução Python (PyMongo) esteja bem aqui.
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
Eu sei que este é um thread antigo, mas se você estiver apenas retornando o valor do Id na matriz, pode ser necessário optar por esta sintaxe. Como não consegui obter o valor indexOf para corresponder a um formato ObjectId mongo.
obj.map = function() {
for(var i = 0; i < inputs.length; i++){
if(this._id.equals(inputs[i])) {
var order = i;
}
}
emit(order, {doc: this});
};
Como converter mongo ObjectId .toString sem incluir o wrapper 'ObjectId ()' - apenas o Value?
Você pode garantir o pedido com a cláusula $ or.
Portanto, use em seu $or: [ _ids.map(_id => ({_id}))]
lugar.
$or
solução alternativa não funcionou desde a v2.6 .
Esta é uma solução de código depois que os resultados são recuperados do Mongo. Usando um mapa para armazenar o índice e, em seguida, trocando os 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++
}
}
}