$lookup from nested array without overwriting array - mongodb

My collection looks like:
collectionName: {
_id: ObjectId("..."),
thing1: 'whatever',
thing2: 100,
arrayOfThings: [{
item1: 'something'
item2: 200,
other_id: ObjectId("..."),
]}
}
Essentially I want to be able to find this entry by its _id, then for each of the items in the arrayOfThings I want to add an "other" field which is the entry in my other collection with the _id in "other_id".
Resulting in:
collectionName: {
_id: ObjectId("..."),
thing1: 'whatever',
thing2: 100,
arrayOfThings: [{
item1: 'something'
item2: 200,
other_id: ObjectId("..."),
other: {
otherField1: 'random data',
otherField2: 3000
]}
}
Everything I've tried either overwrites the entire arrayOfThings array with the array that is returned from the other collection or returns several objects, each with only one entry in the arrayOfThings array by doing something like this:
aggregate([
{ $match: { _id: req.params._id }},
{ $unwind: "$arrayOfThings" },
{ $lookup: { from: "otherCollection", localField: "arrayOfThings.other_id", foreignField: "_id", as: "arrayOfThings.other" }},
]);
Any help is appreciated, thanks.

$match, $unwind and $lookup stages remain same
$addFields to get first element from lookup result
$group by _id and reconstruct arrayOfThings, and get other fields using $first
db.col1.aggregate([
// $match, $unwind and $lookup skipping,
{
$addFields: {
"arrayOfThings.other": { $arrayElemAt: ["$arrayOfThings.other", 0] }
}
},
{
$group: {
_id: "$_id",
arrayOfThings: { $push: "$arrayOfThings" },
thing1: { $first: "$thing1" },
thing2: { $first: "$thing2" }
}
}
])
Playground

Related

$Lookup inside an array with more properties MongoDB

I am trying $lookup in Array but can't merge it in my object after $lookup.
Collection
_id: '5f7a1053477c8a1ae88e22cf',
name: 'Demo'
price: 423,
related: [
{
idProduct: '61140763ab806726a8ab7aea'
quantity: 2
},
{
idProduct: '61140763ab806726a8ab7aeb'
quantity: 6
},
]
Expected Output:
_id: '5f7a1053477c8a1ae88e22cf',
name: 'Demo',
price: 423,
related: [
{
idProduct: {
_id: '61140763ab806726a8ab7aea',
name: 'related1',
price: 22
},
quantity: 2
},
{
idProduct: {
_id: '61140763ab806726a8ab7aeb',
name: 'related2',
price: 53
},
quantity: 6
},
]
I need $lookup, idProduct and keep that data structure.
Any help? Thanks in advance
$unwind deconstruct related array
$lookup with collection 2, pass related.idProduct as local field and _id as foreign field and set in related.idProduct
$group by _id and reconstruct related array and get first required firlds
db.col1.aggregate([
{ $unwind: "$related" },
{
$lookup: {
from: "col2",
localField: "related.idProduct",
foreignField: "_id",
as: "related.idProduct"
}
},
{
$group: {
_id: "$_id",
name: { $first: "$name" },
price: { $first: "$price" },
related: { $push: "$related" }
}
}
])
Playground
The second approach without using $unwind stage,
$lookup with collection 2,
$map to iterate loop of related array
$filter to iterate loop of col2 array by idProduct field
$arrayElemAt to get first element from above filtered result
$mergeObjects to merge current object with updated idProduct field
db.col1.aggregate([
{
$lookup: {
from: "col2",
localField: "related.idProduct",
foreignField: "_id",
as: "col2"
}
},
{
$addFields: {
col2: "$$REMOVE",
related: {
$map: {
input: "$related",
as: "r",
in: {
$mergeObjects: [
"$$r",
{
idProduct: {
$arrayElemAt: [
{
$filter: {
input: "$col2",
cond: { $eq: ["$$r.idProduct", "$$this._id"] }
}
},
0
]
}
}
]
}
}
}
}
}
])
Playground

return match item only from array of object mongoose

[
{
item: "journal",
instock: [
{
warehouse: "A",
qty: 5,
items: null
},
{
warehouse: "C",
qty: 15,
items: [
{
name: "alexa",
age: 26
},
{
name: "Shawn",
age: 26
}
]
}
]
}
]
db.collection.find({
"instock.items": {
$elemMatch: {
name: "alexa"
}
}
})
This returns whole items array where as i just want items array with one item {name: 'alexa', age: 26}
Playground Link : https://mongoplayground.net/p/0gB4hNswA6U
You can use the .aggregate(pipeline) function.
Your code will look like:
db.collection.aggregate([{
$unwind: {
path: "$instock"
}
}, {
$unwind: {
path: "$instock.items"
}
}, {
$replaceRoot: {
newRoot: "$instock.items"
}
}, {
$match: {
name: "alexa"
}
}])
Commands used in this pipeline:
$unwind - deconstructs an array of items into multiple documents which all contain the original fields of the original documents except for the unwinded field which now have a value of all the deconstructed objects in the array.
$replaceRoot - takes the inner object referenced on newRoot and puts it as the document.
$match - a way to filter the list of documents you ended up with by some condition. Basically the first argument in the .find() function.
For more information about aggregation visit MongoDB's website:
Aggregation
$unwind
$replaceRoot
$match
EDIT
The wanted result was to get single item arrays as a response, to achieve that you can simply remove the $replaceRoot stage in the pipeline.
Final pipeline:
db.collection.aggregate([{
$unwind: {
path: "$instock"
}
}, {
$unwind: {
path: "$instock.items"
}
}, {
$match: {
"instock.items.name": "alexa"
}
}])
You have to use $elemMatch in projection section:
db.collection.find({
// whatever your query is
}, {
"instock.items": {
$elemMatch: {
name: "alexa"
}
}
});
UPDATE
Here is a good example of this usage. From the official mongodb documentation.
An alternative approach where $filter is the key at the project stage.
db.collection.aggregate([
{
$match: {
"instock.items.name": "alexa"
}
},
{
$unwind: "$instock"
},
{
$project: {
"item": "$item",
qty: "$instock.qty",
warehouse: "$instock.warehouse",
items: {
$filter: {
input: "$instock.items",
as: "item",
cond: {
$eq: [
"$$item.name",
"alexa"
]
}
}
}
}
},
{
$group: {
_id: "$_id",
instock: {
$push: "$$ROOT"
}
}
}
])
The idea is to:
have $match as top level filter
then $unwind instock array items to prepare for the $filter
Use $project for rest of the fields as they are, and use $filter on items array field
Finally $group them back since $unwind was used previously.
Play link

MongoDB Lookup in array

Good Morning.
I'm trying to query mongoDB by joining two collections. But I'm not able to do that when the primary identifier is inside an array in the secondary collection.
See my code and my collection
Pipe Colletion
{ _id: 1, deal_id: 25698}
{ _id: 2, deal_id: 45879}
{ _id: 3, deal_id: 54142}
Leads Colletion
{ _id: 1, name:"Teste A", deals_id:[25698,45879]}
{ _id: 2, name:"Teste B", deals_id:[54142]}
Desired result when searching for deal_id from the Pipe collection:
Results
{ _id: 1, deal_id: 25698, name:"Teste A"}
{ _id: 2, deal_id: 45879, name:"Teste A"}
{ _id: 3, deal_id: 54142, name:"Teste B"}
My Code:
db.pipedrive.aggregate([
{
$lookup: {
from: "leads",
'let': {
deal_id: '$deal_id'
},
pipeline: [
{
$match: {
$expr: {
$and: [
{$eq: ["$$deal_id", "$deals_id"]}
]
}
}
}
],
as: "Results"
}
}
]);
As I am currently doing, the analytics line is always returning me empty.
Can you help me?
With your original approach, exchanging $eq with $in should work, and since you only have one condition $and is not necessary. Note that $deals_id should exists in foreign document and must be an array, as $in requires the second parameter to be an array. So in case it doesn't exist we will have to wrap it with $ifNull
db.pipedrive.aggregate([{
$lookup: {
from: "leads",
let: {
deal_id: '$deal_id'
},
pipeline: [
{
$match: {
$expr: { $eq: ["$$deal_id", { $ifNull: ["$deals_id", []] ] }
}
}
],
as: "Results"
}
}
// optional other stages to output as desired shape
]);
A Better alternative would be just to use regular $lookup, since mongodb is able to compare the with the elements of the foreign field array. Just like with find query. This will also account for missing fields or non array fields.
db.pipedrive.aggregate([{
$lookup: {
from: "leads",
localField: "deal_id",
foreignField: "deals_id",
as: "Results"
}
}
// optional other stages to output as desired shape
{
$unwind: "$Results"
},
{
$project: {
deal_id: true,
name: "$Results.name"
}
}
]);
Mongo playground
You can use this query to get desired results. $lookup will take care of array as well.
db.pipe.aggregate([
{
$lookup: {
from: "leads",
localField: "deal_id",
foreignField: "deals_id",
as: "results"
}
},
{
$unwind: "$results"
},
{
$project: {
_id: 1,
deal_id: 1,
name: "$results.name"
}
}
])
Here is the link to check results,
https://mongoplayground.net/p/mhF6zHD9d26

mongodb 2 level aggregate lookup

I have those collection schemas
Schema.users = {
name : "string",
username : "string",
[...]
}
Schema.rooms = {
name : "string",
hidden: "boolean",
user: "string",
sqmt: "number",
service: "string"
}
Schema.room_price = {
morning : "string",
afternoon: "string",
day: "string",
room:'string'
}
I need to aggregate the users with the rooms and foreach room the specific room prices.
the expected result would be
[{
_id:"xXXXXX",
name:"xyz",
username:"xyz",
rooms:[
{
_id: 1111,
name:'room1',
sqmt: '123x',
service:'ppp',
room_prices: [{morning: 123, afternoon: 321}]
}
]}]
The first part of the aggregate could be
db.collection('users').aggregate([
{$match: cond},
{$lookup: {
from: 'rooms',
let: {"user_id", "$_id"},
pipeline: [{$match:{expr: {$eq: ["$user", "$$user_id"]}}}],
as: "rooms"
}}])
but I can't figure out how to get the room prices within the same aggregate
Presuming that room from the room_prices collection has the matching data from the name of the rooms collection, then that would the expression to match on for the "inner" pipeline of the $lookup expression with yet another $lookup:
db.collection('users').aggregate([
{ $match: cond },
{ $lookup: {
from: 'rooms',
let: { "user_id": "$_id" },
pipeline: [
{ $match:{ $expr: { $eq: ["$user", "$$user_id"] } } },
{ $lookup: {
from: 'room_prices',
let: { 'name': '$name' },
pipeline: [
{ $match: { $expr: { $eq: [ '$room', '$$name'] } } },
{ $project: { _id: 0, morning: 1, afternoon: 1 } }
],
as: 'room_prices'
}}
],
as: "rooms"
}}
])
That's also adding a $project in there to select only the fields you want from the prices. When using the expressive form of $lookup you actually do get to express a "pipeline", which can be any aggregation pipeline combination. This allows for complex manipulation and such "nested lookups".
Note that using mongoose you can also get the collection name from the model object using something like:
from: RoomPrice.collection.name
This is generally future proofing against possible model configuration changes which might possibly change the name of the underlying collection.
You can also do pretty much the same with the "legacy" form of $lookup prior to the sub-pipeline syntax available from MongoDB 3.6 and upwards. It's just a bit more processing and reconstruction:
db.collection('users').aggregate([
{ $match: cond },
// in legacy form
{ $lookup: {
from: 'rooms',
localField: 'user_id',
foreignField: 'user',
as: 'rooms'
}},
// unwind the output array
{ $unwind: '$rooms' },
// lookup for the second collection
{ $lookup: {
from: 'room_prices',
localField: 'name',
foreignField: 'room',
as: 'rooms.room_prices'
}},
// Select array fields with $map
{ $addFields: {
'rooms': {
'room_prices': {
$map: {
input: '$rooms.room_prices',
in: {
morning: '$this.morning',
afternoon: '$this.afternoon'
}
}
}
}
}},
// now group back to 'users' data
{ $group: {
_id: '$_id',
name: { $first: '$name' },
username: { $first: '$username' },
// same for any other fields, then $push 'rooms'
rooms: { $push: '$rooms' }
}}
])
That's a bit more overhead mostly from usage of $unwind and also noting that the "field selection" does actually mean you did return the "whole documents" from room_prices "first", and only after that was complete can you select the fields.
So there are advantages to the newer syntax, but it still could be done with earlier versions if you wanted to.

Mongo Aggregation Grouping and Mapping of Object Array

I'm need to group a mongo collection and aggregate.
Example Collection
[{id:"1", skill: "cooking"},{id:"1", skill: "fishing"}]
Lookup Collection
[{ name: "cooking", value: 3 }, { name: "fishing", value: 2 }]
Desired Result
[{id: "1", skills: [{ value: 3, "cooking" }, { value: 2, "fishing"}]}]
Here's how far I am.
db.talent.aggregate([
{
$group: '$id'
skills: { $addToSet: '$skill' }
},
])
Result:
[{id: "1", skills: ["cooking", "fishing"]}]
I'm wondering if this is even possible.
I miss SQL, need help!
We can do this using $lookup, $group and $project in the aggregation pipeline
Shown below is the mongodb shell query
db.example_collection.aggregate([
{
$lookup: {
from: "lookup_collection",
localField: "skill",
foreignField: "name",
as: "skills"
}
},
{
$group: {
_id: "$id",
skills: {
$push: "$skills"
}
}
},
{
$project: {
"id": "$_id",
"skills.name": 1,
"skills.value": 1,
"_id": 0
}
}
])