MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

Agregação do Mongodb $ grupo, restringir o comprimento da matriz

Moderno


A partir do MongoDB 3.6, existe uma abordagem "nova" para isso usando $lookup para executar uma "autojunção" da mesma maneira que o processamento original do cursor demonstrado abaixo.

Como nesta versão você pode especificar um "pipeline" argumento para $lookup como uma fonte para o "join", isso significa essencialmente que você pode usar $match e $limit para reunir e "limitar" as entradas do array:
db.messages.aggregate([
  { "$group": { "_id": "$conversation_ID" } },
  { "$lookup": {
    "from": "messages",
    "let": { "conversation": "$_id" },
    "pipeline": [
      { "$match": { "$expr": { "$eq": [ "$conversation_ID", "$$conversation" ] } }},
      { "$limit": 10 },
      { "$project": { "_id": 1 } }
    ],
    "as": "msgs"
  }}
])

Você pode opcionalmente adicionar projeção adicional após o $lookup para tornar os itens da matriz simplesmente os valores em vez de documentos com um _id key, mas o resultado básico está lá simplesmente fazendo o acima.

Ainda existe o SERVER-9277 pendente que, na verdade, solicita um "limite para enviar" diretamente, mas usando $lookup desta forma é uma alternativa viável nesse ínterim.

OBSERVAÇÃO :Há também $slice que foi introduzido depois de escrever a resposta original e mencionado por "problema pendente do JIRA" no conteúdo original. Embora você possa obter o mesmo resultado com pequenos conjuntos de resultados, isso envolve ainda "enviar tudo" para o array e depois limitar a saída final do array para o comprimento desejado.

Essa é a principal distinção e por que geralmente não é prático $slice para grandes resultados. Mas é claro que pode ser usado alternadamente nos casos em que é.

Há mais alguns detalhes sobre os valores do grupo mongodb por vários campos sobre o uso alternativo.

Original


Como afirmado anteriormente, isso não é impossível, mas certamente um problema horrível.

Na verdade, se sua principal preocupação é que suas matrizes resultantes sejam excepcionalmente grandes, a melhor abordagem é enviar para cada "conversation_ID" distinto como uma consulta individual e depois combinar seus resultados. Na sintaxe do MongoDB 2.6, que pode precisar de alguns ajustes, dependendo do que sua implementação de linguagem realmente é:
var results = [];
db.messages.aggregate([
    { "$group": {
        "_id": "$conversation_ID"
    }}
]).forEach(function(doc) {
    db.messages.aggregate([
        { "$match": { "conversation_ID": doc._id } },
        { "$limit": 10 },
        { "$group": {
            "_id": "$conversation_ID",
            "msgs": { "$push": "$_id" }
        }}
    ]).forEach(function(res) {
        results.push( res );
    });
});

Mas tudo depende se é isso que você está tentando evitar. Então vamos à verdadeira resposta:

O primeiro problema aqui é que não há função para "limitar" o número de itens que são "enviados" para um array. Certamente é algo que gostaríamos, mas a funcionalidade não existe atualmente.

O segundo problema é que mesmo ao enviar todos os itens para um array, você não pode usar $slice , ou qualquer operador semelhante no pipeline de agregação. Portanto, não há uma maneira atual de obter apenas os "10 principais" resultados de uma matriz produzida com uma operação simples.

Mas você pode realmente produzir um conjunto de operações para efetivamente "fatiar" seus limites de agrupamento. É bastante envolvido e, por exemplo, aqui reduzirei os elementos da matriz "fatiados" para apenas "seis". A principal razão aqui é demonstrar o processo e mostrar como fazer isso sem ser destrutivo com arrays que não contêm o total para o qual você deseja "fatiar".

Dada uma amostra de documentos:
{ "_id" : 1, "conversation_ID" : 123 }
{ "_id" : 2, "conversation_ID" : 123 }
{ "_id" : 3, "conversation_ID" : 123 }
{ "_id" : 4, "conversation_ID" : 123 }
{ "_id" : 5, "conversation_ID" : 123 }
{ "_id" : 6, "conversation_ID" : 123 }
{ "_id" : 7, "conversation_ID" : 123 }
{ "_id" : 8, "conversation_ID" : 123 }
{ "_id" : 9, "conversation_ID" : 123 }
{ "_id" : 10, "conversation_ID" : 123 }
{ "_id" : 11, "conversation_ID" : 123 }
{ "_id" : 12, "conversation_ID" : 456 }
{ "_id" : 13, "conversation_ID" : 456 }
{ "_id" : 14, "conversation_ID" : 456 }
{ "_id" : 15, "conversation_ID" : 456 }
{ "_id" : 16, "conversation_ID" : 456 }

Você pode ver lá que ao agrupar por suas condições você obterá um array com dez elementos e outro com "cinco". O que você quer fazer aqui reduza ambos para os "seis" superiores sem "destruir" a matriz que corresponderá apenas a "cinco" elementos.

E a seguinte consulta:
db.messages.aggregate([
    { "$group": {
        "_id": "$conversation_ID",
        "first": { "$first": "$_id" },
        "msgs": { "$push": "$_id" },
    }},
    { "$unwind": "$msgs" },
    { "$project": {
        "msgs": 1,
        "first": 1,
        "seen": { "$eq": [ "$first", "$msgs" ] }
    }},
    { "$sort": { "seen": 1 }},
    { "$group": {
        "_id": "$_id",
        "msgs": { 
            "$push": {
               "$cond": [ { "$not": "$seen" }, "$msgs", false ]
            }
        },
        "first": { "$first": "$first" },
        "second": { "$first": "$msgs" }
    }},
    { "$unwind": "$msgs" },
    { "$project": {
        "msgs": 1,
        "first": 1,
        "second": 1,
        "seen": { "$eq": [ "$second", "$msgs" ] }
    }},
    { "$sort": { "seen": 1 }},
    { "$group": {
        "_id": "$_id",
        "msgs": { 
            "$push": {
               "$cond": [ { "$not": "$seen" }, "$msgs", false ]
            }
        },
        "first": { "$first": "$first" },
        "second": { "$first": "$second" },
        "third": { "$first": "$msgs" }
    }},
    { "$unwind": "$msgs" },
    { "$project": {
        "msgs": 1,
        "first": 1,
        "second": 1,
        "third": 1,
        "seen": { "$eq": [ "$third", "$msgs" ] },
    }},
    { "$sort": { "seen": 1 }},
    { "$group": {
        "_id": "$_id",
        "msgs": { 
            "$push": {
               "$cond": [ { "$not": "$seen" }, "$msgs", false ]
            }
        },
        "first": { "$first": "$first" },
        "second": { "$first": "$second" },
        "third": { "$first": "$third" },
        "forth": { "$first": "$msgs" }
    }},
    { "$unwind": "$msgs" },
    { "$project": {
        "msgs": 1,
        "first": 1,
        "second": 1,
        "third": 1,
        "forth": 1,
        "seen": { "$eq": [ "$forth", "$msgs" ] }
    }},
    { "$sort": { "seen": 1 }},
    { "$group": {
        "_id": "$_id",
        "msgs": { 
            "$push": {
               "$cond": [ { "$not": "$seen" }, "$msgs", false ]
            }
        },
        "first": { "$first": "$first" },
        "second": { "$first": "$second" },
        "third": { "$first": "$third" },
        "forth": { "$first": "$forth" },
        "fifth": { "$first": "$msgs" }
    }},
    { "$unwind": "$msgs" },
    { "$project": {
        "msgs": 1,
        "first": 1,
        "second": 1,
        "third": 1,
        "forth": 1,
        "fifth": 1,
        "seen": { "$eq": [ "$fifth", "$msgs" ] }
    }},
    { "$sort": { "seen": 1 }},
    { "$group": {
        "_id": "$_id",
        "msgs": { 
            "$push": {
               "$cond": [ { "$not": "$seen" }, "$msgs", false ]
            }
        },
        "first": { "$first": "$first" },
        "second": { "$first": "$second" },
        "third": { "$first": "$third" },
        "forth": { "$first": "$forth" },
        "fifth": { "$first": "$fifth" },
        "sixth": { "$first": "$msgs" },
    }},
    { "$project": {
         "first": 1,
         "second": 1,
         "third": 1,
         "forth": 1,
         "fifth": 1,
         "sixth": 1,
         "pos": { "$const": [ 1,2,3,4,5,6 ] }
    }},
    { "$unwind": "$pos" },
    { "$group": {
        "_id": "$_id",
        "msgs": {
            "$push": {
                "$cond": [
                    { "$eq": [ "$pos", 1 ] },
                    "$first",
                    { "$cond": [
                        { "$eq": [ "$pos", 2 ] },
                        "$second",
                        { "$cond": [
                            { "$eq": [ "$pos", 3 ] },
                            "$third",
                            { "$cond": [
                                { "$eq": [ "$pos", 4 ] },
                                "$forth",
                                { "$cond": [
                                    { "$eq": [ "$pos", 5 ] },
                                    "$fifth",
                                    { "$cond": [
                                        { "$eq": [ "$pos", 6 ] },
                                        "$sixth",
                                        false
                                    ]}
                                ]}
                            ]}
                        ]}
                    ]}
                ]
            }
        }
    }},
    { "$unwind": "$msgs" },
    { "$match": { "msgs": { "$ne": false } }},
    { "$group": {
        "_id": "$_id",
        "msgs": { "$push": "$msgs" }
    }}
])

Você obtém os principais resultados na matriz, até seis entradas:
{ "_id" : 123, "msgs" : [ 1, 2, 3, 4, 5, 6 ] }
{ "_id" : 456, "msgs" : [ 12, 13, 14, 15 ] }

Como você pode ver aqui, muito divertido.

Depois de ter agrupado inicialmente, você basicamente quer "estalar" o $first valor fora da pilha para os resultados da matriz. Para simplificar um pouco esse processo, fazemos isso na operação inicial. Assim o processo fica:
  • $unwind a matriz
  • Compare com os valores já vistos com um $eq correspondência de igualdade
  • $sort os resultados para "flutuar" false valores não vistos para o topo (isso ainda mantém a ordem)
  • $group novamente e "estoure" o $first valor não visto como o próximo membro da pilha. Além disso, isso usa o $cond operador para substituir valores "vistos" na pilha de matrizes por false para ajudar na avaliação.

A ação final com $cond está lá para garantir que as iterações futuras não estejam apenas adicionando o último valor da matriz repetidamente, onde a contagem de "fatias" é maior que os membros da matriz.

Todo esse processo precisa ser repetido para quantos itens você quiser "fatiar". Como já encontramos o "primeiro" item no agrupamento inicial, isso significa n-1 iterações para o resultado de fatia desejado.

As etapas finais são apenas uma ilustração opcional da conversão de tudo de volta em arrays para o resultado como finalmente mostrado. Então, realmente apenas empurrando itens condicionalmente ou false de volta por sua posição correspondente e finalmente "filtrando" todos os false valores para que os arrays finais tenham "seis" e "cinco" membros, respectivamente.

Portanto, não há um operador padrão para acomodar isso, e você não pode simplesmente "limitar" o push para 5 ou 10 ou quaisquer itens na matriz. Mas se você realmente tem que fazer isso, então esta é a sua melhor abordagem.

Você poderia abordar isso com mapReduce e abandonar a estrutura de agregação todos juntos. A abordagem que eu tomaria (dentro de limites razoáveis) seria efetivamente ter um mapa de hash na memória no servidor e acumular arrays para isso, enquanto usa o JavaScript slice para "limitar" os resultados:
db.messages.mapReduce(
    function () {

        if ( !stash.hasOwnProperty(this.conversation_ID) ) {
            stash[this.conversation_ID] = [];
        }

        if ( stash[this.conversation_ID.length < maxLen ) {
            stash[this.conversation_ID].push( this._id );
            emit( this.conversation_ID, 1 );
        }

    },
    function(key,values) {
        return 1;   // really just want to keep the keys
    },
    { 
        "scope": { "stash": {}, "maxLen": 10 },
        "finalize": function(key,value) {
            return { "msgs": stash[key] };                
        },
        "out": { "inline": 1 }
    }
)

Então, basicamente, cria o objeto "na memória" combinando as "chaves" emitidas com uma matriz que nunca excede o tamanho máximo que você deseja buscar de seus resultados. Além disso, isso nem se incomoda em "emitir" o item quando a pilha máxima é atingida.

A parte de redução na verdade não faz nada além de essencialmente apenas reduzir para "chave" e um único valor. Portanto, caso nosso redutor não tenha sido chamado, como seria verdade se existisse apenas 1 valor para uma chave, a função finalize se encarrega de mapear as chaves "stash" para a saída final.

A eficácia disso varia de acordo com o tamanho da saída, e a avaliação do JavaScript certamente não é rápida, mas possivelmente mais rápida do que processar grandes arrays em um pipeline.

Vote nas questões do JIRA para realmente ter um operador "slice" ou até mesmo um "limite" em "$push" e "$addToSet", o que seria útil. Pessoalmente esperando que pelo menos alguma modificação possa ser feita no $map operador para expor o valor "índice atual" durante o processamento. Isso permitiria efetivamente "fatiar" e outras operações.

Realmente você gostaria de codificar isso para "gerar" todas as iterações necessárias. Se a resposta aqui obtiver amor suficiente e/ou outro tempo pendente que eu tenha em tuits, posso adicionar algum código para demonstrar como fazer isso. Já é uma resposta razoavelmente longa.

Código para gerar pipeline:
var key = "$conversation_ID";
var val = "$_id";
var maxLen = 10;

var stack = [];
var pipe = [];
var fproj = { "$project": { "pos": { "$const": []  } } };

for ( var x = 1; x <= maxLen; x++ ) {

    fproj["$project"][""+x] = 1;
    fproj["$project"]["pos"]["$const"].push( x );

    var rec = {
        "$cond": [ { "$eq": [ "$pos", x ] }, "$"+x ]
    };
    if ( stack.length == 0 ) {
        rec["$cond"].push( false );
    } else {
        lval = stack.pop();
        rec["$cond"].push( lval );
    }

    stack.push( rec );

    if ( x == 1) {
        pipe.push({ "$group": {
           "_id": key,
           "1": { "$first": val },
           "msgs": { "$push": val }
        }});
    } else {
        pipe.push({ "$unwind": "$msgs" });
        var proj = {
            "$project": {
                "msgs": 1
            }
        };
        
        proj["$project"]["seen"] = { "$eq": [ "$"+(x-1), "$msgs" ] };
       
        var grp = {
            "$group": {
                "_id": "$_id",
                "msgs": {
                    "$push": {
                        "$cond": [ { "$not": "$seen" }, "$msgs", false ]
                    }
                }
            }
        };

        for ( n=x; n >= 1; n-- ) {
            if ( n != x ) 
                proj["$project"][""+n] = 1;
            grp["$group"][""+n] = ( n == x ) ? { "$first": "$msgs" } : { "$first": "$"+n };
        }

        pipe.push( proj );
        pipe.push({ "$sort": { "seen": 1 } });
        pipe.push(grp);
    }
}

pipe.push(fproj);
pipe.push({ "$unwind": "$pos" });
pipe.push({
    "$group": {
        "_id": "$_id",
        "msgs": { "$push": stack[0] }
    }
});
pipe.push({ "$unwind": "$msgs" });
pipe.push({ "$match": { "msgs": { "$ne": false } }});
pipe.push({
    "$group": {
        "_id": "$_id",
        "msgs": { "$push": "$msgs" }
    }
}); 

Isso cria a abordagem iterativa básica até maxLen com os passos de $unwind para $group . Também estão embutidos os detalhes das projeções finais necessárias e a declaração condicional "aninhada". A última é basicamente a abordagem adotada nesta questão:

A cláusula $in do MongoDB garante a ordem?