Quiero hacer esta consulta de clasificación compleja en MongoDB pero no puedo lograrlo.
El modelo de la colección se ve así:
_id: UUID('some-id'), isDeleted: false, date: ISODate('some-date'), responses: [{ _id: UUID('some-id'), userId: UUID('some-id'), response: 0 }, { _id: UUID('some-id'), userId: UUID('some-id'), response: 1 }]
Una cosa a tener en cuenta es que la matriz de respuestas siempre tendrá 2 o 3 objetos dentro. Ni más, ni menos. Además, la respuesta solo tendrá tres valores, ya sea 0, 1 o 2.
Y lo que quiero hacer es ordenarlos de manera diferente para cada usuario, según su respuesta.
Así que digamos que mi colección llamada Events
tiene muchos objetos en la base de datos. Quiero que cuando los filtre, la clasificación se haga así:
If my response is 0 and others are either 0 or 1, then sort them always first. If all responses are 1, sort them after. Others (if any response is 2, or if my response is 1 but others are 1 or 0), sort them last.
Podemos encontrar si es mi respuesta pasando el ID de usuario en la consulta.
Además de eso, tendré que tener paginación, por lo que tendré que implementar $skip y $limit.
Estaba intentándolo con $unwind y luego $project tratando de hacer una clasificación basada en puntaje , pero no pude lograrlo.
La clasificación de puntuación se vería así:
if my response is 0 and others are 0 or 1 -> score = 100 if all responses are 1 -> score = 50 all others -> score = 0
De esta forma podríamos ordenarlos por puntuación. Pero no sé cómo puedo crear esta propiedad sobre la marcha.
Estaba pensando que podríamos crear una propiedad como esta:
$project: { myScore: { $cond: { if: { $in: [ UUID('my-user-id'), "$responses.userId" ], then: "$respones.response", //this is returning array here with all responses else: 0 } } }, totalScore: { $sum: "$respones.response" } }
Y luego podríamos hacer otra etapa en la que clasificamos estos números de alguna manera.
¡Gracias! :)
Aquí hay un conjunto de entrada ligeramente simplificado. También incluimos un campo target
para ayudar a probar el algoritmo de puntuación; no es necesario para la canalización final, donde la puntuación es A, B, C para el primero, medio y último en el orden de clasificación. La partitura puede ser "cualquier cosa" siempre que se ordene correctamente. Usé A, B y C porque es visualmente diferente a los códigos de respuesta (0,1,2) que estamos viendo, por lo que las funciones de canalización son un poco más comprensibles, pero podrían ser 10, 20, 30 o 5,10 ,15.
var myUserId = 1; var r = [ { target: 'C', // last, myUserId response is 1 responses: [ {userId:0, response:0}, {userId:1, response:1} ] } ,{ target: 'C', // last, myUserId response is 1 responses: [ {userId:1, response:1}, {userId:0, response:0} ] } ,{ target: 'A', // first, myUserId response is 0 responses: [ {userId:1, response:0}, {userId:0, response:0} ] } ,{ target: 'B', // mid, all 1s responses: [ {userId:7, response:1}, {userId:9, response:1} ] } ,{ target: 'C', // last, a 2 exists responses: [ {userId:4, response:2}, {userId:3, response:1}, {userId:1, response:0} ] } ];
Esta canalización producirá el resultado deseado:
db.foo.aggregate([ {$addFields: {score: {$cond: [ {$in: [2, '$responses.response']}, // any 2s? 'C', // yes, assign last {$cond: [ // else // All responses 1 (ie set diff is from 1 is empty set []? {$eq: [ {$setDifference:['$responses.response',[1]]}, [] ] }, 'B', // yes, assign mid {$cond: [ // else // Does myUserId have a response of 0? filter the // array on these 2 fields and if the size of the // filtered array != 0, that means you found one! {$ne:[0, {$size:{$filter:{input:'$responses', cond:{$and:[ {$eq:['$$this.userId',myUserId]}, {$eq:['$$this.response',0]} ]} }} } ]}, 'A', // yes, assign first 'C', // else last for rest ]} ]} ]} }} ,{$sort: {'score':1}} // TEST: Show items where target DOES NOT equal score. If the pipeline // logic is working, this stage will produce zero output; that's // how you know it works. //,{$match: {$expr: {$ne:['$score','$target']}} } ]);
Cualquiera que se pregunte sobre esto, esto es lo que se me ocurrió. PD: También decidí que necesito ignorar todos los elementos si alguna respuesta contiene la respuesta 2, por lo que me centraré solo en los valores 0 y 1.
db.invites.aggregate([ { $match: { "$responses.response": { $ne: 2 } } }, { $addFields: { "myScore": { "$let": { "vars": { "invite": { // get only object that contains my userId and get firs item from the list (as it will always be one in the list) "$arrayElemAt": [{ "$filter": { "input": "$responses", "as": "item", "cond": {"$eq": ["$$item.userId", UUID('some-id')]} }} ,0] } }, // ger response value of that object that contains my userId "in": "$$invite.response" } }, // as they are only 0 and 1s in the array items, we can see how many percent have voted with one. // totalScore = sum(responses.response) / size(responses) "totalScore": { $divide: [{$sum: "$responses.response"} , {$size: "$responses"}] } } }, { $sort: { //sort by my score, so if I have responded with 0, show first "myScore": 1, //sort by totalScore, so if I have responded 1, show those that have all 1s first. "totalScore": -1 } } ])