MongoDB $sort on $concatArrays - node.js

I'm trying to concatenate two nested arrays (using $concatArrays) into one new field. I'd like to sort the output of the concatenation (Model.timeline) by a property that exists in both sets of objects. I can't seem to get it working with $unwind. Here's the query without any sorting:
Model.aggregate([
{
$match: {
'id': id
}
},
{
$project: {
id: 1,
name: 1,
flagged: 1,
updatedAt: 1,
lastEvent: {
$arrayElemAt: ['$events', -1]
},
lastimage: {
$arrayElemAt: ['$images', -1]
},
timeline: {
$concatArrays: [
{ $filter: {
input: '$events',
as: 'event',
cond: { $and: [
{ $gte: ['$$event.timestamp', startAt] },
{ $lte: ['$$event.timestamp', endAt] }
]}
}},
{ $filter: {
input: '$images',
as: 'image',
cond: { $and: [
{ $gte: ['$$image.timestamp', startAt] },
{ $lte: ['$$image.timestamp', endAt] }
]}
}}
]
}
}
}
]);
Am I missing something obvious?

You need three pipeline stages after your match and project. First $unwind, then $sort and then re $group. Use the $first operator to retain all the fields.
{
$undwind : "$timeline",
},
{
$sort : {"your.sortable.field" : 1}
},
{
$group : {
_id : "$_id",
name : {$first : 1},
flagged : {$first : 1},
updatedAt : {$first : 1},
lastEvent : {$first : 1},
lastimage : {$first : 1},
timeline : {$push : "$timeline"}
}
}
Please note that this will work even when you have more than one document after the match phase. So basically this will sort the elements of an array within each document.

Your $match and $project aggregation stages worked after I substituted id with _id, and filled in the values for id, startAt and endAt like so:
db.items.aggregate([
{
$match: {
'_id': '58'
}
},
{
$project: {
'_id': 1,
name: 1,
flagged: 1,
updatedAt: 1,
lastEvent: {
$arrayElemAt: ['$events', -1]
},
lastimage: {
$arrayElemAt: ['$images', -1]
},
timeline: {
$concatArrays: [
{ $filter: {
input: '$events',
as: 'event',
cond: { $and: [
{ $gte: ['$$event.timestamp', ISODate("2016-01-19T20:15:31Z")] },
{ $lte: ['$$event.timestamp', ISODate("2016-12-01T20:15:31Z")] }
]}
}},
{ $filter: {
input: '$images',
as: 'image',
cond: { $and: [
{ $gte: ['$$image.timestamp', ISODate("2016-01-19T20:15:31Z")] },
{ $lte: ['$$image.timestamp', ISODate("2016-12-01T20:15:31Z")] }
]}
}}
]
}
}
}
]);

Related

Mongodb search field with range inside array of object

I have multiple documents in a collection like this
[
{
_id: 123,
data: 1,
details: [
{
item: "a",
day: 1
},
{
item: "a",
day: 2
},
{
item: "a",
day: 3
},
{
item: "a",
day: 4
}
],
someMoreField: "xyz"
}
]
Now I want document with _id: 123 and details field should only contain day within range of 1 to 3. So the result will be like below.
{
_id: 123,
data: 1,
details: [
{
item: 'a',
day: 1,
},
{
item: 'a',
day: 2,
},
{
item: 'a',
day: 3,
},
],
someMoreField: 'xyz',
};
I tried to do this by aggregate query as:
db.collectionaggregate([
{
$match: {
_id: id,
'details.day': { $gt: 1, $lte: 3 },
},
},
{
$project: {
_id: 1,
details: {
$filter: {
input: '$details',
as: 'value',
cond: {
$and: [
{ $gt: ['$$value.date', 1] },
{ $lt: ['$$value.date', 3] },
],
},
},
},
},
},
])
But this gives me empty result. Could someone please guide me through this?
You are very close, you just need to change the $gt to $gte and $lt to $lte.
Another minor syntax error is you're accessing $$value.date but the schema you provided does not have that field, it seems you need to change it to $$value.day, like so:
db.collection.aggregate([
{
$match: {
_id: 123,
"details.day": {
$gt: 1,
$lte: 3
}
}
},
{
$project: {
_id: 1,
details: {
$filter: {
input: "$details",
as: "value",
cond: {
$and: [
{
$gte: [
"$$value.day",
1
]
},
{
$lte: [
"$$value.day",
3
]
},
],
},
},
},
},
},
])
Mongo Playground

How to merge two queries into a single query in MongoDB

Here I have two query for one table. Second query's $match contain result from first query.
db.histories.aggregate([
{$match: {
from: userid,
connectTime: {"$gte":yesterday},
status:'completed'
}},
{$group: {
_id: '$groupId',
groupId: { $last: '$groupId' },
callId:{ $last: '$callId' },
}},
{ $sort: {
connectTime:-1
} }
])
The result of first query contain "callId", using that "callId" I'm aggregating second query.
db.histories.aggregate([
{$match: {
$or: [{ callId: {"$in": groupCallIdArray} }, { connectedCallId: {"$in": groupCallIdArray} }],
status:'completed'
}},
{$group: {
_id: {'groupId':'$groupId'},
from: { $last: '$from' },
to: { $last: '$to' },
minimumTime: { $last: '$minimumTime' },
noOfcalls: {$sum:1},
duration:{ $sum: '$duration' },
}}
])
Do we have any way to merge two queries into a single query.
Input JSON
[{
"_id" : ObjectId("60fe4bf5c0fe3d0017059776"),
"callId" : "CAbfadc16eed3f493f742b208e283848af",
"connectedCallId" : "CA0c61d90d1694ef219b42412246570c63",
"from" : ObjectId("6062f39c9ccebd00178bf302"),
"to" : ObjectId("606d59547db42d00178234a6"),
"callConnectTime" : 2021-07-26 05:44:19.573Z,
"status" : "completed",
"minimumTime" : 15,
"duration" : 24,
"groupId" : ObjectId("60c8908e55242c00170e3e00"),
}]
First QUERY OUTPUT
[
{
_id:60fe4bf5c0fe3d0017059776,
gigsId: 60fe4bf5c0fe3d0017059776,
callSid: 'CAbfadc16eed3f493f742b208e283848af'
}
]
using first queries output I'm creating match query for second query.
groupCallIdArray = ["CAbfadc16eed3f493f742b208e283848af"]
Second Query Input
[{
_id:{groupId:ObjectId("60fe4bf5c0fe3d0017059776")},
from:ObjectId("6062f39c9ccebd00178bf302"),
to:ObjectId("606d59547db42d00178234a6"),
minimumTime:15,
noOfcalls:1,
duration:24,
}...
]
You could use $lookup to pull in the matches.
Something like
db.histories.aggregate([
{$match: {
from: userid,
connectTime: {"$gte":yesterday},
status:'completed'
}},
{ $sort: {
connectTime:-1
} }
{$group: {
_id: '$groupId',
groupId: { $last: '$groupId' },
callId:{ $last: '$callId' },
}},
{$group: {
_id: null,
groupCallIdArray: { $push: '$callId'}
}},
{$lookup: {
from: 'histories',
let: { groupCallIdArray: '$groupCallIdArray'},
pipeline: [
{$match:{
status:'completed',
{$expr: {
$or: [{ callId: {"$in": '$$groupCallIdArray'} }, {
connectedCallId: {"$in": '$$groupCallIdArray'} }]}}
}
},
{$group: {
_id: {'groupId':'$groupId'},
from: { $last: '$from' },
to: { $last: '$to' },
minimumTime: { $last: '$minimumTime' },
noOfcalls: {$sum:1},
duration:{ $sum: '$duration' },
}}
],
as: 'results'
}}
])

Last value by type with _id relation

I have a collection of Event containing a type that can be 0, 1, 2, 3, 4, 5 and a createdAt date. Each event is related to an other collection called RTS.
I want to gather for each Rts, the last event of each type.
Problem using my soluce :
The problem is, I have to describe each types one by one in order to make it work. Is there any solution to have dynamical key creation induced by the type value ?
Here is what I get now :
I sort the data
Group by idRTS whch contains the link to the second collection. For each type, push the values inside of a specific array.
Remove the null values from the types arrays.
Keep the first value only (the most updated).
Makes the data presentable.
[
{
$sort: {
idRTS: -1,
createdAt: -1
}
},
{
$group: {
_id: '$idRTS',
type0: {
$push: {
$cond: {
if: {
$eq: [
'$type', 0
]
},
then: '$$ROOT',
else: null
}
}
},
type5: {
$push: {
$cond: {
if: {
$eq: [
'$type', 5
]
},
then: '$$ROOT',
else: null
}
}
}
}
},
{
$project: {
_id: '$_id',
type0: {
'$filter': {
'input': '$type0',
'as': 'd',
'cond': {
'$ne': [
'$$d', null
]
}
}
},
type5: {
$filter: {
input: '$type5',
as: 'd',
cond: {
$ne: [
'$$d', null
]
}
}
}
}
},
{
$project: {
_id: '$_id',
type0: {
$arrayElemAt: [
'$type0', 0
]
},
type5: {
'$arrayElemAt': [
'$type5', 0
]
}
}
}
]
$match type in 0 or 5
$sort by idRTS and createdAt in descending order
$group by both idRTS and createdAt field and get first object, this will get first document of both type
$group by idRTS and make array of both types, in k(key) and v(value) format
$project to convert type array to object using $objectToArray
db.collection.aggregate([
{ $match: { type: { $in: [0, 5] } } },
{
$sort: {
idRTS: -1,
createdAt: -1
}
},
{
$group: {
_id: {
idRTS: "$idRTS",
type: "$type"
},
type: { $first: "$$ROOT" }
}
},
{
$group: {
_id: "$_id.idRTS",
type: {
$push: {
k: { $toString: "$type.type" },
v: "$type"
}
}
}
},
{ $project: { type: { $arrayToObject: "$type" } } }
])
Playground

MongoDb - Aggregation: How to $filter array documents with $elemMatch on a subdocument array?

I'm trying use $filter on an aggregation. I need to filter an array called 'extras' that intersect at least one of his 'seasons' with the giving 'dates'.
Here an example:
var from = new Date(2016, 0, 1);
var to = new Date(2016, 0, 5)
var projection = {
name: 1,
units: 1,
capacity: 1,
availabilitySeasons: { // This works
$filter: {
input: '$availabilitySeasons',
as: 'avSeason',
cond: {
$and: [{
$lte: ['$$avSeason.from', to]
}, {
$gte: ['$$avSeason.to', from]
}]
}
}
},
rate: 1,
features: 1,
extras: {
$filter: { // $elemMatch is no valid
input: '$extras',
as: 'extra',
cond: {
$elemMatch: ['$$extra.seasons', {
from: {
$lte: from
},
to: {
$gte: to
}
}]
}
}
}
};
It's possible to use $filter in this case? Do I have to rush through a $unwind/$group hell?
Yes, you can do it.
As for the following example, use a condition with $lte and $gte inside a $and condition.
$project: {
items: {
$filter: {
input: "$items",
as: "item",
cond: {
$and: [
$lt: [ "$$item.price", 100 ],
$gt: [ "$$item.price", 50 ]
] }
}
}
}

MongoDB: Aggregation, group by several fields

I have a collection with documents, of that structure
{
type: 'a',
date: '2014-01-04'
},
{
type: 'b',
date: '2014-01-04'
},
{
type: 'b',
date: '2014-01-04
},
{
type: 'c',
date: '2014-01-03'
},
{
type: 'a',
date: '2014-01-03'
}
I want to aggregate that data by date and type (group by date and count by type):
{
date: '2014-01-04': {
'a': 1,
'b': 2
},
date: '2014-01'03': {
'a': 1,
'c': 1
}
}
I have aggregate function, like this
db.items.aggregate([
{
$match: { user: user },
},
{
$group: { _id: {date: '$date'}, count: {$sum: 1}, services: {$push: '$type'}}
}
], function (err, results) {
But doing that I still need to reduce results by services.
Can this be done with one aggregation query?
You can of course group by more than one field:
{ $group: { _id: { date: '$date', services: '$services' } }
But that is not what you want it seems. You can not every easily convert data to keys, unless you can do that all by hand. The following query would be an option:
db.test.aggregate( [
{ $group: {
'_id' : { date: '$date' },
a: { $sum: {
$cond: [ { $eq: [ '$type', 'a' ] }, 1, 0 ]
} },
b: { $sum: {
$cond: [ { $eq: [ '$type', 'b' ] }, 1, 0 ]
} },
c: { $sum: {
$cond: [ { $eq: [ '$type', 'c' ] }, 1, 0 ]
} },
} },
{ $project: {
_id: 0,
date: '$_id.date',
a: '$a',
b: '$b',
c: '$c',
} }
] );
You will need to manually add a line for each new type.
By assuming you have fixed number of types, you can solve it as follows :
db.collection.aggregate(
{$group : {_id : "$date",
a:{$sum:{$cond:[{$eq:['$type','a']},1,0]}},
b:{$sum:{$cond:[{$eq:['$type','b']},1,0]}},
c:{$sum:{$cond:[{$eq:['$type','c']},1,0]}}
}},
{$project : {_id : 0, date : "$_id", a: "$a", b : "$b", c : "$c"}}
)

Resources