How To Count Values Inside Deeply Nested Array Of Objects in Mongodb Aggregation - mongodb

I want to sum of values inside array of objects which have another array of objects.
In my case; how can I count 'url' values in all documents inside 'urls' array under 'iocs' array;
Mongo playground: open
Here is document example;
[
{
"_id": {
"$oid": "63b4993d0625ebe8b6f5b06e"
},
"iocs": [
{
"urls": [
{
"url": "7.1.5.2",
}
],
},
{
"urls": [
{
"url": "https://l-ink.me/GeheimeBegierde",
},
{
"url": "GeheimeBegierde.ch",
}
],
},
{
"urls": [
{
"url": "https://l-ink.me/GeheimeBegierde",
}
],
}
],
type: "2"
},
{
"_id": {
"$oid": "63b4993d0624ebe8b6f5b06e"
},
"iocs": [
{
"urls": [
{
"url": "7.1.5.2",
}
],
},
{
"urls": [
{
"url": "https://l-ink.me/GeheimeBegierde",
},
{
"url": "GeheimeBegierde.ch",
}
],
},
{
"urls": [
{
"url": "https://l-ink.me/GeheimeBegierde",
}
],
}
],
type: "3"
},
{
"_id": {
"$oid": "63b4993d0615ebe8b6f5b06e"
},
"iocs": [
{
"urls": [
{
"url": "www.google.com",
}
],
},
{
"urls": [
{
"url": "abc.xyz",
},
{
"url": "GeheimeBegierde.ch",
}
],
},
{
"urls": [
{
"url": "https://123.12",
}
],
}
],
type: "1"
}
]
expected output be like;
url: "7.1.5.2",
count:2,
types:[2,3]
url: "https://l-ink.me/GeheimeBegierde",
count:4,
types:[2,3],
url: "abc.xyz",
count:1,
types:[1],
I tried unwind iocs then project urls but can't figure out how to get this output. I think i must use group but how ? Newbie in mongodb.
Any help would be appreciated. Thanks all.
NOTE: All the answers are working. Thank you all for the contributing.

You could do something like this !
db.collection.aggregate([
{
"$unwind": "$iocs"
},
{
"$unwind": "$iocs.urls"
},
{
"$group": {
"_id": "$iocs.urls.url",
"count": {
"$sum": 1
},
"types": {
"$addToSet": "$type"
}
}
},
{
"$project": {
url: "$_id",
_id: 0,
types: 1,
count: 1
}
},
])
https://mongoplayground.net/p/hhMqh2zI_SX

Here's one way you could do it.
db.collection.aggregate([
{"$unwind": "$iocs"},
{"$unwind": "$iocs.urls"},
{
"$group": {
"_id": "$iocs.urls.url",
"count": {"$count": {}},
"types": {"$addToSet": {"$toInt": "$type"}}
}
},
{
"$set": {
"url": "$_id",
"_id": "$$REMOVE"
}
}
])
Try it on mongoplayground.net.

You can try this query:
Double $unwind to deconstruct the nested array.
Then group by url get the count using $sum nad add the types into a set (to avoid duplicates, otherwise you can use simply $push)
db.collection.aggregate([
{
"$unwind": "$iocs"
},
{
"$unwind": "$iocs.urls"
},
{
"$group": {
"_id": "$iocs.urls.url",
"count": {
"$sum": 1
},
"types": {
"$addToSet": "$type"
}
}
}
])
Example here

Since $unwind is considered an inefficient operation, another option is to use $reduce and only $unwind once:
db.collection.aggregate([
{$project: {
type: 1,
urls: {
$reduce: {
input: "$iocs",
initialValue: [],
in: {$concatArrays: ["$$value", "$$this.urls.url"]}
}
}
}},
{$unwind: "$urls"},
{$group: {
_id: "$urls",
type: {$addToSet: "$type"},
count: {$sum: 1}
}},
{$project: {url: "$_id", count: 1, type: 1}}
])
See how it works on the playground example

Related

MongoDB. Get every element from array in new field

I have a document with a nested array array_field:
{
"_id": {
"$oid": "1"
},
"id": "1",
"array_field": [
{
"data": [
{
"regions": [
{
"result": {
"item": [
"4",
"5",
"3"
]
}
},
{
"result": {
"item": [
"5"
]
}
},
{
"result": {
"item": [
"1"
]
}
}
]
}
]
}
]
}
I need add new field, new_added_field for example, with each array element from array_field.data.regions.result.item and remove array_field from document.
For example:
{
"_id": {
"$oid": "1"
},
"id": "1",
"new_added_field": [4,5,3,5,1]
}
I think i can do this with help of $unwind or $map but have difficulties and need dome hint, how i can do it with help op aggregation?
As you said,
db.collection.aggregate([
{
"$project": {
newField: {
"$map": {
"input": "$array_field",
"as": "m",
"in": "$$m.data.regions.result.item"
}
}
},
},
{ "$unwind": "$newField" },
{ "$unwind": "$newField" },
{ "$unwind": "$newField" },
{ "$unwind": "$newField" },
{
"$group": {
"_id": "$_id",
"newField": { "$push": "$newField" }
}
}
])
Working Mongo playground

Find duplicate name in MongoDB

I'm having a problem in getting the duplicate name in my mongodb to delete duplicates.
{
"users": [
{
"_id": {
"$oid": "61441890a6566a001623b8ed"
},
"name": "Jollibee",
},
{
"_id": {
"$oid": "61441890a6566a001623b8ed"
},
"name": "Jollibee",
},
{
"_id": {
"$oid": "61441890a6566a001623b8ed"
},
"name": "MCDO",
},
{
"_id": {
"$oid": "61441890a6566a001623b8ed"
},
"name": "Burger King",
},
]
}
I want to show in my output only the duplicate names. which is Jollibee.
tried this approach but it only returns me the count of all the users not the duplicated ones. I want to show 2 Jollibee only.
db.collection.aggregate([
{
"$unwind": "$users"
},
{
"$group": {
"_id": "$_id",
"count": {
"$sum": 1
}
}
},
{
"$match": {
"_id": {
"$ne": null
},
"count": {
"$gt": 1
}
}
}
])
Suppose the documents are:
[
{
"_id": {
"$oid": "6226dd742ef592186422ad1d"
},
"name": "Stack test"
},
{
"_id": {
"$oid": "6226dd7d2ef592186422ad1e"
},
"name": "Stack test"
},
{
"_id": {
"$oid": "6226dd912ef592186422ad1f"
},
"name": "Stack test 001"
}
]
Aggreagtion Query:
db.users.aggregate(
[
{
$group: {
_id: "$name",
names: {$push: "$name"}
}
}
]
)
Result:
{
_id: 'Stack test',
names: [ 'Stack test', 'Stack test' ]
},
{
_id: 'Stack test 001',
names: [ 'Stack test 001' ]
}
But a better way to do it will be
Aggregation Query:
db.users.aggregate(
[
{
$group: {
_id: "$name",
count: {$sum: 1}
}
}
]
)
Result:
{
_id: 'Stack test',
count: 2
},
{
_id: 'Stack test 001',
count: 1
}
Now, you can iterate through the count and use the name value in _id
since the $unwind step gives you same _id for all documents grouping by _id is not correct. Instead try grouping by users.name
db.collection.aggregate([
{
"$unwind": "$users"
},
{
"$group": {
"_id": "$users.name",
"count": {
"$sum": 1
}
}
},
{
"$match": {
"_id": {
"$ne": null
},
"count": {
"$gt": 1
}
}
}
])
demo

MongoDB match filters with grouping and get total count

My sample data:
{
"_id": "random_id_1",
"priority": "P1",
"owners": ["user-1", "user-2"],
},
{
"_id": "random_id_2",
"priority": "P1",
"owners": ["user-1", "user-2"],
},
{
"_id": "random_id_3",
"priority": "P2",
"owners": ["user-1", "user-2"],
},
I want to run an aggregation pipeline on the data involving match filters and grouping, also I want to limit the number of groups returned as well as the number of items in each group.
Essentially, if limit=2, limit_per_group=1, group_by=owner, priority=P1, I want the following results:
[
{
"data": [
{
"group_key": "user-1",
"total_items_in_group": 2,
"limited_items": [
{
"_id": "random_id_1",
"priority": "P1",
"owners": ["user-1", "user-2"],
},
],
},
{
"group_key": "user-2",
"total_items_in_group": 2,
"limited_items": [
{
"_id": "random_id_1",
"priority": "P1",
"owners": ["user-1", "user-2"],
},
],
},
]
},
{
"metadata": {
"total_items_matched": 2,
"total_groups": 2
}
},
]
Need some help on how to write an aggregation pipeline to get the required result.
My current query is as follows:
{
"$match": {
"priority": "P1"
}
},
{
"$facet": {
"data": [
{
$addFields: {
"group_by_owners": "$owners"
}
},
{
$unwind: "$group_by_owners"
},
{
$group: {
"_id": "$group_by_owners",
"total_items_in_group": {
$sum: 1
},
"items": {
$push: "$$ROOT"
}
}
},
{
$sort: {
"total": -1
}
},
{
$unset: "items.group_by_owners"
},
{
$project: {
"_id": 1,
"total_items_in_group": 1,
"limited_items": {
$slice: [
"$items",
1
]
}
}
},
{
"$limit": 2
}
],
"metadata": [
{
$count: "total_items_matched"
}
]
}
}
Mongo playground link
I am unable to calculate the total number of groups.
add new stage of $addfields at the end of pipeline
db.collection.aggregate([
{
"$match": {
"priority": "P1"
}
},
{
"$facet": {
"data": [
{
$addFields: {
"group_by_owners": "$owners"
}
},
{
$unwind: "$group_by_owners"
},
{
$group: {
"_id": "$group_by_owners",
"total_items_in_group": {
$sum: 1
},
"items": {
$push: "$$ROOT"
}
}
},
{
$sort: {
"total": -1
}
},
{
$unset: "items.group_by_owners"
},
{
$project: {
"_id": 0,
"group_key": "$_id",
"total_items_in_group": 1,
"limited_items": {
$slice: [
"$items",
1
]
}
}
},
{
"$limit": 2
}
],
"metadata": [
{
$count: "total_items_matched",
}
]
}
},
{
"$addFields": {
"metadata.total_groups": {
"$size": "$data"
}
}
}
])
https://mongoplayground.net/p/y5a0jvr6fxI

Paginate MongoDB aggregation response

I have a database of users that have skills. I have set up a way to find users in the database using am aggregation method included in mongoose. Depending on the search criteria I input into the aggregation, the results may be too big to actually display on my front end app. I am curious how I can paginate an aggregation query with the typical limit, page, and skip variables like you would do in a typical GET request.
Here is my aggregation query:
const foundUsers = await User.aggregate([
{
$addFields: {
matchingSkills: {
$filter: {
input: '$skills',
cond: {
$or: test,
},
},
},
requiredSkills,
},
},
{
$addFields: {
// matchingSkills: '$$REMOVE',
percentageMatch: {
$multiply: [
{ $divide: [{ $size: '$matchingSkills' }, skillSearch.length] }, // yu already know how many values you need to pass, thats' why `2`
100,
],
},
},
},
{
$addFields: {
matchingSkillsNames: {
$map: {
input: '$matchingSkills',
as: 'matchingSkill',
in: '$$matchingSkill.skill',
},
},
},
},
{
$addFields: {
missingSkills: {
$filter: {
input: '$requiredSkills',
cond: {
$not: {
$in: ['$$this', '$matchingSkillsNames'],
},
},
},
},
},
},
{
$match: { percentageMatch: { $gte: 25 } },
},
]);
Passing these skills to this aggregate function:
{
"skillSearch": [
{
"class": "skills",
"skill": "SQL",
"operator": "GT",
"yearsExperience": 6
},
{
"class": "skills",
"skill": "C",
"operator": "GT",
"yearsExperience": 1
}
]
}
Will result in a response similar to this:
{
"_id": "60184ce81e65633873d709aa",
"name": "Brad",
"email": "brad#gmail.com",
"password": "$2a$12$37v2RwaO5LhSMT8GJQSZyel.Aawn6AmlqqSOkZtopqIIXyJ0LRBfu",
"__v": 0,
"skills": [
{
"_id": "60a306ce819cde701c1934a8",
"skill": "SQL",
"yearsExperience": 8
},
{
"_id": "60a306ce819cde701c1934a9",
"skill": "C",
"yearsExperience": 5
},
{
"_id": "60a306ce819cde701c1934aa",
"skill": "PL/I",
"yearsExperience": 2
},
{
"_id": "60a306ce819cde701c1934ab",
"skill": "Awk",
"yearsExperience": 9
}
],
"matchingSkills": [
{
"_id": "60a306ce819cde701c1934a8",
"skill": "SQL",
"yearsExperience": 8
},
{
"_id": "60a306ce819cde701c1934a9",
"skill": "C",
"yearsExperience": 5
}
],
"requiredSkills": [
"SQL",
"C"
],
"percentageMatch": 100,
"matchingSkillsNames": [
"SQL",
"C"
],
"missingSkills": []
},
For the pagination, you need to pass the page and size form the front end
$sort to sort the documents,
$skip skip the documents. For eg : if you are in page two and u need 10 rows , u need to skip first 10 documents
$limit to how many documents you need to show after skip
here is the code
db.collection.aggregate([
{
$sort: {
_id: 1
}
},
{
$skip: 0 // page*size
},
{
$limit: 10 // size
}
])
Working Mongo playground
More than, the pagination requires total elements too, for that
db.collection.aggregate([
{
"$facet": {
"elements": [
{
"$group": {
"_id": null,
"count": { "$sum": 1 }
}
}
],
"data": [
{ $sort: { _id: 1 } },
{ $skip: 0 }, // page*size
{ $limit: 10 } // size
]
}
},
{ "$unwind": "$elements" },
{
"$addFields": {
"elements": "$$REMOVE",
"totalRecords": "$elements.count"
}
}
])
Working Mongo playground

MongoDB merge $facet results with corresponding items

Hello I have a collection like this
"_id" :"601bd0f4be72d839303adcd3",
"title":"Payment-1",
"initialBalance": {"$numberDecimal": "75"},
"paymentHistory":[
{"_id": "601bd1542df40f2ca8a769df","payment": {"$numberDecimal": "10"}},
{"_id": "601bd1542df40f2ca8a769de","payment": {"$numberDecimal": "20"}},
]
I want to calculate active balance (initialBalance - total of paymentHistory) for each payment.
I calculated total payments from paymentHistory for each document in collection.
this.aggregate([
{$match:{...}},
{
$facet:{
info:[
{$project:{_id:1,title:1,initialBalance:1}}
],
subPayments:[
{$unwind:"$paymentHistory"},
{$group:{_id:"$_id",total:{$sum:"$paymentHistory.payment"}}},
]
}
}
])
I get this result for above query.
"info": [
{
"_id": "601bd0f4be72d839303adcd3",
"title": "Payment-1",
"initialBalance": {"$numberDecimal": "580"},
},
...
],
"subPayments": [
{
"_id": "601bd0f4be72d839303adcd3",
"total": {"$numberDecimal": "80.75"}
},
...
]
I added following lines to aggregation.
{$facet:{...}},
{$project: {payments:{$setUnion:['$info','$subPayments']}}},
{$unwind: '$payments'},
{$replaceRoot: { newRoot: "$payments" }},
Now, I get this result
{
"_id": "601bd0f4be72d839303adcd3",
"total": {"$numberDecimal": "80.75"}
},
{
"_id": "601bd0f4be72d839303adcd3",
"title": "Payment-1",
"initialBalance": {"$numberDecimal": "580"},
},
{...}
I think if I group them via _id, then I calculate activeBalance in $project aggregation.
...
{
$group:{
_id:"$_id",
title:{$first:"$title"},
initialBalance:{$first:"$initialBalance"},
totalPayment:{$first: "$total"},
}
},
{
$set:{activeBalance:{$subtract:[{$ifNull:["$initialBalance",0]}, {$ifNull:["$totalPayment",0]}]}}
}
The problem is after $group aggregation fields return null.
{
"_id": "601bd0f4be72d839303adcd3",
"title": null,
"initialBalance": null,
"totalPayment": {
"$numberDecimal": "80.75"
},
"activeBalance": {
"$numberDecimal": "-80.75"
}
}
How can I solve this problem?
This is what #prasad_ suggested:
db.accounts.aggregate([
{
$addFields: {
activeBalance: {
$subtract: [
"$initialBalance",
{
$reduce: {
input: "$paymentHistory",
initialValue: 0,
in: { $sum: ["$$value", "$$this.payment"] }
}
}
]
}
}
}
]);