aggregation lookup and match a nested array - mongodb

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

Related

How to join two Mongo DB Collections together, with one being an Array of Objects inside the Other

I have two collections, one being Companies and the others being Projects. I am trying to write an aggregation function that first grabs all Companies with the status of "Client", then from there write a pipeline that will return all filtered Companies where the company._id === project.companyId, as an Array of Objects. An example of the shortened Collections are below:
Companies
{
_id: ObjectId('2341908342'),
companyName: "Meta",
address: "123 Facebook Lane",
status: "Client"
}
Projects
{
_id: ObjectId('234123840'),
companyId: '2341908342',
name: "Test Project",
price: 97450,
}
{
_id: ObjectId('23413456'),
companyId: '2341908342',
name: "Test Project 2",
price: 100000,
}
My desired outcome after the Aggregation:
Companies
{
_id: ObjectId('2341908342'),
companyName: "Meta",
address: "123 Facebook Lane",
projects: [ [Project1], [Project2],
}
The projects field does not currently exist on the Companies collection, so I imagine we would have to add it. I also begun writing a $match function to filter by clients, but I am not sure if this is correct. I am trying to use $lookup for this but can not figure out the pipeline. Can anyone help me?
Where I'm currently stuck:
try {
const allClientsWithProjects = await companyCollection
.aggregate([
{
$match: {
orgId: {
$in: [new ObjectId(req.user.orgId)],
},
status: { $in: ["Client"] },
},
},
{
$addFields: {
projects: [{}],
},
},
{
$lookup: { from: "projects", (I am stuck here) },
},
])
.toArray()
Thank you for any help anyone can provide.
UPDATE*
I am seemingly so close I feel like... This is what I have currently, and it is returning everything but Projects is still an empty array.
try {
const allClients = await companyCollection
.aggregate([
{
$match: {
orgId: {
$in: [new ObjectId(req.user.orgId)],
},
status: {
$in: ["Client"],
},
},
},
{
$lookup: {
from: "projects",
let: {
companyId: {
$toString: [req.user.companyId],
},
},
pipeline: [
{
$match: {
$expr: {
$eq: ["$companyId", "$$companyId"],
},
},
},
],
as: "projects",
},
},
])
.toArray()
All of my company information is being returned correctly for multiple companies, but that projects Array is still []. Any help would be appreciated, and I will still be troubleshooting this.
One option is using a $lookup with a pipeline:
db.company.aggregate([
{
$match: {
_id: {
$in: [
ObjectId("5a934e000102030405000000")
],
},
status: {
$in: [
"Client"
]
},
},
},
{
$lookup: {
from: "Projects",
let: {
companyId: {
$toString: "$_id"
}
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$companyId",
"$$companyId"
]
}
}
}
],
as: "projects"
}
}
])
See how it works on the playground example
Final answer for my question:
try {
const allClientsAndProjects = await companyCollection
.aggregate([
{
$match: {
orgId: {
$in: [new ObjectId(req.user.orgId)],
},
status: {
$in: ["Client"],
},
},
},
{
$lookup: {
from: "projects",
let: {
companyId: {
$toString: "$_id",
},
},
pipeline: [
{
$match: {
$expr: {
$eq: ["$companyId", "$$companyId"],
},
},
},
],
as: "projects",
},
},
])
.toArray()

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

How do I use mongo aggregation to lookup data in a subdocument?

I want to $lookup data from a subdocument in another collection. I have survey answers, and I want to group them by the question category's name.
The survey documents looks like this:
{
_id: new ObjectId("62555be60401f0a21553da9a"),
name: 'new survey',
questions: [
{
text: 'question 1',
category_id: new ObjectId("62555be60401f0a21553da99"),
options: [Array],
_id: new ObjectId("62555be60401f0a21553da9c"),
},
...
}
Category collection is just name and _id:
{
_id: new ObjectId("62555be60401r0a27553da99"),
name: "category name"
}
I have answer data like this:
[
{
answers: {
k: '62555be60401f0a21553da9c',
v: new ObjectId("62555880da8fb89651f6a292")
},
},
{
answers: {
k: '62555880da8fb89651f6a29b',
v: new ObjectId("62555880da8fb89651f6a29e")
},
}
...
]
k is a string that matches to the _id in the survey.questions array.
I'd like to get the resulting data like this:
[
{
answers: {
k: 'question 1',
v: new ObjectId("62555880da8fb89651f6a292")
},
category: 'category name'
},
{
answers: {
k: 'question 2',
v: new ObjectId("62555880da8fb89651f6a29e")
},
category: 'other category name'
}
...
]
any help would be greatly appreciated!
I think I could probably figure out the category part, but I cannot figure out how to use $lookup to get info from a subdocument. From the docs I'm guessing its maybe some pipeline within a lookup. Pretty stumped though.
You can do something like this, using a pipeline to match only surveys that have questions._id that matches the answer.k value
db.answer.aggregate([
{
$lookup: {
"from": "survey",
"let": {
"k": {
"$toObjectId": "$answers.k"
}
},
pipeline: [
{
"$match": {
"$expr": {"$in": ["$$k", "$questions._id"]}
}
}
],
as: "details"
}
},
{
$project: {
answers: 1,
details: {"$arrayElemAt": ["$details", 0]}
}
},
{
$project: {
answers: 1,
categoryData: {
$filter: {
input: "$details.questions",
as: "item",
cond: {$eq: ["$$item._id", {"$toObjectId": "$answers.k"}]}
}
}
}
},
{
$project: {
answers: 1,
catData: {"$arrayElemAt": ["$categoryData", 0]}
}
},
{
$lookup: {
from: "Category",
localField: "catData.category_id",
foreignField: "_id",
as: "cat"
}
},
{
$project: {
answers: 1,
_id: 0,
category: {"$arrayElemAt": ["$cat", 0]}
}
},
{
$project: {answers: 1, name: "$category.name"}
}
])
As you can see on the playground
Maybe it is possible to filter the results during the $lookup in order to simplify the rest of the query

Aggregate $lookup Array of Objects

I have collection schools with field groups and I am attempting to $lookup documents from the users collection. I am getting empty results however and an extra schools document.
Schools Schema
const SchoolSchema = new Schema({
groups: [
{
name: { type: String },
color: { type: String },
userDrivenName: { type: String },
},
]
});
module.exports = School = mongoose.model("School", SchoolSchema);
User Schema
const UserSchema = new Schema({
name: {
type: String,
required: true,
},
groups: [
{
groupId: { type: String },
name: { type: String },
color: { type: String },
userDrivenName: { type: String },
},
]
});
Query
db.schools.aggregate([
{
$match: {
_id: ObjectId("5d836e584a24e20e6090fd7b")
}
},
{
$project: {
groups: 1
}
},
{
$unwind: "$groups"
},
{
$lookup: {
from: "users",
let: {
groupId: "$groups._id"
},
pipeline: [
{
$match: {
"groups.groupId": "$$groupId"
}
}
],
as: "groups",
},
},
])
Results:
[
{
"_id": "5d836e584a24e20e6090fd7b",
"groups": []
},
{
"_id": "5d836e584a24e20e6090fd7b",
"groups": []
}
]
Expected Results:
[
{
"_id":"5d836e584a24e20e6090fd7b",
"groups":[
{
"_id":"5ec01fdc1dfb0a4f08316dfe",
"name":"GROUP 1",
"users":[
{
"name":"Luke Skywalker"
}
]
}
]
}
]
MongoPlayground
Two things:
There's a type mismatch between groupId and groups.groupId so you need to use $toString (based on your Mongo Playground example),
$lookup with custom pipelines allows only expression when you use $match so you need $in and $expr:
{
$lookup: {
from: "users",
let: { groupId: { $toString: "$groups._id" } },
pipeline: [
{
$match: {
$expr: {
$in: ["$$groupId","$groups.groupId"]
}
}
}
],
as: "groups"
}
}
Mongo Playground