I need your help in aggregate functions in Mongo.
I have such aggregation:
const likes = await this.aggregate([
{
$match: { post: postId },
},
{
$group: {
_id: '$likeType',
count: { $sum: 1 },
},
},
]);
It collects all likes/dislikes for a post and returns this:
[ { _id: 'pos', count: 40 }, { _id: 'neg', count: 3 } ]
I faced a problem: if there is only one type of likes (for example only 'pos'), it returns this:
[ { _id: 'pos', count: 40 } ]
But I need this array to show zero value too:
[ { _id: 'pos', count: 40 }, { _id: 'neg', count: 0 } ]
Is there any way to set default values for all types of _ids?
I understand that it can't find any 'neg's and it can't return them. So I want to set defaults to let the system know, that there are only two types: 'pos' and 'neg'.
Are there any solutions for such cases?
Thanks!
My suggestion is:
Get distinct Ids: https://docs.mongodb.com/manual/reference/method/db.collection.distinct/
Do your search with your query param.
Filter distinct Ids which is not your query param. Append default values to result.
Related
I have this one schema
{
_id: "123456",
id: "123",
inventory: [
{
id: "foo",
count: 0
},
{
id: "bar",
count: 3
}
]
}
I wanted every "count" keys in the inventory array to be "price" which will look like this at the end:
{
_id: "123456",
id: "123",
inventory: [
{
id: "foo",
price: 0
},
{
id: "bar",
price: 3
}
]
}
And I've tried this
Model.updateOne({ id: "123" }, { $unset: { inventory: [{ count: 1 }] } } )
But it seems to be deleting the "inventory" field itself
The first thing here is to try to use $rename but how the docs explain:
$rename does not work if these fields are in array elements.
So is necessary to look for another method. So you can use this update with aggregation query:
This query uses mainly $map, $arrayToObject and $objectToArray. The trick here is:
Create a new field called inventory (overwrite existing one)
Iterate over every value of the array with $map, and then for each object in the array use $objectToArray to create an array and also iterate over that second array using again $map.
Into this second iteration create fields k and v. Field v will be the same (you don't want to change the value, only the key). And for field k you have to change only the one whose match with your condition, i.e. only change from count to price. If this condition is not matched then the key remain.
db.collection.update({},
[
{
$set: {
inventory: {
$map: {
input: "$inventory",
in: {
$arrayToObject: {
$map: {
input: {$objectToArray: "$$this"},
in: {
k: {
$cond: [
{
$eq: ["$$this.k","count"]
},
"price",
"$$this.k"
]
},
v: "$$this.v"
}
}
}
}
}
}
}
}
])
Example here
In MongoDB shell version v4.4.6
the following code works perfectly.
db['pri-msgs'].findOne({tag:'aaa&%qqq'},{msgs:{$slice:-2}})
But in nodeJs mongoDB the following code doesn't work.
db.collection('pri-msgs').findOne({
tag: 'aaa&%qqq'
}, {
msgs: {
slice: -2
}
})
My document-->
{"_id":{"$oid":"60c4730fadf6891850db90f9"},"tag":"aaa&%qqq","msgs":[{"msg":"abc","sender":0,"mID":"ctYAR5FDa","time":1},{"msg":"bcd","sender":0,"mID":"gCjgPf85z","time":2},{"msg":"def","sender":0,"mID":"lAhc4yLr6","time":3},{"msg":"efg","sender":0,"mID":"XcBLC2rGf","time":4,"edited":true},{"msg":"fgh","sender":0,"mID":"9RWVcEOlD","time":5},{"msg":"hij","sender":0,"mID":"TJXVTuWrR","time":6},{"msg":"jkl","sender":0,"mID":"HxUuzwrYN","time":7},{"msg":"klm","sender":0,"mID":"jXEOhARC2","time":8},{"msg":"mno","sender":0,"mID":"B8sVt4kCy","time":9}]}
Actually what I'm trying to do is Get last 2 itmes from msgs Array where time is greater than 'n'. Here 'n' is a number.
You can use aggregation-pipeline to get the results you are looking for. The steps are the following.
Match the documents you want by tag.
Unwind the msgs array.
Sort descending by msgs.time.
Limit first 2 elements.
Match the time you are looking for using a range query.
Group the documents back by _id.
Your query should look something like this:
db['pri-msgs'].aggregate([
{ $match: { tag: 'aaa&%qqq' } },
{ $unwind: '$msgs' },
{
$sort: {
'msgs.time': -1 //DESC
}
},
{ $limit: 2 },
{
$match: {
'msgs.time': {
$gt: 2 //n
}
}
},
{
$group: {
_id: '$_id',
tag: { $first: '$tag' },
msgs: {
$push: { msg: '$msgs.msg', sender: '$msgs.sender', mID: '$msgs.mID', time: '$msgs.time' }
}
}
}
]);
I have an object and the server will receive a user_id and a part_id. I need to get the certain user, then filter the parts by the provided part ID and get the price.
{
_id: 6086b8eec1f5325278846983,
user_id: '13',
car_name: 'Car name 1',
created_at: 2008-11-25T00:46:52.000Z,
parts: [
{
_id: 6086ee212681320190c3c8e0,
part_id: 'P456',
part_name: 'Part name 1',
image: 'image url',
stats: {
price: 10,
}
},
{
_id: 6087e7795e2ca925fc6ead27,
part_id: 'P905',
part_name: 'Part name 2',
image: 'image url',
stats: {
price: 15,
}
}
]
}
I tried to run the following, but ignores the part_id filter and returns every parts in the array.
Custumers.findOne({'user_id': '13', 'parts.part_id': 'P456'})
Also tried with aggregate but still no luck.
Customers.aggregate([
{ $match: { 'user_id': '13'}}
]).unwind('parts')
I checked the mongoose documentation but cannot wrap my head around it. Please let me know what I am missing.
Mongoose version: 5.12.4
Option - 1
This will work if you've only 1 matching parts
$ (projection)
The $ operator projects the first matching array element from each document in a collection based on some condition from the query statement.
Demo - https://mongoplayground.net/p/tgFL01fK3Te
db.collection.find(
{ "user_id": "13", "parts.part_id": "P456" },
{ "parts.$": 1, car_name: 1 } // add fields you need to projection
)
Option -2
$unwind
Demo - https://mongoplayground.net/p/_PeP0WHVpJH
db.collection.aggregate([
{
$match: {
"user_id": "13",
"parts.part_id": "P456"
}
},
{
$unwind: "$parts" // break into individual documents
},
{
$match: {
"parts.part_id": "P456"
}
}
])
Let's say I've got these values in database:
{
name: '1',
values: [{
subname: 'awesome'
}, {
surname: 'cool'
}]
}
how could I filter the array with only the value I'm interested in?
I would like to get as result of my find:
{
name: '1',
values: [{
subname: 'awesome'
}]
}
I thought maybe there is a possibility with select? Something like
MyCollection.find({name: '1'}).select(BLACK_MAGIC);
Where BLACK_MAGIC filters my array with the values I'm interested in, in this example values.subname = 'awesome'
Thx in advance for any ideas
Side note: I'm interesting to solve this with Mongoose queries and functions, not a solution with a post javascript on the resulting array
I think you could use aggregation for this.
You would $unwind so that each values object is in a separate document.
Then filter the results with $match.
MyCollection.aggregate([
{
$unwind: '$values'
},
{
$match: {
'values.subname': 'awesome'
}
},
// EDIT
{
$group: {
_id: '$_id',
name: {
$first: "$name"
},
values: {
$push: { subname: "$values.subname" }
}
}
}
], function (err, results) {
});
If it works, you are little bit closer. The only thing is that values is an object, not an array of one object. You could probably use $group with $first to get desired result.
Good afternoon all,
I am having a really tough time working with aggregation queries in MongoDB 3.4. I have a problem that is asking me to do push the results of my aggregation query into an empty array called categories which I have been able to do successfully using this code:
var categories = [];
database.collection("item").aggregate([{
$group : {
_id : "$category",
num : {$sum : 1}
}},
{$sort:{_id:1}}]).toArray(function(err, data){
categories.push(...data);
callback(categories);
console.log(categories);
})
}
categories looks like this:
[ { _id: 'Apparel', num: 6 },
{ _id: 'Books', num: 3 },
{ _id: 'Electronics', num: 3 },
{ _id: 'Kitchen', num: 3 },
{ _id: 'Office', num: 2 },
{ _id: 'Stickers', num: 2 },
{ _id: 'Swag', num: 2 },
{ _id: 'Umbrellas', num: 2 } ]
Next I have the following task:
In addition to the categories created by your aggregation query,
include a document for category "All" in the array of categories
passed to the callback. The "All" category should contain the total
number of items across all categories as its value for "num". The
most efficient way to calculate this value is to iterate through
the array of categories produced by your aggregation query, summing
counts of items in each category.
The problem is that it seems like inside my .toArray() method the data parameter sometimes acts like an array and sometimes not. For example if I wanted to add perhaps just the value of the num key to the categories array like so: categories.push(...data["num"]) I get an error stating undefined is not iterable.
Since I cannot iterate over each data.num key I cannot extract it's value and add it to a running total of all data.num values.
What am I not understanding about what is going on here?
You don't need to use application logic to group data, mongoDB aggregation is made for this task. Add another $group to your query with a new field All that $sum your $num field and $push all documents to a new field called categories :
db.item.aggregate([{
$group: {
_id: "$category",
num: { $sum: 1 }
}
}, { $sort: { _id: 1 } }, {
$group: {
_id: 1,
All: { $sum: "$num" },
categories: {
$push: {
_id: "$_id",
num: "$num"
}
}
}
}])
It gives :
{
"_id": 1,
"All": 23,
"categories": [{
"_id": "Swag",
"num": 2
}, {
"_id": "Office",
"num": 2
}, {
"_id": "Stickers",
"num": 2
}, {
"_id": "Apparel",
"num": 6
}, {
"_id": "Umbrellas",
"num": 2
}, {
"_id": "Kitchen",
"num": 3
}, {
"_id": "Books",
"num": 3
}, {
"_id": "Electronics",
"num": 3
}]
}
For consuming the output, data is an array, to access the first element use data[0] :
var categories = [];
database.collection("item").aggregate([{
$group: {
_id: "$category",
num: { $sum: 1 }
}
}, { $sort: { _id: 1 } }, {
$group: {
_id: 1,
All: { $sum: "$num" },
categories: {
$push: {
_id: "$_id",
num: "$num"
}
}
}
}]).toArray(function(err, data) {
var totalCount = data[0]["All"];
console.log("total count is " + totalCount);
categories = data[0]["categories"];
for (var i = 0; i < categories.length; i++) {
console.log("category : " + categories[i]._id + " | count : " + categories[i].num);
}
})
What I wanted to achieve was pushing or unshifting as we'll see in a moment an object that looked like this into my categories array:
var allCategory = {
_id: "All",
num: [sum of all data.num values]
}
I ended up messing with .reduce() method and used it on the categories array. I got lucky through some console.log-ing and ended up making this:
var categories = [];
database.collection("item").aggregate([{
$group : {
_id : "$category",
num : {$sum : 1}
}},
{$sort:{_id:1}}]).toArray(function(err, data){
categories.push(...data);
var sum = categories.reduce(function(acc, val){
// console.log(acc, val["num"])
return acc + val["num"]
},0);
var allCategory = {
_id: "All",
num: sum
}
categories.unshift(allCategory)
callback(categories);
})
First I use a spread operator to push all the objects from data into categories. Then declare sum which runs .reduce() on categories returning the accumulation of val["num"] which is really data.num (console log is life). I create the allCategory document/object then use .unshift to place it at the beginning of my categories array (this placement was a requirement) then use my callback.
I think it's a hacky way of accomplishing my goal and I had to go through some trial and error as to the correct order of methods and variables in my .toArray(). Yet it worked and I learned something. Thanks for the help #Bertrand Martel .