Mongodb Match value of array object by Query Method? [duplicate] - mongodb

I wasn't quite sure how to title this!
So I have an array of product IDs
var productIds = [139,72,73,1,6]
And a bunch of customer documents in MongoDB
{
name: 'James',
products: [ 73, 139 ],
_id: 5741cff3f08e992598d0a39b
}
{
name: 'John',
products: [ 72, 99 ],
_id: 5741d047f08e992598d0a39e
}
I would like to find customers when all of their products appear in the array of product IDs (productIds)
I tried:
'products' : {
'$in': productIds
}
But that returns John, even though 99 doesn't exist in the list of product IDs
I also tried:
'products' : {
'$all': productIds
}
Which returns nothing because none of the customer have ALL the products
Is there a way to achieve what I need in a single query or am I going to have to do some post query processing?
I also tried
'products': {
'$in': productIds,
'$not': {
'$nin': productIds
}
}
but this also seems to return customers when not all product IDs match

You can do this using the .aggregate() method and the $redact operator. In your $cond expressions you need to use the $setIsSubset in order to check if all the elements in the "products" array are in "productIds". This is because you cannot use $in in the conditional expression
var productIds = [139,72,73,1,6];
db.customers.aggregate([
{ "$redact": {
"$cond": [
{ "$setIsSubset": [ "$products", productIds ] },
"$$KEEP",
"$$PRUNE"
]
}}
])

Related

MongoDb aggregation - check if values in array are present in collection

I'm pretty new to MongoDb. I have a collection of some products, some of them contains an array of ids of other products in the same collection:
[
{
"id": 1,
"relatedProducts": [
"1", "2"
]
},
{
"id": 2,
"relatedProducts": [
"4", "5"
]
}
]
Problem is, not all of the products that are in that relatedProducts array are available in that collection.
I have to create an aggregation that will modify those arrays so only id of available products are present in it. So, for example, if product of id = 5 is not present in that collection, relatedProducts of object with id 2 above will have only one entry in the array ("4").
I recommend you first fetch all the product _id's, then update the documents accordingly.
Here's how I would do this:
const ids = await db.collection.distinct('id');
await db.collection.updateMany(
{
relatedProducts: {$nin: ids}
},
[
{
$set: {
relatedProducts: {
$setIntersection: [{$ifNull: ["$relatedProducts", []]}, ids]
}
}
}
]
)

How to Count Matched array elements

I have a collection and each document in that collection has an array field countries. I want to select all documents which include any of below countries:
China, USA, Australia
And the output should show the number of above countries each document has.
I use below aggregate command:
db.movies.aggregate([
{
$match: { countries: { $in: ["USA", 'China', 'Australia'] } }
},
{
$project: {
countries: {$size: '$countries'}
}
}
]);
it doesn't work as expected. It shows the number of all countries in the document who has the above-listed country. For example, if a document has China, Japan in its countries field, I expected it return 1 (because only China is in the above country list) but it returns two. How can I do that in the aggregation command?
The $in operator just "queries" documents that contain one of the possible values, so it does not remove anything from the array.
If you want to count "only matches" then apply $setIntersection to the array before $size:
db.movies.aggregate([
{
$match: { countries: { $in: ["USA", 'China', 'Australia'] } }
},
{
$project: {
countries: {
$size: {
"$setIntersection": [["USA", 'China', 'Australia'], '$countries' ]
}
}
}
]);
That returns the "set" of "unique" matches to the array provided against the array in the document.
There is an alternate of $in as an aggregation operator in modern releases ( MongoDB 3.4 at least ). This works a bit differently in "testing" a "singular" value against an array of values. In array comparison you would apply with $filter:
db.movies.aggregate([
{
$match: { countries: { $in: ["USA", 'China', 'Australia'] } }
},
{
$project: {
countries: {
$size: {
$filter: {
input: '$countries',
cond: { '$in': [ '$$this', ["USA", 'China', 'Australia'] ] }
}
}
}
}
]);
That really should only be important to you where the array "within the document" contains entries that are not unique. i.e:
{ countries: [ "USA", "Japan", "USA" ] }
And you needed to count 2 for "USA", as opposed to 1 which would be the "set" result of $setIntersection

Compare document array size to other document field

The document might look like:
{
_id: 'abc',
programId: 'xyz',
enrollment: 'open',
people: ['a', 'b', 'c'],
maxPeople: 5
}
I need to return all documents where enrollment is open and the length of people is less than maxPeople
I got this to work with $where:
const
exists = ['enrollment', 'maxPeople', 'people'],
query = _.reduce(exists, (existsQuery, field) => {
existsQuery[field] = {'$exists': true}; return existsQuery;
}, {});
query['$and'] = [{enrollment: 'open'}];
query['$where'] = 'this.people.length<this.maxPeople';
return db.coll.find(query, {fields: {programId: 1, maxPeople: 1, people: 1}});
But could I do this with aggregation, and why would it be better?
Also, if aggregation is better/faster, I don't understand how I could convert the above query to use aggregation. I'm stuck at:
db.coll.aggregate([
{$project: {ab: {$cmp: ['$maxPeople','$someHowComputePeopleLength']}}},
{$match: {ab:{$gt:0}}}
]);
UPDATE:
Based on #chridam answer, I was able to implement a solution like so, note the $and in the $match, for those of you that need a similar query:
return Coll.aggregate([
{
$match: {
$and: [
{"enrollment": "open"},
{"times.start.dateTime": {$gte: new Date()}}
]
}
},
{
"$redact": {
"$cond": [
{"$lt": [{"$size": "$students" }, "$maxStudents" ] },
"$$KEEP",
"$$PRUNE"
]
}
}
]);
The $redact pipeline operator in the aggregation framework should work for you in this case. This will recursively descend through the document structure and do some actions based on an evaluation of specified conditions at each level. The concept can be a bit tricky to grasp but basically the operator allows you to proccess the logical condition with the $cond operator and uses the special operations $$KEEP to "keep" the document where the logical condition is true or $$PRUNE to "remove" the document where the condition was false.
This operation is similar to having a $project pipeline that selects the fields in the collection and creates a new field that holds the result from the logical condition query and then a subsequent $match, except that $redact uses a single pipeline stage which restricts contents of the result set based on the access required to view the data and is more efficient.
To run a query on all documents where enrollment is open and the length of people is less than maxPeople, include a $redact stage as in the following::
db.coll.aggregate([
{ "$match": { "enrollment": "open" } },
{
"$redact": {
"$cond": [
{ "$lt": [ { "$size": "$people" }, "$maxPeople" ] },
"$$KEEP",
"$$PRUNE"
]
}
}
])
You can do :
1 $project that create a new field featuring the result of the comparison for the array size of people to maxPeople
1 $match that match the previous comparison result & enrollment to open
Query is :
db.coll.aggregate([{
$project: {
_id: 1,
programId: 1,
enrollment: 1,
cmp: {
$cmp: ["$maxPeople", { $size: "$people" }]
}
}
}, {
$match: {
$and: [
{ cmp: { $gt: 0 } },
{ enrollment: "open" }
]
}
}])

Querying arrays into arrays on MongoDB [duplicate]

I wasn't quite sure how to title this!
So I have an array of product IDs
var productIds = [139,72,73,1,6]
And a bunch of customer documents in MongoDB
{
name: 'James',
products: [ 73, 139 ],
_id: 5741cff3f08e992598d0a39b
}
{
name: 'John',
products: [ 72, 99 ],
_id: 5741d047f08e992598d0a39e
}
I would like to find customers when all of their products appear in the array of product IDs (productIds)
I tried:
'products' : {
'$in': productIds
}
But that returns John, even though 99 doesn't exist in the list of product IDs
I also tried:
'products' : {
'$all': productIds
}
Which returns nothing because none of the customer have ALL the products
Is there a way to achieve what I need in a single query or am I going to have to do some post query processing?
I also tried
'products': {
'$in': productIds,
'$not': {
'$nin': productIds
}
}
but this also seems to return customers when not all product IDs match
You can do this using the .aggregate() method and the $redact operator. In your $cond expressions you need to use the $setIsSubset in order to check if all the elements in the "products" array are in "productIds". This is because you cannot use $in in the conditional expression
var productIds = [139,72,73,1,6];
db.customers.aggregate([
{ "$redact": {
"$cond": [
{ "$setIsSubset": [ "$products", productIds ] },
"$$KEEP",
"$$PRUNE"
]
}}
])

How to remove an array value from item in a nested document array

I want to remove "tag4" only for "user3":
{
_id: "doc"
some: "value",
users: [
{
_id: "user3",
someOther: "value",
tags: [
"tag4",
"tag2"
]
}, {
_id: "user1",
someOther: "value",
tags: [
"tag3",
"tag4"
]
}
]
},
{
...
}
Note: This collection holds items referencing many users. Users are stored in a different collection. Unique tags for each user are also stored in the users collection. If an user removes a tag (or multiple) from his account it should be deleted from all items.
I tried this query, but it removes "tag4" for all users:
{
"users._id": "user3",
"users.tags": {
$in: ["tag4"]
}
}, {
$pullAll: {
"users.$.tags": ["tag4"]
}
}, {
multi: 1
}
I tried $elemMatch (and $and) in the selector but ended up with the same result only on the first matching document or noticed some strange things happen (sometimes all tags of other users are deleted).
Any ideas how to solve this? Is there a way to "back reference" in $pull conditions?
You need to use $elemMatch in your query object so that it will only match if both the _id and tags parts match for the same element:
db.test.update({
users: {$elemMatch: {_id: "user3", tags: {$in: ["tag4"]}}}
}, {
$pullAll: {
"users.$.tags": ["tag4"]
}
}, {
multi: 1
})