MongoDb: aggregation $lookup - mongodb

Given following collections.
I have to get the title field and combine to identifier
CREDENTIAL:
{
_id: ..
title: ..
}
USER_CREDENTIAL:
{
_id: ..
credential_id: .. (from credential collection)
created_at: ..
identifier: {
first_name: ..
middle_name: ..
last_name: ..
}
}
The response should be:
{
user_credential_id:
member: {
first_name:
middle_name:
last_name:
title:
created_at:
}
}

$lookup - Join user and user_credential_id with a pipeline to match the condition ($match) and decorate the document ($project).
$unwind - Deconstruct member array to multiple documents.
$project - Decorate the output document.
db.user.aggregate([
{
"$lookup": {
"from": "user_credential",
let: {
"user_credential_id": "$_id",
"title": "$title"
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$credential_id",
"$$user_credential_id"
]
}
}
},
{
$project: {
first_name: "$identifier.first_name",
middle_name: "$identifier.middle_name",
last_name: "$identifier.last_name",
title: "$$title",
created_at: "$created_at",
}
}
],
"as": "member"
}
},
{
$unwind: "$member"
},
{
$project: {
_id: 0,
user_credential_id: "$_id",
member: 1
}
}
])
Sample Mongo Playground

You can try this query
db.user.aggregate([
{
$lookup: {
from: "credentials",
localField: "credential_id",
foreignField: "_id",
as: "user_credential"
}
},
{
$unwind: "$user_credential"
},
{
$project: {
_id: 0,
user_credential_id: "$credential_id",
member: {
first_name: "$identifier.first_name",
middle_name: "$identifier.middle_name",
last_name: "$identifier.last_name",
title: "$user_credential.title",
created_at: "$created_at",
}
}
}
])
You can check it out here

Related

MongoDB Aggregation: Filter array with _id as string by ObjectId

I have the following collections:
const movieSchema = new Schema({
title: String
...
})
const userSchema = new Schema({
firstName: String,
lastName: String,
movies: [
movie: {
type: Schema.Types.ObjectId,
ref: 'Movie'
},
status: String,
feeling: String
]
...
})
I am trying to match up the movie (with all its details) with the user status and feeling for that movie, with the aggregation:
Movie.aggregate([
{ $match: { _id: ObjectId(movieId) } },
{
$lookup: {
from: 'users',
as: 'user_status',
pipeline: [
{ $match: { _id: ObjectId(userId) } },
{
$project: {
_id: 0,
movies: 1
}
},
{ $unwind: '$movies' }
]
}
},
])
Which returns:
[
{
_id: 610b678702500b0646925542,
title: 'The Shawshank Redemption',
user_status: [
{
"movies": {
"_id": "610b678702500b0646925542",
"status": "watched",
"feeling": "love"
}
},
{
"movies": {
"_id": "610b678502500b0646923627",
"status": "watched",
"feeling": "like"
}
},
{
"movies": {
"_id": "610b678502500b0646923637",
"status": "watched",
"feeling": "like"
}
},
]
}
]
My desired result is to match the first movie in user_status to get the eventual final result:
[
{
_id: 610b678702500b0646925542,
title: 'The Shawshank Redemption',
status: "watched",
feeling: "love"
}
]
I thought the next step in my pipeline would be:
{
$addFields: {
user_status: {
$filter: {
input: '$user_status',
cond: {
$eq: ['$$this.movies._id', '$_id']
}
}
}
}
}
But it doesn't work - Not sure if this $addFields is correct, and one problem I know is that my first _id is an ObjectId and the second appears to be a string.
If I understand correctly, you can $filter the user in the already existing $lookup pipeline, which will make things more simple later:
db.movies.aggregate([
{$match: {_id: ObjectId(movieId)}},
{
$lookup: {
from: "users",
as: "user_status",
pipeline: [
{$match: {_id: ObjectId(userId)}},
{$project: {
movies: {
$first: {
$filter: {
input: "$movies",
cond: {$eq: ["$$this.movie", ObjectId(movieId)]}
}
}
}
}
}
]
}
},
{
$project: {
title: 1,
feeling: {$first: "$user_status.movies.feeling"},
status: {$first: "$user_status.movies.status"}
}
}
])
See how it works on the playground example

Boost search score from data in another collection

I use Atlas Search to return a list of documents (using Mongoose):
const searchResults = await Resource.aggregate()
.search({
text: {
query: searchQuery,
path: ["title", "tags", "link", "creatorName"],
},
}
)
.match({ approved: true })
.addFields({
score: { $meta: "searchScore" }
})
.exec();
These resources can be up and downvoted by users (like questions on Stackoverflow). I want to boost the search score depending on these votes.
I can use the boost operator for that.
Problem: The votes are not a property of the Resource document. Instead, they are stored in a separate collection:
const resourceVoteSchema = mongoose.Schema({
_id: { type: String },
userId: { type: mongoose.Types.ObjectId, required: true },
resourceId: { type: mongoose.Types.ObjectId, required: true },
upDown: { type: String, required: true },
After I get my search results above, I fetch the votes separately and add them to each search result:
for (const resource of searchResults) {
const resourceVotes = await ResourceVote.find({ resourceId: resource._id }).exec();
resource.votes = resourceVotes
}
I then subtract the downvotes from the upvotes on the client and show the final number in the UI.
How can I incorporate this vote points value into the score of the search results? Do I have to reorder them on the client?
Edit:
Here is my updated code. The only part that's missing is letting the resource votes boost the search score, while at the same time keeping all resource-votes documents in the votes field so that I can access them later. I'm using Mongoose syntax but an answer with normal MongoDB syntax will work for me:
const searchResults = await Resource.aggregate()
.search({
compound: {
should: [
{
wildcard: {
query: queryStringSegmented,
path: ["title", "link", "creatorName"],
allowAnalyzedField: true,
}
},
{
wildcard: {
query: queryStringSegmented,
path: ["topics"],
allowAnalyzedField: true,
score: { boost: { value: 2 } },
}
}
,
{
wildcard: {
query: queryStringSegmented,
path: ["description"],
allowAnalyzedField: true,
score: { boost: { value: .2 } },
}
}
]
}
}
)
.lookup({
from: "resourcevotes",
localField: "_id",
foreignField: "resourceId",
as: "votes",
})
.addFields({
searchScore: { $meta: "searchScore" },
})
.facet({
approved: [
{ $match: matchFilter },
{ $skip: (page - 1) * pageSize },
{ $limit: pageSize },
],
resultCount: [
{ $match: matchFilter },
{ $group: { _id: null, count: { $sum: 1 } } }
],
uniqueLanguages: [{ $group: { _id: null, all: { $addToSet: "$language" } } }],
})
.exec();
It could be done with one query only, looking similar to:
Resource.aggregate([
{
$search: {
text: {
query: "searchQuery",
path: ["title", "tags", "link", "creatorName"]
}
}
},
{$match: {approved: true}},
{$addFields: {score: {$meta: "searchScore"}}},
{
$lookup: {
from: "ResourceVote",
localField: "_id",
foreignField: "resourceId",
as: "votes"
}
}
])
Using the $lookup step to get the votes from the ResourceVote collection
If you want to use the votes to boost the score, you can replace the above $lookup step with something like:
{
$lookup: {
from: "resourceVote",
let: {resourceId: "$_id"},
pipeline: [
{
$match: {$expr: {$eq: ["$resourceId", "$$resourceId"]}}
},
{
$group: {
_id: 0,
sum: {$sum: {$cond: [{$eq: ["$upDown", "up"]}, 1, -1]}}
}
}
],
as: "votes"
}
},
{$addFields: { votes: {$arrayElemAt: ["$votes", 0]}}},
{
$project: {
"wScore": {
$ifNull: [
{$multiply: ["$score", "$votes.sum"]},
"$score"
]
},
createdAt: 1,
score: 1
}
}
As you can see on this playground example
EDIT: If you want to keep the votes on the results, you can do something like:
db.searchResults.aggregate([
{
$lookup: {
from: "ResourceVote",
localField: "_id",
foreignField: "resourceId",
as: "votes"
}
},
{
"$addFields": {
"votesCount": {
$reduce: {
input: "$votes",
initialValue: 0,
in: {$add: ["$$value", {$cond: [{$eq: ["$$this.upDown", "up"]}, 1, -1]}]}
}
}
}
},
{
$addFields: {
"wScore": {
$add: [{$multiply: ["$votesCount", 0.1]}, "$score"]
}
}
}
])
As can be seen here

aggregation lookup and match a nested array

Hello i am trying to join two collections...
#COLLECTION 1
const valuesSchema= new Schema({
value: { type: String },
})
const categoriesSchema = new Schema({
name: { type: String },
values: [valuesSchema]
})
mongoose.model('categories', categoriesSchema )
#COLLECTION 2
const productsSchema = new Schema({
name: { type: String },
description: { type: String },
categories: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'categories',
}]
})
mongoose.model('productos', productsSchema )
Now, what i pretend to do is join these collections and have an output like this.
#Example Product Document
{
name: 'My laptop',
description: 'Very ugly laptop',
categories: ['5f55949054f3f31db0491b5c','5f55949054f3f31db0491b5b'] // these are _id of valuesSchema
}
#Expected Output
{
name: 'My laptop',
description: 'Very ugly laptop',
categories: [{value: 'Laptop'}, {value: 'PC'}]
}
This is what i tried.
{
$lookup: {
from: "categories",
let: { "categories": "$categories" },
as: "categories",
pipeline: [
{
$match: {
$expr: {
$in: [ '$values._id','$$categories']
},
}
},
]
}
}
but this query is not matching... Any help please?
You can try,
$lookup with categories
$unwind deconstruct values array
$match categories id with value id
$project to show required field
db.products.aggregate([
{
$lookup: {
from: "categories",
let: { cat: "$categories" },
as: "categories",
pipeline: [
{ $unwind: "$values" },
{ $match: { $expr: { $in: ["$values._id", "$$cat"] } } },
{
$project: {
_id: 0,
value: "$values.value"
}
}
]
}
}
])
Playground
Since you try to use the non-co-related queries, I appreciate it, you can easily achieve with $unwind to flat the array and then $match. To regroup the array we use $group. The $reduce helps to move on each arrays and store some particular values.
[
{
$lookup: {
from: "categories",
let: {
"categories": "$categories"
},
as: "categories",
pipeline: [
{
$unwind: "$values"
},
{
$match: {
$expr: {
$in: [
"$values._id",
"$$categories"
]
},
}
},
{
$group: {
_id: "$_id",
values: {
$addToSet: "$values"
}
}
}
]
}
},
{
$project: {
categories: {
$reduce: {
input: "$categories",
initialValue: [],
in: {
$concatArrays: [
"$$this.values",
"$$value"
]
}
}
}
}
}
]
Working Mongo template

How to do lookup on an aggregated collection in mongodb that is being grouped?

For some reason, I can't retrieve the author name from another collection on my aggregate query.
db.getCollection('books').aggregate([
{
$match: {
authorId: { $nin: [ObjectId('5b9a008575c50f1e6b02b27b'), ObjectId('5ba0fb3275c50f1e6b02b2f5'), ObjectId('5bc058b6ae9a2a4d6df330b1')]},
isBorrowed: { $in: [null, false] },
status: 'ACTIVE',
},
},
{
$lookup: {
from: "authors",
localField: "authorId", // key of author id in "books" collection
foreignField: "_id", // key of author id in "authors" collection
as: "bookAuthor",
}
},
{
$group: {
_id: {
author: '$authorId',
},
totalSalePrice: {
$sum: '$sale.amount',
},
},
},
{
$project: {
author: '$_id.author',
totalSalePrice: '$totalSalePrice',
authorName: '$bookAuthor.name', // I can't make this appear
_id: 0,
},
},
{ $sort: { totalSalePrice: -1 } },
])
Any advice on where I had it wrong? Thanks for the help.
Two things that are missing here: you need $unwind to convert bookAuthor from an array into single object and then you need to add that object to your $group stage (so that it will be available in next stages), try:
db.getCollection('books').aggregate([
{
$match: {
authorId: { $nin: [ObjectId('5b9a008575c50f1e6b02b27b'), ObjectId('5ba0fb3275c50f1e6b02b2f5'), ObjectId('5bc058b6ae9a2a4d6df330b1')]},
isBorrowed: { $in: [null, false] },
status: 'ACTIVE',
},
},
{
$lookup: {
from: "authors",
localField: "authorId",
foreignField: "_id",
as: "bookAuthor", // this will be an array
}
},
{
$unwind: "$bookAuthor"
},
{
$group: {
_id: {
author: '$authorId',
},
bookAuthor: { $first: "$bookAuthor" },
totalSalePrice: {
$sum: '$sale.amount',
},
},
},
{
$project: {
author: '$_id.author',
totalSalePrice: '$totalSalePrice',
authorName: '$bookAuthor.name',
_id: 0,
},
},
{ $sort: { totalSalePrice: -1 } },
])
Actually you have lost the bookAuthor field in the $group stage. You have to use $first accumulator to get it in the next $project stage.
{ "$group": {
"_id": { "author": "$authorId" },
"totalSalePrice": { "$sum": "$sale.amount" },
"authorName": { "$first": "$bookAuthor" }
}},
{ "$project": {
"author": "$_id.author",
"totalSalePrice": "$totalSalePrice",
"authorName": { "$arrayElemAt": ["$bookAuthor.name", 0] }
"_id": 0,
}}

Using $slice with $map on $lookup in MongoDB?

-
Hi , i have an aggregation query with lookup, i need to project specific fields from this lookup and slice them. This is what I've done so far.
{
$lookup: {
from: 'users',
localField: 'users',
foreignField: '_id',
as: 'users',
}},
I've added the unwind statement
{
$unwind: {
path: '$users',
preserveNullAndEmptyArrays: true
}},
I've added the group statement
{
$group: {
_id: {
_id: '$_id',
createdAt: '$createdAt',
updatedAt: '$updatedAt'
},
users: {
$addToSet: '$users',
}
}
},
And to project specific fields in array of users i did:
{
$project: {
_id: '$_id._id',
createdAt: '$_id.createdAt',
updatedAt: '$_id.updatedAt',
// users: {
// $slice: [
// "$users",
// skip,
// limit
// ]
// },
users: {
$map: {
input: '$users',
as: 'user',
in: {
email: '$$user.email',
name: '$$user.name',
username: '$$user.username',
updatedAt: '$$user.updatedAt'
}
}
}
}},
My question is , How can i use $slice in this scope ?
I don't know how 'legit' is this but , I've added fields to $addToSet statement in $group , so now i can use $slice with mapped fields.
{
$group: {
_id: {
_id: '$_id',
createdAt: '$createdAt',
updatedAt: '$updatedAt',
},
users: {
$addToSet: {
_id: '$users._id',
email: '$users.email',
name: '$users.name',
username: '$users.username',
updatedAt: '$users.updatedAt'
}
}
}
}
Now i can easily do $slice in $project statement.
{
$project: {
_id: '$_id._id',
users: {
$slice: [
"$users",
skip,
limit
]
}
},
}
If someone has a better solution , i would like to know.