sql >> Base de Datos >  >> NoSQL >> MongoDB

Grupo $ de agregación de Mongodb, restringe la longitud de la matriz

Moderno

Desde MongoDB 3.6 hay un enfoque "novedoso" para esto usando $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 "unirse", esto esencialmente significa que puede usar $match y $limit para reunir y "limitar" las entradas de la matriz:

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 $lookup para hacer que los elementos de la matriz sean simplemente valores en lugar de documentos con un _id clave, pero el resultado básico está allí simplemente haciendo lo anterior.

Todavía está pendiente el 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 JIRA" en el contenido original. Si bien puede obtener el mismo resultado con conjuntos de resultados pequeños, aún implica "empujar todo" en la matriz y luego limitar la salida final de la matriz a la longitud deseada.

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

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

Originales

Como se indicó anteriormente, esto no es imposible, pero ciertamente es un problema terrible.

En realidad, si su principal preocupación es que sus arreglos resultantes van a ser excepcionalmente grandes, entonces su mejor enfoque es enviar cada "ID_conversación" distinto como una consulta individual y luego combinar sus resultados. En la misma 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ás tratando de evitar. Así que vamos a la respuesta real:

El primer problema aquí es que no hay una función para "limitar" la cantidad de elementos que se "empujan" en una matriz. Ciertamente es algo que nos gustaría, pero la funcionalidad no existe actualmente.

El segundo problema es que incluso al colocar todos los elementos en una matriz, no puede usar $slice , o cualquier operador similar en la canalización de agregación. Por lo tanto, no existe una forma actual de obtener solo los "10 mejores" resultados de una matriz producida con una operación simple.

Pero en realidad puede producir un conjunto de operaciones para "cortar" efectivamente los límites de su agrupación. Es bastante complicado y, por ejemplo, aquí reduciré los elementos de la matriz "en rodajas" a "seis" solamente. La razón principal aquí es demostrar el proceso y mostrar cómo hacerlo sin ser destructivo con matrices que no contienen el total al que desea "rebanar".

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á una matriz con diez elementos y otra con "cinco". Lo que quiere hacer aquí reduce ambos a los "seis" superiores sin "destruir" la matriz 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" }
    }}
])

Obtiene los mejores resultados en la matriz, hasta seis entradas:

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

Como puedes ver aquí, mucha diversión.

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

  • $unwind la matriz
  • Compare con los valores ya vistos con un $eq coincidencia de igualdad
  • $sort los resultados "flotan" false valores invisibles en la parte superior (esto aún conserva el orden)
  • $group de nuevo y "abre" el $first valor invisible como el siguiente miembro en la pila. También esto usa el $cond operador para reemplazar los valores "vistos" en la pila de matriz 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 matriz una y otra vez donde el recuento de "segmentos" sea mayor que los miembros de la matriz.

Todo el proceso debe repetirse para tantos elementos como desee "cortar". Dado que ya encontramos el "primer" elemento en la agrupación inicial, eso significa n-1 iteraciones para obtener el resultado de división deseado.

Los pasos finales son realmente solo una ilustración opcional de convertir todo nuevamente en matrices para obtener el resultado que se muestra finalmente. Entonces, en realidad solo empuja elementos condicionalmente o false de vuelta por su posición coincidente y finalmente "filtrando" todos los false valores para que las matrices finales tengan "seis" y "cinco" miembros respectivamente.

Por lo tanto, no hay un operador estándar para acomodar esto, y no puede simplemente "limitar" el empuje a 5 o 10 o cualquier elemento en la matriz. Pero si realmente tiene que hacerlo, entonces este es su 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 memoria en el servidor y acumular matrices para eso, mientras usa 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 construye el objeto "en memoria" que hace coincidir las "claves" emitidas con una matriz que nunca excede 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 acumulación máxima.

La parte de reducción en realidad no hace nada más que esencialmente reducir a "clave" y un solo valor. Entonces, en caso de que no se llamara a nuestro reductor, como sería cierto si solo existiera 1 valor para una clave, la función finalizar se encarga de asignar las claves "almacenadas" 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 canalización.

Vote por los problemas de JIRA para tener un operador de "segmento" o incluso un "límite" en "$push" y "$addToSet", que serían útiles. Personalmente espero 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 "rebanar" y otras operaciones.

Realmente le gustarí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 tubería:

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 a $group . También incrustados hay detalles de las proyecciones finales requeridas y la declaración condicional "anidada". El último es básicamente el enfoque adoptado en esta pregunta:

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