Update document with aggregation pipeline with $lookup - node.js

I have a product collection which has fields -userId,referenceProductId,
I want to add new field buyerUserId to all doc where its value will be equal to userId for documents where its _id is equal to referenceProduct_id
For example-for following 2 doc
{
"_id": { "$oid": "61ded34c1e7007b17a86f889" },
"userId": { "$oid": "6190b06b113314ad2183db09" },
"referenceProductId": { "$oid": "61ded15fdd1363aa1ce09c55" }
}
{
"_id": { "$oid": "61ded15fdd1363aa1ce09c55" },
"userId": { "$oid": "6190b06b113314ad2183db09" },
"referenceProductId": { "$oid": "61ded34c1e7007b17a86f889" }
}
BuyerUserId for doc1 will be 6190b06b113314ad2183db09 since doc1's _id is equal to referenceProductId of doc2
I am new to mongoDB, trying to update with below code but doesn't work
{ "$match": { "status": "purchased" }},
{ $lookup:{
from:"product",
let:{
"id":"$Id",
"referenceProductId":"$ReferenceProductId",
"userId":"$UserId",
},
pipeline:[
{
$match:{
$expr:{
$eq: ["$$id", "$referenceProductId"] ,
}
}
},
],
as:"products"
}
},{
$project:{
"buyerUserId":"$products.userId"
}
}
])

As noted in a comment above, $merge can be used as "doc to doc" update mechanism. Starting in v4.4 you may output the results of the merge directly back onto the collection being aggregated.
db.foo.aggregate([
{$lookup: {from: "foo",
let: {rid: "$_id"},
pipeline: [
{$match:{$expr:{$eq: ["$$rid", "$referenceProductId"]}}}
],
as: "Z"}}
// If _id->refId match happened, set buyerUserId else do not set
// anything not even null ($$REMOVE means "do nothing"):
,{$project: {buyerUserId: {$cond: [ {$ne:[0,{$size: "$Z"}]}, "$_id", "$$REMOVE"] }} }
// At this point in pipeline we only have _id and maybe buyerUserId. Use _id
// from each doc to merge buyerUserId back into the main collection.
// Since the docs came from this collection, we should fail if something
// does not match.
,{$merge: {
into: "foo",
on: [ "_id" ],
whenMatched: "merge",
whenNotMatched: "fail"
}}
]);

Related

Mongoose - How to query field in the last object of an array of objects

I have MongoDB documents structured like this:
{
"_id": "5d8b987f9f8b9f9c8c8b9f9",
"targetsList": [
{
"target": "user",
"statusList": [
{
"date": "2018-01-01",
"type": "OK"
},
{
"date": "2018-01-02",
"type": "FAILD"
}
]
}
]
}
And I want to count all documents that in their "targetList" array, there is an object with "target"=="user" - and also that object conatin on the last element of its "statusList" array, an object with "type" != "FAILD".
Any ideas on how to implement this kind of query?
Mongo playground:
https://mongoplayground.net/p/3bCoHRnh-KQ
In this example, I expected the count to be 1, because only the second object meets the conditions.
An aggregation pipeline
1st step - Filtering out where "targetsList.target": "user"
2nd step - $unwind on targetsList to get it out of array
3rd step - getting the last element of the targetsList.statusList array using $arrayElemAt
4th step - getting the results where that last element is not FAILD
5th step - getting the count
demo - you can try removing parts of the pipeline to see what the intermediate results are
db.collection.aggregate([
{
$match: {
"targetsList.target": "user"
}
},
{
$unwind: "$targetsList"
},
{
$project: {
"targetsList.statusList": {
$arrayElemAt: [
"$targetsList.statusList",
-1
]
},
}
},
{
$match: {
"targetsList.statusList.type": {
$ne: "FAILD"
}
}
},
{
$count: "withoutFailedInLastElemCount"
}
])
Unless it's crucial that the element be the last index, this should work for your case.
db.collection.find({
"targetsList.statusList.type": {
$in: [
"FAILD"
]
}
})
This will retrieve documents where the type value is FAILD. To invert this you can swap $in for $nin.
Updated playground here
Here's another way to do it with a leading monster "$match".
db.collection.aggregate([
{
"$match": {
"targetsList.target": "user",
"$expr": {
"$reduce": {
"input": "$targetsList",
"initialValue": false,
"in": {
"$or": [
"$$value",
{
"$ne": [
{
"$last": "$$this.statusList.type"
},
"FAILD"
]
}
]
}
}
}
}
},
{
"$count": "noFailedLastCount"
}
])
Try it on mongoplayground.net.

Fetch grouped items from database

I'm trying to group values together in mongoose. I have a "Review" schema with the following fields:
{ userId, rating, comment }
There are many documents with the same userId. How can I retrieve them in the following format:
{userId: [...allRatings]
Or even better, is there a way to retrieve the averages for each userId? so like this: {userId: 2.8}
I know it's possible and very simple to do in node.js, but is there a way of doing it with mongoose?
Mongoose is really just a vehicle to pass commands to your mondoDB server, so accomplishing what you want in mongoose isn't dissimilar to accomplishing it in the mongo shell.
Here is the aggregation you're looking for:
db.collection.aggregate([
{
"$group": {
"_id": "$userId",
"ratings": {
$push: "$rating"
}
}
},
{
"$project": {
"_id": false,
"userId": "$_id",
"avgRating": {
"$avg": "$ratings"
}
}
}
])
The first stage of the pipeline groups all ratings by useId. The second stage calculates the ratings average and pretties up the key display. That's it. The result will be this:
[
{
"avgRating": 2.8,
"userId": 110
},
{
"avgRating": 3.275,
"userId": 100
}
]
Here is a playground for you: https://mongoplayground.net/p/yXVxk4klabB
As for how to specifically run this command in mongoose, well that's pretty straightforward:
const YourModel = mongoose.model('your_model');
...
YourModel.aggregate([
{
"$group": {
"_id": "$userId",
"ratings": {
$push: "$rating"
}
}
},
{
"$project": {
"_id": false,
"userId": "$_id",
"avgRating": {
"$avg": "$ratings"
}
}
}
])
.then(result => {
console.log(result);
})

How to have a conditional aggregate lookup on a foreign key

After many many tries, I can't have a nice conditional aggregation of my collections.
I use two collections :
races which have a collection of reviews.
I need to obtain for my second pipeline only the reviews published.
I don't want to use a $project.
Is it possible to use only the $match ?
When I use localField, foreignField, it works perfect, but I need to filter only the published reviews.
I struggled so much on this, I don't understand why the let don't give me the foreignKey.
I tried : _id, $reviews, etc..
My $lookup looks like this :
{
$lookup: {
from: "reviews",
as: "reviews",
let: { reviewsId: "$_id" },
pipeline: [
{
$match: {
$expr: {
$and: [
// If I comment the next line, it give all the reviews to all the races
{ $eq: ["$_id", "$$reviewsId"] },
{ $eq: ["$is_published", true] }
]
}
}
}
]
// localField: "reviews",
// foreignField: "_id"
}
},
Example of a race :
{
"description":"Nice race",
"attendees":[
],
"reviews":[
{
"$oid":"5c363ddcfdab6f1d822d7761"
},
{
"$oid":"5cbc835926fa61bd4349a02a"
}
],
...
}
Example of a review :
{
"_id":"5c3630ac5d00d1dc26273dab",
"user_id":"5be89576a38d2b260bfc1bfe",
"user_pseudo":"gracias",
"is_published":true,
"likes":[],
"title":"Best race",
"__v":10,
...
}
I will become crazy soon :'(...
How to accomplish that ?
Your problem is this line:
{ $eq: ["$is_published", true] }
You are using this document _id field to match the reviews one.
The correct version looks like this:
(
[
{
"$unwind" : "$reviews"
},
{
"$lookup" : {
"from" : "reviews",
"as" : "reviews",
"let" : {
"reviewsId" : "$reviews"
},
"pipeline" : [
{
"$match" : {
"$expr" : {
"$and" : [
{
"$eq" : [
"$_id",
"$$reviewsId"
]
},
{ $eq: ["$is_published", true] }
]
}
}
}
]
}
}
],
);
and now if your want to restore the old structure add:
{
$group: {
_id: "$_id",
reviews: {$push: "$reviews"},
}
}
First you have to take correct field to get the data from the referenced collection i.e. reviews. And second you need to use $in aggregation operator as your reviews field is an array of ObjectIds.
db.getCollection('races').aggregate([
{ "$lookup": {
"from": "reviews",
"let": { "reviews": "$reviews" },
"pipeline": [
{ "$match": {
"$expr": { "$in": [ "$_id", "$$reviews" ] },
"is_published": true
}}
],
"as": "reviews"
}}
])

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

MongoDB: Concatenate Multiple Arrays

I have 3 arrays of ObjectIds I want to concatenate into a single array, and then sort by creation date. $setUnion does precisely what I want, but I'd like to try without using it.
Schema of object I want to sort:
var chirpSchema = new mongoose.Schema({
interactions: {
_liked : ["55035390d3e910505be02ce2"] // [{ type: $oid, ref: "interaction" }]
, _shared : ["507f191e810c19729de860ea", "507f191e810c19729de860ea"] // [{ type: $oid, ref: "interaction" }]
, _viewed : ["507f1f77bcf86cd799439011"] // [{ type: $oid, ref: "interaction" }]
}
});
Desired result: Concatenate _liked, _shared, and _viewed into a single array, and then sort them by creation date using aggregate pipeline. See below
["507f1f77bcf86cd799439011", "507f191e810c19729de860ea", "507f191e810c19729de860ea", "55035390d3e910505be02ce2"]
I know I'm suppose to use $push, $each, $group, and $unwind in some combination or other, but I'm having trouble piecing together the documenation to make this happen.
Update: Query
model_user.aggregate([
{ $match : { '_id' : { $in : following } } }
, { $project : { 'interactions' : 1 } }
, { $project : {
"combined": { $setUnion : [
"$interactions._liked"
, "$interactions._shared"
, "$interactions._viewed"
]}
}}
])
.exec(function (err, data) {
if (err) return next(err);
next(data); // Combined is returning null
})
If all the Object _id values are "unique" then $setUnion is your best option. It is of course not "ordered" in any way as it works with a "set", and that does not guarantee order. But you can always unwind and $sort.
[
{ "$project": {
"combined": { "$setUnion": [
{ "$ifNull": [ "$interactions._liked", [] ] },
{ "$ifNull": [ "$interactions._shared", [] ] },
{ "$ifNull", [ "$interactions._viewed", [] ] }
]}
}},
{ "$unwind": "$combined" },
{ "$sort": { "combined": 1 } },
{ "$group": {
"_id": "$_id",
"combined": { "$push": "$combined" }
}}
]
Of course again since this is a "set" of distinct values you can do the old way instead with $addToSet, after processing $unwind on each array:
[
{ "$unwind": "$interactions._liked" },
{ "$unwind": "$interactions._shared" },
{ "$unwind": "$interactions._viewed" },
{ "$project": {
"interactions": 1,
"type": { "$const": [ "liked", "shared", "viewed" ] }
}}
{ "$unwind": "$type" },
{ "$group": {
"_id": "$_id",
"combined": {
"$addToSet": {
"$cond": [
{ "$eq": [ "$type", "liked" ] },
"$interactions._liked",
{ "$cond": [
{ "$eq": [ "$type", "shared" ] },
"$interactions._shared",
"$interactions._viewed"
]}
]
}
}
}},
{ "$unwind": "$combined" },
{ "$sort": { "combined": 1 } },
{ "$group": {
"_id": "$_id",
"combined": { "$push": "$combined" }
}}
]
But still the same thing applies to ordering.
Future releases even have the ability to concatenate arrays without reducing to a "set":
[
{ "$project": {
"combined": { "$concatArrays": [
"$interactions._liked",
"$interactions._shared",
"$interactions._viewed"
]}
}},
{ "$unwind": "$combined" },
{ "$sort": { "combined": 1 } },
{ "$group": {
"_id": "$_id",
"combined": { "$push": "$combined" }
}}
]
But still there is no way to re-order the results without procesing $unwind and $sort.
You might therefore consider that unless you need this grouped across multiple documents, that the basic "contenate and sort" operation is best handled in client code. MongoDB has no way to do this "in place" on the array at present, so per document in client code is your best bet.
But if you do need to do this grouping over multiple documents, then the sort of approaches as shown here are for you.
Also note that "creation" here means creation of the ObjectId value itself and not other properties from your referenced objects. If you need those, then you perform a populate on the id values after the aggregation or query instead, and of course sort in client code.

Resources