Add a field with increasing value in MongoDB Aggregation based on condition - mongodb

Sample of my collection :
[
{
_id: "bmasndvhjbcw",
name: "lucas",
occupation: "scientist",
age: 55,
location: "texas",
joining_date: 2019-01-01T15:24:15.068+00:00
},
{
_id: "bmasndvhjbcx",
name: "mark",
occupation: "scientist",
age: 45,
location: "texas",
joining_date: 2019-01-01T15:24:15.068+00:00
},
{
_id: "bmasndvhjbca",
name: "stuart",
occupation: "lab assistant",
age: 25,
location: "texas",
joining_date: 2019-01-02T20:25:16.068+00:00
},
{
_id: "bmasndvhjbcq",
name: "cooper",
occupation: "physicist",
age: 69,
location: "texas"
}
]
Which ever docs has joining_date column need to add a field with increasing value by checking the date like joining_date_count:1
if the dates are same like in two cases mark and lucas . count should consider it as different values and increase the count.
Expected Output :
[
{
_id: "bmasndvhjbcw",
name: "lucas",
occupation: "scientist",
age: 55,
location: "texas",
joining_date: 2019-01-01T15:24:15.068+00:00,
joining_date_count:1
},
{
_id: "bmasndvhjbcx",
name: "mark",
occupation: "scientist",
age: 45,
location: "texas",
joining_date: 2019-01-01T15:24:15.068+00:00,
joining_date_count:2
},
{
_id: "bmasndvhjbca",
name: "stuart",
occupation: "lab assistant",
age: 25,
location: "texas",
joining_date: 2019-01-02T20:25:16.068+00:00,
joining_date_count:3
},
{
_id: "bmasndvhjbcq",
name: "cooper",
occupation: "physicist",
age: 69,
location: "texas"
}
]

This aggregation adds a field with a counter:
db.collection.aggregate( [
{
$match: {
joining_date: { $exists: true }
}
},
{
$group: {
_id: null,
docs: { $push: "$$ROOT" }
}
},
{
$project: {
_id: 0,
R: {
$map: {
input: { $range: [ 0, { $size: "$docs" } ] },
in: {
$mergeObjects: [
{ joining_date_count: { $add: [ "$$this", 1 ] } },
{ $arrayElemAt: [ "$docs", "$$this" ] }
]
}
}
}
}
},
{
$unwind: "$R"
},
{
$replaceRoot: { newRoot: "$R" }
}
] )

You can try below query :
db.collection.aggregate([
/** Sort on joining_date field which will arrange docs with missing field at top & ascending where field exists */
{
$sort: {
joining_date: 1
}
},
/** group on empty & push every doc in collection to an array field named data */
{
$group: {
_id: "",
data: {
$push: "$$ROOT"
}
}
},
/** split data array into two array one has doc which doesn't field & other has docs which does have field */
{
$addFields: {
data: {
$reduce: {
input: "$data",
initialValue: {
missingField: [],
fieldExists: []
},
in: {
missingField: {
$cond: [
{
"$ifNull": [
"$$this.joining_date",
false
]
},
"$$value.missingField",
{
$concatArrays: [
"$$value.missingField",
[
"$$this"
]
]
}
]
},
fieldExists: {
$cond: [
{
"$ifNull": [
"$$this.joining_date",
false
]
},
{
$concatArrays: [
"$$value.fieldExists",
[
"$$this"
]
]
},
"$$value.fieldExists"
]
}
}
}
}
}
},
/** Add new field 'joining_date_count' to docs based on that doc index in fieldExists array &
* finally concatinate missingField with newly formed fieldExists array */
{
$addFields: {
"data": {
$concatArrays: [
"$data.missingField",
{
$map: {
input: "$data.fieldExists",
in: {
$mergeObjects: [
"$$this",
{
joining_date_count: {
$add: [
1,
{
$indexOfArray: [
"$data.fieldExists",
"$$this"
]
}
]
}
}
]
}
}
}
]
}
}
},
/** unwind data array */
{
$unwind: "$data"
},
/** replace each docs root as data field */
{
$replaceRoot: {
newRoot: "$data"
}
}
])
Test : MongoDB-Playground

Related

mongodb - How to sort by distance using geoNear in addition to looking up another collection

I have two functionalities working individually but want to combine them.
Functionality 1 - Sort users by their geoNear distance.
Functionality 2 - The users should not have already been liked by the
current user (look up partnership collection)
How to update this query to start from the user's collection so I can do geoNear?
The output in the below mongoplayground is correct except that the resulting users are not sorted by calculatedDist which is a field calculated by geoNear.
$geoNear: {
near: { type: "Point", coordinates: [x,y },
distanceField: "calculatedDist",
spherical: true
}
geoNear needs location which is only available in users collection hence I think below query needs to be modified to start in user's collection.
https://mongoplayground.net/p/7H_NxciKezB
db={
users: [
{
_id: "abc",
name: "abc",
group: 1,
location: {
type: "Point",
coordinates: [
54.23,
67.12
]
},
calculatedDist: 13
},
{
_id: "xyz",
name: "xyyy",
group: 1,
location: {
type: "Point",
coordinates: [
54.23,
67.12
]
},
calculatedDist: 11
},
{
_id: "123",
name: "yyy",
group: 1,
location: {
type: "Point",
coordinates: [
54.23,
67.12
]
},
calculatedDist: 2
},
{
_id: "rrr",
name: "tttt",
group: 1,
location: {
type: "Point",
coordinates: [
54.23,
67.12
]
},
calculatedDist: 11
},
{
_id: "eee",
name: "uuu",
group: 1,
location: {
type: "Point",
coordinates: [
54.23,
67.12
]
},
calculatedDist: 7
},
],
partnership: [
{
_id: "abc_123",
fromUser: "abc",
toUser: "123"
},
{
_id: "eee_rrr",
fromUser: "eee",
toUser: "rrr"
},
{
_id: "rrr_abc",
fromUser: "rrr",
toUser: "abc"
},
{
_id: "abc_rrr",
fromUser: "abc",
toUser: "rrr"
},
{
_id: "xyz_rrr",
fromUser: "xyz",
toUser: "rrr"
},
{
_id: "rrr_eee",
fromUser: "rrr",
toUser: "eee"
},
]
}
geoNear as far as I know has to be the first thing to be done so my query should start with the users collection. This breaks my partnership check because for that to work, I start at partnership collection.
In the playground above, the user eee has a lesser calculated distance as a result of geoNear but it shows after user abc.
Try this out:
db.partnership.aggregate([
// $geoNear
{
$match: {
$or: [
{
fromUser: "rrr"
},
{
toUser: "rrr"
}
]
}
},
{
$group: {
_id: 0,
from: {
$addToSet: "$fromUser"
},
to: {
$addToSet: "$toUser"
}
}
},
{
$project: {
_id: 0,
users: {
$filter: {
input: {
$setIntersection: [
"$from",
"$to"
]
},
cond: {
$ne: [
"$$this",
"rrr"
]
}
}
}
}
},
{
$lookup: {
from: "users",
let: {
userId: "$users"
},
pipeline: [
{
"$geoNear": {
"near": {
"type": "Point",
"coordinates": [
31.4998,
-61.4065
]
},
"distanceField": "calculatedDist",
"spherical": true
}
},
{
"$match": {
"$expr": {
"$in": [
"$_id",
"$$userId"
]
}
}
}
],
as: "users"
}
},
{
$project: {
users: 1,
count: {
$size: "$users"
}
}
}
])
Here, we use the pipelined form of lookup.
The lookup is on the user's collection, in which we specify a pipeline with the $geoNear stage as the first stage.
And finally filter out and only keep the users belonging to a partnership.
This is the playground link. Let me know if it works, on the playground I can't test it because $geoNear requires a 2d index.
While using calculatedDist, it looks like this:
db.partnership.aggregate([
// $geoNear
{
$match: {
$or: [
{
fromUser: "rrr"
},
{
toUser: "rrr"
}
]
}
},
{
$group: {
_id: 0,
from: {
$addToSet: "$fromUser"
},
to: {
$addToSet: "$toUser"
}
}
},
{
$project: {
_id: 0,
users: {
$filter: {
input: {
$setIntersection: [
"$from",
"$to"
]
},
cond: {
$ne: [
"$$this",
"rrr"
]
}
}
}
}
},
{
$lookup: {
from: "users",
let: {
userId: "$users"
},
pipeline: [
{
$sort: {
calculatedDist: 1
}
},
{
"$match": {
"$expr": {
"$in": [
"$_id",
"$$userId"
]
}
}
}
],
as: "users"
}
},
{
$project: {
users: 1,
count: {
$size: "$users"
}
}
}
])
Playground.

How to $count and $group within MongoDB aggregation?

I would like to count the status and group them by country.
Data:
[
{ id: 100, status: 'ordered', country: 'US', items: [] },
{ id: 101, status: 'ordered', country: 'UK', items: [] },
{ id: 102, status: 'shipped', country: 'UK', items: [] },
]
Desired aggregation outcome:
[
{ _id: 'US', status: { ordered: 1} },
{ _id: 'UK', status: { ordered: 1, shipped: 1 } }
]
I can $count and $group, but I am not sure how to put this together. Any hint is appreciated.
Thanks,
bluepuama
$group by country and status, and count total
$group by only country and construct array of status and count in key-value format
$set to update status field to object using $arrayToObject
db.collection.aggregate([
{
$group: {
_id: { country: "$country", status: "$status" },
count: { $sum: 1 }
}
},
{
$group: {
_id: "$_id.country",
status: { $push: { k: "$_id.status", v: "$count" } }
}
},
{ $set: { status: { $arrayToObject: "$status" } } }
])
Playground
You can do it with a single $group stage like so:
db.collection.aggregate([
{
$group: {
_id: "$country",
"shipped": {
$sum: {
$cond: [
{
$eq: [
"$status",
"ordered"
]
},
0,
1
]
}
},
"ordered": {
$sum: {
$cond: [
{
$eq: [
"$status",
"shipped"
]
},
0,
1
]
}
}
}
},
{
$project: {
_id: 1,
status: {
shipped: "$shipped",
ordered: "$ordered"
}
}
}
])
Mongo Playground

how to convert mongodb fields to subdocument fields?

sorry it may be silly question, mongodb contains thousands of documents .
hard to be changed manually
original format
{"name" : "aaaa",
"price" : 111,
"ing1" : "abcd",
"ing1Conc" : 50 ,
"ing2" : "wxyz",
"ing2conc": 100}
needed to be converted to
{"name" : "aaaa",
"price" : 111,
"content":[
{ "ing1" : "abcd", "ing1Conc" : 50},
{ "ing2" : "wxyz", "ing2conc": 100}
]
}
The trivial solution would be this one:
db.collection.aggregate([
{
$project: {
name: 1,
price: 1,
content: [
{ ing1: "$ing1", ing1Conc: "$ing1Conc" },
{ ing2: "$ing1", ing2conc: "$ing2conc" }
]
}
}
])
A more generic solution would be this one:
db.collection.aggrega
{
$project: {
name: 1,
price: 1,
data: {
$filter: {
input: { $objectToArray: "$$ROOT" },
cond: { $regexMatch: { input: "$$this.k", regex: "^ing\\d+" } }
}
}
}
},
{ $unwind: "$data" },
{ $set: { i: { $regexFind: { input: "$data.k", regex: "\\d+" } } } },
{ $set: { i: "$i.match" } },
{
$group: {
_id: {
name: "$name",
price: "$price",
i: "$i"
},
content: { $push: "$data" }
}
},
{ $sort: { "_id.i": 1 } },
{ $set: { content: { $arrayToObject: "$content" } } },
{
$group: {
_id: { name: "$_id.name", price: "$_id.price" },
content: { $push: "$content" }
}
},
{ $replaceRoot: { newRoot: { $mergeObjects: [ "$$ROOT", "$_id" ] } } },
{ $unset: "_id" }
])
Mongo Playground
However, I think this structure is still bad. I would suggest something like
content: {
ing: [ "abcd", "wxyz"],
conc: [ 50, 100 ]
}
content: [
{ ing: "abcd", conc: 50 },
{ ing: "wxyz", conc :100 }
]
content: [
{ idx: 1, ing: "abcd", conc: 50 },
{ idx: 2, ing: "wxyz", conc: 100 }
]

Add date to all records in a collection in ascending order using mongodb aggregation pipeline

is it possible in aggregation to add dates for all records from a particular date
[
{
_id: "bmasndvhjbcw",
name: "lucas",
occupation: "scientist",
present_working:true,
age: 55,
location: "texas",
},
{
_id: "bmasndvhjbcx",
name: "mark",
occupation: "scientist",
age: 45,
present_working:true,
location: "texas",
},
{
_id: "bmasndvhjbcq",
name: "cooper",
occupation: "physicist",
age: 69,
location: "texas",
}
]
is there a way to add dates for all records from this date date:2019-11-25T01:00:00.000+00:00 in ascending order like this
[
{
_id: "bmasndvhjbcw",
name: "lucas",
occupation: "scientist",
present_working:true,
age: 55,
location: "texas",
date:2019-11-25T01:00:00.000+00:00
},
{
_id: "bmasndvhjbcx",
name: "mark",
occupation: "scientist",
age: 45,
present_working:true,
location: "texas",
date:2019-11-26T01:00:00.000+00:00
},
{
_id: "bmasndvhjbcq",
name: "cooper",
occupation: "physicist",
age: 69,
location: "texas",
date:2019-11-27T01:00:00.000+00:00
}
]
mongodb version 4.0
You can achieve this with the following query :
db.collection.aggregate([
{
$group: {
_id: null,
docs: {
$push: "$$ROOT"
}
}
},
{
$unwind: {
path: "$docs",
includeArrayIndex: "index"
}
},
{
$addFields: {
"docs.date": {
$add: [
{
$dateFromString: {
dateString: "2019-11-25T01:00:00.000"
}
},
{
$multiply: [
"$index",
{
$multiply: [
24,
60,
60,
1000
]
}
]
}
]
}
}
},
{
$replaceRoot: {
newRoot: "$docs"
}
}
])
Test it here
This aggregation gets the expected result:
THIS_DATE = ISODate("2019-11-25T01:00:00.000+00:00")
db.collection.aggregate( [
{
$group: {
_id: null,
docs: { $push: "$$ROOT" }
}
},
{
$project: {
_id: 0,
docs: {
$map: {
input: { $range: [ 0, { $size: "$docs" } ] },
in: {
$mergeObjects: [
{ date: { $add: [ THIS_DATE, { $multiply: [ "$$this", 24*60*60*1000 ] } ] } },
{ $arrayElemAt: [ "$docs", "$$this" ] }
]
}
}
}
}
},
{
$unwind: "$docs"
},
{
$replaceRoot: { newRoot: "$docs" }
}
] )

Grouping and counting across documents?

I have a collection with documents similar to the following format:
{
departure:{name: "abe"},
arrival:{name: "tom"}
},
{
departure:{name: "bob"},
arrival:{name: "abe"}
}
And to get output like so:
{
name: "abe",
departureCount: 1,
arrivalCount: 1
},
{
name: "bob",
departureCount: 1,
arrivalCount: 0
},
{
name: "tom",
departureCount: 0,
arrivalCount: 1
}
I'm able to get the counts individually by doing a query for the specific data like so:
db.sched.aggregate([
{
"$group":{
_id: "$departure.name",
departureCount: {$sum: 1}
}
}
])
But I haven't figured out how to merge the arrival and departure name into one document along with counts for both. Any suggestions on how to accomplish this?
You should use a $map to split your doc into 2, then $unwind and $group..
[
{
$project: {
dep: '$departure.name',
arr: '$arrival.name'
}
},
{
$project: {
f: {
$map: {
input: {
$literal: ['dep', 'arr']
},
as: 'el',
in : {
type: '$$el',
name: {
$cond: [{
$eq: ['$$el', 'dep']
}, '$dep', '$arr']
}
}
}
}
}
},
{
$unwind: '$f'
}, {
$group: {
_id: {
'name': '$f.name'
},
departureCount: {
$sum: {
$cond: [{
$eq: ['$f.type', 'dep']
}, 1, 0]
}
},
arrivalCount: {
$sum: {
$cond: [{
$eq: ['$f.type', 'arr']
}, 1, 0]
}
}
}
}, {
$project: {
_id: 0,
name: '$_id.name',
departureCount: 1,
arrivalCount: 1
}
}
]