O pedido de garantia da cláusula $ in do MongoDB

Respostas:

78

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 _idem seus documentos com um array que será passado para o $inas [ 4, 2, 8 ].

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

])

Então essa seria a forma expandida. O que basicamente acontece aqui é que, assim como a matriz de valores é passada para $invocê, você também constrói uma $condinstruçã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 );

Abordagem usando mapReduce


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 $incondição em que você já tem essa lista em uma determinada ordem.

Neil Lunn
fonte
2
Ótima resposta. Para quem precisa, uma versão em coffeescript aqui
Lawrence Jones
1
@NeilLunn Eu tentei a abordagem usando agregado, mas consegui o id e o peso. Você sabe como recuperar os posts (objeto)?
Juanjo Lainez Reche
1
@NeilLunn Na verdade eu fiz (está aqui stackoverflow.com/questions/27525235/… ) Mas o único comentário foi se referindo aqui, embora eu tenha verificado isso antes de postar minha pergunta. Você pode me ajudar aí? Obrigado!
Juanjo Lainez Reche
1
sei que isso é antigo, mas perdi muito tempo depurando por que inputs.indexOf () não correspondia a this._id. Se você está apenas retornando o valor do Id do objeto, pode ter que optar por esta sintaxe: obj.map = function () {for (var i = 0; i <inputs.length; i ++) {if (this. _id.equals (entradas [i])) {ordem var = i; }} emit (ordem, {doc: this}); };
NoobSter
1
você pode usar "$ addFields" em vez de "$ project" se quiser ter todos os campos originais também
Jodo
39

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 addFieldsoperador anexa um novo ordercampo a cada documento quando o encontra e esse ordercampo representa a ordem original do nosso array que fornecemos. Em seguida, simplesmente classificamos os documentos com base neste campo.

Jyotman Singh
fonte
existe uma maneira de armazenar a matriz de ordem como uma variável na consulta para que não tenhamos essa consulta massiva da mesma matriz duas vezes se a matriz for grande?
Ethan SK
24

Se você não quiser usar aggregate, outra solução é usar finde classificar os resultados do documento do lado do cliente usando array#sort:

Se os $invalores 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 $invalores forem tipos não primitivos como ObjectIds, outra abordagem é necessária como indexOfcomparação por referência nesse caso.

Se você estiver usando o Node.js 4.x +, pode usar Array#findIndexe ObjectID#equalspara lidar com isso alterando a sortfunçã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); });
});
JohnnyHK
fonte
como a função equal sabe comparar uma propriedade id com id 'return a.equals (id);', porque a contém todas as propriedades retornadas para aquele modelo?
lboyel de
1
@lboyel Eu não queria que fosse tão inteligente :-), mas funcionou porque estava usando o Mongoose Document#equalspara comparar com o _idcampo do doc . Atualizado para tornar a _idcomparação explícita. Obrigado por perguntar.
JohnnyHK
6

Semelhante à solução JonnyHK , você pode reordenar os documentos retornados de findseu cliente (se seu cliente estiver em JavaScript) com uma combinação de mape a Array.prototype.findfunçã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:

  • O código acima está usando o driver Mongo Node e não o Mongoose
  • O idArrayé uma matriz deObjectId
  • Não testei o desempenho desse método em comparação com a classificação, mas se você precisar manipular cada item retornado (o que é muito comum), pode fazer isso no mapretorno de chamada para simplificar seu código.
tebs1200
fonte
5

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
}
Arne Jenssen
fonte
1
Isso faz exatamente o que preciso e é muito mais simples do que o comentário principal.
dyarbrough
@dyarbrough esta solução só funciona para consultas que buscam todos os documentos (sem limite ou salto). O comentário principal é mais complexo, mas funciona para todos os cenários.
marian2js
4

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
Dennis Golomazov
fonte
3

Sempre? Nunca. A ordem é sempre a mesma: indefinida (provavelmente a ordem física em que os documentos são armazenados). A menos que você classifique.

esquisito
fonte
$naturalordem normalmente, o que é lógico ao invés de físico
Sammaye
1

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?

NoobSter
fonte
0

Você pode garantir o pedido com a cláusula $ or.

Portanto, use em seu $or: [ _ids.map(_id => ({_id}))]lugar.

Fakenickels
fonte
2
A $orsolução alternativa não funcionou desde a v2.6 .
JohnnyHK de
0

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