MongoDB $lookup on array of documents containing object id - mongodb

I want to make aggregation pipeline in MongoDB v4.4.
I have two collections.
users collection
[
{ _id: "user1", username: "john" },
{ _id: "user2", username: "bob" },
]
articles collection
[
{
_id: "article1",
view_history: [
{ user: "user1", viewed_at: ISODate("...") },
{ user: "user2", viewed_at: ISODate("...") },
{ user: "user1", viewed_at: ISODate("...") },
]
}
]
My desired result after running aggregation on articles collection is:
[
{
_id: "article1",
view_history: [
{ user: "john", viewed_at: ISODate("...") },
{ user: "bob", viewed_at: ISODate("...") },
{ user: "john", viewed_at: ISODate("...") },
]
}
]
So user fields on each history item in view_history are substituted with the user's username.
I tried many answers on SO, but can't find any solution for my situation.
Here's what I got so far:
db.articles.aggregate([
{
$lookup: {
from: "users",
localField: "view_history.user",
foreignField: "_id",
as: "output",
}
}
])
Which yields:
[
{
_id: "article1",
view_history: [
{ user: "user1", viewed_at: ISODate("...") },
{ user: "user2", viewed_at: ISODate("...") },
{ user: "user1", viewed_at: ISODate("...") },
],
output: [
{ _id: "user1", username: "john" },
{ _id: "user2", username: "bob" },
]
}
]
(There's only one user1 in output, and viewed_at not included)

$unwind deconstruct view_history array
$lookup with users collection
get first element from view_history.user.username because it will return array using $arrayElemAt
$group by _id and re-construct view_history array
db.articles.aggregate([
{ $unwind: "$view_history" },
{
$lookup: {
from: "users",
localField: "view_history.user",
foreignField: "_id",
as: "view_history.user"
}
},
{
$addFields: {
"view_history.user": {
$arrayElemAt: ["$view_history.user.username", 0]
}
}
},
{
$group: {
_id: "$_id",
view_history: {
$push: "$view_history"
}
}
}
])
Playground
Second approach:
$lookup with users collection
$map to iterate loop of view_history array
$filter to iterate loop of output array and match user field
$let declare variable u for above filter, in to get first element from filtered result using $arrayElemAt
db.articles.aggregate([
{
$lookup: {
from: "users",
localField: "view_history.user",
foreignField: "_id",
as: "output"
}
},
{
$project: {
view_history: {
$map: {
input: "$view_history",
as: "v",
in: {
user: {
$let: {
vars: {
u: {
$filter: {
input: "$output",
cond: { $eq: ["$$this._id", "$$v.user"] }
}
}
},
in: { $arrayElemAt: ["$$u.username", 0] }
}
},
viewed_at: "$$v.viewed_at"
}
}
}
}
}
])
Playground

Related

MongoDb query to exclude omission of rows based on criteria

In below example, looking for new partner suggestions for user abc. abc has already sent a request to 123 so that can be ignored. rrr has sent request to abc but rrr is in the fromUser field so rrr is still a valid row to be shown as suggestion to abc
I have two collections:
User collection
[
{
_id: "abc",
name: "abc",
group: 1
},
{
_id: "xyz",
name: "xyyy",
group: 1
},
{
_id: "123",
name: "yyy",
group: 1
},
{
_id: "rrr",
name: "tttt",
group: 1
},
{
_id: "eee",
name: "uuu",
group: 1
}
]
Partnership collection (if users have already partnered)
[
{
_id: "abc_123",
fromUser: "abc",
toUser: "123"
},
{
_id: "rrr_abc",
fromUser: "rrr",
toUser: "abc"
},
{
_id: "xyz_rrr",
fromUser: "xyz",
toUser: "rrr"
}
]
My query below excludes the user rrr but it should not because its not listed in toUser field in the partnership collection corresponding to the user abc.
How to modify this query to include user rrr in this case?
db.users.aggregate([
{
$match: {
group: 1,
_id: {
$ne: "abc"
}
}
},
{
$lookup: {
from: "partnership",
let: {
userId: "$_id"
},
as: "prob",
pipeline: [
{
$set: {
users: [
"$fromUser",
"$toUser"
],
u: "$$userId"
}
},
{
$match: {
$expr: {
$and: [
{
$in: [
"$$userId",
"$users"
]
},
{
$in: [
"abc",
"$users"
]
}
]
}
}
}
]
}
},
{
$match: {
"prob.0": {
$exists: false
}
}
},
{
$sample: {
size: 1
}
},
{
$unset: "prob"
}
])
https://mongoplayground.net/p/utGMeHFRGmt
Your current query does not allow creating an existing connection regardless of the connection direction. If the order of the connection is important use:
db.users.aggregate([
{$match: {
group: 1,
_id: {$ne: "abc"}
}
},
{$lookup: {
from: "partnership",
let: { userId: {$concat: ["abc", "_", "$_id"]}},
as: "prob",
pipeline: [{$match: {$expr: {$eq: ["$_id", "$$userId"]}}}]
}
},
{$match: {"prob.0": {$exists: false}}},
{$sample: {size: 1}},
{$unset: "prob"}
])
See how it works on the playground example
For MongoDB 5 and later, I'd propose the following aggregation pipeline:
db.users.aggregate([
{
$match: {
group: 1,
_id: {
$ne: "abc"
}
}
},
{
$lookup: {
from: "partnership",
as: "prob",
localField: "_id",
foreignField: "toUser",
pipeline: [
{
$match: {
fromUser: "abc",
}
}
]
}
},
{
$match: {
"prob.0": {
$exists: false
}
}
},
{
$unset: "prob"
}
])
The following documents are returned (full result without the $sample stage):
[
{
"_id": "eee",
"group": 1,
"name": "uuu"
},
{
"_id": "rrr",
"group": 1,
"name": "tttt"
},
{
"_id": "xyz",
"group": 1,
"name": "xyyy"
}
]
The main difference is that the lookup connects the collections by the toUser field (see localField, foreignField) and uses a minimal pipeline to restrict the results further to only retrieve the requests from the current user document to "abc".
See this playground to test.
When using MongoDB < 5, you cannot use localField and foreignField to run the pipeline only on a subset of the documents in the * from*
collection. To overcome this, you can use this aggregation pipeline:
db.users.aggregate([
{
$match: {
group: 1,
_id: {
$ne: "abc"
}
}
},
{
$lookup: {
from: "partnership",
as: "prob",
let: {
userId: "$_id"
},
pipeline: [
{
$match: {
$expr: {
$and: [
{
$eq: [
"$fromUser",
"abc"
]
},
{
$eq: [
"$toUser",
"$$userId"
]
}
]
}
}
}
]
}
},
{
$match: {
"prob.0": {
$exists: false
}
}
},
{
$unset: "prob"
}
])
The results are the same as for the upper pipeline.
See this playground to test.
For another, another way, this query starts from the partnership collection, finds which users to exclude, and then does a "$lookup" for everybody else. The remainder is just output formatting, although it looks like you may want to add a "$sample" stage at the end.
db.partnership.aggregate([
{
"$match": {
"fromUser": "abc"
}
},
{
"$group": {
"_id": null,
"exclude": {"$push": "$toUser" }
}
},
{
"$lookup": {
"from": "users",
"let": {
"exclude": {"$concatArrays": [["abc"], "$exclude"]
}
},
"pipeline": [
{
"$match": {
"$expr": {
"$not": {"$in": ["$_id", "$$exclude"]}
}
}
}
],
"as": "output"
}
},
{
"$project": {
"_id": 0,
"output": 1
}
},
{"$unwind": "$output"},
{"$replaceWith": "$output"}
])
Try it on mongoplayground.net.

Mongodb lookup for array of ids with nested array of objects in other collection

I am new to mongo db i am trying to find a lookup for two collection
one collection is users which has tags like
{
_id: "fdkjkjs",
first_name: "",
last_name: "",
role: "admin",
tags:
[
{ _id: "tag_1_id", name: "Tag 1" },
{ _id: "tag_2_id", name: "Tag 2" },
{ _id: "tag_3_id", name: "Tag 3" },
{ _id: "tag_4_id", name: "Tag 4" }
]
}
and a post collection is as below
{
_id: "fdkjkjs",
title: "",
slug: "",
tags: ["tag_1_id", tag_3_id]
}
So I want to get all the tags in post list API with the names that are in users collection.
so result i wanted belike
[{
_id: "fdkjkjs",
title: "",
slug: "",
tags: ["tag_1_id", tag_3_id],
selectedTags: [
{ _id: "tag_1_id", name: "Tag 1" },
{ _id: "tag_3_id", name: "Tag 3" }
],
}]
Method 1
db.posts.aggregate([
{
"$lookup": {
"from": "users",
"localField": "tags",
"foreignField": "tags._id",
"as": "selectedTags"
}
},
{
"$set": {
"selectedTags": {
"$filter": {
"input": { "$first": "$selectedTags.tags" },
"as": "item",
"cond": { $in: [ "$$item._id", "$tags" ] }
}
}
}
}
])
mongoplayground
Method 2
db.posts.aggregate([
{
$lookup: {
from: "users",
let: { tags_post: "$tags" },
pipeline: [
{
"$unwind": "$tags"
},
{
$match: {
$expr: {
$in: [ "$tags._id", "$$tags_post" ]
}
}
},
{
"$replaceWith": "$tags"
}
],
as: "selectedTags"
}
}
])
mongoplayground
Method 3
db.posts.aggregate([
{
$lookup: {
from: "users",
localField: "tags",
foreignField: "tags._id",
let: { tags_post: "$tags" },
pipeline: [
{
"$unwind": "$tags"
},
{
$match: {
$expr: {
$in: [ "$tags._id", "$$tags_post" ]
}
}
},
{
"$replaceWith": "$tags"
}
],
as: "selectedTags"
}
}
])
mongoplayground
Method 2 arrange data first and then lookup, while method 3 lookup first and then arrange data. Though 2 and 3 looks similar, I think method 3 is faster than method 2.

How to aggregate with value matched in an array then sort

I have these collections:
lists
{_id: 1, item: "a", owner: 1}
users
{_id: 1, subs: [{_id: 1, active: "Y"},{_id: 2, active: "N"}]}
subs
{_id: 1, text: "A"}
{_id: 2, text: "B"}
I want to have a result of lists with user info and with subs info that is active.
{_id: 1, item: "a", owner: {_id: 1, subs: [{_id: {_id: 1, text: "A"}, active: "Y"}]}}
I want also to sort it based on "text" field.
I tried aggregation but failed,
db.getCollection("lists").aggregate(
[
{
"$lookup" : {
"from" : "users",
"localField" : "owner",
"foreignField" : "_id",
"as" : "owner"
}
},
{
"$match" : {
"owner.0.subs" : {
"$elemMatch" : {
"active" : "Y"
}
}
}
}
],
{
"allowDiskUse" : false
}
);
I am also using Mongoose and failed using populate.
Any way to get my result?
Here, I updated my aggregation pipeline,
[
{
$lookup: {
from: "users",
as: "owner",
let: { owner: "$owner" },
pipeline: [
{ $match: { $expr: { $eq: ["$$owner", "$_id"] } } },
{ $unwind: { path:"$sub", preserveNullAndEmptyArrays: false} },
{ $match: { "subs.active": "Y" } },
{
$lookup: {
from: "plans",
localField: "subs._id",
foreignField: "_id",
as: "subs.plans"
}
},
{ $unwind: { path:"$subs.plans", preserveNullAndEmptyArrays: false} },
]
}
},
{ $unwind: { path: "$owner", preserveNullAndEmptyArrays: true} },
{ '$sort': { item: 1 } },
{ '$skip': 0 },
{ '$limit': 20 } ]
You can use lookup with pipeline and nested lookup,
inside lookup pipelines are:
$match your owner id in users collection
$unwind deconstruct subs array because we need to lookup with subs collection
$match subs is active or not
$lookup with subs collection
$unwind deconstruct subs._id that we joined from subs collection
$group reconstruct subs array
$unwind deconstruct owner array
$sort by item and pagination by $skip and $limit
db.getCollection("lists").aggregate([
{
$lookup: {
from: "users",
as: "owner",
let: { owner: "$owner" },
pipeline: [
{ $match: { $expr: { $eq: ["$$owner", "$_id"] } } },
{ $unwind: "$subs" },
{ $match: { "subs.active": "Y" } },
{
$lookup: {
from: "subs",
localField: "subs._id",
foreignField: "_id",
as: "subs._id"
}
},
{ $unwind: "$subs._id" },
{
$group: {
_id: "$_id",
subs: {
$push: {
_id: "$subs._id._id",
text: "$subs._id.text",
active: "$subs.active"
}
}
}
}
]
}
},
{ $unwind: "$owner" },
{ $sort: { item: 1 } },
{ $skip: 0 },
{ $limit: 20 }
], { allowDiskUse: false })
Playground
Your Second Edit: there is wrong key name sub in first lookup inside first $unwind, correct this,
{ $unwind: { path:"$sub", preserveNullAndEmptyArrays: false} }
to
{ $unwind: { path:"$subs", preserveNullAndEmptyArrays: false} }
Your Working Query

mongodb - summations of array length with same ids

I am creating a platform where people can share their memes. On one page I want to show them who are the most popular members on the platform. so, there is a collection of 'meme' and 'user'
for example,
There is two content with same ids:
{
_id: 1,
username: "name",
bio: "bio",
image: "url",
};
memes
{
_id: 0,
user_id: 1,
image: "meme1.jpg",
likes: [
{
user_id: 4
}
]
},
{
_id: 1,
user_id: 1,
image: "meme2.jpg",
likes: [
{
user_id: 5
},
{
user_id: 6
}
]
}
and I want to output something like this way
{
user_id:1,
username:"name"
likes:3,
}
I wrote this query using aggregate functions but I am not understanding how to identify ids are the same or not?
meme
.aggregate([
{
$lookup: {
from: "users",
localField: "user_id",
foreignField: "_id",
as: "userDetails",
},
},
{
$project: {
user_id: "$user_id",
username: "$userDetails.username",
likes: {
$size: "$likes",
},
},
},
{
$sort: { likes: 1 },
},
])
.exec()
.then((result) => {
console.log(result);
});
It will be easier to start query with users.
You can use $sum, $map, $size aggregations to get the total likes, and add it using $addFields.
db.users.aggregate([
{
$lookup: {
from: "memes",
localField: "_id",
foreignField: "user_id",
as: "userDetails"
}
},
{
$addFields: {
"likes": {
"$sum": {
"$map": {
"input": "$userDetails",
"in": {
"$size": "$$this.likes"
}
}
}
}
}
},
{
$project: {
_id: 0,
user_id: "$_id",
username: 1,
likes: 1
}
}
])
Playground
Result:
[
{
"likes": 3,
"user_id": 1,
"username": "name"
}
]
You could project the length of the likes-array and group each projection by the user_id and cound the results. Something like this should work:
db.getCollection('memes').aggregate([{
$lookup: {
from: "users",
localField: "user_id",
foreignField: "_id",
as: "userDetails"
}
}, {
"$project": {
"user_id": 1,
"likesSize": {
"$size": "$likes"
}
}
}, {
$group: {
_id: "$user_id",
"count": {
"$sum": "$likesSize"
}
}
}
])
The above query should return:
{
"_id" : 1,
"count" : 3
}

How to use $lookup on array of subdocuments

I have these Schemas:
const chatbots = new Schema({
name: String,
campaigns: [{
name: String,
channels: [{
_id: String,
name: String,
budget: Number
}]
}]
});
const chatbotusers = new Schema({
name: String,
campaign_channel: String
})
And I need to get a list of Campaigns where, for each Channel, I have the total of ChatbotUsers. Something like this:
[
{
"name": "Campaign #1",
"channels": {
"_id": "eyRyZ1gD0",
"name": "Channel #1",
"users": 10
}
},
{
"name": "Campaign #1",
"channels": {
"_id": "tsKH7WxE",
"name": "Channel #2",
"users": 4
}
}
]
Any ideas?
The furthest I got was something like this:
{
$lookup: {
from: "chatbotusers",
localField: "channels._id",
foreignField: "campaign_channel",
as: "users",
}
},
{
$project: {
name: "$name",
channels: {
$map: {
input: "$channels",
as: "channel",
in: {
_id: "$$channel._id",
name: "$$channel.name",
users: { $size: "$users" },
}
}
}
}
}
But it sums the users for the Campaign, not the Channel.
(Sorry if the question title is not appropriate, I didn't even know how to ask this properly)
You can try this query :
db.chatbots.aggregate([
{
$lookup: {
from: "chatbotusers",
localField: "campaigns.channels._id",
foreignField: "campaign_channel",
as: "users"
}
},
{
$addFields: {
campaigns: {
$map: {
input: "$campaigns",
as: "eachCampaign",
in: {
$mergeObjects: ['$$eachCampaign', {
channels:
{
$reduce: {
input: "$$eachCampaign.channels",
initialValue: [],
in: {
$concatArrays: [
"$$value",
[
{
$mergeObjects: [
"$$this",
{
user: {
$size: {
$filter: {
input: "$users",
as: "e",
cond: {
$eq: [
"$$e.campaign_channel",
"$$this._id"
]
}
}
}
}
}
]
}
]
]
}
}
}
}]
}
}
}
}
},
{
$project: {
users: 0
}
}
])
Note : There can be multiple ways to do this, but this way we're working on same no.of docs from the chatbots collection rather than exploding docs by doing $unwind which may be helpful when you've huge dataset.
Test : MongoDB-Playground
This above query should get you what is needed, but in any case if it's slow or you think to enhance it then here :
{
user: {
$size: {
$filter: {
input: "$users", as: "e",
cond: {
$eq: [
"$$e.campaign_channel",
"$$this._id"
]
}
}
}
}
}
Where We're iterating thru users array for every channel in every campaign, So instead of iterating every time, right after lookup - You can iterate over users for once using reduce to get count of each unique campaign_channel replace this data as users array, that way you can get count of users directly. In general main intention of above query is to preserve original document structure with less stages being used.
Alternatively you can use this query, which doesn't preserve original doc structure (also no.of docs in output can be more than what you've in collection) but can do what you needed :
db.chatbots.aggregate([
{
$unwind: "$campaigns"
},
{
$unwind: "$campaigns.channels"
},
{
$lookup: {
from: "chatbotusers",
localField: "campaigns.channels._id",
foreignField: "campaign_channel",
as: "users"
}
},
{
$addFields: {
"channels": "$campaigns.channels",
campaigns: "$campaigns.name"
}
},
{
$addFields: {
"channels.users": {
$size: "$users"
}
}
},
{
$project: {
users: 0
}
}
])
Test : MongoDB-Playground