Saltar al contenido

Agregación de Mongodb $ grupo, restringir la longitud de array

Nuestro grupo de trabajo ha pasado horas buscando la resolución a tus búsquedas, te ofrecemos la respuestas por eso esperamos serte de mucha apoyo.

Solución:

Moderno

Desde MongoDB 3.6 hay un enfoque “novedoso” al usar $lookup para realizar una “autounión” de la misma manera que el procesamiento del cursor original que se muestra a continuación.

Dado que en esta versión puede especificar un "pipeline" argumento para $lookup como fuente para la “unión”, esto básicamente significa que puede usar $match y $limit para recopilar y “limitar” las entradas para el 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"
  
])

Opcionalmente, puede agregar una proyección adicional después de la $lookup para hacer el array elementos simplemente los valores en lugar de documentos con un _id clave, pero el resultado básico está ahí simplemente haciendo lo anterior.

Todavía existe el destacado SERVER-9277 que en realidad solicita un “límite para empujar” directamente, pero usando $lookup de esta manera es una alternativa viable en el ínterin.

NOTA: También hay $slice que se introdujo después de escribir la respuesta original y se mencionó como “problema pendiente de la JIRA” en el contenido original. Si bien puede obtener el mismo resultado con conjuntos de resultados pequeños, todavía implica “empujar todo” en el array y luego limitando la final array salida a la longitud deseada.

Esa es la principal distinción y la razón por la que generalmente no es práctico $slice para obtener grandes resultados. Pero, por supuesto, se puede utilizar alternativamente en los casos en que lo sea.

Hay algunos detalles más sobre los valores del grupo mongodb por múltiples campos sobre el uso alternativo.


Original

Como se dijo anteriormente, esto no es imposible, pero ciertamente es un problema horrible.

En realidad, si su principal preocupación es que sus matrices resultantes van a ser excepcionalmente grandes, entonces el mejor enfoque es enviar cada “conversación_ID” distinta como una consulta individual y luego combinar sus resultados. En la sintaxis de MongoDB 2.6, que podría necesitar algunos ajustes dependiendo de cuál sea realmente la implementación de su lenguaje:

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

Pero todo depende de si eso es lo que está tratando de evitar. Así que pasemos a la respuesta real:


El primer problema aquí es que no hay ninguna función para “limitar” el número de elementos que se “empujan” en un array. Ciertamente es algo que nos gustaría, pero la funcionalidad no existe actualmente.

El segundo problema es que incluso cuando se introducen todos los elementos en un array, no se puede utilizar $slice, o cualquier operador similar en la tubería de agregación. Por lo tanto, no existe una forma actual de obtener solo los “10 mejores resultados” de un array con una simple operación.

Pero en realidad puede producir un conjunto de operaciones para “cortar” eficazmente los límites de su agrupación. Es bastante complicado y, por ejemplo, aquí reduciré la array elementos “cortados” a “seis” solamente. La razón principal aquí es demostrar el proceso y mostrar cómo hacer esto sin ser destructivo con matrices que no contienen el total que desea “cortar”.

Dada una muestra 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 

Puede ver allí que al agrupar por sus condiciones obtendrá uno array con diez elementos y otro con “cinco”. Lo que quiere hacer aquí reducir ambos a los “seis” primeros sin “destruir” el array que solo coincidirá con “cinco” elementos.

Y la siguiente 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" 
    
])

Obtienes los mejores resultados en el array, hasta seis entradas:

 "_id" : 123, "msgs" : [ 1, 2, 3, 4, 5, 6 ] 
 "_id" : 456, "msgs" : [ 12, 13, 14, 15 ] 

Como puede ver aquí, muy divertido.

Después de haber agrupado inicialmente, básicamente desea “hacer estallar” el $first valor fuera de la pila para el array resultados. Para simplificar un poco este proceso, lo hacemos en la operación inicial. Entonces el proceso se convierte en:

  • $unwind los array
  • Compare con los valores ya vistos con un $eq partido de igualdad
  • $sort los resultados a “flotar” false valores invisibles en la parte superior (esto aún conserva el orden)
  • $group volver de nuevo y “hacer estallar” el $first valor invisible como el siguiente miembro de la pila. También esto usa el $cond operador para reemplazar los valores “vistos” en el array apilar con false para ayudar en la evaluación.

La acción final con $cond ¿Está ahí para asegurarse de que las iteraciones futuras no solo agreguen el último valor de la array una y otra vez donde el recuento de “rebanadas” es mayor que el array miembros.

Todo ese proceso debe repetirse para tantos elementos como desee “cortar”. Como ya encontramos el “primer” elemento en la agrupación inicial, eso significa n-1 iteraciones para el resultado de corte deseado.

Los pasos finales son en realidad solo una ilustración opcional de convertir todo de nuevo en matrices para obtener el resultado como se muestra finalmente. Así que en realidad solo empuja condicionalmente elementos o false por su posición coincidente y finalmente “filtrando” todos los false valores por lo que las matrices finales tienen “seis” y “cinco” miembros respectivamente.

Por lo tanto, no existe un operador estándar para adaptarse a esto, y no puede simplemente “limitar” el envío a 5 o 10 o cualquier elemento en el array. Pero si realmente tienes que hacerlo, entonces este es tu mejor enfoque.


Posiblemente podría abordar esto con mapReduce y abandonar el marco de agregación por completo. El enfoque que tomaría (dentro de límites razonables) sería tener efectivamente un mapa hash en la memoria en el servidor y acumular matrices para eso, mientras uso el segmento de JavaScript para “limitar” los 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 
    
)

Básicamente, eso crea el objeto "en memoria" que coincide con las "claves" emitidas con un array nunca supere el tamaño máximo que desea obtener de sus resultados. Además, esto ni siquiera se molesta en "emitir" el elemento cuando se alcanza la pila máxima.

La parte de reducción en realidad no hace nada más que esencialmente reducir a "clave" y un valor único. Entonces, en caso de que no se llame a nuestro reductor, como sería true si solo existía 1 valor para una clave, la función finalizar se encarga de mapear las claves "ocultas" a la salida final.

La efectividad de esto varía según el tamaño de la salida, y la evaluación de JavaScript ciertamente no es rápida, pero posiblemente más rápida que procesar matrices grandes en una tubería.


Vota los problemas de JIRA para tener un operador de "corte" o incluso un "límite" en "$ push" y "$ addToSet", que serían útiles. Personalmente, con la esperanza de que al menos se pueda hacer alguna modificación al $map operador para exponer el valor del "índice actual" al procesar. Eso permitiría efectivamente el "corte" y otras operaciones.

Realmente querría codificar esto para "generar" todas las iteraciones requeridas. Si la respuesta aquí recibe suficiente amor y / u otro tiempo pendiente que tengo en tuits, entonces podría agregar algún código para demostrar cómo hacer esto. Ya es una respuesta razonablemente larga.


Código para generar canalización:

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

Eso construye el enfoque iterativo básico hasta maxLen con los pasos de $unwind para $group. También se incluyen detalles de las proyecciones finales requeridas y la declaración condicional "anidada". El último es básicamente el enfoque adoptado sobre esta cuestión:

¿La cláusula $ in de MongoDB garantiza el orden?

A partir de Mongo 4.4, los $group etapa tiene un nuevo operador de agregación $accumulator permitiendo acumulaciones personalizadas de documentos a medida que se agrupan, a través de funciones definidas por el usuario de JavaScript.

Por lo tanto, para seleccionar solo n mensajes (por ejemplo 2) para cada conversación:

//  "conversationId" : 3, "messageId" : 14 
//  "conversationId" : 5, "messageId" : 34 
//  "conversationId" : 3, "messageId" : 39 
//  "conversationId" : 3, "messageId" : 47 
db.collection.aggregate([
   $group: 
    _id: "$conversationId",
    messages: 
      $accumulator: 
        accumulateArgs: ["$messageId"],
        init: function()  return [] ,
        accumulate:
          function(messages, message)  return messages.concat(message).slice(0, 2); ,
        merge:
          function(messages1, messages2)  return messages1.concat(messages2).slice(0, 2); ,
        lang: "js"
      
    
  
])
//  "_id" : 5, "messages" : [ 34 ] 
//  "_id" : 3, "messages" : [ 14, 39 ] 

El acumulador:

  • se acumula en el campo messageId (accumulateArgs)
  • se inicializa a un vacío array (init)
  • acumula messageId elementos en un array y solo mantiene un máximo de 2 (accumulate y merge)

Puntuaciones y comentarios

Agradecemos que quieras animar nuestra publicación fijando un comentario o dejando una valoración te lo agradecemos.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *