How to $unset a embedded field MongoDB? - mongodb

Here my document for example:
[{
_id: ObjectId("609391f436e519039a634311"),
name: "Class A",
numOfStudents: 10,
students: [{
name: "Student A",
age: 10,
}, {
name: "Student B",
age: 10,
}]
}]
I want to update some values of class and remove some informations of all students in class. So I am using $set and $unset in updateOne, like below:
db.class.updateOne({
_id: ObjectId("609391f436e519039a634311")
}, {
$set: { something: "Something for describe" },
$unset: { "students.$[].age": "" }
})
But now, I want $set a value to something by value of another field, I have to convert above script to a pipeline like below:
db.class.updateOne({
_id: ObjectId("609391f436e519039a634311")
}, [
{
$set: { something: "$name" },
}, {
$unset: [
"students.$[].age"
]
}
])
But it didn't work, it threw an Error:
Invalid $unset :: caused by :: FieldPath field names may not start with '$'. Consider using $getField or $setField.
Please give me a suggestion for this.

You can't use paths that we use in update operators in aggregation.
When aggregate you can only use aggregate operators, ONLY exception is the match stage that you can use query operators also.
Query1
unset age
Test code here
update(
{"_id": ObjectId("609391f436e519039a634311")},
[{"$set": {"something": "$name"}},
{"$unset": ["students.age"]}])
Query2
you can use the "$$REMOVE" system variable, if a field gets this value its removed
here is like setting all age fields to have value $$REMOVE so they are removed
Test code here
update(
{"_id": ObjectId("609391f436e519039a634311")},
[{"$set": {"something": "$name", "students.age": "$$REMOVE"}}])
Query3
from students we only keep the name (=> age is removed)
you have to write by hand all the fields you want to keep
Test code here
update(
{"_id": ObjectId("609391f436e519039a634311")},
[{"$set": {"something": "$name",
"students":
{"$map": {"input": "$students", "in": {"name": "$$this.name"}}}}}])

Related

Bucketing based on whether a field exists or not

I have data that looks like this:
{ fname: "bob", age: 33, description: "brown eyes"}
{ fname: "sally", age: 20, description: "tall"}
{ fname: "jim", age: 48 }
I'm trying to, in one query, get results that look something like this:
{
hasDescription: ["bob", "sally"],
noDescription: ["jim"]
}
I've been playing around with $bucketAuto with something like this:
{
groupBy: { "$cond": [{ "$eq": [ "description", null ] }, true, false ] },
buckets: 2,
}
but am not having any success. Any guidance would be appreciated!
Query
group with expression on group, to make 2 groups, one that have description and one that doesnt (instead of a field to create the group, we create 2 groups based on a condtion)
the other 3 stages is to fix the structure to be like the expected output
*i don't think you need bucket, bucket is for range of values
*instead of the bellow facet could be used also, but facet is like running the aggregation 1 time per field so its slower
Playmongo (mouse to the end of each stage => see in/out of each stage)
aggregate(
[{"$group":
{"_id":
{"$cond":
[{"$ne": [{"$type": "$description"}, "missing"]}, "hasDescription",
"noDescription"]},
"names": {"$push": "$fname"}}},
{"$replaceRoot":
{"newRoot":
{"$cond":
[{"$eq": ["$_id", "noDescription"]}, {"noDescription": "$names"},
{"hasDescription": "$names"}]}}},
{"$group": {"_id": null, "docs": {"$mergeObjects": "$$ROOT"}}},
{"$replaceRoot": {"newRoot": "$docs"}}])

How to find documents according to a common field value from another collection in mongodb

Assume I have 2 collections:
student:
{name: Joe, school: A}
{name: Kelly, school: B}
{name: Mike, school: C}
{name: Tom, school: D}
schoolRank: (all the school rank is stored in one document)
{rank: [{school: A, value: 1},{school: B, value: 2},{school: C, value: 3},{school: D, value: 4}]}
Now, my question is how could I find the student whoes school rank is higher than 3. (I am a newbie to mongodb. It seems like I need to use lookup but I am not sure how to do it exactly.) Thank you in advance!
You need to use $lookup. Is like a "join" in SQL.
But, first of all. Your document could be much better. schoolRank collection could have every school in a document instead of a unique array wit all values.
Check here the difference between the query with your schema and the schema with schoolRank splited into diffretend documents.
The second query return only the document where field school match. The other will return the entire array for each document, because in each document exist a field school that also exists into rank array.
So, with your schema you need extra stages. Maybe there is another way more efficent, but I'm not used to do $lookup with a bad schema (sorry).
I've try this query:
First $lookup to join both collections (as I've said before, the join is basically add the entire array into each document).
Then an extra stage to get the value returned from $lookup using $set with the element at first position.
After that, using $project te query can filter the field rank_school and overwrite it to get only the element which field school is the same as student.school.
Note that the above steps could be omitted using another schema.
Then, after the $project there is a $match stage to get the documents whose rank_school.value is greater or equal than 3.
And the last stage is another $project to remove the field rank_school.
This is the query:
db.student.aggregate([
{
"$lookup": {
"from": "schoolRank",
"localField": "school",
"foreignField": "rank.school",
"as": "rank_school"
}
},
{
"$set": { "rank_school": { "$arrayElemAt": [ "$rank_school", 0 ] } }
},
{
"$project": {
"_id": "$_id",
"name": "$name",
"school": "$school",
"rank_school": {
"$filter": {
"input": "$rank_school.rank",
"as": "rank_school_filter",
"cond": { "$eq": [ "$$rank_school_filter.school", "$school" ] }
}
}
}
},
{
"$match": { "rank_school.value": { "$gte": 3 } }
},
{
"$project": { "rank_school": 0 }
}
])
Example here.
And the output is:
[
{
"_id": ObjectId("5a934e000102030405000003"),
"name": "Mike",
"school": "C"
},
{
"_id": ObjectId("5a934e000102030405000004"),
"name": "Tom",
"school": "D"
}
]

Link each element of array in a document to the corresponding element in an array of another document with MongoDB

Using MongoDB 4.2 and MongoDB Atlas to test aggregation pipelines.
I've got this products collection, containing documents with this schema:
{
"name": "TestProduct",
"relatedList": [
{id:ObjectId("someId")},
{id:ObjectId("anotherId")}
]
}
Then there's this cities collection, containing documents with this schema :
{
"name": "TestCity",
"instructionList": [
{ related_id: ObjectId("anotherId"), foo: bar},
{ related_id: ObjectId("someId"), foo: bar}
{ related_id: ObjectId("notUsefulId"), foo: bar}
...
]
}
My objective is to join both collections to output something like this (the operation is picking each related object from the instructionList in the city document to put it into the relatedList of the product document) :
{
"name": "TestProduct",
"relatedList": [
{ related_id: ObjectId("someId"), foo: bar},
{ related_id: ObjectId("anotherId"), foo: bar},
]
}
I tried using the $lookup operator for aggregation like this :
$lookup:{
from: 'cities',
let: {rId:'$relatedList._id'},
pipeline: [
{
$match: {
$expr: {
$eq: ["$instructionList.related_id", "$$rId"]
}
}
},
]
}
But it's not working, I'm a bit lost with this complex pipeline syntax.
Edit
By using unwind on both arrays :
{
{$unwind: "$relatedList"},
{$lookup:{
from: "cities",
let: { "rId": "$relatedList.id" },
pipeline: [
{$unwind:"$instructionList"},
{$match:{$expr:{$eq:["$instructionList.related_id","$$rId"]}}},
],
as:"instructionList",
}},
{$group: {
_id: "$_id",
instructionList: {$addToSet:"$instructionList"}
}}
}
I am able to achieve what I want, however,
I'm not getting a clean result at all :
{
"name": "TestProduct",
instructionList: [
[
{
"name": "TestCity",
"instructionList": {
"related_id":ObjectId("someId")
}
}
],
[
{
"name": "TestCity",
"instructionList": {
"related_id":ObjectId("anotherId")
}
}
]
]
}
How can I group everything to be as clean as stated for my original question ?
Again, I'm completely lost with the Aggregation framework.
the operation is picking each related object from the instructionList in the city document to put it into the relatedList of the product document)
Given an example document on cities collection:
{"_id": ObjectId("5e4a22a08c54c8e2380b853b"),
"name": "TestCity",
"instructionList": [
{"related_id": "a", "foo": "x"},
{"related_id": "b", "foo": "y"},
{"related_id": "c", "foo": "z"}
]}
and an example document on products collection:
{"_id": ObjectId("5e45cdd8e8d44a31a432a981"),
"name": "TestProduct",
"relatedList": [
{"id": "a"},
{"id": "b"}
]}
You can achieve try using the following aggregation pipeline:
db.products.aggregate([
{"$lookup":{
"from": "cities",
"let": { "rId": "$relatedList.id" },
"pipeline": [
{"$unwind":"$instructionList"},
{"$match":{
"$expr":{
"$in":["$instructionList.related_id", "$$rId"]
}
}
}],
"as":"relatedList",
}},
{"$project":{
"name":"$name",
"relatedList":{
"$map":{
"input":"$relatedList",
"as":"x",
"in":{
"related_id":"$$x.instructionList.related_id",
"foo":"$$x.instructionList.foo"
}
}
}
}}
]);
To get a result as the following:
{ "_id": ObjectId("5e45cdd8e8d44a31a432a981"),
"name": "TestProduct",
"relatedList": [
{"related_id": "a", "foo": "x"},
{"related_id": "b", "foo": "y"}
]}
The above is tested in MongoDB v4.2.x.
But it's not working, I'm a bit lost with this complex pipeline syntax.
The reason why it's slightly complex here is because you have an array relatedList and also an array of subdocuments instructionList. When you refer to instructionList.related_id (which could mean multiple values) with $eq operator, the pipeline doesn't know which one to match.
In the pipeline above, I've added $unwind stage to turn instructionList into multiple single documents. Afterward, using $in to express a match of single value of instructionList.related_id in array relatedList.
I believe you just need to $unwind the arrays in order to lookup the relation, then $group to recollect them. Perhaps something like:
.aggregeate([
{$unwind:"relatedList"},
{$lookup:{
from:"cities",
let:{rId:"$relatedList.id"}
pipeline:[
{$match:{$expr:{$eq:["$instructionList.related_id", "$$rId"]}}},
{$unwind:"$instructionList"},
{$match:{$expr:{$eq:["$instructionList.related_id", "$$rId"]}}},
{$project:{_id:0, instruction:"$instructionList"}}
],
as: "lookedup"
}},
{$addFields: {"relatedList.foo":"$lookedup.0.instruction.foo"}},
{$group: {
_id:"$_id",
root: {$first:"$$ROOT"},
relatedList:{$push:"$relatedList"}
}},
{$addFields:{"root.relatedList":"$relatedList"}},
{$replaceRoot:{newRoot:"$root"}}
])
A little about each stage:
$unwind duplicates the entire document for each element of the array,
replace the array with the single element
$lookup can then consider each element separately. The stages in $lookup.pipeline:
a. $match so we only unwind the document with matching ID
b. $unwind the array so we can consider individual elements
c. repeat the $match so we are only left with matching elements (hopefully just 1)
$addFields assigns the foo field retrieved from the lookup to the object from relatedList
$group collects together all of the documents with the same _id (i.e. that were unwound from a single original document), stores the first as 'root', and pushes all of the relatedList elements back into an array
$addFields moves the relatedList in to root
$replaceRoot returns the root, which should now be the original document with the matching foo added to each relatedList element

Scala / MongoDB - removing duplicate

I have seen very similar questions with solutions to this problem, but I am unsure how I would incorporate it in to my own query. I'm programming in Scala and using a MongoDB Aggregates "framework".
val getItems = Seq (
Aggregates.lookup(Store...)...
Aggregates.lookup(Store.STORE_NAME, "relationship.itemID", "uniqueID", "item"),
Aggregates.unwind("$item"),
// filter duplicates here ?
Aggregates.lookup(Store.STORE_NAME, "item.content", "ID", "content"),
Aggregates.unwind("$content"),
Aggregates.project(Projections.fields(Projections.include("store", "item", "content")))
)
The query returns duplicate objects which is undesirable. I would like to remove these. How could I go about incorporating Aggregates.group and "$addToSet" to do this? Or any other reasonable solution would be great too.
Note: I have to omit some details about the query, so the store lookup aggregate is not there. However, I want to remove the duplicates later in the query so it hopefully shouldn't matter.
Please let me know if I need to provide more information.
Thanks.
EDIT: 31/ 07/ 2019: 13:47
I have tried the following:
val getItems = Seq (
Aggregates.lookup(Store...)...
Aggregates.lookup(Store.STORE_NAME, "relationship.itemID", "uniqueID", "item"),
Aggregates.unwind("$item"),
Aggregates.group("$item.itemID,
Accumulators.first("ID", "$ID"),
Accumulators.first("itemName", "$itemName"),
Accumulators.addToSet("item", "$item")
Aggregates.unwind("$items"),
Aggregates.lookup(Store.STORE_NAME, "item.content", "ID", "content"),
Aggregates.unwind("$content"),
Aggregates.project(Projections.fields(Projections.include("store", "items", "content")))
)
But my query now returns zero results instead of the duplicate result.
You can use $first to remove the duplicates.
Suppose I have the following data:
[
{"_id": 1,"item": "ABC","sizes": ["S","M","L"]},
{"_id": 2,"item": "EFG","sizes": []},
{"_id": 3, "item": "IJK","sizes": "M" },
{"_id": 4,"item": "LMN"},
{"_id": 5,"item": "XYZ","sizes": null
}
]
Now, let's aggregate it using $first and $unwind and see the difference:
First let's aggregate it using $first
db.collection.aggregate([
{ $sort: {
item: 1
}
},
{ $group: {
_id: "$item",firstSize: {$first: "$sizes"}}}
])
Output
[
{"_id": "XYZ","firstSize": null},
{"_id": "ABC","firstSize": ["S","M","L" ]},
{"_id": "IJK","firstSize": "M"},
{"_id": "EFG","firstSize": []},
{"_id": "LMN","firstSize": null}
]
Now, Let's aggregate it using $unwind
db.collection.aggregate([
{
$unwind: "$sizes"
}
])
Output
[
{"_id": 1,"item": "ABC","sizes": "S"},
{"_id": 1,"item": "ABC","sizes": "M"},
{"_id": 1,"item": "ABC","sizes": "L},
{"_id": 3,"item": "IJK","sizes": "M"}
]
You can see $first removes the duplicates where as $unwind keeps the duplicates.
Using $unwind and $first together.
db.collection.aggregate([
{ $unwind: "$sizes"},
{
$group: {
_id: "$item",firstSize: {$first: "$sizes"}}
}
])
Output
[
{"_id": "IJK", "firstSize": "M"},
{"_id": "ABC","firstSize": "S"}
]
group then addToSet is an effective way to deal with your problem !
it looks like this in mongoshell
db.sales.aggregate(
[
{
$group:
{
_id: { day: { $dayOfYear: "$date"}, year: { $year: "$date" } },
itemsSold: { $addToSet: "$item" }
}
}
]
)
in scala you can do it like
Aggregates.group("$groupfield", Accumulators.addToSet("fieldName","$expression"))
if you have multiple field to group
Aggregates.group(new BasicDBObject().append("fieldAname","$fieldA").append("fieldBname","$fieldB")), Accumulators.addToSet("fieldName","expression"))
then unwind

Mongoose/MongoDB - Sort by highest occurrence

I am trying to sort my response based on the highest occurrence of a postID and within a date.
My collection structure:
[
{
"postID": "2",
"date": "2017-04-11 21:40:52",
},
{
"postID": "1",
"date": "2017-04-11 21:40:52",
},
{
"postID": "2",
"date": "2017-04-11 21:40:52",
},
{
"postID": "2",
"date": "2017-04-11 21:40:52",
},
]
So in this case, the highest occurrence is the postID: 2. So I want something like:
{
postID: 2,
postID: 1
}
Note: Remember that I need to search in between two dates as well.
Sorry if this is too obvious. I don't even know how to begin making the Mongo's/mongoose' find search.
My Stack:
Node with mongoose
MongoDB 3.2.5
Any input will help. Thanks
You can use the MongoDB aggregation framework to group on a specific property. Mongoose has a low-level wrapper around the aggregation framework.
You can $group with the postID as the _id which will give you the unique values for each. Then you can $sort. You can also use $project to rename the property back to postID if you prefer:
Collection.aggregate([
{$group: {_id: "$postID"}},
{$sort: {_id: -1}},
{$project: {postID: "$_id", _id: 0}}
]);