MongoDB Query - Get frequency map of an array - mongodb

[
{
"_id": ObjectId("id-1"),
"tests": [
{
"category": "cat1",
"status": "status1",
},
{
"category": "cat1",
"status": "status2",
},
{
"category": "cat2",
"status": "status2",
},
],
},
{
"_id": ObjectId("id-2"),
"tests": [
{
"category": "cat2",
"status": "status1",
},
{
"category": "cat1",
"status": "status1",
},
{
"category": "cat1",
"status": "status2",
},
],
}
]
I have the above collection, my intention is to generate the below result. Please note that the statuses and categories are dynamic.
[
{
"id" : id-1,
"status": {
"status1": count,
"status2": count
},
"category": {
"cat1": count of it,
"cat2": count of it
}
},
{
"id" : id-2,
"status": {
"status1": count of it,
"status2": count of it
},
"category": {
"cat1": count of it,
"cat2": count of it
}
}
]
What I've attempted to do till now, is
Unwinded tests field, then
{
"$group": {
"_id": {
"id": "$_id",
"testStatus": "$tests.status"
},
"val": {
"$sum": 1
}
}
},
{
"$group": {
"_id": {
"id": "$_id.id",
},
"resGroup": {
"$addToSet": {
k: "$_id.testStatus",
v: "$val"
}
}
}
},
{
"$project": {
"_id": "$_id.id",
"statusGroup": {
"$arrayToObject": "$resGroup"
}
}
}
I've done the same for the category field and used $facet to run multiple aggregations.
But, am unable to fetch the result in the required format.
Any help on this will be appreciated.
Thanks
MongoDB Version: 3.4

$map to iterate loop of tests array and convert the object to an array using $objectToArray
$unwind deconstruct tests array
$unwind again deconstruct tests array because it's a nested array
$group by _id, k, and v and get the total count
$group by _id and k and construct the array of the status field in items
$arrayToObject convert items key-value array to an object
$group by _id and construct the array of items
$arrayToObject convert items array to object
db.collection.aggregate([
{
$project: {
tests: {
$map: {
input: "$tests",
in: { $objectToArray: "$$this" }
}
}
}
},
{ $unwind: "$tests" },
{ $unwind: "$tests" },
{
$group: {
_id: {
_id: "$_id",
k: "$tests.k",
v: "$tests.v"
},
count: { $sum: 1 }
}
},
{
$group: {
_id: {
_id: "$_id._id",
k: "$_id.k"
},
items: {
$push: {
k: "$_id.v",
v: "$count"
}
}
}
},
{
$group: {
_id: "$_id._id",
items: {
$push: {
k: "$_id.k",
v: { $arrayToObject: "$items" }
}
}
}
},
{ $project: { items: { $arrayToObject: "$items" } } }
])
Playground

Related

return specific fields from grouped docs

I'm using $group to return the docs based on their level.it is returning a fields array with their docs which are working fine.
I'm trying to return certain fields of the single doc.
current result :
"master": [
{
"_id": "63099a4c00b3c49e75e642e6",
"level": "master",
"uid": "l7AafULWLtP5mpcWKihdo",
"createdAt": "2022-08-27T04:15:08.905Z",
"updatedAt": "2022-08-27T04:15:08.905Z",
"slug": "text-2",
"__v": 0
}
],
Expected result :
"master": [
{
"uid": "l7AafULWLtP5mpcWKihdo",
"slug": "text-2",
}
],
$group:
{
$group: {
_id: "$level",
fields: { $push: "$$ROOT" }
}
},
{
$group: {
_id: null,
fields: {
$push: {
k: "$_id",
v: "$fields"
}
}
}
},
{
$replaceWith: { $arrayToObject: "$fields" }
},
{
$sort: { createdAt: -1 },
},
is this possible to achieve? I'm new to MongoDB any help is appreciated

Lodash `countBy` equivalent in MongoDB?

Let's say I have the input docs below:
[
{
"_id": "6225ca4052e7c226e2dd836d",
"data": [
"07",
"07",
"12",
"19",
"07",
"32"
]
},
{
"_id": "6225ca4052e7c226e2dd888f",
"data": [
"99",
"97",
"52",
"99",
"58",
"92"
]
}
]
I want to count the occurrences of every element in data string array per document. In JS, I can use countBy. How can I achieve the same using MongoDB Aggregation Framework?
I have tried to $reduce but MongoDB seems to not support assigning dynamic field to object.
{
$reduce: {
input: '$data',
initialValue: {},
in: { // assign `$$this` with count to `$$value`, but failed! }
}
}
Below is the desired output.
[
{
"_id": "6225ca4052e7c226e2dd836d",
"freqs": {
"12": 1,
"19": 1,
"32": 1,
"07": 3
}
},
{
"_id": "6225ca4052e7c226e2dd888f",
"freqs": {
"52": 1,
"58": 1,
"92": 1,
"97": 1,
"99": 2
}
}
]
db.collection.aggregate([
{
$match: {}
},
{
$unwind: "$data"
},
{
$group: {
_id: "$data",
c: { $sum: 1 },
id: { $first: "$_id" }
}
},
{
$group: {
_id: "$id",
data: { $push: { k: "$_id", v: "$c" } }
}
},
{
$set: {
data: { $arrayToObject: "$data" }
}
}
])
mongoplayground
db.collection.aggregate([
{
$set: {
data: {
$function: {
body: "function(d) {let obj = {}; d.forEach(e => {if(obj[e]==null) { obj[e]=1; }else{ obj[e]++; }}); return obj;}",
args: [
"$data"
],
lang: "js"
}
}
}
}
])
mongoplayground

MongoDB aggregate: count of elements in an object key

I have a MongoDB document like this:
[
{
"_id": {
"$oid": "5ff09030cd55d6d9f378d460"
},
"username": "a value",
"userid": "123456",
"last_access_ts": 1612426253,
"last_access": "2021-2-4 9:10:53",
"anotherid": 12345678910,
"verified_date": "2021-1-2 16:24:32",
"verified_ts": 1609601072,
"group_users": {
"-1001159747589": [
{
"anotherid": 12345678910,
"userid": "123456"
}
],
"-1001143137644": [
{
"anotherid": 12345678910,
"userid": "123456"
}
],
"-1001368608972": [
{
"anotherid": 12345678910,
"userid": "123456"
}
]
},
"registered_access": "2021-1-2 16:24:42",
}
]
I've two questions.
First one: I need to count the elements inside each group_users[key] object, and I'm stuck with this aggregate:
db.collection.aggregate([
{
$match: {
username: "a value"
}
},
{
$project: {
_id: 1,
userid: 1,
"groups": {
"$objectToArray": "$group_users"
}
}
},
{
$unwind: "$groups",
},
])
This aggregate gives me this result:
[
{
"_id": ObjectId("5ff09030cd55d6d9f378d460"),
"groups": {
"k": "-1001449720492",
"v": [
{
"anotherid": 12345678910,
"userid": "123456"
}
]
},
"userid": "123456"
},
{
"_id": ObjectId("5ff09030cd55d6d9f378d460"),
"groups": {
"k": "-1001159747589",
"v": [
{
"anotherid": 12345678910,
"userid": "123456"
}
]
},
"userid": "123456"
},
{
"_id": ObjectId("5ff09030cd55d6d9f378d460"),
"groups": {
"k": "-1001143137644",
"v": [
{
"anotherid": 12345678910,
"userid": "123456"
}
]
},
"userid": "123456"
}
]
How can I count each single groups[v] and then re-group the data? I would like to have a result like:
{
... some user data
"groups": {
"group_key": "count",
"second_group_key": "count",
"third_group_key": "count"
}
}
Is it possible with aggregate or I need to loop in the code?
My second question is always about the group_users. Is possible to have, recursively, the user data inside a group_users object?
I mean, every object inside group_users is an array of users; from this array can I have the user data (maybe with $graphLookup?) using the userid field or the anotherid field?
As a result from this second aggregate I would like to have something like this:
{
... some user data
"groups": {
"group_key": [{"userid": userid, "username": username}],
"second_group_key": [{"userid": userid, "username": username}],
"third_group_key": [{"userid": userid, "username": username}]
}
}
Obviously I can limit this "recursion" to 10 elements per time.
Thanks for any advice.
$objectToArray convert group_users object to array
$let to Bind variable group_arr for use in the specified expression, and returns the result of the expression.
$map to iterate loop of bind variable group_arr, get size of total element of v and return k and v,
$arrayToObject convert returned array from $map to object
db.collection.aggregate([
{ $match: { username: "a value" } },
{
$project: {
_id: 1,
userid: 1,
groups: {
$let: {
vars: { group_arr: { $objectToArray: "$group_users" } },
in: {
$arrayToObject: {
$map: {
input: "$$group_arr",
in: {
k: "$$this.k",
v: { $size: "$$this.v" }
}
}
}
}
}
}
}
}
])
Playground
Second question,
$unwind deconstruct groups array
$lookup with pipeline, match anotherid $in condition and return required fields
$group by _id and reconstruct groups array
$arrayToObject convert groups array to object
db.collection.aggregate([
{ $match: { username: "a value" } },
{
$project: {
_id: 1,
userid: 1,
groups: { $objectToArray: "$group_users" }
}
},
{ $unwind: "$groups" },
{
$lookup: {
from: "collection",
let: { anotherid: "$groups.v.anotherid" },
pipeline: [
{ $match: { $expr: { $in: ["$anotherid", "$$anotherid"] } } },
{
$project: {
_id: 0,
userid: 1,
username: 1
}
}
],
as: "groups.v"
}
},
{
$group: {
_id: "$_id",
groups: { $push: "$groups" },
userid: { $first: "$userid" }
}
},
{ $addFields: { groups: { $arrayToObject: "$groups" } } }
])
Playground

Mongo add new key-value pair in $group stage

I have a collection of documents like:
{
"tagName": "Tag1",
"value": 1,
"ts": "2020/01/01 20:00"
},
{
"tagName": "Tag2",
"value": 2,
"ts": "2020/01/01 20:00"
},
{
"tagName": "Tag3",
"value": 3,
"ts": "2020/02/01 20:00"
}
I'd like to group the documents by ts and dictionarized the tagName and value.
so the intention output would be:
[
{
"Tag1": 1
"Tag2": 2,
"ts": "2020/01/01 20:00"
},
{
"Tag3": 3,
"ts": "2020/02/01 20:00"
}
]
I've tried the following pipeline:
{
$group: {
'_id': '$ts',
'output': {
$push: {
'$t': '$v'
}
}
}
}
Thanks for your help!
When $group by ts, you should add tagName and value to an array -> convert it back to tagName: value by $arrayToObject then merge it back to root to get your expected result.
Playground
db.collection.aggregate([
{
$group: {
_id: "$ts",
"tags": {
"$push": {
"k": "$tagName",
"v": "$value"
}
}
}
},
{
$project: {
"ts": "$_id",
"tagsObj": {
"$arrayToObject": "$tags"
}
}
},
{
$replaceRoot: {
newRoot: {
$mergeObjects: [
{
_id: "$_id",
},
"$tagsObj"
]
}
}
}
])

Perform union in mongoDB

I'm wondering how to perform a kind of union in an aggregate in MongoDB. Let's imaging the following document in a collection (the structure is for the sake of the example) :
{
linkedIn: {
people : [
{
name : 'Fred'
},
{
name : 'Matilda'
}
]
},
twitter: {
people : [
{
name : 'Hanna'
},
{
name : 'Walter'
}
]
}
}
How to make an aggregate that returns the union of the people in twitter and linkedIn ?
{
{ name :'Fred', source : 'LinkedIn'},
{ name :'Matilda', source : 'LinkedIn'},
{ name :'Hanna', source : 'Twitter'},
{ name :'Walter', source : 'Twitter'},
}
There are a couple of approaches to this that you can use the aggregate method for
db.collection.aggregate([
// Assign an array of constants to each document
{ "$project": {
"linkedIn": 1,
"twitter": 1,
"source": { "$cond": [1, ["linkedIn", "twitter"],0 ] }
}},
// Unwind the array
{ "$unwind": "$source" },
// Conditionally push the fields based on the matching constant
{ "$group": {
"_id": "$_id",
"data": { "$push": {
"$cond": [
{ "$eq": [ "$source", "linkedIn" ] },
{ "source": "$source", "people": "$linkedIn.people" },
{ "source": "$source", "people": "$twitter.people" }
]
}}
}},
// Unwind that array
{ "$unwind": "$data" },
// Unwind the underlying people array
{ "$unwind": "$data.people" },
// Project the required fields
{ "$project": {
"_id": 0,
"name": "$data.people.name",
"source": "$data.source"
}}
])
Or with a different approach using some operators from MongoDB 2.6:
db.people.aggregate([
// Unwind the "linkedIn" people
{ "$unwind": "$linkedIn.people" },
// Tag their source and re-group the array
{ "$group": {
"_id": "$_id",
"linkedIn": { "$push": {
"name": "$linkedIn.people.name",
"source": { "$literal": "linkedIn" }
}},
"twitter": { "$first": "$twitter" }
}},
// Unwind the "twitter" people
{ "$unwind": "$twitter.people" },
// Tag their source and re-group the array
{ "$group": {
"_id": "$_id",
"linkedIn": { "$first": "$linkedIn" },
"twitter": { "$push": {
"name": "$twitter.people.name",
"source": { "$literal": "twitter" }
}}
}},
// Merge the sets with "$setUnion"
{ "$project": {
"data": { "$setUnion": [ "$twitter", "$linkedIn" ] }
}},
// Unwind the union array
{ "$unwind": "$data" },
// Project the fields
{ "$project": {
"_id": 0,
"name": "$data.name",
"source": "$data.source"
}}
])
And of course if you simply did not care what the source was:
db.collection.aggregate([
// Union the two arrays
{ "$project": {
"data": { "$setUnion": [
"$linkedIn.people",
"$twitter.people"
]}
}},
// Unwind the union array
{ "$unwind": "$data" },
// Project the fields
{ "$project": {
"_id": 0,
"name": "$data.name",
}}
])
Not sure if using aggregate is recommended over a map-reduce for that kind of operation but the following is doing what you're asking for (dunno if $const can be used with no issue at all in the .aggregate() function) :
aggregate([
{ $project: { linkedIn: '$linkedIn', twitter: '$twitter', idx: { $const: [0,1] }}},
{ $unwind: '$idx' },
{ $group: { _id : '$_id', data: { $push: { $cond:[ {$eq:['$idx', 0]}, { source: {$const: 'LinkedIn'}, people: '$linkedIn.people' } , { source: {$const: 'Twitter'}, people: '$twitter.people' } ] }}}},
{ $unwind: '$data'},
{ $unwind: '$data.people'},
{ $project: { _id: 0, name: '$data.people.name', source: '$data.source' }}
])