Concatenate array elements in MongoDB 3.4 with Aggregation Framework - mongodb

What I have :
{ "_id" : ObjectId("577dc9d61a0b7e0a40499f90"), "equ" : 123456, "key" : "p" }
{ "_id" : ObjectId("577c789b1a0b7e0a403f1b52"), "equ" : 123456, "key" : "r" }
{ "_id" : ObjectId("577b27481a0b7e0a4033965a"), "equ" : 123456, "key" : "r" }
{ "_id" : ObjectId("5779d6111a0b7e0a40282dc7"), "equ" : 123456, "key" : "o" }
What I want :
{ "_id" : ObjectId("5779d6111a0b7e0a40282dc7"), "equ" : 123456, "keys" : "prro" }
What I tried :
db.table.aggregate([{"$group":{"_id":0, "keys":{"$push":"$key"}}}])
returns an array and not a string:
{"_id":0, "keys":["p","r","r","o"]}
Do you have any idea?

TL; DR
Use this aggregation pipeline:
db.col.aggregate([
{$group: {_id: "$equ", last: {$last: "$_id"}, keys: {$push: "$key"}}},
{
$project: {
equ: "$_id",
_id: "$last",
keys: {
$reduce: {
input: "$keys",
initialValue: "",
in: {$concat: ["$$value", "$$this"]}
}
}
}
}
])
More details
First you should group the documents based on the equ value and also maintain an array of keys along with the _id of the last group member:
var groupByEqu = {
$group: {
_id: "$equ",
last: {$last: "$_id"},
keys: {$push: "$key"}
}
}
Just applying this pipeline operation would result in:
{
"_id" : 123456,
"last" : ObjectId("5779d6111a0b7e0a40282dc7"),
"keys" : [ "p", "r", "r", "o" ]
}
You should use a Projection to transform the preceding document into your desired one. The first two transformations are trivial. For joining the array elements you can use the new $reduce operator:
var project = {
$project: {
equ: "$_id",
_id: "$last",
keys: {
$reduce: {
input: "$keys",
initialValue: "",
in: {$concat: ["$$value", "$$this"]}
}
}
}
}
Applying these two pipeline operations would give you the desired result:
db.col.aggregate([groupByEqu, project])
Which is:
{
"equ" : 123456,
"_id" : ObjectId("5779d6111a0b7e0a40282dc7"),
"keys" : "prro"
}

Related

Mongo Aggregate Group List / Filtered Results

Here my records for mongdb
{ "_id" : 1,"userId" : "x", "name" : "Central", "borough": "Manhattan"},
{ "_id" : 2,"userId" : "x", "name" : "Rock", "borough" : "Queens"},
{ "_id" : 3,"userId" : "y", "name" : "Empire", "borough" : "Brooklyn"},
{ "_id" : 4,"userId" : "y", "name" : "Stana", "borough" : "Manhattan"},
{ "_id" : 5,"userId" : "y", "name" : "Jane", "borough" :"Brooklyn"}
how can we take result with aggregate by userId field like this
[
{
x : [{"_id":1,"name":"Central"},{"_id":2,"name":"Rock"}],
y:[{ "_id" : 3,"name":"Empire"},{ "_id" : 4,"name":"Stana"},{ "_id" : 5,"name":"Jane"}]
}
]
$group by userId and construct docs array with required fields
$group by null and construct array of docs in key-value format
$arrayToObject convert docs array to object
$replaceRoot to replace above converted object to root
db.collection.aggregate([
{
$group: {
_id: "$userId",
docs: {
$push: {
_id: "$_id",
name: "$name"
}
}
}
},
{
$group: {
_id: null,
docs: {
$push: {
k: "$_id",
v: "$docs"
}
}
}
},
{ $replaceRoot: { newRoot: { $arrayToObject: "$docs" } } }
])
Playground

Maintain order as original document while using addToSet in MongoDb

I read the documentation and found that addToSet doesn't guarantee order.
But is there any way I can preserve the order as the original document.
My Query is :-
aggregate([{$match: {
$or:[{"Name.No":"119"},{"Name.No":"120"}]
}}, {$project: {
x:{$objectToArray:"$Results"}
}},{$unwind: "$x"},{$group: {_id: "$x.k", distinctVals: {$addToSet: "$x.v.TCR"}}}])
Sample Data:
{"Name" : {"No." : "119","Time" : "t"},
"Results":{"K1" : {"Counters" : x, "TCR" : [{"Name" : "K11", "Result" : "PASSED"},
{"Name" : "K12","Result" : "FAILED"},
{"Name" : "K13","Result" : "PASSED"}]
},
"K2" : {"Counters": y, "TCR" : [{"Name" : "K21","Result" : "PASSED"},
{"Name" : "K22","Result" : "PASSED"}]
}
}
}
}
Job2;
{"Name" : {"No." : "120","Time" : "t1"},
"Results":{"K1" : {"Counters" : x, "TCR" : [{"Name" : "K11", "Result" : "PASSED"},
{"Name" : "K12","Result" : "PASSED"},
{"Name" : "K13","Result" : "FAILED"}]
},
"K3" : {"Counters": y, "TCR" : [{"Name" : "K31","Result" : "PASSED"},
{"Name" : "K32","Result" : "PASSED"}]
}
}
}
Expected;
{"Name" : {"No." : "119-120","Time" : "lowest(t,t1)"},
"Results":{"K1" : {"Counters" : x, "TCR" : [{"Name" : "K11", "Result" : "PASSED"},
{"Name" : "K12","Result" : "PASSED"},
{"Name" : "K13","Result" : "PASSED"}]
},
"K2" : {"Counters": y, "TCR" : [{"Name" : "K21","Result" : "PASSED"},
{"Name" : "K22","Result" : "PASSED"}]
},
"K3" : {"Counters": y, "TCR" : [{"Name" : "K31","Result" : "PASSED"},
{"Name" : "K32","Result" : "PASSED"}]
}
}
}
I want to maintain the order same as original document, also every time document would change,so I cant sort based on any parameter.
convert Results object to array format using $objectToArray
$unwind deconstruct Results array
$unwind deconstruct Results.v.TCR array
$match to filter PASSED Result
$group by Results.k and get first Name, get first Counters, construct array of Results.v.TCR
$group by null and get minimum Time, construct unique array of No, construct Results array in key-value pair, $reduce to iterate loop of TCR and remove duplicate documents
$project to show required fields, convert Results array to object using $arrayToObject, convert No array to string and concat with "-"
db.collection.aggregate([
{ $addFields: { Results: { $objectToArray: "$Results" } } },
{ $unwind: "$Results" },
{ $unwind: "$Results.v.TCR" },
{ $match: { "Results.v.TCR.Result": "PASSED" } },
{
$group: {
_id: "$Results.k",
Name: { $first: "$Name" },
Counters: { $first: "$Results.v.Counters" },
TCR: { $push: "$Results.v.TCR" }
}
},
{
$group: {
_id: null,
Time: { $min: "$Name.Time" },
No: { $addToSet: "$Name.No" },
Results: {
$push: {
k: "$_id",
v: {
Counters: "$Counters",
TCR: {
$reduce: {
input: "$TCR",
initialValue: [],
in: {
$cond: [
{
$in: [
{
Name: "$$this.Name",
Result: "$$this.Result"
},
"$$value"
]
},
"$$value",
{
$concatArrays: [
"$$value",
[
{
Name: "$$this.Name",
Result: "$$this.Result"
}
]
]
}
]
}
}
}
}
}
}
}
},
{
$project: {
_id: 0,
Results: { $arrayToObject: "$Results" },
Name: {
Time: "$Time",
No: {
$reduce: {
input: "$No",
initialValue: "",
in: {
$concat: [
"$$value",
{ $cond: [{ $eq: ["$$value", ""]}, "", "-"] },
"$$this"
]
}
}
}
}
}
}
])
Playground
The "." (dot) in "No." field is not valid, it may cause issue in mongodb query operations, i would suggest do not use "." (dot) as field name.

How to filter Mongodb $lookup results to get only the matched nested objects?

I have a customers collection such as;
{
"_id" : ObjectId("5de8c07dc035532b489b2e23"),
"name" : "sam",
"orders" : [{"ordername" : "cola"},{"ordername" : "cheesecake"}]
}
And waiters collection such as;
{
"_id" : ObjectId("5de8bc24c035532b489b2e20"),
"waiter" : "jack",
"products" : [{"name" : "cola", "price" : "4"},
{"name" : "water", "price" : "2"},
{"name" : "coffee", "price" : "8" }]
}
{
"_id" : ObjectId("5de8bdc7c035532b489b2e21"),
"waiter" : "susan",
"products" : [{"name" : "cheesecake", "price" : "12" },
{"name" : "apple pie", "price" : "14" }]
}
I want to join the objects from waiters collection into the customers collection by matching "products.name" and "orders.ordername". But, the result includes the whole document from the waiters collection, however, I want only the matched objects inside the document. Here is what I want;
ordered:[
{"name" : "cola", "price" : "4"},
{"name" : "cheesecake", "price" : "12" },
]
I tried $lookup with and without pipeline, and filter but could not get this result. Thanks in advance.
You had the right idea, we just have to "massage" the data a bit due to its structure like so:
db.collection.aggregate([
{
$addFields: {
"orderNames":
{
$reduce: {
input: "$orders",
initialValue: [],
in: {$concatArrays: [["$$this.ordername"], "$$value"]}
}
}
}
},
{
$lookup:
{
from: "waiters",
let: {orders: "$orderNames"},
pipeline: [
{
$unwind: "$products"
},
{
$match:
{
$expr:{$in: ["$products.name", "$$orders"]},
}
},
{
$group: {
_id: "$products.name",
price: {$first: "$products.price"}
}
},
{
$project: {
_id: 0,
price: 1,
name: "$_id"
}
}
],
as: "ordered"
}
}
])
It feels like you could benefit from a new collection of mapping items to prices. Could potentially save you a lot of time.

MongoDB aggregate nested values

In a MongoDB collection, there is data nested in an absence array.
{
"_id" : ObjectId("5c6c62f3d0e85e6ae3a8c842"),
"absence" : [
{
"date" : ISODate("2017-05-10T17:00:00.000-07:00"),
"code" : "E",
"type" : "E",
"isPartial" : false
},
{
"date" : ISODate("2018-02-24T16:00:00.000-08:00"),
"code" : "W",
"type" : "E",
"isPartial" : false
},
{
"date" : ISODate("2018-02-23T16:00:00.000-08:00"),
"code" : "E",
"type" : "E",
"isPartial" : false
},
{
"date" : ISODate("2018-02-21T16:00:00.000-08:00"),
"code" : "U",
"type" : "U",
"isPartial" : false
},
{
"date" : ISODate("2018-02-20T16:00:00.000-08:00"),
"code" : "R",
"type" : "E",
"isPartial" : false
}
]
}
I'd like to aggregate by absence.type to return a count of every type and the total number of absence children. The results might look like:
{
"_id" : ObjectId("5c6c62f3d0e85e6ae3a8c842"),
"U" : 1,
"E" : 4,
"total" : 5
}
There are several similar questions posted here but I'm yet to successfully adapt the answers my schema. Any help is greatly appreciated.
Also, are there GUI modeling tools to help with MongoDB query building? The transition from RDBMS queries to the Mongo aggregation pipeline has been quite difficult.
You can use below aggregation:
db.col.aggregate([
{
$unwind: "$absence"
},
{
$group: {
_id: { _id: "$_id", type: "$absence.type" },
count: { $sum: 1 }
}
},
{
$group: {
_id: "$_id._id",
types: { $push: { k: "$_id.type", v: "$count" } },
total: { $sum: "$count" }
}
},
{
$replaceRoot: {
newRoot: {
$mergeObjects: [ "$$ROOT", { $arrayToObject: "$types" } ]
}
}
},
{
$project: {
types: 0
}
}
])
$unwind allows you to get single document per absence. Then you need double $group, first one to count by type and _id and second one to aggregate the data per _id. Having one document per _id you just need $replaceRoot with $mergeObjects to promote your dynamically created keys and values (by $arrayToObject) to the root level.
output:
{ "_id" : ObjectId("5c6c62f3d0e85e6ae3a8c842"), "total" : 5, "U" : 1, "E" : 4 }
If you know all the possible values of "absence.type" then $filter the array on the value and compute the $size of the filtered array. This won't work if you don't know all the possible values in the "absence.type".
db.col.aggregate([
{ $project: { U: { $size: { $filter: { input: "$absence", as: "a", cond: { $eq: [ "$$a.type", "U"]} }}},
E: { $size: { $filter: { input: "$absence", as: "a", cond: { $eq: [ "$$a.type", "E"]} }}} }},
{ $project: { total: { $add: [ "$U", "$E" ]}, U: 1, E: 1}},
])

How to join two Aggregation results in MongoDB?

I have a data set looks as
{"BrandId":"a","SessionId":100,"Method": "POST"}
{"BrandId":"a","SessionId":200,"Method": "PUT"}
{"BrandId":"a","SessionId":200,"Method": "GET"}
{"BrandId":"b","SessionId":300,"Method": "GET"}
I wrote aggregation count distinct session id by brandid:
db.collection.aggregate([
{$group: {
"_id": {
brand: "$BrandId",
session: "$SessionId"
},
count: {$sum: 1}
}},
{$group: {
_id: "$_id.brand",
countSession:{$sum:1}
}}
])
The expected result of the query is :
{ "_id" : "a", "countSession" : 2 }
{ "_id" : "b", "countSession" : 1 }
Another query is to count where the Method is POST by brand:
db.collection.aggregate([
{$match: {Method:"POST"}},
{$group: {
_id: '$BrandId',
countPOST:{$sum:1}
}}
])
The expected result:
{ "_id" : "a", "countPOST" : 1 }
{ "_id" : "b", "countSession" : 0 }
And now, I want to combine these two query and get the expected result as following:
{"BrandId:"a","countSession":2,"countPOST":1}
{"BrandId:"b","countSession":1,"countPOST":0}
I do not how to combine these two result of two aggregation, anyone can help?
You can use $cond operator as follows.
db.Collection.aggregate(
{
'$group': {
'_id': {'BrandId':'$BrandId','Session': '$SessionId'},
'countPOST':{
'$sum':{
'$cond': [{'$eq':['$Method','POST']},1,0]
}
}
}
},
{
'$group': {
'_id': '$_id.BrandId',
'countSession': {'$sum':1},
'countPOST': {'$sum': '$countPOST'}
}
}
)
Ouput:
{
"result" : [
{
"_id" : "a",
"countSession" : 2,
"countPOST" : 1
},
{
"_id" : "b",
"countSession" : 1,
"countPOST" : 0
}
],
"ok" : 1
}