Mongoose & getting average of ratings in a collection? - javascript

I have a NodeJS based application using Mongoose. I am wanting to create a response where there values are an average of ratings provided by people who have responded to a questionnaire, for each question asked.
My Schema looks as follows:
const questionnaireResultSchema = new Schema({
user: { type: ObjectId, ref: 'User' },
questionnaire: { type: ObjectId, ref: 'Questionnaire' },
rating: [{
id: Number,
question: String,
value: Number
}]
},{
timestamps: true
}).index({user: 1, questionnaire: 1}, {unique: true});
I have looked at the Mongoose aggregator operator, but I am not sure how I would apply it to my case. Pseudo code would look as follows:
Find all questionnaire results for questionnaire of id xyz
Provide a result where the ratings for each questionnaire result has be averaged
For example the response would look as follows:
[{
id: 1,
question: 'How strongly do you feel about candidate A?',
value: 12 // averaged value
},{
id: 2,
question: 'How strongly do you think we should change the sky color to green?',
value: 31 // averaged value
},{
id: 3,
question: 'How strongly do you think your answers count?',
value: 20 // averaged value
}]
I have tried:
QuestionnaireResultSchema.aggregate([{
$match: {
questionnaire: questionnaire
}},
{$project: {
scores: { $avg: '$scores'}
}}
]);
This just provides the JSON:
[{
"_id": "57bbd4b495407f6145b3ba9f",
"scores": null
}]
A sample document in a collection would like:
{
user: ObjectId("57bca30536e376c653f439bb")
questionnaire: ObjectId("37bca0feedb0bc470353ab")
scores: [{
id: 1,
question: 'How strongly do you feel about candidate A?',
value: 3
},{
id: 2,
question: 'How strongly do you think we should change the sky color to green?',
value: 4 // averaged value
},{
id: 3,
question: 'How strongly do you think your answers count?',
value: 5 // averaged value
}]
}
While I could calculate the averages myself, if Mongoose provides the functionality, I would rather leverage that.
Any help would be appreciated.

This should work for your aggregation pipeline:
[
{
$match: {
questionnaire: ObjectId("37bca0feedb0bc470353ab")
}
},
{
$unwind: "$rating"
},
{
$group:{
_id:{
"rating_id": "$rating.id",
"question": "$rating.question",
},
avg_rating: {$avg:"rating.value"}
}
},
{
$project:{
"id": "$_id.rating_id",
"question": "$_id.question",
"avg_rating": "$avg_rating"
}
}
]
Although your sample doc has "scores" instead of "rating" in which case you'd use:
[
{
$match: {
questionnaire: ObjectId("237bca0feedb0bc470353aba")
}
},
{
$unwind: "$scores"
},
{
$group:{
_id:{
"rating_id": "$scores.id",
"question": "$scores.question",
},
avg_rating: {$avg:"scores.value"}
}
},
{
$project:{
"id": "$_id.rating_id",
"question": "$_id.question",
"avg_rating": "$avg_rating"
}
}
]
Also, some of the ObjectId's that you are using are not valid. I'm assuming those are just stubbed.

Related

Edit multiple objects in array using mongoose (MongoDB)

So I tried several ways, but I can't, I can modify several objects with the same key but I can't modify any with different keys, if anyone can help me is quite a complex problem
{
id: 123,
"infos": [
{ name: 'Joe', value: 'Disabled', id: 0 },
{ name: 'Adam', value: 'Enabled', id: 0 }
]
};
In my database I have a collection with an array and several objects inside which gives this.
I want to modify these objects, filter by their name and modify the value.
To give you a better example, my site returns me an object with the new data, and I want to modify the database object with the new object, without clearing the array, the name key never changes.
const object = [
{ name: 'Joe', value: 'Hey', id: 1 },
{ name: 'Adam', value: 'None', id: 1 }
];
for(const obj in object) {
Schema.findOneAndUpdate({ id: 123 }, {
$set: {
[`infos.${obj}.value`]: "Test"
}
})
}
This code works but it is not optimized, it makes several requests, I would like to do everything in one request, and also it doesn't update the id, only the value.
If anyone can help me that would be great, I've looked everywhere and can't find anything
My schema structure
new Schema({
id: { "type": String, "required": true, "unique": true },
infos: []
})
I use the $addToSet method to insert objects into the infos array
Try This :
db.collection.update({
id: 123,
},
{
$set: {
"infos.$[x].value": "Value",
"infos.$[x].name": "User"
}
},
{
arrayFilters: [
{
"x.id": {
$in: [
1
]
}
},
],
multi: true
})
The all positional $[] operator acts as a placeholder for all elements in the array field.
In $in you can use dynamic array of id.
Ex :
const ids = [1,2,..n]
db.collection.update(
//Same code as it is...
{
arrayFilters: [
{
"x.id": {
$in: ids
}
},
],
multi: true
})
MongoPlayGround Link : https://mongoplayground.net/p/Tuz831lkPqk
Maybe you look for something like this:
db.collection.update({},
{
$set: {
"infos.$[x].value": "test1",
"infos.$[x].id": 10,
"infos.$[y].value": "test2",
"infos.$[y].id": 20
}
},
{
arrayFilters: [
{
"x.name": "Adam"
},
{
"y.name": "Joe"
}
],
multi: true
})
Explained:
You define arrayFilters for all names in objects you have and update the values & id in all documents ...
playground

Mongoose - renaming object key within array

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

Nested object text search in mongoDB

I am not sure how I am going to solve this problem:
I want to search in a mongoDB collection and return only the nested objects that fits the search query (using text search on all of the fields).
All documents in the collection have this format:
{
arr: [
{
_id: 1,
name: 'Random',
description: 'Hello world'
},
{
_id: 2,
name: 'World',
description: 'This is a random description'
},
{
_id: 3,
name: 'Random',
description: 'Hi'
}
]
}
In this case, if my search query is 'world', then this should be the result:
[
{
_id: 1,
name: 'Random',
description: 'Hello world'
},
{
_id: 2,
name: 'World',
description: 'This is a random description'
},
//... objects from other documents in the collection that fits the query
]
If this is not possible in mongoDB, are there any JavaScript libraries that can achieve this? Would greatly appreciate the help!
With the aggregation framework it could look like so
db.getCollection('yourCollection').aggregate([
{
$unwind: '$arr'
},
{
$match: {
$or: [
{ 'arr.name': /world/i },
{ 'arr.description': /world/i }
]
}
},
{
$project: {
_id: '$arr._id',
name: '$arr.name',
description: '$arr.description'
}
}
])
which will result in the following output for your example data:
{
"_id" : 1,
"name" : "Random",
"description" : "Hello world"
}
{
"_id" : 2,
"name" : "World",
"description" : "This is a random description"
}
If you have the need for a single array with the resulting documents as shown in your question, you can simply chain a toArray() call at the end of the pipeline - keep in mind though that this may cause increased memory consumption in case of large result sets as pointed out by SSDMS in the comments.

Push data to MongoDB without editing the whole entry in MEAN

So i've got a single page application running via MEAN Stack. I've got the following scheme/model for a Klausur (Klausur is german for exam, i want to manage exams).
var KlausurSchema = new Schema(
{
name: String,
semester: String,
krankmeldungen: [Number],
aufgaben: [{
name: String,
punkte: Number
}],
studenten: [{
matrnr: Number,
vorname: String,
nachname: String,
bewertung: [{
punkte: Number
}],
pversuch: String,
pvermerk: String,
freiverm: String,
labnr: Number,
porgnr: Number,
aenddat: String
}]
}
);
Multiple users can edit the entrys, otherwise i would just overwrite the entry. I want to have a table consisting of the "studenten" (students), is it possible to PUSH one student to my "studenten" without PUTTING (edit) the whole "Klausur", i.e. I want to push information to an array without overwriting the whole db entry!
Thanks in advance!
Please Check Docs
If you want to insert new students array. you can use below mentioned MongoDB query.
Using MongoDB
db.klausur.update(
{ name: "David" },
$addToSet: {
studenten: {
$each: [
{
matrnr: 123,
vorname: "ABC",
nachname: "XYZ",
bewertung: [{
punkte: 123
}]
},
{
matrnr: 124,
vorname: "ABCD",
nachname: "XYZA",
bewertung: [{
punkte: 1234
}]
}]
}
);
Using Mongoose
ModelName.update(
{ name: "David" },
$addToSet: {
studenten: {
$each: [
{
matrnr: 123,
vorname: "ABC",
nachname: "XYZ",
bewertung: [{
punkte: 123
}]
}]
}
);
you can also use $push instead of $addToSet. but $addToSet handle duplicates insertion issue. One more thing if you want to add single student object then just use above query without $each. for example
db.klausur.update(
{ name: "David" },
$addToSet: {
studenten: {
matrnr: 123,
vorname: "ABC",
nachname: "XYZ",
bewertung: [{
punkte: 123
}]
}
}
);
Pass an object to be updated to native mongoDB update query.
The pseudo query will be,
db.model.update(selector, objectToUpsert);
db.student.update(
{ name: "David" },
{
name: "Will",
marks: 75,
grade: A
},
{ upsert: true }
)
First find Klausur=exam say
Klausur.findOne({_id:sample_id}).exec(function (error, closure){
closure.students.push(newstudentobject);
closure.save();
})

Is It possible to use query projection on the same collection that has a $elemMatch projection?

I understand that you can limit the items in a subcollection array using $elemMatch as a projection. When using it as such it returns all fields of the subdocuments that match regardless if query projections are also specified.
Is it possible to limit the fields returned in the matching subdocuments? How would you do so?
Using version 3.8.9.
Given the simple schemas:
var CommentSchema = mongoose.Schema({
body: String,
created: {
by: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now
}
}
});
var BlogSchema = mongoose.Schema({
title: String,
blog: String,
created: {
by: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now
}
},
comments: [CommentSchema]
});
var Blog = mongoose.model('Blog',modelSchema);
Example
Blog.findOne({_id: id}, {_id: 1, comments: {$elemMatch: {'created.by': 'Jane'}, body: 1}}, function(err, blog) {
console.log(blog.toJSON());
});
// outputs:
{
_id: 532cb63e25e4ad524ba17102,
comments: [
_id: 53757456d9c99f00009cdb5b,
body: 'My awesome comment',
created: { by: 'Jane', date: Fri Mar 21 2014 20:34:45 GMT-0400 (EDT) }
]
}
// DESIRED OUTPUT
{
_id: 532cb63e25e4ad524ba17102,
comments: [
body: 'My awesome comment'
]
}
Yes there are two ways to do this. So you can either use the $elemMatch on the projection side as you already have, with slight changes:
Model.findById(id,
{ "comments": { "$elemMatch": {"created.by": "Jane" } } },
function(err,doc) {
Or just add to the query portion and use the positional $ operator:
Model.findOne(
{ "_id": id, "comments.created.by": "Jane" },
{ "comments.$": 1 },
function(err,doc) {
Either way is perfectly valid.
If you wanted something a little more involved than that, you can use the .aggregate() method and it's $project operator instead:
Model.aggregate([
// Still match the document
{ "$match": "_id": id, "comments.created.by": "Jane" },
// Unwind the array
{ "$unwind": "$comments" },
// Only match elements, there can be more than 1
{ "$match": "_id": id, "comments.created.by": "Jane" },
// Project only what you want
{ "$project": {
"comments": {
"body": "$comments.body",
"by": "$comments.created.by"
}
}},
// Group back each document with the array if you want to
{ "$group": {
"_id": "$_id",
"comments": { "$push": "$comments" }
}}
],
function(err,result) {
So the aggregation framework can be used for a lot more than simply aggregating results. It's $project operator gives you more flexibility than is available to projection using .find(). It also allows you to filter and return multiple array results, which is also something that cannot be done with projection in .find().

Categories

Resources