Performing sorting query in MongoDB - mongodb

I want to make this complex sorting query in MongoDB but I am failing to achieve it.
The model in the collection looks like this:
_id: UUID('some-id'),
isDeleted: false,
date: ISODate('some-date'),
responses: [{
_id: UUID('some-id'),
userId: UUID('some-id'),
response: 0
}, {
_id: UUID('some-id'),
userId: UUID('some-id'),
response: 1
}]
One thing to keep in mind is that the responses array will always have 2 or 3 objects inside it. Not more, not less. Also, the response will only have three values, either 0, 1, or 2.
And what I want to do is that I want to sort them differently for each user, based on their response.
So let's say that my collection which is called Events has a lot of objects in the database. I want that when I filter them, the sorting will be done like this:
If my response is 0 and others are either 0 or 1, then sort them always first.
If all responses are 1, sort them after.
Others (if any response is 2, or if my response is 1 but others are 1 or 0), sort them last.
We can find if its my response by passing the userId in the query.
On top of that, I will need to have pagination so I will need to implement the $skip and $limit.
Was giving it a try with $unwind then $project trying to do some scoring based sorting, but couldn't achieve it.
The scoring sorting would look something like this:
if my response is 0 and others are 0 or 1 -> score = 100
if all responses are 1 -> score = 50
all others -> score = 0
In this way we could order them by score. But I dont know how I can actually create this property in the fly.
Was thinking that we could create one property like this:
$project: {
myScore: {
$cond: {
if: {
$in: [
UUID('my-user-id'),
"$responses.userId"
],
then: "$respones.response", //this is returning array here with all responses
else: 0
}
}
},
totalScore: {
$sum: "$respones.response"
}
}
And then we would be able to do another stage where we sort on these numbers somehow.
Thank you! :)

Here is a slightly simplified input set. We also include a target field for help in testing the scoring algo; it is not necessary for the final pipeline, where score is A, B, C for first, middle, and last in sort order. The score can be "anything" as long as it sorts properly. I used A, B, and C because it is visually different than the response codes (0,1,2) we are looking at so the pipeline functions are a little more comprehensible but it could be 10, 20, 30 or 5,10,15.
var myUserId = 1;
var r = [
{
target: 'C', // last, myUserId response is 1
responses: [
{userId:0, response:0},
{userId:1, response:1}
]
}
,{
target: 'C', // last, myUserId response is 1
responses: [
{userId:1, response:1},
{userId:0, response:0}
]
}
,{
target: 'A', // first, myUserId response is 0
responses: [
{userId:1, response:0},
{userId:0, response:0}
]
}
,{
target: 'B', // mid, all 1s
responses: [
{userId:7, response:1},
{userId:9, response:1}
]
}
,{
target: 'C', // last, a 2 exists
responses: [
{userId:4, response:2},
{userId:3, response:1},
{userId:1, response:0}
]
}
];
This pipeline will produce the desired output:
db.foo.aggregate([
{$addFields: {score:
{$cond: [
{$in: [2, '$responses.response']}, // any 2s?
'C', // yes, assign last
{$cond: [ // else
// All responses 1 (i.e. set diff is from 1 is empty set []?
{$eq: [ {$setDifference:['$responses.response',[1]]}, [] ] },
'B', // yes, assign mid
{$cond: [ // else
// Does myUserId have a response of 0? filter the
// array on these 2 fields and if the size of the
// filtered array != 0, that means you found one!
{$ne:[0, {$size:{$filter:{input:'$responses',
cond:{$and:[
{$eq:['$$this.userId',myUserId]},
{$eq:['$$this.response',0]}
]}
}} } ]},
'A', // yes, assign first
'C', // else last for rest
]}
]}
]}
}}
,{$sort: {'score':1}}
// TEST: Show items where target DOES NOT equal score. If the pipeline
// logic is working, this stage will produce zero output; that's
// how you know it works.
//,{$match: {$expr: {$ne:['$score','$target']}} }
]);

Anyone wondering about this, here's what I came up with. p.s. I also decided that I need to ignore all items if any response contains response 2, so I will focus only on values 0 and 1.
db.invites.aggregate([
{
$match: {
"$responses.response": {
$ne: 2
}
}
},
{
$addFields: {
"myScore": {
"$let": {
"vars": {
"invite": {
// get only object that contains my userId and get firs item from the list (as it will always be one in the list)
"$arrayElemAt": [{
"$filter": {
"input": "$responses",
"as": "item",
"cond": {"$eq": ["$$item.userId", UUID('some-id')]}
}} ,0]
}
},
// ger response value of that object that contains my userId
"in": "$$invite.response"
}
},
// as they are only 0 and 1s in the array items, we can see how many percent have voted with one.
// totalScore = sum(responses.response) / size(responses)
"totalScore": {
$divide: [{$sum: "$responses.response"} , {$size: "$responses"}]
}
}
},
{
$sort: {
//sort by my score, so if I have responded with 0, show first
"myScore": 1,
//sort by totalScore, so if I have responded 1, show those that have all 1s first.
"totalScore": -1
}
}
])

Related

Trying to get one element from an array using mongoose

I am trying to get randomly one item from an array using mongoose, I use .aggregate:
const winner = await gSchema.aggregate(
[
{ "$unwind": "$Users" },
{ "$sample": { "size": 1 } }
]
)
I console.log(winner) I get:
[
{
_id: new ObjectId("62c0943a789817d59c19bfa4"),
Guild: '1234567889',
Host: '1234567889',
Channel: '1234567889',
MessageID: '1234567889',
Time: '86400000',
Date: 2022-07-02T18:53:46.981Z,
Users: '1234567889',
__v: 0
}
]
Instead, I want to only get the value of Users like: 1234567889 in my console, not the whole Schema, any idea how to achieve that?
Also is there a way to use filter when using aggregate?
In order to get only the Users data add a projection step:
const winner = await gSchema.aggregate(
[
{$unwind: "$Users"},
{$sample: {size: 1}},
{$project: {Users: 1, _id:0}}
]
)
In order to filter, add a $match step.
Quick update about the issue, using console.log(winner[0].Users) solved my problem

MongoDB $lookup with conditional foreignField

Playground: https://mongoplayground.net/p/OxMnsCFZpmQ
My MongoDB version: 4.2.
I have a collection car_parts and customers.
As the name suggests car_parts has car parts, where some of them can have a field sub_parts which is a list of car_parts._ids this part consists of.
Every customer that bought something at us is stored in customers. The parts field for a customer contains a list of parts the customer bought together on a certain date.
I would like to have an aggregate query in MongoDB that returns a mapping of which car parts were bought (bought_parts) from which customers. However, if the car_parts has the field sub_parts, the customer should show up for the subparts only.
So the query in the playground gives almost the correct result already, except for the sub_parts topic.
Example for customer_3:
{
"_id": "customer_3",
"parts": [
{
"bought_parts": [
3
],
date: "15.07.2020"
}
]
}
Since bought_parts has car_parts._id = 3:
{
"_id": 3,
"name": "steering wheel",
"sub_parts": [
1, // other car_parts._id s
2
]
}
The result should show customer_3 as a customer of car parts 1 and 2.
I'm not sure how to accomplish this, but I assume a "temporary" replacement of the id 3 in bought_parts with the actual ids [1,2] might solve it.
Expected output:
[
{
"_id": 1,
"customers": [
"customer_1",
"customer_2",
"customer_3" // <- since customer_3 bought car part 3 which has 1 in sub_parts
]
},
{
"_id": 2,
"customers": [
"customer_3" // <- since customer_3 bought car part 3 which has 2 in sub_parts
]
},
{
"_id": 3,
"customers": [
"customer_1", // <- since car_parts.id = 3 has [1, 2] in sub_parts, show customers of ids [1, 2]
"customer_2",
"customer_3"
]
},
{
"_id": 4,
"customers": [
"customer_1",
"customer_2"
]
}
]
Thanks a lot in advance!
EDIT: One way to do it is:
db.car_parts.aggregate([
{
$project: {
topLevel: {$concatArrays: [{$ifNull: ["$sub_parts", []]}, ["$_id"]]},
sub_parts: 1
}
},
{$unwind: "$topLevel"},
{
$group: {
_id: "$topLevel",
parts: {$push: "$_id"},
sub_parts: {$first: "$sub_parts"}
}
},
{
$project: {
parts: {$concatArrays: [{"$ifNull": ["$sub_parts", []]}, "$parts"]}
}
},
{
$lookup: {
from: "customers",
localField: "parts",
foreignField: "parts.scanned_parts",
as: "customers"
}
},
{$project: {customers: "$customers._id"}}
])
As you can see working on this playground.
Since you said there is only one level of sub-parts, I used another idea: creating a top level before the $lookup. Since you want customers that used part 3 for example, to be registered under parts 1,2 which are sub-parts of 3, the idea is to group them. This connection is a bit clumsy after the $lookup, but if we use the data that we have on the car_parts collection before the $lookup, we actually knows already that parts 1,2 are subpart of 3. Creating a topLevel temporary field, allows to group, in advance, all the parts and sub-parts that if a customer used on of them, he should be registered under this top level part. This makes things much more elegant...

How to Update field with result of $multiply MongoDB

I have a collection of documents that look like this:
{
portfolio: [
0: {
price: 27
amount: 10
balance: 0
}
1: {
price: 28
amount: 30
balance: 0
}
2: {
price: 39
amount: 20
balance: 0
}
]
}
I'm trying to update the value of balance for all elements in the array with the multiple of price and amount
This is the line of code I'm trying to execute:
collection.update({}, [{"$set": {"portfolio.$[].balance": {"$multiply": ["portfolio.$.price","portfolio.$.amount"]}}}])
I keep getting the error:
builtins.TypeError: document must be an instance of dict, bson.son.SON, or any other type that inherits from collections.Mapping
What am I doing wrong?
PS:
I'm using [] brackets around {$set} of let update() know it's an aggregation pipeline. I'm using $[] to update all values for all elements of portfolio
UPDATE:
Can complete the task with logic spelled out in python for anyone interested:
accounts = collection.find()
for account in accounts:
portfolio = account["portfolio"]
accountid = ObjectId(account["_id"])
for i, asset in enumerate(portfolio):
asset["balance"] = asset["price"] * asset["amount"]
portfolio[i] = asset
collection.update({"_id": accountid}, {"$set": {"portfolio": portfolio}})
If there is a way to do this in MongoDB, please let me know.
I think the below query is what you are looking for.
Note: This query will only work on MongoDB version >= 4.2
You can make use of the $map operator in a $projection pipeline stage and finally the $set pipeline to update the result obtained. All this is achieved since MongoDB >= 4.2 supports Aggregation Pipeline stages within its update parameter.
db.tmp1.update({},
[
{
"$project": {
"portfolio": {
"$map": {
"input": "$portfolio",
"as": "pf",
"in": {
"balance": {
"$multiply": [
"$$pf.price",
"$$pf.amount"
]
},
"price": "$$pf.price",
"amount": "$$pf.amount",
}
}
}
}
},
{
"$set": {
"portfolio": "$portfolio",
}
},
], { multi: true })
$[] will work only when you want to update a single static value to all the array elements.

MongoDB: Add field to all objects in array, based on other fields on same object?

I am fairly new to MongoDB and cant seem to find a solution to this problem.
I have a database of documents that has this structure:
{
id: 1
elements: [ {elementId: 1, nr1: 1, nr2: 3}, {elementId:2, nr1:5, nr2: 10} ]
}
I am looking for a query that can add a value nr3 which is for example nr2/nr1 to all the objects in the elements array, so that the resulting document would look like this:
{
id: 1
elements: [ {elementId: 1, nr1: 1, nr2: 3, nr3:3}, {elementId:2, nr1:5, nr2: 10, nr3: 2} ]
}
So I imagine a query along the lines of this:
db.collection.updateOne({id:1}, {$set:{"elements.$[].nr3": nr2/nr1}})
But I cant find how to get the value of nr2 and nr1 of the same object in the array.
I found some similar questions on stackoverflow stating this is not possible, but they were 5+ years old, so I thought maybe they have added support for something like this.
I realize I can achieve this with first querying the document and iterate over the elements-array doing updates along the way, but for the purpose of learning I would love to see if its possible to do this in one query.
You can use update with aggregation pipeline starting from MongoDB v4.2,
$map to iterate loop of elements
divide nr2 with nr1 using $divide
merge current object and new field nr3 using $mergeObjects
db.collection.updateOne(
{ id: 1 },
[{
$set: {
elements: {
$map: {
input: "$elements",
in: {
$mergeObjects: [
"$$this",
{ nr3: { $divide: ["$$this.nr2", "$$this.nr1"] } }
]
}
}
}
}
}]
)
Playground
db.collection.update(
{ id:1},
{ "$set": { "elements.$[elem].nr3":elements.$[elem].nr2/elements.$[elem].nr1} },
{ "multi": true }
);
I guess this should work

Mongo filter documents by array of objects

I have to filter candidate documents by an array of objects.
In the documents I have the following fields:
skills = [
{ _id: 'blablabla', skill: 'Angular', level: 3 },
{ _id: 'blablabla', skill: 'React', level: 2 },
{ _id: 'blablabla', skill: 'Vue', level: 4 },
];
When I make the request I get other array of skills, for example:
skills = [
{ skill: 'React', level: 2 },
];
So I need to build a query to get the documents that contains this skill and a greater or equal level.
I try doing the following:
const conditions = {
$elemMatch: {
skill: { $in: skills.map(item => item.skill) },
level: { $gte: { $in: skills.map(item => item.level) } }
}
};
Candidate.find(conditions)...
The first one seems like works but the second one doesn't work.
Any idea?
Thank you in advance!
There are so many problems with this query...
First of all item.tech - it had to be item.skill.
Next, $gte ... $in makes very little sense. $gte means >=, greater or equal than something. If you compare numbers, the "something" must be a number. Like 3 >= 5 resolves to false, and 3 >= 1 resolves to true. 3 >= [1,2,3,4,5] makes no sense since it resolves to true to the first 3 elements, and to false to the last 2.
Finally, $elemMatch doesn't work this way. It tests each element of the array for all conditions to match. What you was trying to write was like : find a document where skills array has a subdocument with skill matching at least one of [array of skills] and level is greater than ... something. Even if the $gte condition was correct, the combination of $elementMatch and $in inside doesen't do any better than regular $in:
{
skill: { $in: skills.map(item => item.tech) },
level: { $gte: ??? }
}
If you want to find candidates with tech skills of particular level or higher, it should be $or condition for each skill-level pair:
const conditions = {$or:
skills.map(s=>(
{skill: { $elemMatch: {
skill:s.skill,
level:{ $gte:s.level }
} } }
))
};