Mongoose get latest document for every unique value in field - node.js

I have a Mongoose schema which is like this:
const mySchema = new mongoose.Schema(
{
_id: String,
coin: String,
closeTime: Number,
volume: Number,
}
I have a bunch of different coins. Is it possible to get the latest document for each unique coin I have based on the closeTime?
Help would be much appreciated! Thanks.

You can use aggregation to sort by latest/newest closeTime, group by coin then getting the first document of that group:
mySchema.aggregate([
{ $sort: { closeTime: -1 } },
{ $group: { _id: "$coin", latest: { $first: "$$ROOT" } } }
])
This is sorting with numeric closeTime in descending order, getting the first/latest document and putting its data into a property called latest. This should create results like:
[{
"_id" : "2",
"latest" : {
"_id" : ObjectId("6149f106742bb30e2529c453"),
"coin" : "foo",
"closeTime" : 5,
"volume" : 1
}
}
{
"_id" : "1",
"latest" : {
"_id" : ObjectId("6149f111742bb30e2529c45f"),
"coin" : "bar",
"closeTime" : 4,
"volume" : 1
}
}]
You can take this one step further with other aggregation stages to extract/project the underlying coin document.

Related

MongoDB - Mongoose find command to get only values from an embedded object

My Schema looks something like this.
{
_id: '1',
items: {
'id1': 'item1',
'id2': 'item2',
'id3': 'item3'
}
}
Following is the query
ItemModel.find({}, {
items: 1,
_id: 0
});
And the result of the find query is:
{ "items" : { "21" : "item21", "22" : "item22", "23" : "item23" } }
{ "items" : { "31" : "item31", "32" : "item32", "33" : "item33" } }
{ "items" : { "11" : "item11", "12" : "item32", "13" : "item13" } }
What I want is:
["item21", "item22", "item23",
"item31", "item32", "item33",
"item11", "item12", "item13"]
Currently, I am doing the processing on the node.js end for getting the above. I want to reduce the output payload size coming from MongoDB. The "items" key is redundant and the IDs mentioned are not required as well when I fetch it. Here, the IDs are quite small like 21, 22, 13, etc. but those are acutally 50 characters in length.
If not the above, any other efficient alternatives are also welcome.
One example of how to achieve that is the following aggregation:
[
{
$project: {
items: {
$objectToArray: '$items',
},
},
},
{ $unwind: '$items' },
{
$project: {
items: '$items.v',
},
},
{
$group: {
_id: null,
items: {
$push: '$items',
},
},
}
];
What this does is first we convert with $project & $objectToArray field to an array so that we could use $unwind. This way we'll have documents with different items. Now we convert with another $project to make it a string instead of an object (which would be { v: <value>, k: <value> }. And, finally, we $group them together.
Final result:
To get exactly that list, you'll need in your code to access items field, like result[0].items ([0] because aggregation will return an array).

How to find the count of objects in an array which is a value for a key in all mongodb documents in a collection

Here is the collection.
{{
"id" : "123",
"likes" : [ {
"member" : "3041"
},
{
"member" : "3141"
}]
},
{
"id" : "124",
"likes" : [ {
"member" : "3241"
},
{
"member" : "3241"
},
{
"member" : "3341"
}]
}}
How to retrieve the count of number of objects of likes key for each document?
In this format:
[{
"id" : "123",
"likesCount" : 2
},
{
"id" : "124",
"likesCount" : 3
}]
This should do it for you:
db.collection.aggregate([
{
$project: {
_id: 0,
id: 1,
likesCount: {
$size: "$likes"
}
}
}
])
You are using an aggregation and in the $project part of it use $size to get the length of the array.
You can see it working here
I think you have to map the collection to transform the objects within it into the shape you want.
In this case we get the object and extract the id and instead of all the likes we just get the length of them.
let newCollection = collection.map(obj => ({
id: obj.id,
likesCount: obj.likes.length
}));

how to group in mongoDB and return all fields in result

I am using aggregate method in mongoDB to group but when I use $group it returns the only field which I used to group. I have tried $project but it is not working either. I also tried $first and it worked but the result data is now in different format.
The response format I need looks like:
{
"_id" : ObjectId("5b814b2852d47e00514d6a09"),
"tags" : [],
"name" : "name here",
"rating" : "123456789"
}
and after adding $group in my query.response is like this, the value of _id changes. (and the $group is taking only _id, if i try any other keyword it throws an error of accumulator something. please explain this also.)
{
"_id" :"name here" //the value of _id changed to the name field which i used in $group condition
}
I have to remove the duplicates in name field, without changing any structure and fields. also I am using nodeJS with mongoose, so please provide the solution that works with it.
You can use below aggregation query.
$$ROOT to keep the whole document per each name followed by $replaceRoot to promote the document to the top.
db.col.aggregate([
{"$group":{"_id":"$name","doc":{"$first":"$$ROOT"}}},
{"$replaceRoot":{"newRoot":"$doc"}}
])
user2683814's solution worked for me but in my case, I have a counter accumulator when we replace the newRoot object, the count field is missing in the final stage so I've used $mergeObjects operator to get my count field back.
db.collection.aggregate([
{
$group: {
_id: '$product',
detail: { $first: '$$ROOT' },
count: {
$sum: 1,
},
},
},
{
$replaceRoot: {
newRoot: { $mergeObjects: [{ count: '$count' }, '$detail'] },
},
}])
When you group data on any database, it means you want to perform accumulated operation on the required field and the other field which will not be include in accumulated operation will be used in group like
db.collection.aggregate([{
$group: {
_id: { field1: "", field1: "" },
acc: { $sum: 1 }
}}]
here in _id object will contains all other fields which you want to hold.
for your data you can try this
db.collection.aggregate([{
$group: {
_id: "$name",
rating: { $first: "$rating" },
tags: { $first: "$tag" },
docid: { $first: "$_id" }
}
},
{
$project: {
_id: "$docid",
name: "$_id",
rating: 1,
tags: 1
}
}])
You can use this query
db.col.aggregate([
{"$group" : {"_id" : "$name","data" : {"$first" : "$$ROOT"}}},
{"$project" : {
"tags" : "$data.tags",
"name" : "$data.name",
"rating" : "$data.rating",
"_id" : "$data._id"
}
}])
I wanted to group my collection by groupById field and store it as key value pairs having key as groupById and value as all the items of that group.
db.col.aggregate([{$group :{_id :"$groupById",newfieldname:{$push:"$"}}}]).pretty()
This is working fine for me..

Mongoose - finding subdocuments by criteria

I've just got stuck with this problem. I've got two Mongoose schemas:
var childrenSchema = mongoose.Schema({
name: {
type: String
},
age: {
type: Number,
min: 0
}
});
var parentSchema = mongoose.Schema({
name : {
type: String
},
children: [childrenSchema]
});
Question is, how to fetch all subdocuments (in this case, childrenSchema objects) from every parent document? Let's suppose I have some data:
var parents = [
{ name: "John Smith",
children: [
{ name: "Peter", age: 2 }, { name: "Margaret", age: 20 }
]},
{ name: "Another Smith",
children: [
{ name: "Martha", age: 10 }, { name: "John", age: 22 }
]}
];
I would like to retrieve - in a single query - all children older than 18. Is it possible? Every answer will be appreciated, thanks!
You can use $elemMatch as a query-projection operator in the most recent MongoDB versions. From the mongo shell:
db.parents.find(
{'children.age': {$gte: 18}},
{children:{$elemMatch:{age: {$gte: 18}}}})
This filters younger children's documents out of the children array:
{ "_id" : ..., "children" : [ { "name" : "Margaret", "age" : 20 } ] }
{ "_id" : ..., "children" : [ { "name" : "John", "age" : 22 } ] }
As you can see, children are still grouped inside their parent documents. MongoDB queries return documents from collections. You can use the aggregation framework's $unwind method to split them into separate documents:
> db.parents.aggregate({
$match: {'children.age': {$gte: 18}}
}, {
$unwind: '$children'
}, {
$match: {'children.age': {$gte: 18}}
}, {
$project: {
name: '$children.name',
age:'$children.age'
}
})
{
"result" : [
{
"_id" : ObjectId("51a7bf04dacca8ba98434eb5"),
"name" : "Margaret",
"age" : 20
},
{
"_id" : ObjectId("51a7bf04dacca8ba98434eb6"),
"name" : "John",
"age" : 22
}
],
"ok" : 1
}
I repeat the $match clause for performance: the first time through it eliminates parents with no children at least 18 years old, so the $unwind only considers useful documents. The second $match removes $unwind output that doesn't match, and the $project hoists children's info from subdocuments to the top level.
In Mongoose, you can also use the elegant .populate() function like this:
parents
.find({})
.populate({
path: 'children',
match: { age: { $gte: 18 }},
select: 'name age -_id'
})
.exec()
A. Jesse Jiryu Davis's response works like a charm, however for later versions of Mongoose (Mongoose 5.x) we get the error:
Mongoose 5.x disallows passing a spread of operators to Model.aggregate(). Instead of Model.aggregate({ $match }, { $skip }), do Model.aggregate([{ $match }, { $skip }])
So the code would simply now be:
> db.parents.aggregate([{
$match: {'children.age': {$gte: 18}}
}, {
$unwind: '$children'
}, {
$match: {'children.age': {$gte: 18}}
}, {
$project: {
name: '$children.name',
age:'$children.age'
}
}])
{
"result" : [
{
"_id" : ObjectId("51a7bf04dacca8ba98434eb5"),
"name" : "Margaret",
"age" : 20
},
{
"_id" : ObjectId("51a7bf04dacca8ba98434eb6"),
"name" : "John",
"age" : 22
}
],
"ok" : 1
}
(note the array brackets around the queries)
Hope this helps someone!

Mongoose nodejs - Grouping with arrays

I have got a collection with the following schema :
{OrderId : id, CustomerId : id, amount: Number, otherPayers : [{name : String, amount : Number}]}
I would like to make an average of the "amount" of all orders for a customerId, and I can do that with aggregate by grouping on customerId with avg on amount.
Now, I'd like the output "amount" field to be not only the avg of "amount", but also of the "amount" field of the "otherPayers" field.
I have had a look into mapreduce but I can't get it to work.
Any idea? In SQL it would have been quite simple with a subquery.
I'm guessing you want to average together all the amounts inside the otherPayers array plus the amount field on the top level of the document. Here is how you would do it with aggregation framework:
unwind = { $unwind : "$otherPayers" };
group = { $ group : { _id : "$CustomerId",
amt : { $first : "$amount" },
count : { $sum : 1 },
total : { $sum : "$otherPayers.amount" }
} };
project = { $project : { _id : 0,
CustomerId : "$_id",
avgPaid : { $divide : [
{ $add : [ "$total", "$amt" ] },
{ $add : [ "$count", 1 ] }
] }
} };
db.collection.aggregate(unwind, group, project);

Resources