Querying arrays into arrays on MongoDB [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

Mongodb Match value of array object by Query Method? [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"
]
}}
])

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" }
]
}
}])

query 2-level array in mongodb

in MongoDB, I have many documents in 2-level array as below:
{
_id:1,
"toPerson": [
[
{
"userid": "test1"
},
{
"userid": "test2"
}
],
[
{
"userid": "test10"
},
{
"userid": "test11"
}
]
]
}
.....
{
_id:99,
"toPerson": [
[
{
"userid": "test2"
},
{
"userid": "test3"
}
],
[
{
"userid": "test100"
},
{
"userid": "test101"
}
]
]
}
Question is how to query all documents that have userid say test2 ?
Have tried:
col.find({'toPerson.userid':'test2'})
it's return nothing. also I have tried using aggregate but found maybe it's not the right direction.
Anyone can help with this?
UPDATE 1
Just read this post
Retrieve only the queried element in an object array in MongoDB collection
but it's different
Structure different: is {field:[ [{ }], [{ }], .... ]}, not { field:[ {}, {} ] }
I want to keep all returned documents structure untouched, $unwind(make toPerson to be 1-level array) or $$PRUNE(remove some fields) will change the structure returned.
UPDATE 2
What I want is to get following result in ONE statement:
col.find({ 'toPerson.0.userid':'test2' })
+ col.find({ 'toPerson.1.userid':'test2' })
+ ... ...
Is there any precise counterpart statement of above results combined together ?
You can query nested arrays like this using two levels of $elemMatch:
db.test.find({toPerson: {$elemMatch: {$elemMatch: {userid: 'test2'}}}})
The outer $elemMatch says match an array element of toPerson where the value passes the inner array $elemMatch test of an element matching {userid: 'test'}.