Verifique si cada elemento en la matriz coincide con la condición

Tengo una colección de documentos:

date: Date users: [ { user: 1, group: 1 } { user: 5, group: 2 } ] date: Date users: [ { user: 1, group: 1 } { user: 3, group: 2 } ] 

Me gustaría consultar en contra de esta colección para encontrar todos los documentos donde cada identificación de usuario en mi matriz de usuarios se encuentre en otra matriz, [1, 5, 7]. En este ejemplo, solo el primer documento coincide.

La mejor solución que he podido encontrar es hacer:

 $where: function() { var ids = [1, 5, 7]; return this.users.every(function(u) { return ids.indexOf(u.user) !== -1; }); } 

Desafortunadamente, esto parece perjudicar el rendimiento que se establece en el $ docs:

$ donde evalúa JavaScript y no puede aprovechar los índices.

¿Cómo puedo mejorar esta consulta?

La consulta que desea es esta:

 db.collection.find({"users":{"$not":{"$elemMatch":{"user":{$nin:[1,5,7]}}}}}) 

Esto dice: encuéntrame todos los documentos que no tengan elementos que estén fuera de la lista 1,5,7.

No sé si es mejor, pero hay algunas formas diferentes de abordar esto, y dependiendo de la versión de MongoDB que tenga disponible.

No estoy seguro si esta es su intención o no, pero la consulta como se muestra coincidirá con el primer ejemplo de documento porque a medida que su lógica se implementa, está haciendo coincidir los elementos dentro de la matriz de ese documento que debe estar dentro de la matriz de muestra.

Entonces, si realmente desea que el documento contenga todos esos elementos, entonces el operador $all sería la elección obvia:

 db.collection.find({ "users.user": { "$all": [ 1, 5, 7 ] } }) 

Pero trabajando con la presunción de que su lógica es realmente intencionada, al menos según la sugerencia, puede “filtrar” esos resultados al combinarse con el operador $in para que haya menos documentos sujetos a su condición $where ** en el JavaScript evaluado:

 db.collection.find({ "users.user": { "$in": [ 1, 5, 7 ] }, "$where": function() { var ids = [1, 5, 7]; return this.users.every(function(u) { return ids.indexOf(u.user) !== -1; }); } }) 

Y obtendrá un índice, aunque el escaneado real se multiplicará por el número de elementos en las matrices de los documentos coincidentes, pero aún mejor que sin el filtro adicional.

O incluso posiblemente considere la abstracción lógica del operador $and usado en combinación con $or posiblemente el operador $size dependiendo de las condiciones reales de su matriz:

 db.collection.find({ "$or": [ { "users.user": { "$all": [ 1, 5, 7 ] } }, { "users.user": { "$all": [ 1, 5 ] } }, { "users.user": { "$all": [ 1, 7 ] } }, { "users": { "$size": 1 }, "users.user": 1 }, { "users": { "$size": 1 }, "users.user": 5 }, { "users": { "$size": 1 }, "users.user": 7 } ] }) 

Así que esta es una generación de todas las permutaciones posibles de su condición coincidente, pero de nuevo el rendimiento probablemente variará según la versión instalada disponible.

NOTA: En realidad, es un error completo en este caso ya que esto hace algo completamente diferente y de hecho da como resultado un $in lógico


Los suplentes están en el marco de agregación, su millaje puede variar según cuál sea más eficiente debido a la cantidad de documentos en su colección, un enfoque con MongoDB 2.6 y siguientes:

 db.problem.aggregate([ // Match documents that "could" meet the conditions { "$match": { "users.user": { "$in": [ 1, 5, 7 ] } }}, // Keep your original document and a copy of the array { "$project": { "_id": { "_id": "$_id", "date": "$date", "users": "$users" }, "users": 1, }}, // Unwind the array copy { "$unwind": "$users" }, // Just keeping the "user" element value { "$group": { "_id": "$_id", "users": { "$push": "$users.user" } }}, // Compare to see if all elements are a member of the desired match { "$project": { "match": { "$setEquals": [ { "$setIntersection": [ "$users", [ 1, 5, 7 ] ] }, "$users" ]} }}, // Filter out any documents that did not match { "$match": { "match": true } }, // Return the original document form { "$project": { "_id": "$_id._id", "date": "$_id.date", "users": "$_id.users" }} ]) 

De modo que ese enfoque utiliza algunos operadores de conjuntos recientemente introducidos para comparar los contenidos, aunque, por supuesto, necesita reestructurar el conjunto para hacer la comparación.

Como se señaló, hay un operador directo para hacer esto en $setIsSubset que hace el equivalente de los operadores combinados anteriores en un solo operador:

 db.collection.aggregate([ { "$match": { "users.user": { "$in": [ 1,5,7 ] } }}, { "$project": { "_id": { "_id": "$_id", "date": "$date", "users": "$users" }, "users": 1, }}, { "$unwind": "$users" }, { "$group": { "_id": "$_id", "users": { "$push": "$users.user" } }}, { "$project": { "match": { "$setIsSubset": [ "$users", [ 1, 5, 7 ] ] } }}, { "$match": { "match": true } }, { "$project": { "_id": "$_id._id", "date": "$_id.date", "users": "$_id.users" }} ]) 

O con un enfoque diferente sin dejar de aprovechar el operador de $size de MongoDB 2.6:

 db.collection.aggregate([ // Match documents that "could" meet the conditions { "$match": { "users.user": { "$in": [ 1, 5, 7 ] } }}, // Keep your original document and a copy of the array // and a note of it's current size { "$project": { "_id": { "_id": "$_id", "date": "$date", "users": "$users" }, "users": 1, "size": { "$size": "$users" } }}, // Unwind the array copy { "$unwind": "$users" }, // Filter array contents that do not match { "$match": { "users.user": { "$in": [ 1, 5, 7 ] } }}, // Count the array elements that did match { "$group": { "_id": "$_id", "size": { "$first": "$size" }, "count": { "$sum": 1 } }}, // Compare the original size to the matched count { "$project": { "match": { "$eq": [ "$size", "$count" ] } }}, // Filter out documents that were not the same { "$match": { "match": true } }, // Return the original document form { "$project": { "_id": "$_id._id", "date": "$_id.date", "users": "$_id.users" }} ]) 

Lo que por supuesto todavía se puede hacer, aunque un poco más largo en versiones anteriores a 2.6:

 db.collection.aggregate([ // Match documents that "could" meet the conditions { "$match": { "users.user": { "$in": [ 1, 5, 7 ] } }}, // Keep your original document and a copy of the array { "$project": { "_id": { "_id": "$_id", "date": "$date", "users": "$users" }, "users": 1, }}, // Unwind the array copy { "$unwind": "$users" }, // Group it back to get it's original size { "$group": { "_id": "$_id", "users": { "$push": "$users" }, "size": { "$sum": 1 } }}, // Unwind the array copy again { "$unwind": "$users" }, // Filter array contents that do not match { "$match": { "users.user": { "$in": [ 1, 5, 7 ] } }}, // Count the array elements that did match { "$group": { "_id": "$_id", "size": { "$first": "$size" }, "count": { "$sum": 1 } }}, // Compare the original size to the matched count { "$project": { "match": { "$eq": [ "$size", "$count" ] } }}, // Filter out documents that were not the same { "$match": { "match": true } }, // Return the original document form { "$project": { "_id": "$_id._id", "date": "$_id.date", "users": "$_id.users" }} ]) 

En general, esto completa las diferentes formas, pruébelas y vea qué funciona mejor para usted. Con toda probabilidad, la simple combinación de $in con su formulario existente probablemente sea la mejor. Pero en todos los casos, asegúrese de tener un índice que se puede seleccionar:

 db.collection.ensureIndex({ "users.user": 1 }) 

Lo cual le dará el mejor rendimiento siempre y cuando tenga acceso a eso de alguna manera, como lo hacen todos los ejemplos aquí.


Veredicto

Estaba intrigado por esto, así que finalmente ideé un caso de prueba para ver cuál tenía el mejor rendimiento. Entonces primero la generación de datos de prueba:

 var batch = []; for ( var n = 1; n < = 10000; n++ ) { var elements = Math.floor(Math.random(10)*10)+1; var obj = { date: new Date(), users: [] }; for ( var x = 0; x < elements; x++ ) { var user = Math.floor(Math.random(10)*10)+1, group = Math.floor(Math.random(10)*10)+1; obj.users.push({ user: user, group: group }); } batch.push( obj ); if ( n % 500 == 0 ) { db.problem.insert( batch ); batch = []; } } 

Con 10000 documentos en una colección con matrices aleatorias desde 1..10 de longitud con valores aleatorios de 1..0, obtuve un recuento de 430 documentos (reducido de 7749 del $in coincidencia) con los siguientes resultados (prom. )

  • JavaScript con la cláusula $in : 420ms
  • Agregado con $size : 395ms
  • Agregado con conteo de conjunto de grupos: 650 ms
  • Agregado con dos operadores de conjunto: 275 ms
  • Agregado con $setIsSubset : 250 ms

Observando que sobre las muestras hechas, todas menos las dos últimas tuvieron una varianza pico de aproximadamente 100 ms más rápido, y las dos últimas exhibieron una respuesta de 220 ms. Las mayores variaciones se encontraron en la consulta de JavaScript, que también exhibió resultados 100 ms más lentos.

Pero el punto aquí es relativo al hardware, que en mi laptop bajo VM no es particularmente bueno, pero da una idea.

Por lo tanto, el agregado, y específicamente la versión MongoDB 2.6.1 con operadores establecidos, claramente gana en rendimiento con la ligera ganancia adicional proveniente de $setIsSubset como operador único.

Esto es particularmente interesante dado (como lo indica el método compatible 2.4) el costo más grande en este proceso será la statement $unwind (más de 100ms avg), así que con la selección de $in una media de 32ms el rest de las etapas de pipeline se ejecutarán en menos de 100ms en promedio. De modo que da una idea relativa de la agregación frente al rendimiento de JavaScript.

Acabo de pasar una parte sustancial de mi día tratando de implementar la solución de Asya anterior con comparaciones de objetos en lugar de una estricta igualdad. Así que pensé que lo compartiría aquí.

Digamos que ha expandido su pregunta de usuarios a usuarios completos. Desea encontrar todos los documentos donde cada elemento de su matriz de users está presente en otra matriz de usuarios: [{user: 1, group: 3}, {user: 2, group: 5},...]

Esto no funcionará: db.collection.find({"users":{"$not":{"$elemMatch":{"$nin":[{user: 1, group: 3},{user: 2, group: 5},...]}}}}}) porque $ nin solo funciona para una igualdad estricta. Entonces, necesitamos encontrar una forma diferente de express “No en conjunto” para matrices de objetos. Y usar $where ralentizaría la consulta demasiado.

Solución:

 db.collection.find({ "users": { "$not": { "$elemMatch": { // if all of the OR-blocks are true, element is not in array "$and": [{ // each OR-block == true if element != that user "$or": [ "user": { "ne": 1 }, "group": { "ne": 3 } ] }, { "$or": [ "user": { "ne": 2 }, "group": { "ne": 5 } ] }, { // more users... }] } } } }) 

Para completar la lógica: $ elemMatch coincide con todos los documentos que tienen un usuario que no está en la matriz. Entonces, $ no coincidirá con todos los documentos que tienen todos los usuarios en la matriz.