How to ORDER BY FIELD VALUE in MongoDB - mongodb

In Mysql I often use the FIELD() function in the ORDER BY clause:
ORDER BY FIElD(id, '1', '6', '3', ...);
How does one get the same results in MongoDB? I tried the following:
.find(...).sort({id: [1, 6, 3]})
This did not work

We can use $indexOfArray
Console
db.collectionName.aggregate([{
$match: {
_id: {
$in: [249, 244]
}
}
}, {
$addFields: {
sort: {
$indexOfArray: [
[249, 244], "$_id"
]
}
}
},{
$sort: {
sort: 1
}
}])
PHP code
$data = $this->mongo->{$collectionName}->aggregate(
[
[
'$match' => ['_id' => ['$in' => $idList]]
],
[
'$addFields' => ['sort' => ['$indexOfArray' => [$idList, '$_id']]]
],
[
'$sort' => ['sort' => 1]
],
[
'$project' => [
'name' => 1
]
]
]
);

So for the record:
Given the array [1,6,3] what you want in your query is this:
db.collection.aggregate([
{ "$project": {
"weight": { "$cond": [
{ "$eq": ["_id": 1] },
3,
{ "$cond": [
{ "$eq": ["_id": 6] },
2,
{ "$cond": [
{ "$eq": ["_id": 3] },
1,
0
]},
]},
]}
}},
{ "$sort": { "weight": -1 } }
])
And that gives you specific "weights" by order of your "array" of inputs to "project" weights upon the results.

Related

mongoDB group by date and other column

i need some help grouping by date and by other column, at the moment i got:
[
{
'$project': {
'date': 1,
'source': 1,
'callDirection': 1,
'status': 1
}
}, {
'$match': {
'$or': [
{
'source': '501'
}, {
'source': '555'
}
]
}
}, {
'$group': {
'_id': 0,
'total': {
'$sum': 1
},
'answered': {
'$sum': {
'$cond': [
{
'$eq': [
'$status', 'ANSWERED'
]
}, 1, 0
]
}
},
'no answer': {
'$sum': {
'$cond': [
{
'$eq': [
'$status', 'NO ANSWER'
]
}, 1, 0
]
}
}
}
}
]
the result i got now is the totals:
_id:0
total:591
answered:443
no answer:129
what i need is to split the data by source and by date so i get the data return like this
date => 2022-01-23 , source => 501, answered => 12, noanswer => 2
date => 2022-01-23 , source => 555, answered => 5, noanswer => 5
date => 2022-01-24 , source => 501, answered => 6, noanswer => 3
date => 2022-01-24 , source => 555, answered => 22, noanswer => 6
example data:
"date": "2021-12-23 10:25:59","source": "501","callDirection": "Outgoing","status": "ANSWERED"
"date": "2021-12-23 11:21:19","source": "501","callDirection": "Outgoing","status": "NO ANSWER"
"date": "2021-12-24 01:21:19","source": "501","callDirection": "Outgoing","status": "ANSWERED"
"date": "2021-12-24 10:25:59","source": "555","callDirection": "Outgoing","status": "ANSWERED"
"date": "2021-12-25 12:55:19","source": "555","callDirection": "Outgoing","status": "ANSWERED"
im new to mongoDb and i need some help ,thanks a lot
Perhaps Something like this:
db.collection.aggregate([
{
$addFields: {
date: {
$substr: [
"$date",
0,
10
]
}
}
},
{
$group: {
_id: {
da: "$date",
so: "$source",
cd: "$callDirection"
},
answer: {
"$sum": {
"$cond": [
{
"$eq": [
"ANSWERED",
"$status"
]
},
1,
0
]
}
},
noanswer: {
"$sum": {
"$cond": [
{
"$eq": [
"ANSWERED",
"$status"
]
},
0,
1
]
}
}
}
},
{
$project: {
date: "$_id.da",
source: "$_id.so",
callDirection: "$_id.cd",
answer: 1,
noanswer: 1
}
}
])
Explained:
Replace the datetime string with date only string
Group by date,source & callDirection generating two new counting fields answer and noanswer from the status field.
Project only the necessary fields are needed
playground

MongoDB - Update a parent array field using another child array field

I've a collection like this
db.aa1.insertMany([
{ parentArr: [] },
{ parentArr: [
{childArr: [ {childField: 2}, {childField: 4} ]}
] },
{ parentArr: [
{childArr: []}
] },
{ parentArr: [
{childArr: [ {childField: 3}, {childField: 5} ]}
] },
])
Now I want the end result to be like
[
{ parentArr: [] },
{ parentArr: [ { childArr: [] } ] },
{ parentArr: [
{
childArr: [ {childField: 2}, {childField: 4} ],
parentField: [2, 4]
},
] },
{ parentArr: [
{
childArr: [ {childField: 3}, {childField: 5} ],
parentField: [3, 5]
}
] },
]
Here I've copied the childArr.childField values in the parentArr.parentField.
Now in plain JS, I could do something like this
parentArr.forEach(p => p.parentField = p.childArr ? p.childArr.map(c => c.childField) : [])
How can I achieve this using a MongoDB Query?
I've tried the following $push $set combinations, of course, one at a time.
For the example sake, I've written all push and set together.
db.myCollection.update(
{
"parentArr.childArr.0": {$exists: true}
},
{
$set: {"parentArr.$[].parentField": ["$parentArr.$[].childArr.$[].childField"]}
$set: {"parentArr.parentField": ["$parentArr.childArr.childField"]}
$push: {
"parentArr.$[].parentField": {$each: ["$parentArr.$[].childArr.$[].childField"]}
}
$push: {
"parentArr.parentField": {$each: ["$parentArr.childArr.childField"]}
}
},
{
upsert: true,
multi: true
}
)
If you're using Mongo version 4.2+ they have introduced pipeline'd updates meaning we now have more power when updating:
db.aa1.updateMany(
{
"parentArr.childArr.childField": {$exists: true}
},
[
{
$set: {
"parentArr.parentField": {
$reduce: {
input: {
$map: {
input: "$parentArr",
as: "parent",
in: {
$map: {
input: "$$parent.childArr",
as: "child",
in: "$$child.childField"
}
}
}
},
initialValue: [],
in: {$setUnion: ["$$value", "$$this"]}
}
}
}
}
]
)
If you're on an older Mongo version then you'll have to do it in code, as you already posted a relevant snippet I have no more to add.

How to use Mongo Aggregation to limit results around a given input?

I looked through the pipeline stages docs, but did not see how to do this.
Suppose you have a user, and each user has points.
User Points
A 22
B 11
C 15
D 7
So, we use '$sort': { points: -1 } to order the users by points.
Is it possible to use a Mongo Aggregation Stage to find the users before and after a given user?
So, given user C (by id), it would return [A, C, B].
Very interesting question. Maybe exists any better solution.
Disclaimer: I assume the user points is unique
We can use $facet to get expected result, but at high cost (very large query)
db.collection.aggregate([
{
$facet: {
"givenUser": [
{
$match: {
"user": "C"
}
}
],
"allUser": [
{
$sort: {
"Points": -1
}
}
],
"orderedPoints": [
{
$sort: {
"Points": -1
}
},
{
$group: {
_id: null,
Points: {
$push: "$Points"
}
}
},
{
$unwind: "$Points"
}
]
}
},
{
$project: {
allUser: 1,
currIndex: {
$indexOfArray: [
"$orderedPoints.Points",
{
$arrayElemAt: [
"$givenUser.Points",
0
]
}
]
},
beforeIndex: {
$add: [
{
$indexOfArray: [
"$orderedPoints.Points",
{
$arrayElemAt: [
"$givenUser.Points",
0
]
}
]
},
-1
]
},
afterIndex: {
$add: [
{
$indexOfArray: [
"$orderedPoints.Points",
{
$arrayElemAt: [
"$givenUser.Points",
0
]
}
]
},
1
]
}
}
},
{
$project: {
result: [
{
$arrayElemAt: [
"$allUser",
{
$cond: {
if: {
$lt: [
"$beforeIndex",
0
]
},
then: 999,
else: "$beforeIndex"
}
}
]
},
{
$arrayElemAt: [
"$allUser",
"$currIndex"
]
},
{
$arrayElemAt: [
"$allUser",
"$afterIndex"
]
}
]
}
}
])
[
{
"result": [
{
"Points": 22,
"_id": ObjectId("5a934e000102030405000000"),
"user": "A"
},
{
"Points": 15,
"_id": ObjectId("5a934e000102030405000002"),
"user": "C"
},
{
"Points": 11,
"_id": ObjectId("5a934e000102030405000001"),
"user": "B"
}
]
}
]
MongoPlayground
Steps:
We keep into separate fields:
Given user (C),
Order all users by points
Order all points and store inside array (I wish MongoDB allows find array index by object too)
Now we find given user index, calculate indexes for "before"/"after" players.
Now, we create result with 3 elements (before, current, after).
Note: If given user is first / last, we ensure to return null for before / after items.

Averaging across list indicies during aggregation pipeline?

I currently have a MongoDB aggregation pipeline that ends with the following type of synthetic documents
[
{
'_id': '2019-09-10',
'grouped_foos':
[
{... 'foo': [1, 78, 100]},
{... 'foo': [8, 66, 98]},
{... 'foo': [99, 5, 33]},
{... 'foo': [120, 32, 2]}
]
},
{
'_id': '2019-09-09',
'grouped_foos':
[
{... 'foo': [10, 27]},
{... 'foo': [19, 66]}
]
},
{
'_id': '2019-09-08',
'grouped_foos':
[
{... 'foo': [1]}
]
}
]
I would like to continue this pipeline and average the indices of the foo lists together to form documents that look like
[
{
'_id': '2019-09-10',
'avg_foo': [57, 45.25, 58.25]
},
{
'_id': '2019-09-09',
'avg_foo': [14.5, 46.5]
},
{
'_id': '2019-09-08',
'avg_foo': [1]
}
]
Is this type of averaging possible during aggregation? Do I potentially need to $unwind the lists with indexing and assign new _id for uniqueness to make documents that look like
[
{
'_id': UUID,
'old_id': '2019-09-10',
'foo': 1,
'index': 0
},
{
'_id': UUID,
'old_id': '2019-09-10',
'foo': 78,
'index': 1
},
........
]
Basically you can try with $unwind but easier and faster approach would be to use $reduce to $map and $sum all the rows from grouped_foos. Then you'll be able to run another $map and use $divide to get the average.
db.collection.aggregate([
{
$project: {
size: { $size: "$grouped_foos" },
foo_sum: {
$reduce: {
input: "$grouped_foos",
initialValue: [],
in: {
$map: {
input: { $range: [ 0, { $size: "$$this.foo" }, 1 ] },
as: "index",
in: {
$add: [
{ $arrayElemAt: [ "$$this.foo", "$$index" ] },
{ $ifNull: [ { $arrayElemAt: [ "$$value", "$$index" ] }, 0 ] }
]
}
}
}
}
}
}
},
{
$project: {
_id: 1,
avg_foo: {
$map: {
input: "$foo_sum",
in: {
$divide: [ "$$this", "$size" ]
}
}
}
}
}
])
Mongo Playground

MongoDB. Aggregate the sum of two arrays sizes

With MongoDB 3.4.10 and mongoose 4.13.6 I'm able to count sizes of two arrays on the User model:
User.aggregate()
.project({
'_id': 1,
'leftVotesCount': { '$size': '$leftVoted' },
'rightVotesCount': { '$size': '$rightVoted' }
})
where my Users are (per db.users.find())
{ "_id" : ObjectId("5a2b21e63023c6117085c240"), "rightVoted" : [ 2 ],
"leftVoted" : [ 1, 6 ] }
{ "_id" : ObjectId("5a2c0d68efde3416bc8b7020"), "rightVoted" : [ 2 ],
"leftVoted" : [ 1 ] }
Here I'm getting expected result:
[ { _id: '5a2b21e63023c6117085c240', leftVotesCount: 2, rightVotesCount: 1 },
{ _id: '5a2c0d68efde3416bc8b7020', leftVotesCount: 1, rightVotesCount: 1 } ]
Question. How can I get a cumulative value of leftVotesCount and rightVotesCount data? I tried folowing:
User.aggregate()
.project({
'_id': 1,
'leftVotesCount': { '$size': '$leftVoted' },
'rightVotesCount': { '$size': '$rightVoted' },
'votesCount': { '$add': [ '$leftVotesCount', '$rightVotesCount' ] },
'votesCount2': { '$sum': [ '$leftVotesCount', '$rightVotesCount' ] }
})
But votesCount is null and votesCount2 is 0 for both users. I'm expecting votesCount = 3 for User 1 and votesCount = 2 for User 2.
$leftVotesCount, $rightVotesCount become available only on the next stage. Try something like:
User.aggregate()
.project({
'_id': 1,
'leftVotesCount': { '$size': '$leftVoted' },
'rightVotesCount': { '$size': '$rightVoted' }
})
.project({
'_id': 1,
'leftVotesCount': 1,
'rightVotesCount': 1
'votesCount': { '$add': [ '$leftVotesCount', '$rightVotesCount' ] },
'votesCount2': { '$sum': [ '$leftVotesCount', '$rightVotesCount' ] }
})
You can't reference the project variables created in the same project stage.
You can wrap the variables in a $let expression.
User.aggregate().project({
"$let": {
"vars": {
"leftVotesCount": {
"$size": "$leftVoted"
},
"rightVotesCount": {
"$size": "$rightVoted"
}
},
"in": {
"votesCount": {
"$add": [
"$$leftVotesCount",
"$$rightVotesCount"
]
},
"leftVotesCount": "$$leftVotesCount",
"rightVotesCount": "$$rightVotesCount"
}
}
})
It turned out that $add supports nested expressions, so I was able to solve the issue by excluding intermediate variables:
User.aggregate().project({
'_id': 1,
'votesCount': { '$add': [ { '$size': '$leftVoted' }, { '$size': '$rightVoted' } ] }
});
// [ {_id: '...', votesCount: 3}, {_id: '...', votesCount: 2} ]