Add Aggregate field in MongoDB pipeline depending on all elements of an array - mongodb

Given the following documents in a collection:
[{
"_id": {
"$oid": "63f06283b80a395adf27780d"
},
"suppliers": [
{
"name": "S1",
"duesPaid": true
},
{
"name": "S2",
"duesPaid": true
}
]
},{
"_id": {
"$oid": "63f06283b80a395adf27780e"
},
"suppliers": [
{
"name": "S1",
"duesPaid": true
},
{
"name": "S2",
"duesPaid": false
}
]
}]
I would like to create an aggregateField in each document that does the following: If the suppliers array has at least 1 element and every element in that has the duesPaid field == true, then add a field to the document suppliersPaid = true. Otherwise add suppliersPaid = false. The resulting documents from the pipeline should look like this:
[{
"_id": {
"$oid": "63f06283b80a395adf27780d"
},
"suppliers": [
{
"name": "S1",
"duesPaid": true
},
{
"name": "S2",
"duesPaid": true
}
],
"suppliersPaid": true,
},{
"_id": {
"$oid": "63f06283b80a395adf27780e"
},
"suppliers": [
{
"name": "S1",
"duesPaid": true
},
{
"name": "S2",
"duesPaid": false
}
],
"suppliersPaid": false,
}]
I have tried the following pipeline:
[{$addFields: {
suppliersPaid: {
$and: [
{ $gte: [{ $size: "$suppliers" }, 1] },
{
suppliers: {
$not: {
$elemMatch: { duesPaid: false },
},
},
},
],
},
}}]
and I get the following error: Invalid $addFields :: caused by :: Unrecognized expression '$elemMatch'
I've tried to eliminate the reliance on $elemMatch per the docs https://www.mongodb.com/docs/manual/reference/operator/query/elemMatch/#single-query-condition as such:
[{$addFields: {
suppliersPaid: {
$and: [
{ $gte: [{ $size: "$suppliers" }, 1] },
{
suppliers: {
$not: {
duesPaid: false
},
},
},
],
},
}}]
But this yields the incorrect result of setting suppliersPaid to true for both documents, which is incorrect.
Note: I would like to avoid using any sort of JS in this code i.e. no $where operators.

For the second condition:
$eq - Compare the result from 1.1 to return an empty array.
1.1. $filter - Filter the documents from suppliers containing { duesPaid: false }.
db.collection.aggregate([
{
$addFields: {
suppliersPaid: {
$and: [
{
$gte: [
{
$size: "$suppliers"
},
1
]
},
{
$eq: [
{
$filter: {
input: "$suppliers",
cond: {
$eq: [
"$$this.duesPaid",
false
]
}
}
},
[]
]
}
]
}
}
}
])
Demo # Mongo Playground

Related

Filter documents that have id in another collection in MongoDB with aggregation framework

So I have two collection. collectionA and collectionB
collection A has following documents
db={
"collectiona": [
{
"_id": "6173ddf33ed09368a094e68a",
"title": "a"
},
{
"_id": "61wefdf33ed09368a094e6dc",
"title": "b"
},
{
"_id": "61wefdfewf09368a094ezzz",
"title": "c"
},
],
"collectionb": [
{
"_id": "6173ddf33ed0wef368a094zq",
"collectionaID": "6173ddf33ed09368a094e68a",
"data": [
{
"userID": "123",
"visibility": false,
"response": false
},
{
"userID": "2345",
"visibility": true,
"response": true
}
]
},
{
"_id": "6173ddf33ed09368awef4e68g",
"collectionaID": "61wefdf33ed09368a094e6dc",
"data": [
{
"userID": "5678",
"visibility": false,
"response": false
},
{
"userID": "674",
"visibility": true,
"response": false
}
]
}
]
}
So What I need is documents from collection A which has response false in collection B
and document should be sorted by first the ones that have visibility false and then the ones that have visibility true
for eg. userID : 123 should get 3 documents
{
"_id": "6173ddf33ed09368a094e68a",
"title": "a"
},
{
"_id": "61wefdf33ed09368a094e6dc",
"title": "b"
},
{
"_id": "61wefdfewf09368a094ezzz",
"title": "c"
},
whereas userID 2345 should get two
{
"_id": "61wefdf33ed09368a094e6dc",
"title": "b"
},
{
"_id": "61wefdfewf09368a094ezzz",
"title": "c"
},
User 674 will receive 3 objects from collection A but second would be in the last as it has visibility true for that document
{
"_id": "6173ddf33ed09368a094e68a",
"title": "a"
},
{
"_id": "61wefdfewf09368a094ezzz",
"title": "c"
},
{
"_id": "61wefdf33ed09368a094e6dc",
"title": "b"
},
MongoDB Playground link : https://mongoplayground.net/p/3rLry0FPlw-
Really appreciate the help. Thanks
You can start from collectionA:
$lookup the collectionB for the record related to the user specified
filter out collectionB documents according to response
assign a helper sortrank field based on the visibility and whether collectionaID is a match
$sort according to sortrank
wrangle back to the raw collection A
db.collectiona.aggregate([
{
"$lookup": {
"from": "collectionb",
let: {
aid: "$_id"
},
"pipeline": [
{
$unwind: "$data"
},
{
$match: {
$expr: {
$and: [
{
$eq: [
"$data.userID",
"2345"
]
},
{
$eq: [
"$collectionaID",
"$$aid"
]
}
]
}
}
}
],
"as": "collB"
}
},
{
$match: {
"collB.data.response": {
$ne: true
}
}
},
{
"$unwind": {
path: "$collB",
preserveNullAndEmptyArrays: true
}
},
{
"$addFields": {
"sortrank": {
"$cond": {
"if": {
$eq: [
"$collB.data.visibility",
false
]
},
"then": 1,
"else": {
"$cond": {
"if": {
$eq: [
"$collB.collectionaID",
"$_id"
]
},
"then": 3,
"else": 2
}
}
}
}
}
},
{
$sort: {
sortrank: 1
}
},
{
$project: {
collB: false,
sortrank: false
}
}
])
Here is the Mongo playground for your reference.

Mongo $cond if expression doesn't work like $match

I have a collection with documents with a "parent" field.
[
{
"parent": "P1",
"tagGroups": [],
},
{
"parent": "P1",
"tagGroups": [
{
group: 1,
tags: {
tag1: {
value: true
},
tag2: {
value: "foo"
},
}
},
{
group: 2,
tags: {}
}
]
},
{
"parent": "P2",
"tagGroups": [],
}
]
I want to make request that retrieves all documents with the same parent when at least one match with my criteria: tag1.value = true.
Expected:
[
{
"parent": "P1",
"tagGroups": [],
},
{
"parent": "P1",
"tagGroups": [
{
group: 1,
tags: {
tag1: {
value: true
},
tag2: {
value: "foo"
},
}
},
{
group: 2,
tags: {}
}
]
}
]
For that I wanted to use the $cond to flag every document, then group by parent.
https://mongoplayground.net/p/WiIlVeLDrY-
But the "if" part seems to work differently that a $match
https://mongoplayground.net/p/_jcoUHE-aOu
Do you have another efficient way to do that kind of query?
Edit: I can use a lookup stage but I'm afraid of bad performances
Thanks
You haven't mentioned what you want to achieve, but you expect that your tried code (first link) should be working. You need to use $in instead of $eq in your query
db.collection.aggregate({
"$addFields": {
"match": {
"$cond": [
{ $in: [ true, "$tagGroups.tags.tag1.value" ] }, 1, 0] }
}
},
{
"$group": {
"_id": "$parent",
"elements": { "$addToSet": "$$ROOT" },
"elementsMatch": { "$sum": "$match" }
}
},
{ "$match": { "elementsMatch": { $gt: 0 } }},
{ "$unwind": "$elements"}
)
Working Mongo playground
Note : You have asked about the efficient way. Better you need to post expected result

Filter result in mongo sub-sub array

I have some collections such us this:
[{
"_id": ObjectId("604f3ae3194f2135b0ade569"),
"parameters": [
{
"_id": ObjectId("602b7455f4b4bf5b41662ec1"),
"name": "Purpose",
"options": [
{
"id": ObjectId("602b764ff4b4bf5b41662ec2"),
"name": "debug",
"sel": false
},
{
"id": ObjectId("602b767df4b4bf5b41662ec3"),
"name": "performance",
"sel": false
},
{
"id": ObjectId("602b764ff4b4bf5b41662ec4"),
"name": "security",
"sel": false
},
{
"id": ObjectId("602b767df4b4bf5b41662ec5"),
"name": "Not Applicable",
"sel": false
}
],
"type": "multiple"
},
{
"_id": ObjectId("602b79d35d4a1333b8b6e5ba"),
"name": "Organization",
"options": [
{
"id": ObjectId("602b79d353c89933b8238325"),
"name": "SW",
"sel": false
},
{
"id": ObjectId("602b79d353c89933b8238326"),
"name": "HW",
"sel": false
}
],
"type": "multiple"
}
]
}]
The parameters are most 30.
I need to implements in mongo a "filter" collections.
If I filter one or more parameters._id, mongo return:
collection _id that have match options.sel of this parameters._id
collection _id that have all options.sel equal to false of this parameters._id
non return collection _id if parameters._id has set up options.name:"Not Applicable" at value options.sel:true
For example, if I match parameters._id:ObjectId("602b7455f4b4bf5b41662ec1") and this parameters.options.id:ObjectId("602b764ff4b4bf5b41662ec2"), I expect:
not collection _id that has, for parameters._id:ObjectId("602b7455f4b4bf5b41662ec1"), the specific parameters.options.id: ObjectId("602b767df4b4bf5b41662ec5") at value options.sel:true
all collection _id that match with query
all collection _id that has, for parameters._id:ObjectId("602b7455f4b4bf5b41662ec1"), all specific parameters.options.sel:false
Next I need to make this rule for more parameters.
I have think to implements three aggregation for every rule...
Do you have suggestion?
Try this query:
db.testCollection.aggregate([
{ $unwind: "$parameters" },
{
$match: {
"parameters._id": ObjectId("602b7455f4b4bf5b41662ec1"),
"parameters.options.id": ObjectId("602b767df4b4bf5b41662ec5")
}
},
{
$addFields: {
options: {
$filter: {
input: "$parameters.options",
as: "option",
cond: {
$and: [
{ $ne: ["$$option.sel", true] },
{ $ne: ["$$option.name", "Not Applicable"] }
]
}
}
}
}
}
])
With idea of #dheemanth-bath,
I have make this query:
db.testCollection.aggregate([
{ $unwind: "$parameters" },
{
$match: {
"parameters._id": ObjectId("602b7455f4b4bf5b41662ec1"),
}
},
{
$addFields: {
match: {
$filter: {
input: "$parameters.options",
as: "option",
cond: {
$and: [
{ $eq: ["$$option.id", ObjectId('602b767df4b4bf5b41662ec5')] },
{ $eq: ["$$option.sel", true] }
]
}
}
},
notDeclared: {
$filter: {
input: "$parameters.options",
as: "option",
cond: {
$and: [
{ $eq: ["$$option.name", "Not Applicable"]},
{ $eq: ["$$option.sel", true] }
]
}
}
}
}
}
])
The idea is that: after query count number of element of notDeclared. If > 0 waste the collection. Otherwise evaluate number of match, if is >0, there is at least one elements can match.
Good. But how I evaluate if all elements of options.sel are false?
And if I check another parameters, I need to make another aggregation (one for parameters)?

mongo $filter and multiple condition in sub-sub array

I have this portion of collections:
[{
"_id": ObjectId("604f3ae3194f2135b0ade569"),
"parameters": [
{
"_id": ObjectId("602b7455f4b4bf5b41662ec1"),
"name": "Purpose",
"options": [
{
"id": ObjectId("602b764ff4b4bf5b41662ec2"),
"name": "debug",
"sel": true
},
{
"id": ObjectId("602b767df4b4bf5b41662ec3"),
"name": "performance",
"sel": false
},
{
"id": ObjectId("602b764ff4b4bf5b41662ec4"),
"name": "security",
"sel": true
},
{
"id": ObjectId("602b767df4b4bf5b41662ec5"),
"name": "Not Applicable",
"sel": false
}
],
"type": "multiple"
}]
}]
And this query:
db.testCollection.aggregate([
{ $unwind: "$parameters" },
{
$match: {
"parameters._id": ObjectId("602b7455f4b4bf5b41662ec1"),
}
},
{
$addFields: {
match: {
$filter: {
input: "$parameters.options",
as: "option",
cond: {
$and: [
{ $eq: ["$$option.id", ObjectId('602b764ff4b4bf5b41662ec2')] },
{ $eq: ["$$option.sel", true] }
]
}
}
}
}
}
])
The result is a new array match with, in this case,
"match": [
{
"id": ObjectId("602b764ff4b4bf5b41662ec2"),
"name": "debug",
"sel": true
}
]
How I can match, for collections, match for example
"id": ObjectId("602b764ff4b4bf5b41662ec2") and
"id": ObjectId("602b764ff4b4bf5b41662ec4")
and have a result array with or two results or empty?
I am not sure it is possible in single condition inside $filter, but you can try with $let,
$let to declare a variable called filtered and store your filtered result
$in to check condition if filtered element size is 2 then return filtered result otherwise blank array
You have to put the number that you are matching number of ids in filter in $eq condition
{
$addFields: {
match: {
$let: {
vars: {
filtered: {
$filter: {
input: "$parameters.options",
as: "option",
cond: {
$and: [
{
$in: [
"$$option.id",
[ObjectId("602b764ff4b4bf5b41662ec2"), ObjectId("602b767df4b4bf5b41662ec3")]
]
},
{ $eq: ["$$option.sel", true] }
]
}
}
}
},
in: {
$cond: [
{ $eq: [{ $size: "$$filtered" }, 2] },
"$$filtered",
[]
]
}
}
}
}
}
Playground

Nested array query does not work with Mongoose

I have a mongo documnet of
{
"locationId": "5f2084f756dc2428e96fcda4",
"information": [{
"category": ["5e5150c6c52a3904b74d6ff7"],
"date": {
"$date": {
"$numberLong": "1601407317343"
}
},
"private": true
}, {
"category": ["5e5150c6c52a3904b74d6ff7"],
"date": {
"$date": {
"$numberLong": "1601407317343"
}
},
"private": false
},
}],
}
So far, what I have is to query the nested array.
const f = await info.aggregate([
{
$match: {
$and: [
{'locationId': '5f2084f756dc2428e96fcda4'},
{'information.private': true}
]
},
},
]);
I am trying to query information.private = true. However, I receive both 'private: false' and 'private: true'. The outcome is
[
{
"_id": ObjectId("5a934e000102030405000000"),
"information": [
{
"category": [
"5e5150c6c52a3904b74d6ff7"
],
"date": ISODate("2020-09-29T19:21:57.343Z"),
"private": true
},
{
"category": [
"5e5150c6c52a3904b74d6ff7"
],
"date": ISODate("2020-09-29T19:21:57.343Z"),
"private": false
}
],
"locationId": "5f2084f756dc2428e96fcda4"
}
]
I also tried with elemMatch and returns the same results. I'd looked up multiple answers on Stackoverflow but the dot notation and elemMatch do not work in this case. I know that I have done something wrong but I can't figure it out.
You can try $filter,
$match your conditions
$addFields to add field information and $filter to iterate loop of array and get match documents as per condition
const f = await info.aggregate([
{
$match: {
locationId: "5f2084f756dc2428e96fcda4",
"information.private": true
}
},
{
$addFields: {
information: {
$filter: {
input: "$information",
cond: { $eq: ["$$this.private", true] }
}
}
}
}
])
Playground
Other option you can try $redact,
$match your condition
$redact Restricts the contents of the documents based on information stored in the documents themselves.
const f = await info.aggregate([
{ $match: { locationId: "5f2084f756dc2428e96fcda4" } },
{
$redact: {
$cond: [{ $eq: ["$private", false] }, "$$PRUNE", "$$DESCEND"]
}
}
])
Playground
The find projection can accept aggregation expressions and syntax starting from MongoDB 4.4.
const f = await info.find([
locationId: "5f2084f756dc2428e96fcda4",
"information.private": true
},
{
locationId: 1,
information: {
$filter: {
input: "$information",
cond: { $eq: ["$$this.private", true] }
}
}
})
Playground