Bucketing based on whether a field exists or not - mongodb

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"}}])

Related

How to insert in the smallest sub-array with mongobd?

I have a game collection that looks like this:
[
{
_id: ObjectId("6314dc4de4ad4c8141ce0b08"),
status: "started",
channel: "myChannel",
teams: [
{
name: "myFirstTeam",
score: 0,
users: [
{
id: 9082376,
name: "myFirstUser"
},
{
id: 289168,
name: "mySecondUser"
},
]
},
{
name: "mySecondTeam",
score: 0,
users: [
{
id: 898323,
name: "myThirdUser"
}
]
}
]
}
]
I managed to add a user to a team of a specific size:
db.collection.update({
"channel": "myChannel",
"teams.users": {
$size: 1
}
},
{
$push: {
"teams.$.users": {
id: 23424234,
name: "myUserName"
}
}
})
My goal is to add a user to a specific game on the smallest team. I'm new with mongodb, I don't even know if that's possible with a request only. I see the $min and $count but I can't find how to use it.
You can try on this playground
BONUS: Check to make sure the userId added is not already on any team of this game, inside the query (but I can check that before or/and after)
Query
update pipeline, because you need more complicated update
it could be smaller with more queries but this does all with 1 query
find the number of members of the smallest team for example size=1
reduce the teams, with initial value {found false new-teams []}
if we are in the team with the smallest size, we add the new member, and turn found to true, else we just add the team as it is
*i didn't added way to check if user exists, its not hard to add it, but i didnt know what to do if it existed and what to do if didnt exist, query is big and you are new, dont know if it will help you, i cant think now of smaller way to do it with 1 query
Playmongo
updateOne(
{"channel": {"$eq": "myChannel"}},
[{"$set": {"new-member": {"id": 23424234, "name": "myUserName"}}},
{"$set":
{"min-size":
{"$min":
{"$map":
{"input": "$teams", "as": "t", "in": {"$size": "$$t.users"}}}}}},
{"$set":
{"teams":
{"$reduce":
{"input": "$teams",
"initialValue": {"added": false, "new-teams": []},
"in":
{"$let":
{"vars": {"v": "$$value", "t": "$$this"},
"in":
{"$cond":
[{"$or":
["$$v.added", {"$gt":
[{"$size": "$$t.users"}, "$min-size"]}]},
{"added": "$$v.added",
"new-teams": {"$concatArrays": ["$$v.new-teams", ["$$t"]]}},
{"added": true,
"new-teams":
{"$concatArrays":
["$$v.new-teams",
[{"$mergeObjects":
["$$t",
{"users":
{"$concatArrays":
["$$t.users", ["$new-member"]]}}]}]]}}]}}}}}}},
{"$set":
{"teams": {"$getField": {"field": "new-teams", "input": "$teams"}}}},
{"$unset": ["new-member", "min-size"]}])

MongoDB - How to use $bucketAuto aggregation where the buckets are grouped by another property

I need to create an aggregation pipeline that return price ranges for each product category.
What I need to avoid is to load all available categories and call the Database again, one by one with a $match on each category. There must be a better way to do it.
Product documents
{
Price: 500,
Category: 'A'
},
{
Price: 7500,
Category: 'A'
},
{
Price: 340,
Category: 'B'
},
{
Price: 60,
Category: 'B'
}
Now I could use a $group stage to group the prices into an array by their category.
{
_id: "$Category",
Prices: {
$addToSet: "$Price"
}
}
Which would result in
{
_id: 'A',
Prices: [500, 7500]
},
{
_id: 'B',
Prices: [340, 60]
}
But If I use $bucketAuto stage after this, I am unable to groupBy multiple properties. Meaning it would not take the categories into account.
I have tried the following
{
groupBy: "$Prices",
buckets: 5,
output: {
Count: { $sum: 1}
}
}
This does not take categories into account, but I need the generated buckets to be organised by category. Either having the category field within the _id as well or have it as another field and have 5 buckets for each distinct category:
{
_id: {min: 500, max: 7500, category: 'A'},
Count: 2
},
{
_id: {min: 60, max: 340, category: 'B'},
Count: 2
}...
Query1
if you want to group by category and find the max and min price for that category you can do it like this
Playmongo
aggregate(
[{"$group":
{"_id": "$Category",
"min-price": {"$min": "$Price"},
"max-price": {"$max": "$Price"}}}])
Query2
if you want to group by category and then apply the bucket inside the array of the prices, to create like 5 buckets like in your example
you can do it with a trick, that allows us to use stage operators to do operators inside the array
the trick is to have 1 extra collection with only 1 document [{}]
you do lookup, you unwind that array, you do what you want on it
here we unwind the array and do $bucketAuto on it, with 5 buckets, like in your example, this way we can have group by category, and the prices in 5 ranges (5 buckets)
Playmongo
aggregate(
[{"$group": {"_id": "$Category", "prices": {"$push": "$Price"}}},
{"$lookup":
{"from": "coll_with_1_empty_doc",
"pipeline":
[{"$set": {"prices": "$$prices"}}, {"$unwind": "$prices"},
{"$bucketAuto": {"groupBy": "$prices", "buckets": 5}}],
"as": "bucket-prices",
"let": {"prices": "$prices", "category": "$_id"}}}])
If none of the above works, if you can give sample documents and example output

mongo - accessing a key-value field while filtering

I have these data:
myMap = {
"2": "facing",
"3": "not-facing"
"1": "hidden"
}
stages [
{
"k": 1,
"v": "hidden"
},
{
"k": 2,
"v": "facing"
},
{
"k": 3,
"v": "not-facing"
}
]
and a aggregate query but, I'm missing a syntax to dynamically fetch the map data:
db.MyCollection.aggregate()
.addFields({
myMaps: myMap
})
.addFields({
stages: stages
})
.addFields({
process: {
$filter: {
input: stages,
as: stageData,
cond: {$eq: [$$stageData.v, $myMaps[$$stageData.k]]}
}
}
})
As you may already note, this syntax: $myMaps[$$stageData.k] doesn't work, how should I access the myMaps based on the value of the k in stageData ?
Query
like your query set mymap and stages as extra field
mymapKeys is an extra field added (you can $unset is in the end)
filter the stages, and if stage.k is contained in mymapKeys we keep that member
*not sure if this is what you need, but looks like from your query
in mongodb query language we dont have getField(doc,$$k) we only have getField(doc,constant_string) which cant be used in your case,
so it costs here more than a hashmap lookup, here its like linear cost (check if member in the array). For arrays we have $getElementAt(array,$$k) if those numbers are always in sequence, 1,2,3 etc you might be able to use arrays instead of objects
Playmongo
aggregate(
[{"$set": {"mymap": {"2": "facing", "3": "not-facing", "1": "hidden"}}},
{"$set":
{"mymapKeys":
{"$map": {"input": {"$objectToArray": "$mymap"}, "in": "$$this.k"}}}},
{"$set":
{"stages":
[{"k": 1, "v": "hidden"}, {"k": 2, "v": "facing"},
{"k": 3, "v": "not-facing"}]}},
{"$set":
{"process":
{"$filter":
{"input": "$stages",
"cond": {"$in": [{"$toString": "$$this.k"}, "$mymapKeys"]}}}}}])

How to merge multiple documents in MongoDB and convert fields' values into fields

I have a MongoDB collection that I have managed to process using an aggregation pipeline to produce the following result:
[
{
_id: 'Complex Numbers',
count: 2
},
{ _id: 'Calculus',
count: 1
}
]
But the result that I am aiming for is something like the following:
{
'Complex Numbers': 2,
'Calculus': 1
}
is there a way to achieve that?
Query
to convert to {} we need somethings like [[k1 v1] ...] OR [{"k" "..." :v "..."}]
first stage
converts each document to [{"k" ".." , "v" ".."}]
then arrayToObject
and replace root
so we have each document like "Complex Numbers": 2
the group is used to combine all those documents in 1 document
and then replace the root with that one document
Test code here
aggregate(
[{"$replaceRoot":
{"newRoot": {"$arrayToObject": [[{"k": "$_id", "v": "$count"}]]}}},
{"$group": {"_id": null, "data": {"$mergeObjects": "$$ROOT"}}},
{"$replaceRoot": {"newRoot": "$data"}}])

How to $unset a embedded field 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"}}}}}])