MongoDB: aggregating by property names - mongodb

I have a collection within which each document looks a bit like this:
{
"_id" : ObjectId("5b9fe6010c969210d442a377"),
"statuses" : {
"SEARCHING" : 3
},
"regions" : {
"eu-central-1" : 1,
"us-west-2": 2
}
}
I want to group and transform this into a result set like this:
eu-central-1|us-west-2
1|2
Is this possible in one step/query?

This is probably what you want:
db.collection.aggregate({
$project: {
"regions": { $objectToArray: "$regions" } // convert sub-document into an array of key-value-pairs in order to get hold of the field names
}
}, {
$unwind: "$regions" // flatten the "regions" array
}, {
$group: {
"_id": "$regions.k",
"count": { $sum: "$regions.v" } //
}
})
Alternatively, if you really want to get a pipe-delimited output here is what you'd do:
db.collection.aggregate({
$project: {
"result": {
$reduce: {
"input": { $objectToArray: "$regions" },
"initialValue": { k: "", v: "" }, // start with empty strings for both key and value
"in": {
k: { $concat: [ "$$value.k", "|", "$$this.k" ] }, // concatenate existing value with "|" followed by currently looked at value for both key and value
v: { $concat: [ "$$value.v", "|", { $substr: [ "$$this.v", 0, 1000 ] } ] } // substr is needed to convert an integer field into a string
//v: { $concat: [ "$$value.v", "|", { $toString: "$$this.v" } ] } // this works from MongoDB v4.0 onwards and looks a bit cleaner
}
}
}
}
}, {
$project: { // remove the leading "|"
"result.k": { $substr: [ "$result.k", 1, -1 ] },
"result.v": { $substr: [ "$result.v", 1, -1 ] }
}
})

Related

how to map and mergeObjects of an array of an array

I want to apply a mergeObjects and map of an Array of an Array:
I am able to complete the mapping and merging when I am one level of Array in but if the Array has another array, I am unsure how to apply another map/merge object.
{
_id: "123",
date: "1900-01-01T11:00:00.0000000",
name: "joe",
birthdayservice: [
{
"date": "1999-01-01",
"team" : [
{
"requestedDate": "1999-01-01"
},
{
"requestedDate": "1999-05-01"
}
},
{
"date": "1998-01-01",
"team" : [
{
"requestedDate": "1999-01-01"
},
{
"requestedDate": "1999-05-01"
}
}
]
}
I am able to map through the first array in birthdayservice to converte date to an ISO_Date/todate such as:
{
"$addFields": {
"birthdayservice": {
$map: {
input: "$birthdayservice", //this is an array
in: {
"$mergeObjects": [
"$$this",
{
"date": {
"$toDate": "$$this.date"
}
}
]}
}
}
}
}
but when I get into more of a nested value, I get
$mergeObjects requires object inputs, but input is an array
{
"$addFields": {
"birthdayservice": {
$map: {
input: "$birthdayservice", //this is an array
in: {
"$mergeObjects": [
"$$this",
{
"date": {
"$toDate": "$$this.date"
}
}
]}
}
}
}
},
{
"$addFields": {
"birthdayservice": {
$map: {
input: "$birthdayservice.team", //also an array
in: {
"$mergeObjects": [
"$$this",
{
"requestedDate": {
"$toDate": "$$this.requestedDate"
}
}
]}
}
}
}
}
My end goal is to relace the date string fields to a date convert
birthdayservice.date
birthdayservice.team.requestedDate
Both to ISO_Dates as below:
{
_id: "123",
date: "1900-01-01T11:00:00.0000000",
name: "joe",
birthdayservice: [
{
"date": ISO_Date("1999-01-01T11:00:00.0000000Z"),
"team" : [
{
"requestedDate": ISO_Date("1999-01-01T11:00:00.0000000Z")
},
{
"requestedDate": ISO_Date("1999-05-01T11:00:00.0000000Z")
}
},
{
"date": "1998-01-01",
"team" : [
{
"requestedDate": ISO_Date("1999-01-01T11:00:00.0000000Z")
},
{
"requestedDate": ISO_Date("1999-05-01T11:00:00.0000000Z")
}
}
]
}
Query
this converts the all date strings to dates inside birthdayservice and teams array
map birthdayservice
convert the "date"
nested map to convert the dates in the team
the way the field is changed value is by mergeObjects
(in mongodb5 we have also setField to do this)
Test code here
aggregate(
[{"$set":
{"birthdayservice":
{"$map":
{"input":"$birthdayservice",
"in":
{"$mergeObjects":
["$$bs",
{"date":{"$toDate":"$$bs.date"},
"team":
{"$map":
{"input":"$$bs.team",
"in":
{"$mergeObjects":
["$$t", {"requestedDate":
{"$toDate":"$$t.requestedDate"}}]},
"as":"t"}}}]},
"as":"bs"}}}}])

Concat int array field inside an array of object into one string field in mongodb aggregate

I would like to concat int array field values inside an array of objects into one string field after dividing them (by 10).
Heres the existing document format:
{
"no" : "2020921008981",
"date" : ISODate("2020-04-01T05:19:02.263+0000"),
"sale" : {
"soldItems" : [
{
"itemRefId" : "5b55ac7f0550de00210a3b24",
"soldPrice" : NumberInt(800),
},
{
"itemRefId" : "5b55ac7f0550de00210a3b25",
"soldPrice" : NumberInt(1000),
}
]
}
}
Expected result :
{
"no" : "2020921008981",
"date" : ISODate("2020-04-01T05:19:02.263+0000"),
"priceList" : "8.0 \n 10.0"
}
The attempt with $reduce :
priceList: {
$reduce: {
input: "$sale.soldItems.soldPrice",
initialValue: "",
in: {
$cond: [ { "$eq": [ { $toString: { $divide: [ "$$value", 10 ] } }, "" ] }, "$$this", { $concat: [ { $toString: { $divide: [ "$$value", 10 ] } }, "\n", "$$this" ] } ]
}
}
}
But end up getting "errmsg" : "$divide only supports numeric types, not string and double" error. Any idea would be appreciated.
db.case.aggregate([
{
$set: {
priceList: {
$reduce: {
input: {
$map: {
input: "$sale.soldItems.soldPrice",
in: { $toString: { $divide: ["$$this", 10] } }
}
},
initialValue: "",
in: { $concat: ["$$value", "$$this", " \n "] }
}
}
}
},
{
$project: {
_id: 0,
no: 1,
date: 1,
priceList: 1
}
}
])
Try the following aggregation query, where the idea is to:
First divide the field soldPrice by 10 or required divisor using $divide
Convert it into string and concat using $toString and $concat
the appender \n gets appended after each reduce op,remove that from the end using $rtrim
create the new field using $addFields
Query:
db.collection.aggregate([
{
$addFields: {
"itemPriceList": {
$rtrim: {
input: {
$reduce: {
input: "$salesOrder.purchaseItems",
initialValue: "",
in: {
$concat: [
"$$value",
{
$toString: {
$divide: [
"$$this.soldPrice",
10
]
}
},
"\n"
]
}
}
},
chars: "\n"
}
}
}
}
]);
Result:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"caseNumber": "2020921008981",
"itemPriceList": "80\n100",
"salesOrder": {
"purchaseItems": [
{
"itemRefId": "5b55ac7f0550de00210a3b24",
"soldPrice": 800
},
{
"itemRefId": "5b55ac7f0550de00210a3b25",
"soldPrice": 1000
}
]
},
"startTime": ISODate("2016-05-18T16:00:00Z")
}
]
Plaground Test Link

Count Both Outer and Inner embedded array in a single query

{
_id: ObjectId("5dbdacc28cffef0b94580dbd"),
"comments" : [
{
"_id" : ObjectId("5dbdacc78cffef0b94580dbf"),
"replies" : [
{
"_id" : ObjectId("5dbdacd78cffef0b94580dc0")
},
]
},
]
}
How to count the number of element in comments and sum with number of relies
My approach is do 2 query like this:
1. total elements of replies
db.posts.aggregate([
{$match: {_id:ObjectId("5dbdacc28cffef0b94580dbd")}},
{ $unwind: "$comments",},
{$project:{total:{$size:"$comments.replies"} , _id: 0} }
])
2. count total elements of comments
db.posts.aggregate([
{$match: {_id:ObjectId("5dbdacc28cffef0b94580dbd")}},
{$project:{total:{$size:"$comments.replies"} , _id: 0} }
])
Then sum up both, do we have any better solution to write the query like return the sum of of total element comments + replies
You can use $reduce and $concatArrays to "merge" an inner "array of arrays" into a single list and measure the $size of that. Then simply $add the two results together:
db.posts.aggregate([
{ "$match": { _id:ObjectId("5dbdacc28cffef0b94580dbd") } },
{ "$addFields": {
"totalBoth": {
"$add": [
{ "$size": "$comments" },
{ "$size": {
"$reduce": {
"input": "$comments.replies",
"initialValue": [],
"in": {
"$concatArrays": [ "$$value", "$$this" ]
}
}
}}
]
}
}}
])
Noting that an "array of arrays" is the effect of an expression like $comments.replies, so hence the operation to make these into a single array where you can measure all elements.
Try using the $unwind to flatten the list you get from the $project before using $count.
This is another way of getting the result.
Input documents:
{ "_id" : 1, "array1" : [ { "array2" : [ { id: "This is a test!"}, { id: "test1" } ] }, { "array2" : [ { id: "This is 2222!"}, { id: "test 222" }, { id: "222222" } ] } ] }
{ "_id" : 2, "array1" : [ { "array2" : [ { id: "aaaa" }, { id: "bbbb" } ] } ] }
The query:
db.arrsizes2.aggregate( [
{ $facet: {
array1Sizes: [
{ $project: { array1Size: { $size: "$array1" } } }
],
array2Sizes: [
{ $unwind: "$array1" },
{ $project: { array2Size: { $size: "$array1.array2" } } },
],
} },
{ $project: { result: { $concatArrays: [ "$array1Sizes", "$array2Sizes" ] } } },
{ $unwind: "$result" },
{ $group: { _id: "$result._id", total1: { $sum: "$result.array1Size" }, total2: { $sum: "$result.array2Size" } } },
{ $addFields: { total: { $add: [ "$total1", "$total2" ] } } },
] )
The output:
{ "_id" : 2, "total1" : 1, "total2" : 2, "total" : 3 }
{ "_id" : 1, "total1" : 2, "total2" : 5, "total" : 7 }

Mogodb aggregation create output as {"key": "value"} from 2 arrays

I have a question about how to create output from 2 arrays one array with translation key and another with translation i would output as
"translation_key":"value"
Current output:
{
"_id" : ObjectId("5bfc0b2b30c4683f585078fb"),
"translation" : [
"hour",
"day"
],
"translation_key" : [
"HOUR_TEXT",
"DAY_TEXT"
],
"locale_id" : "EN_en"
}
OUTPUT should be :
{
"EN_en" :{
"HOUR_TEXT" :"hour",
"DAY_TEXT" :"day",
}
}
You can use below aggregation
You can use $range to get index from both the arrays and can create key (k) and value (v) pair for the resultant array and then finally $arrayToObject to convert resultant array into single document.
db.collection.aggregate([
{ "$replaceRoot": {
"newRoot": {
"$arrayToObject": [[
{
"k": "$locale_id",
"v": {
"$arrayToObject": {
"$map": {
"input": { "$range": [0, { "$size": "$translation_key" }] },
"in": {
"k": { "$arrayElemAt": ["$translation_key", "$$this"] },
"v": { "$arrayElemAt": ["$translation", "$$this"] }
}
}
}
}
}
]]
}
}}
])
Which returns
[
{
"EN_en": {
"DAY_TEXT": "day",
"HOUR_TEXT": "hour"
}
}
]
You can try below aggregation:
db.col.aggregate([
{
$project: {
array: [
{
k: "$locale_id",
v: {
$arrayToObject: {
$map: {
input: { $range: [0, { $size: "$translation" }] },
as: "index",
in: {
k: { $arrayElemAt: [ "$translation", "$$index" ] },
v: { $arrayElemAt: [ "$translation_key", "$$index" ] }
}
}
}
}
}
]
}
},
{
$replaceRoot: {
newRoot: { $arrayToObject: "$array" }
}
}
])
Basically you need to use $arrayToObject operator to manipulate your key names and in your case it should be used twice. This operator expects an array of objects with two properties k and v and therefore you should use $map to generate that values based on translation, $range is used to generate indexes to traverse translation and translation_key arrays. Then you can use $replaceRoot to promote dynamically generated key into root level of your document.
Output:
{ "EN_en" : { "hour" : "HOUR_TEXT", "day" : "DAY_TEXT" } }

How to write a custom function to split a document into multiple documents of same Id

I am trying to split a document which has the following fields of string type:
{
"_id" : "17121",
"firstName": "Jello",
"lastName" : "New",
"bio" :"He is a nice person."
}
I want to split the above document into three new documents For Example:
{
"_id": "17121-1",
"firstName": "Jello"
}
{
"_id": "17121-2",
"firstName": "New"
}
{
"_id": "17121-3",
"bio": "He is a nice person."
}
Can anyone suggest how to proceed?
db.coll1.find().forEach(function(obj){
// I want to extract every single field. How to iterate on the field within this Bson object(obj) to collect every field.?
});
or any suggestion to do with aggregation pipeline in MongoDB.
You can use the below aggregation query.
The below query will convert each document fields into key value document array followed by $unwind while keeping the index and $replaceRoot with merge to produce the desired output.
$objectToArray to produce array (keyvalarr) with key (name of the array field)-value (array field) pair.
$match to remove the _id key value document.
$arrayToObject to produce the named key value while adding new _id key value pair and flatten array key values.
db.coll.aggregate([
{
"$project": {
"keyvalarr": {
"$objectToArray": "$$ROOT"
}
}
},
{
"$unwind": {
"path": "$keyvalarr",
"includeArrayIndex": "index"
}
},
{
"$match": {
"keyvalarr.k": {
"$ne": "_id"
}
}
},
{
"$replaceRoot": {
"newRoot": {
"$arrayToObject": [
{
"k": "_id",
"v": {
"$concat": [
{
"$substr": [
"$_id",
0,
-1
]
},
"-",
{
"$substr": [
"$index",
0,
-1
]
}
]
}
},
"$keyvalarr"
]
}
}
}
])
Anu. Here are two options you can use.
The first option is pretty straightforward, but it requires you to hardcode _id' indexes yourself.
db.users.aggregate([
{
$project: {
pairs : [
{ firstName: '$firstName', _id : { $concat : [ { $substr : [ '$_id', 0, 50 ] }, '-1' ] } },
{ lastName: '$lastName', _id : { $concat : [ '$_id', '-2' ] } },
{ bio: '$bio', _id : { $concat : [ { $substr : [ '$_id', 0, 50 ] }, '-3' ] } }
]
}
},
{
$unwind : '$pairs'
},
{
$replaceRoot: { newRoot: '$pairs' }
}
])
The second option does a little bit more job and is somewhat more tricky. But it is probably easier to extend if you ever need to add another field.
db.users.aggregate([
{
$project: {
pairs : [
{ firstName: '$firstName' },
{ lastName: '$lastName' },
{ bio: '$bio' }
]
}
},
{
$addFields: {
pairsReference : '$pairs'
}
},
{
$unwind: '$pairs'
},
{
$addFields: {
'pairs._id' : { $concat: [ { $substr : [ '$_id', 0, 50 ] }, '-', { $substr: [ { $indexOfArray : [ '$pairsReference', '$pairs' ] }, 0, 2 ] } ] }
}
},
{
$replaceRoot: { newRoot: '$pairs' }
}
])
You can redirect results of both queries into another collection by using $out stage.
UPD:
The only reason you get the error is that one of the _ids is not a string.
Replace the first parameter of $concat ($_id) with the following expression:
{ $substr : [ '$_id', 0, 50 ] }