Returning a document with two fields from the same array in MongoDB - mongodb

Given documents such as
{
_id: 'abcd',
userId: '12345',
activities: [
{ status: 'login', timestamp: '10000001' },
{ status: 'logout', timestamp: '10000002' },
{ status: 'login', timestamp: '10000003' },
{ status: 'logout', timestamp: '10000004' },
]
}
I am trying to create a pipeline such as all users that have their latest login/logout activities recorded between two timestamps will be returned. For example, if the two timestamp values are between 10000002 and 10000003, the expected document should be
{
_id: 'abcd',
userId: '12345',
login: '10000003',
logout: '10000002'
}
Of if the two timestamp values are between -1 and 10000001, the expected document should be :
{
_id: 'abcd',
userId: '12345',
login: '10000001',
logout: null
}
Etc.
I know it has to do with aggregations, and I need to $unwind, etc., but I'm not sure about the rest, namely evaluating two fields from the same document array

You can try below aggregation:
db.col.aggregate([
{
$unwind: "$activities"
},
{
$match: {
$and: [
{ "activities.timestamp": { $gte: "10000001" } },
{ "activities.timestamp": { $lte: "10000002" } }
]
}
},
{
$sort: {
"activities.timestamp": -1
}
},
{
$group: {
_id: "$_id",
userId: { $first: "$userId" },
activities: { $push: "$activities" }
}
},
{
$addFields: {
login: { $arrayElemAt: [ { $filter: { input: "$activities", as: "a", cond: { $eq: [ "$$a.status", "login" ] } } } , 0 ] },
logout: { $arrayElemAt: [ { $filter: { input: "$activities", as: "a", cond: { $eq: [ "$$a.status", "logout" ] } } } , 0 ] }
}
},
{
$project: {
_id: 1,
userId: 1,
login: { $ifNull: [ "$login.timestamp", null ] },
logout: { $ifNull: [ "$logout.timestamp", null ] }
}
}
])
We need to use $unwind + $sort + $group to make sure that our activities will be sorted by timestamp. After $unwind you can use $match to apply filtering condition. Then you can use $filter with $arrayElemAt to get first (latest) value of filtered array. In the last $project you can explicitly use $ifNull (otherwise JSON key will be skipped if there's no value)

You can use below aggregation
Instead of $unwind use $lte and $gte with the $fitler aggregation.
db.collection.aggregate([
{ "$project": {
"userId": 1,
"login": {
"$max": {
"$filter": {
"input": "$activities",
"cond": {
"$and": [
{ "$gte": ["$$this.timestamp", "10000001"] },
{ "$lte": ["$$this.timestamp", "10000004"] },
{ "$lte": ["$$this.status", "login"] }
]
}
}
}
},
"logout": {
"$max": {
"$filter": {
"input": "$activities",
"cond": {
"$and": [
{ "$gte": ["$$this.timestamp", "10000001"] },
{ "$lte": ["$$this.timestamp", "10000004"] },
{ "$lte": ["$$this.status", "logout"] }
]
}
}
}
}
}}
])

Related

MongoDB to return formatted object when no results can be found

I have the following stage in my MongoDB aggregation pipeline that returns the qty and sum of sales, which works fine:
{
$lookup: {
from: 'sales',
let: { part: '$_id' },
pipeline: [
{ $match: { $and: [{ $expr: { $eq: ['$partner', '$$part'] } }] } },
{ $group: { _id: null, qty: { $sum: 1 }, soldFor: { $sum: '$soldFor' } } },
{ $project: { _id: 0, qty: 1, soldFor: 1 } }],
as: 'sales'}},
{ $unwind: { path: '$sales', preserveNullAndEmptyArrays: true } },
{ $project: { _id: 1, sales: 1 }
}
However, if there are no sales, then the $project projection returns an empty sales object, but what I'd really like is it to return a completed object, but with 0 - like this:
{
sales: {
qty: 0,
soldFor: 0
}
}
You can use $cond operator here
{
"$project": {
"_id": 1,
"sales": {
"$cond": [
{ "$eq": [{ "$size": "$sales" }, 0] },
{
"sales": {
"qty": 0,
"soldFor": 0
}
},
"$sales"
]
}
}
}

Mongodb - group by value and get count

I have a aggregate query , which returns result like
{
count:1,
status: 'FAILED',
article_id: 1
},
{
count:1,
status: 'DELIVERED',
article_id: 1
}
I want to group by on the article_id and get the count based on the status , something like this:
{
article_id:1,
FAILED:1,
DELIVERED:2
}
How can i archive this?
Thanks in advance.
The other answers may work in principle, however they are limited hard-coded to status FAILED and DELIVERED.
In case you like to have a generic solution for arbitrary status, you can use this one:
db.collection.aggregate([
{ $set: { data: [{ k: "$status", v: "$count" }] } },
{
$replaceRoot: {
newRoot: {
$mergeObjects: [
{ $arrayToObject: "$data" }, { article_id: "$article_id" }
]
}
}
},
{
$group: {
_id: "$article_id",
status: { $push: "$$ROOT" }
}
},
{ $set: { status: { $mergeObjects: ["$status"] } } },
{ $replaceRoot: { newRoot: "$status" } },
])
Mongo playground
Try this code
db.getCollection('artwork').aggregate([
{
$group: {
_id: '$article_id',
FAILED: {
'$sum': {
"$cond": [{ "$eq": ["$status", "FAILED"] }, 1, 0]
}
},
DELIVERED: {
'$sum': {
"$cond": [{ "$eq": ["$status", "DELIVERED"] }, 1, 0]
}
}
}
}
])
mongoplayground
{
$group:{
_id:"$_id",
articleId:{$addToset:"$article_id"},
failed:{$addToset:"$failed"},
delivered:{$addToset:"$delivered"}
}
},
{ $addFields:{
article_id:{$size:articleId},
fail:{$size:"$faild"},
deliver:{$size:"$faild"},
}
In group $addToSet will return array and in addField will return total length of array. you cans earch $size in mongo
}

$group inner array values without $unwind

I want to group objects in the array by same value for specified field and produce a count.
I have the following mongodb document (non-relevant fields are not present).
{
arrayField: [
{ fieldA: value1, ...otherFields },
{ fieldA: value2, ...otherFields },
{ fieldA: value2, ...otherFields }
],
...otherFields
}
The following is what I want.
{
arrayField: [
{ fieldA: value1, ...otherFields },
{ fieldA: value2, ...otherFields },
{ fieldA: value2, ...otherFields }
],
newArrayField: [
{ fieldA: value1, count: 1 },
{ fieldA: value2, count: 2 },
],
...otherFields
}
Here I grouped embedded documents by fieldA.
I know how to do it with unwind and 2 group stages the following way. (irrelevant stages are ommited)
Concrete example
// document structure
{
_id: ObjectId(...),
type: "test",
results: [
{ choice: "a" },
{ choice: "b" },
{ choice: "a" }
]
}
db.test.aggregate([
{ $match: {} },
{
$unwind: {
path: "$results",
preserveNullAndEmptyArrays: true
}
},
{
$group: {
_id: {
_id: "$_id",
type: "$type",
choice: "$results.choice",
},
count: { $sum: 1 }
}
},
{
$group: {
_id: {
_id: "$_id._id",
type: "$_id.type",
result: "$results.choice",
},
groupedResults: { $push: { count: "$count", choice: "$_id.choice" } }
}
}
])
You can use below aggregation
db.test.aggregate([
{ "$addFields": {
"newArrayField": {
"$map": {
"input": { "$setUnion": ["$arrayField.fieldA"] },
"as": "m",
"in": {
"fieldA": "$$m",
"count": {
"$size": {
"$filter": {
"input": "$arrayField",
"as": "d",
"cond": { "$eq": ["$$d.fieldA", "$$m"] }
}
}
}
}
}
}
}}
])
The below adds a new array field, which is generated by:
Using $setUnion to get unique set of array items, with inner $map to
extract only the choice field
Using $map on the unique set of items,
with inner $reduce on the original array, to sum all items where
choice matches
Pipeline:
db.test.aggregate([{
$addFields: {
newArrayField: {
$map: {
input: {
$setUnion: [{
$map: {
input: "$results",
in: { choice: "$$this.choice" }
}
}
]
},
as: "i",
in: {
choice: '$$i.choice',
count: {
$reduce: {
input: "$results",
initialValue: 0,
in: {
$sum: ["$$value", { $cond: [ { $eq: [ "$$this.choice", "$$i.choice" ] }, 1, 0 ] }]
}
}
}
}
}
}
}
}])
The $reduce will iterate over the results array n times, where n is the number of unique values of choice, so the performance will depend on that.

Using document field as JS object field-name in aggregation pipeline

I have an JS object called tasksCountMap:
const tasksCountMap = {
'Freshman': 46,
'Senior': 10
}
and I need get count of task for each user type in my aggregation pipe, 'Freshman', 'Senior' it's document field called gradeLevel. I'm trying do it like this:
status: {
$let: {
vars: {
tasksCount: tasksCountMap['$gradeLevel'],
completedTasksCount: '$completedTasksCount[0].count'
},
in: {
$cond: {
if: { $or: [
{ $eq: ['$$tasksCount', '$$completedTasksCount'] },
{ $lte: ['$$tasksCount', '$$completedTasksCount'] },
]},
then: 'On track',
else: 'High priority'
}
}
}
}
Also '$completedTasksCount[0].count' doesn't work to...
Can someone show right way to do this?
All pipeline:
{
$match: {
type: 'student',
counselorUserName: username
},
$project: {
username: '$username',
email: '$email',
phone: '$phone',
fullName: '$fullName',
gradeLevel: {
$switch: {
branches: [{
case: {
$eq: ['$gradeLevel', '9']
},
then: 'Freshman'
},
{
case: {
$eq: ['$gradeLevel', '10']
},
then: 'Sophomore'
},
{
case: {
$eq: ['$gradeLevel', '11']
},
then: 'Junior'
},
{
case: {
$eq: ['$gradeLevel', '12']
},
then: 'Senior'
}
],
default: "Freshman"
}
}
},
$lookup: {
from: 'RoadmapTasksCompleted',
let: {
username: '$username',
gradeLevel: '$gradeLevel'
},
pipeline: [{
$match: {
monthToComplete: {
$in: prevMonthsNames
},
$expr: {
$and: [{
$eq: ['$username', '$$username']
},
{
$eq: ['$gradeLevel', '$$gradeLevel']
}
]
}
}
},
{
$count: 'count'
}
],
as: 'completedTasksCount'
},
$project: {
username: '$username',
email: '$email',
phone: '$phone',
fullName: '$fullName',
completedTask: { $arrayElemAt: ['$completedTasksCount', 0] },
status: {
$let: {
vars: {
tasksCount: tasksCountMap['$gradeLevel'],
completedTasksCount: '$completedTasksCount[0].count'
},
in: {
$cond: {
if: { $or: [
{ $eq: ['$$tasksCount', '$$completedTasksCount'] },
{ $lte: ['$$tasksCount', '$$completedTasksCount'] },
]},
then: '$$tasksCount',
else:'$$tasksCount'
}
}
}
}
}
$limit: 10,
$skip: 10,
}
You have to move the js map into the aggregation pipeline for you to be able to access the map.
I split $project stage into two and took the liberty to clean up the status calculation.
Something like
{"$project":{
"username":"$username",
"email":"$email",
"phone":"$phone",
"fullName":"$fullName",
"completedTask":{"$arrayElemAt":["$completedTasksCount",0]},
"tasksCountMap":[{"k":"Freshman","v":46},{"k":"Senior","v":10}]
}},
{"$addFields":{
"status":{
"$let":{
"vars":{
"tasksCount":{
"$arrayElemAt":[
"$tasksCountMap.v",
{"$indexOfArray":["$tasksCountMap.k","$gradeLevel"]}
]
},
"completedTasksCount":"$completedTask.count"
},
"in":{
"$cond":{
"if":{"$lte":["$$tasksCount","$$completedTasksCount"]},
"then":"$$tasksCount",
"else":"$$completedTasksCount"
}
}
}
}
}}

Mongodb - aggregation $push if conditional

I am trying to aggregate a batch of documents. There are two fields in the documents I would like to $push. However, lets say they are "_id" and "A" fields, I only want $push "_id" and "A" if "A" is $gt 0.
I tried two approaches.
First one.
db.collection.aggregate([{
"$group":{
"field": {
"$push": {
"$cond":[
{"$gt":["$A", 0]},
{"id": "$_id", "A":"$A"},
null
]
}
},
"secondField":{"$push":"$B"}
}])
But this will push a null value to "field" and I don't want it.
Second one.
db.collection.aggregate([{
"$group":
"field": {
"$cond":[
{"$gt",["$A", 0]},
{"$push": {"id":"$_id", "A":"$A"}},
null
]
},
"secondField":{"$push":"$B"}
}])
The second one simply doesn't work...
Is there a way to skip the $push in else case?
ADDED:
Expected documents:
{
"_id":objectid(1),
"A":2,
"B":"One"
},
{
"_id":objectid(2),
"A":3,
"B":"Two"
},
{
"_id":objectid(3),
"B":"Three"
}
Expected Output:
{
"field":[
{
"A":"2",
"_id":objectid(1)
},
{
"A":"3",
"_id":objectid(2)
},
],
"secondField":["One", "Two", "Three"]
}
You can use "$$REMOVE":
This system variable was added in version 3.6 (mongodb docs)
db.collection.aggregate([{
$group:{
field: {
$push: {
$cond:[
{ $gt: ["$A", 0] },
{ id: "$_id", A:"$A" },
"$$REMOVE"
]
}
},
secondField:{ $push: "$B" }
}
])
In this way you don't have to filter nulls.
This is my answer to the question after reading the post suggested by #Veeram
db.collection.aggregate([{
"$group":{
"field": {
"$push": {
"$cond":[
{"$gt":["$A", 0]},
{"id": "$_id", "A":"$A"},
null
]
}
},
"secondField":{"$push":"$B"}
},
{
"$project": {
"A":{"$setDifference":["$A", [null]]},
"B":"$B"
}
}])
One more option is to use $filter operator:
db.collection.aggregate([
{
$group : {
_id: null,
field: { $push: { id: "$_id", A : "$A"}},
secondField:{ $push: "$B" }
}
},
{
$project: {
field: {
$filter: {
input: "$field",
as: "item",
cond: { $gt: [ "$$item.A", 0 ] }
}
},
secondField: "$secondField"
}
}])
On first step you combine your array and filter them on second step
$group: {
_id: '$_id',
tasks: {
$addToSet: {
$cond: {
if: {
$eq: [
{
$ifNull: ['$tasks.id', ''],
},
'',
],
},
then: '$$REMOVE',
else: {
id: '$tasks.id',
description: '$tasks.description',
assignee: {
$cond: {
if: {
$eq: [
{
$ifNull: ['$tasks.assignee._id', ''],
},
'',
],
},
then: undefined,
else: {
id: '$tasks.assignee._id',
name: '$tasks.assignee.name',
thumbnail: '$tasks.assignee.thumbnail',
status: '$tasks.assignee.status',
},
},
},
},
},
},
},
}