MongoDB - aggregation - array of array data selection - mongodb

I'm want to create an aggregation for the following contents of a collection:
{ "_id": ObjectId("574ffe9bda461e4b4b0043ab"),
"list1": [
"_id": "54",
"list2": [
{
"lang": "EN",
"value": "val1"
},
{
"lang": "ES",
"value": "val2"
},
{
"lang": "FR",
"value": "val3"
},
{
"lang": "IT",
"value": "val3"
}
]
]
}
From this collection i want to get as Object ("id": "54", "value": "val3") the returned Object is based on condition : list1.id = "54" and list2.lang = "IT"

You can try a simple combination of $match and $unwind to traverse your nested arrays:
db.collection.aggregate([
{
$unwind: "$list1"
},
{
$match: { "list1._id": "54" }
},
{
$unwind: "$list1.list2"
},
{
$match: { "list1.list2.lang": "IT" }
},
{
$project: {
_id: "$list1._id",
val: "$list1.list2.value"
}
}
])
Mongo Playground.
If the list._id field is unique you can index it and swap first first two pipeline stages to filter out other documents before running $unwind:
db.collection.aggregate([
{
$match: { "list1._id": "54" }
},
{
$unwind: "$list1"
},
{
$unwind: "$list1.list2"
},
{
$match: { "list1.list2.lang": "IT" }
},
{
$project: {
_id: "$list1._id",
val: "$list1.list2.value"
}
}
])

Related

MongoDB Query - Get frequency map of an array

[
{
"_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

count based on nested key mongodb

How can i count based on xTag key is on doc
I tried this but it does not provide me actual count
db.collection.find({
"products.xTag": {
$exists: false
}
}).count();
when you run with $exist:true i would expect result 1
When you run with $exist:false i would expect result 3
Playground: https://mongoplayground.net/p/_gf7RzGc8oB
Structure:
[
{
"item": 1,
"products": [
{
"name": "xyz",
"xTag": 32423
},
{
"name": "abc"
}
]
},
{
"item": 2,
"products": [
{
"name": "bob",
},
{
"name": "foo"
}
]
}
]
It is not possible with find(), You can use aggregate(),
$unwind deconstruct products array
$match your condition
$count total documents
db.collection.aggregate([
{ $unwind: "$products" },
{ $match: { "products.xTag": { $exists: false } } },
{ $count: "count" }
])
Playground

Aggregation at each document level mongodb

I have a list of documents like this
[{
"_id": "5dbc95f921d7625303fe2369",
"name": "John",
"itemsPurchased": [{
"offer": "o1",
"items": ["p1"]
},{
"offer": "o1",
"items": ["p1"]
},
{
"offer": "o1",
"items": ["p2"]
},
{
"offer": "o2",
"items": ["p1"]
}, {
"offer": "o7",
"items": ["p1"]
}
]
},
{
"_id": "zbc95f921d7625303fe2363",
"name": "Doe",
"itemsPurchased": [{
"offer": "o1",
"items": ["p11"]
},{
"offer": "o1",
"items": ["p11"]
},
{
"offer": "o2",
"items": ["p13"]
},
{
"offer": "o1",
"items": ["p22"]
},
{
"offer": "o2",
"items": ["p11"]
}, {
"offer": "o3",
"items": ["p11"]
}
]
}
]
And i am trying to compute unique offers on unique products by each customer, expecting the resultant to be like:
[
{
"_id": "5dbc95f921d7625303fe2369",
"name": "John",
"offersAndProducts": {
"o1":2,
"o2":2,
"o3":1
},
{
"_id": "zbc95f921d7625303fe2363",
"name": "Doe",
"offersAndProducts": {
"o1":2,
"o2":1,
"o7":1
}
]
I want to apply aggregations per document, After performing $unwind on itemsPurchased, applied $group on items and then on offer to eliminate the duplication:
{
"$group" : {
"_id" : {
"item" : {
"$arrayElemAt" : [
"$itemsPurchased.item",
0.0
]
},
"count" : {
"$sum" : 1.0
},
"offer" : "$itemsPurchased.offer"
}
}
}
then,
{
"$group" : {
"_id" : "$_id.offer",
"count" : {
"$sum" : 1.0
}
}
}
this gives the array of products and offers for all documents:
[
{o1:4,o2:3,o3:1,o7:1}
]
But i need it at document level.
tried $addFeild, but $unwind and $match operators gives invalid error.
Any other way of achieving this?
Generally speaking, it's an anti-pattern to $unwind an array and then to $group on the original _id since most operations can be done on the array directly, in a single stage. Here is what such a stage would look like:
{$addFields:{
offers:{$arrayToObject:{
$map:{
input:{$setUnion:"$itemsPurchased.offer"},
as:"o",
in:[
"$$o",
{$size:{$setUnion:{$let:{
vars:{items:{$filter:{
input:"$itemsPurchased",
cond:{$eq:["$$this.offer","$$o"]}
}}},
in:{$reduce:{
input:"$$items",
initialValue:[],
in:{$concatArrays:["$$value","$$items.items"]}
}}
}}}
}]
}
}}
}}
What this does is create an array where each element is a two element array (which is a syntax that $arrayToObject can convert to an object where first element is key name and second is value) and the input is a unique set of offers and for each we accumulate an array of products, get rid of duplicates (with $setUnion) and then get the size of the result. What this produces on your input is this:
"offers" : {
"o1" : 2,
"o2" : 2,
"o3" : 1
}
You need to run $unwind and $group twice. To count only unique items you can use $addToSet. To build your keys dynamically you need to use $arrayToObject:
db.collection.aggregate([
{
$unwind: "$itemsPurchased"
},
{
$unwind: "$itemsPurchased.items"
},
{
$group: {
_id: {
_id: "$_id",
offer: "$itemsPurchased.offer"
},
name: { $first: "$name" },
items: { $addToSet: "$itemsPurchased.items" }
}
},
{
$group: {
_id: "$_id._id",
name: { $first: "$name" },
offersAndProducts: { $push: { k: "$_id.offer", v: { $size: "$items" } } }
}
},
{
$project: {
_id: 1,
name: 1,
offersAndProducts: { $arrayToObject: "$offersAndProducts" }
}
}
])
Mongo Playground

Aggregation on complex objects

I have a collection with documents like the following:
{
"towers": [
{
"name": "foo",
"towers": [
{
"name": "A",
"buildType": "Apartament"
},
{
"name": "B",
"buildType": "Apartament"
}
]
},
{
"name": "xpto",
"towers": [
{
"name": "C",
"buildType": "House"
},
{
"name": "D",
"buildType": "Office"
}
]
}
]
}
All I need to know is what are all the possible values for "buildType", like:
Apartment
House
Office
It's a complex object and the data to aggregate is deep inside it. Is there any way to achieve the results I want?
You need to $unwind the two nested array that is "towers" and "towers.towers" and then use $group with "towers.towers.buildType" field to get the distinct values
db.collection.aggregate([
{ "$unwind": "$towers" },
{ "$unwind": "$towers.towers" },
{ "$group": {
"_id": "$towers.towers.buildType"
}}
])
Output
[
{
"_id": "Office"
},
{
"_id": "House"
},
{
"_id": "Apartament"
}
]
db.collection.aggregate(
// Pipeline
[
// Stage 1
{
$unwind: {
path: "$towers",
}
},
// Stage 2
{
$unwind: {
path: "$towers.towers",
}
},
// Stage 3
{
$group: {
_id: '$_id',
buildType: {
$addToSet: '$towers.towers.buildType'
}
}
},
]
);

Distinct array element with condition

My documents look like this:
{
"_id": "1",
"tags": [
{ "code": "01-01", "type": "machine" },
{ "code": "04-06", "type": "gearbox" },
{ "code": "07-01", "type": "machine" }
]
},
{
"_id": "2",
"tags": [
{ "code": "03-04","type": "gearbox" },
{ "code": "01-01", "type": "machine" },
{ "code": "04-11", "type": "machine" }
]
}
I want to get distinct codes only for tags whose type is "machine". so, for the example above, the result should be ["01-01", "07-01", "04-11"].
How do I do this?
Using $unwind and then $group with the tag as the key will give you each tag in a separate document in your result set:
db.collection_name.aggregate([
{
$unwind: "$tags"
},
{
$match: {
"tags.type": "machine"
}
},
{
$group: {
_id: "$tags.code"
}
},
{
$project:{
_id:false
code: "$_id"
}
}
]);
Or, if you want them put into an array within a single document, you can use $push within a second $group stage:
db.collection_name.aggregate([
{
$unwind: "$tags"
},
{
$match: {
"tags.type": "machine"
}
},
{
$group: {
_id: "$tags.code"
}
},
{
$group:{
_id: null,
codes: {$push: "$_id"}
}
}
]);
Another user suggested including an initial stage of { $match: { "tags.type": "machine" } }. This is a good idea if your data is likely to contain a significant number of documents that do not include "machine" tags. That way you will eliminate unnecessary processing of those documents. Your pipeline would look like this:
db.collection_name.aggregate([
{
$match: {
"tags.type": "machine"
}
},
{
$unwind: "$tags"
},
{
$match: {
"tags.type": "machine"
}
},
{
$group: {
_id: "$tags.code"
}
},
{
$group:{
_id: null,
codes: {$push: "$_id"}
}
}
]);
> db.foo.aggregate( [
... { $unwind : "$tags" },
... { $match : { "tags.type" : "machine" } },
... { $group : { "_id" : "$tags.code" } },
... { $group : { _id : null , "codes" : {$push : "$_id"} }}
... ] )
{ "_id" : null, "codes" : [ "04-11", "07-01", "01-01" ] }
A better way would be to group directly on tags.type and use addToSet on tags.code.
Here's how we can achieve the same output in 3 stages of aggregation :
db.name.aggregate([
{$unwind:"$tags"},
{$match:{"tags.type":"machine"}},
{$group:{_id:"$tags.type","codes":{$addToSet:"$tags.code"}}}
])
Output : { "_id" : "machine", "codes" : [ "04-11", "07-01", "01-01" ] }
Also, if you wish to filter out tag.type codes, we just need to replace "machine" in match stage with desired tag.type.