Mongodb Aggregate with natural order - node.js

I am looking for a way to query database to fetch last saved entry in the collection.
I have a function which saves the document to the collection this is hwo my saved collection looks like.
{
"_id": {
"$oid": "5ebbf2b4586b4946226e2c88"
},
"name": "Stept_1",
"description": "",
"coordinates": {
"pose": {
"$numberInt": "0"
},
"x": {
"$numberDouble": "-9.760518723445719"
},
"y": {
"$numberDouble": "-3.4586615766853854"
},
"z": {
"$numberInt": "0"
}
},
"depth": {
"$numberInt": "1"
},
"_neighbours": [],
"optional": {},
"__v": {
"$Reference": "1111111"
}
}
Each document is saved with name in ascending order e.g. name:Step_1, Step_2 etc.
I have tried fetching the last saved documents using aggregate method like
db.collection.aggregate([
{ $sort: { name: -1 } },
{ $group: { _id: "$Reference", name: { $first: "$name" } } }
])
This returns the data in ascending order till name:Step_10 i.e. "Step_1,2,3,..." but once after name:Step_1o if I restart the app and again start fetching the last saved document the index of ascending order returns to name:Step_9 which then lead some duplicate entries of few documents. What I am looking for is it should always return follow natural order(i.e. Step_1,2,3,..11,12,13 etc) while fetching those documents.
Thanks in advance.

You can use collation to specify numericOrdering like so:
db.collection.aggregate([
{ $sort: { name: -1 } },
{ $group: { _id: "$Reference", name: { $first: "$name" } } }
],
{ collation : {
locale: "en_US",
numericOrdering: true
}}
)
From mongo docs:
numericOrdering boolean
Optional. Flag that determines whether to compare numeric strings as numbers or as strings.
If true, compare as numbers; i.e. "10" is greater than "2".
If false, compare as strings; i.e. "10" is less than "2".
Default is false.

Related

How to delete documents in MongoDb according to date

So what I want to do is group all documents having same hash whose count is more than 1 and only keep the oldest record according to startDate
My db structure is as follows:
[{
"_id": "82bacef1915f4a75e6a18406",
"Hash": "cdb3d507734383260b1d26bd3edcdfac",
"duration": 12,
"price": 999,
"purchaseType": "Complementary",
"startDate": {
"$date": {
"$numberLong": "1656409841000"
}
},
"endDate": {
"$date": {
"$numberLong": "1687859441000"
}
}
}]
I was using this query which I created
db.Mydb.aggregate([
{
"$group": {
_id: {hash: "$Hash"},
dups: { $addToSet: "$_id" } ,
count: { $sum : 1 }
}
},{"$sort":{startDate:-1}},
{
"$match": {
count: { "$gt": 1 }
}
}
]).forEach(function(doc) {
doc.dups.shift();
db.Mydb.deleteMany({
_id: {$in: doc.dups}
});
})
this gives a result like this:
{ _id: { hash: '1c01ef475d072f207c4485d0a6448334' },
dups:
[ '6307501ca03c94389f09b782',
'6307501ca03c94389f09b783',
'62bacef1915f4a75e6a18l06' ],
count: 3 }
The problem with this is that the _id's in dups array are random everytime I run this query i.e. not sorted according to startDate field.
What can be done here?
Any help is appreciated. Thanks!
After $group stage, startDate field will not pre present in the results, so you can not sort based on that field. So, as stated in the comments, you should put $sort stage first in the Aggregation pipeline.
db.Mydb.aggregate([
{
"$sort": { startDate: -1}
},
{
"$group": {
_id: {hash: "$Hash"},
dups: { $addToSet: "$_id" } ,
count: { $sum : 1 }
},
{
"$match": { count: { "$gt": 1 }
}
]
Got the solution. I was using $addToSet in the group pipeline stage which does not allow duplicate values. Instead, I used $push which allows duplicate elements in the array or set.

MongoDB aggregation: Check if user has already rated a product

I'm having some trouble coming up with a solution to this problem by using mongo directly instead of node.
I have a collection of products that have several fields, including an array of ratings. The ratings consist of the id of the user who rated it and the score. They look like this:
{
"_id": {
"$oid": "62812fac06f29f5fe874c0db"
},
"model": "fire",
"allRatings": [
{
"user_id": "6282962e3aa8163084776092",
"rating": 9
},
{
"user_id": "62811d520f52b64990a94e99",
"rating": 6
}
],
}
I'd like to return all of the fields, but instead of all the ratings, I just want to return a boolean that shows if the current user, whose ID I have in the code, has already rated the product. I have tried with aggregations, but haven't found any solution that works.
Something like this:
{
"_id": {
"$oid": "62812fac06f29f5fe874c0db"
},
"model": "fire",
"hasRated": true,
}
If I've understood correctly you can use this aggregation query:
This query looks for the object you want (matching by _id) and then, into a $project stage it set the value hasRated if the desired id exists into the array.
db.collection.aggregate([
{
$match: {
_id: ObjectId("62812fac06f29f5fe874c0db")
}
},
{
$project: {
model: 1,
hasRated: {
"$in": [
"6282962e3aa8163084776092",
"$allRatings.user_id"
]
}
}
}
])
Example here
To do in JS you can simply create the object adding the id as a variable (something like this, is pseudocode, not tested):
const id = 'your_id_here'
const agg = [{
$match: {
_id: new ObjectId("62812fac06f29f5fe874c0db")
}
},
{
$project: {
model: 1,
hasRated: {
"$in": [
id,
"$allRatings.user_id"
]
}
}
}]
db.aggregate(agg)

Mongodb project one field as collective array in one document as result

This is data
[ {
"_id": "5c75802b1312ca10e63d2ca7",
"external_user_id": "5cbc86081e06c111f9b16fbf"
},
{
"_id": "5c75a35a3cd9af224c0622c1",
"external_user_id": "5cbc86081e06c111f9b16fbf"
},
{
"_id": "5c82c3bede451c0fd74e6739",
"external_user_id": "5cbc86081e06c111f9b16fd5"
},
{
"_id": "5c85432c1a515a17f2d7a2e7",
"external_user_id": "5cbc86081e06c111f9b16fbc"
},
{
"_id": "5c8e8132bfda140998c2f1c4",
"external_user_id": "5cbc86081e06c111f9b16fbf"
}]
I want mongodb query of something like which results different document fields into one field array according to query following:
{
external_user_id: ["5cbc86081e06c111f9b16fbc", "5cbc86081e06c111f9b16fbf", "5cbc86081e06c111f9b16fd5"]
}
You can use below aggregation
db.collection.aggregate([
{ "$group": {
"_id": null,
"external_user_id": {
"$addToSet": "$external_user_id"
}
}}
])

findOne() returns entire document, instead of a single object

I'm trying to query this set of data using findOne():
{
"_id": {
"$oid": "5c1a4ba1482bf501ed20ae4b"
},
"wardrobe": {
"items": [
{
"type": "T-shirt",
"colour": "Gray",
"material": "Wool",
"brand": "Filson",
"_id": "5c1a4b7d482bf501ed20ae4a"
},
{
"type": "T-shirt",
"colour": "White",
"material": "Acrylic",
"brand": "H&M",
"_id": "5c1a4b7d482bf501ed20ae4a"
}
]
},
"tokens": [],
"email": "another#new.email",
"password": "$2a$10$quEXGjbEMX.3ERdjPabIIuMIKu3zngHDl26tgRcCiIDBItSnC5jda",
"createdAt": {
"$date": "2018-12-19T13:46:09.365Z"
},
"updatedAt": {
"$date": "2018-12-19T13:47:30.123Z"
},
"__v": 2
}
I want to return a single object from the items array using _Id as a filter. This is how I'm doing that:
exports.deleteItem = (req, res, next) => {
User.findOne({ 'wardrobe.items': { $elemMatch: { "_id": "5c1a4b7d482bf501ed20ae4a",} } }, (err, item) => {
console.log(item);
if (err) {
return console.log("error: " + err);
}
res.redirect('/wardrobe');
});
};
However, console.log(item) returns the whole document—like so:
{ wardrobe: { items: [ [Object], [Object] ] },
tokens: [],
_id: 5c1a4ba1482bf501ed20ae4b,
email: 'another#new.email',
password:
'$2a$10$quEXGjbEMX.3ERdjPabIIuMIKu3zngHDl26tgRcCiIDBItSnC5jda',
createdAt: 2018-12-19T13:46:09.365Z,
updatedAt: 2018-12-19T13:47:30.123Z,
__v: 2 }
I want to eventually use this to delete single items, so I need to filter to the single object from the subdocument.
Concerning your question:
MongoDB always returns the full object matching your query, unless you add a projection specifying which fields should be returned.
If you really want to only return a nested object, you could use the aggregation pipeline with the $replaceRoot operator like this:
User.aggregate([
// you can directly query for array fields instead of $elemMatching them
{ $match: { 'wardrobe.items._id': "5c1a4b7d482bf501ed20ae4a"}}},
// this "lifts" the fields wardrobe up and makes it the new root
{ $replaceRoot: {newRoot: '$wardrobe'}
// this "splits" the array into separate objects
{ $unwind: '$items'},
// this'll remove all unwanted elements
{ $match: { 'items._id': "5c1a4b7d482bf501ed20ae4a" },
},
])
This should return only the wanted items.
A note though: If you plan to remove elements from arrays anyways, I'd rather suggest you have a look at the $pull operation, which can remove an element from an array if it matches a certain condition:
https://docs.mongodb.com/manual/reference/operator/update/pull/
User.update(
{ 'wardrobe.items._id': "5c1a4b7d482bf501ed20ae4a"},
{ $pull: { 'wardrobe.items': {_id: "5c1a4b7d482bf501ed20ae4a"}},
{ multi: true }
)

Mongoose (MongoDB): Exclude Properties in a Dictionary Like Schema Type

I have a Schema of the following structure:
var schema = mongoose.Schema({
answers: {type: mongoose.Schema.Types.Mixed}
});
I use the answers field as an object (associative array to implement something like a dictionary). Here is an example:
{
"__v": 0,
"_id": {
"$oid": "53a0251c50d0536c1bfc6006"
},
"answers": {
"fea": {
"viewed": false
},
"3d2": {
"viewed": true,
"value": true
},
"4fr": {
"viewed": true,
"value": true
},
"84h": {
"viewed": false
},
...
}
}
In a query I want to select only the "value" field of each entry. How is that possible through the select syntax? This of course doesn't work:
XY.find(...)
.select({'answers': true, 'answers.*.value': false})
.exec(...);
Maybe I have to design the data in another fashion?
Best regards,
Kersten
You should never model with "explicit values" as the "key" names. This is very bad practice. Consider what you would do in a SQL database. Would you create "fields/columns" for the different "names" of the things you want?
No you would not. You have a generic field that specifies a "type" and then you have others that hold the data. Nothing changes here:
{
"_id": {
"$oid": "53a0251c50d0536c1bfc6006"
},
"answers": [
{ "type": "fea", "viewed": false },
{ "type": "3d2", "viewed": true, "value": true },
{ "type": "4fr", "viewed": true, "value": true },
{ "type": "84h", "viewed": false },
...
]
}
Now this is easy to use something like the aggregation framework to make the projection of the content like you want:
With Modern MongoDB 2.6 and onwards you can use $map and $setDifference to filter the array without using $unwind:
Model.aggregate(
[
{ "$project": {
"answers": {
"$setDifference": [
{
"$map": {
"input": "$answers",
"as": "el",
"in": {
"$cond": [
1,
{
"type": "$$el.type",
"value": { "$ifNull": [ "$$el.value", false ] }
},
false
]
}
}
},
[false]
]
}
}}
],
function(err,result) {
}
);
Or with older versions pre 2.6:
Model.aggregate(
[
{ "$unwind": "$answers" },
{ "$group": {
"_id": "$_id",
"answers": {
"$push": {
"type": "$answers.type",
"value": { "$ifNull": [ "$answers.value", false ] }
}
}
}}
],
function(err,result) {
}
);
Of course you can "filter" the array results to certain conditions by either adding a logical evaluation as the first argument to the $cond operator in the $map implementation. Or by using a $match pipeline stage in between the $unwind and $group stages.
Either form allows you to re-shape the result without problem and is the fastest way to process this, which is a strong advantage of using arrays as opposed to embedded objects whose keys are actually a "data" item.
If you are stuck with this then you need to process with JavaScript evaluation like mapReduce. This runs much slower than the aggregation framework due to the need to invoke and run in a JavaScript interpreter process:
Model.mapReduce(
{
"map": function() {
for ( var k in this.answers ) {
this.answers[k] = this.answers[k].hasOwnProperty("value")
? this.answers[k].value : false;
}
var id = this._id;
delete this._id;
emit( id, this );
},
"reduce": function(){}
},
function(err,docs) {
}
)
But really, consider changing your structure as it makes things much more flexible for queries and other operations.

Resources