We currently have a query for a Movies collection to return 'compilation' documents matching the title from user-generated 'compilations'.
The user can apply a few filters: audience, provider, and format.
Currently, this find() combination returns the following document, even though the matched 'title' does not fit the filters, instead the second movie in the 'compilation' matches the filters..
{
"_id": "551781485d9d35e4720c9057",
"name": "Consciousness",
"audience": {
"adults": true,
"teenagers": true,
"kids": false
},
"compilation": [
{
"title": "2001",
"_id": "551781485d9d35e4720c9059",
"provider": {
"amazon": false,
"apple": true,
"hulu": false,
"google": true,
"xfinity": false
},
"format": {
"hd": false,
"sd": false,
"4k": true
}
},
{
"title": "Matrix",
"_id": "551781485d9d35e4720c9059",
"provider": {
"amazon": false,
"apple": true,
"hulu": false,
"google": true,
"xfinity": false
},
"format": {
"hd": true,
"sd": false,
"4k": false
}
}
]
}
How can this be rewritten so that the $or + $and queries relate specifically to the $elemMatch? It works perfectly for 'compilations' that have a single movie, but not multiple movies..
Models.Movies.find(
{
"collection": {
"$elemMatch": {
"title": "2001"
}
}
}
)
.and([
{$or : audienceQuery},
{$or : providerQuery}
])
.and(formatQuery)
Where the filtering queries look like this:
audienceQuery == {"audience.adults":true}, {"audience.teenagers":true}, {"audience.kids":false};
providerQuery == {"compilation.provider.apple":true}, {"compilation.provider.google":true};
formatQuery == {"compilation.format.hd":true};
Consider using the aggregation framework where you can take advantage of early filtering with the $match operations in the beginning of the pipeline stage to restrict the documents that enter the pipeline. When placed at the beginning of a pipeline, $match operations use suitable indexes to scan only the matching documents in a collection. Your second pipeline stage will involve the use of $unwind operation on the compilation array so that a subsequent application of the $match operation will filter the deconstructed array: The remaining pipeline operations $group and $project then passes along the grouped documents with only the specified fields from the previous filtered input documents and newly computed fields:
So your aggregation pipeline will look like:
Models.Movies.aggregate([
{
"$match": {
"compilation.title": "2001",
"$and": [
{ "$or": [{"audience.adults": true}, {"audience.teenagers": true}, {"audience.kids": false}] },
{ "$or": [{"compilation.provider.apple": true}, {"compilation.provider.google": true}] }
],
"compilation.format.hd": true
}
},
{
"$unwind": "$compilation"
},
{
"$match": {
"compilation.title": "2001",
"$and": [
{ "$or": [{"audience.adults": true}, {"audience.teenagers": true}, {"audience.kids": false}] },
{ "$or": [{"compilation.provider.apple": true}, {"compilation.provider.google": true}] }
],
"compilation.format.hd": true
}
},
{
"$group": {
"_id": {
"_id": "$_id",
"name": "$name",
"audience": "$audience"
},
"compilation": {
"$push": "$compilation"
}
}
},
{
"$project": {
"_id": "$_id._id",
"name": "$_id.name",
"audience": "$_id.audience",
"compilation": 1
}
}
])
Result:
/* 0 */
{
"result" : [
{
"_id" : "551781485d9d35e4720c9057",
"compilation" : [
{
"title" : "2001",
"_id" : "551781485d9d35e4720c9059",
"provider" : {
"amazon" : false,
"apple" : true,
"hulu" : false,
"google" : true,
"xfinity" : false
},
"format" : {
"hd" : true,
"sd" : false,
"4k" : true
}
}
],
"name" : "Consciousness",
"audience" : {
"adults" : true,
"teenagers" : true,
"kids" : false
}
}
],
"ok" : 1
}