Mongodb lookup and get output flat tree structure? - mongodb

I have two collections points collection and users collection here i want to do aggregation based on userid.
here i am trying aggregation and everything is working fine but i need to modify result
db.points.aggregate([
$match: {
store: "001",
date: {$lte: ISODate("2017-11-10T08:15:39.736Z"), $gte: ISODate("2017-11-10T08:15:39.736Z")}
},
{
$lookup: {
from: "users",
localField: "userid",
foreignField: "_id",
as: "user"
}
},
{
$project:
{
_id:0,
"userdata.purchasedetails.purchaseid" : 1,
"userdata.purchasedetails.data" : 1,
usermobinumber: { "$arrayElemAt": [ "$user.mobinumber", 0 ] }
}}
Data stored like this
{
"userpoint": "2",
"date":ISODate("2017-11-10T08:15:39.736Z"),
"store":"001",
"userid":[
objectID("5a7565ug8945rt67"),
objectID("8a35553d3446rt78")
],
"userdata":{
"profile":[],
"purchasedetails":
[{
"purchaseid":"dj04944",
"data":"awe"
}]
}
}
getting result like this :
{
"usermobinumber":"9611",
"userdata":{
"purchasedetails":
[{
"purchaseid":"dj04944",
"data":"awe"
}]
}
my expecting result:
I dont want tree structure output i need like this
{
"usermobinumber":"9611",
"purchaseid":"dj04944",
"data":"awe"
}
how can i do this help me out

You can do something like this here,
db.points.aggregate([
//Whatever you are doing here now (lookup and match)
{
$project:{
"usermobinumber":"$usermobinumber",
"purchaseid":"$userdata.purchasedetails.purchaseid"
}
}
])
This will give you:
{
"usermobinumber" : "9611",
"purchaseid" : [
"dj04944"
]
}
EDIT:
db.points.aggregate([
//Whatever you are doing here now (lookup and match)
{
{
$project:{
"usermobinumber":"$usermobinumber",
"purchaseid":"$userdata.purchasedetails.purchaseid",
"data":"$userdata.purchasedetails.data"
}
}
}
])
This will give you
{
"usermobinumber":"9611",
"purchaseid":["dj04944"],
"data":["awe"]
}
purchaseid and data are array because purchasedetails is an array.

You can add an $unwind stage to flatten the result....

Related

Mongodb lookup like search: local field as array of objects

I have two collections userProfile and skills,
Eg:userProfile
{
"_id": "5f72c6d4e23732390c96b031",
"name":"name"
"other_skills": [
"1","2"
],
"primary_skills": [
{
"_id": "607ffd1549e13876fef7f2c5",
"years": 4.5,
"skill_id": "1"
},
{
"_id": "607ffd1549e13876fef7f2c6",
"years": 2,
"skill_id": "2"
},
{
"_id": "607ffd1549e13876fef7f2c7",
"years": 1,
"skill_id": "3"
}
]
}
Eg:Skills
{
"_id":1,
"name": "Ruby on Rails",
}
{
"_id":2,
"name": "PHP",
}
{
"_id":3,
"name": "php",
}
I want to retrieve the userprofile based on the skills
eg: input of skill php i want to retrieve the userprofiles that matches either in primary_skills or other_skills
But I got confused about the implementation, I think it can do with pipeline in lookup and the elemMatch. This is the query I tried so far
const skills = ['php','PHP']
userProfile.aggrigate([{
$lookup:{
from:'skills',
let:{'primary_skills':'$primary_skills'},
pipeline:[
{
$match:{
primary_skills:{
$elemMatch:{
name:'' //not sure how to write match
}
}
}
}
]
}
}])
Can somebody help me with this, Thanks in advance
I'll first show you how to correct your pipeline to work, however this approach is very inefficient as you will have to $lookup on every single user in your db which is obviously a lot of overhead.
Here is how to properly match your condition:
const skills = ['php','PHP']
db.userProfile.aggregate([
{
$lookup: {
from: "skills",
let: {
"primary_skills": {
$map: {
input: "$primary_skills",
as: "skill",
in: "$$skill.skill_id"
}
},
"other_skills": "$other_skills"
},
pipeline: [
{
$match: {
$expr: {
"$in": [
"$_id",
{
"$concatArrays": [
"$$other_skills",
"$$primary_skills"
]
}
]
}
}
}
],
as: "skills"
}
},
{
$match: {
'skills.name': {$in: skills}
}
}
])
Mongo Playground
As I've said I recommend you do not do this. what I suggest you do is split it into 2 calls, first fetch the relevant skill ids. and then query on users.
By doing this you can also utilize indexes for much faster queries, like so:
const skills = ['php', 'PHP'];
const matchedSkillIds = await skills.distinct('_id', {name: {$in: skills}});
const users = await userProfile.find({
$or: [
{
'primary_skills.skill_id': {$in: matchedSkillIds}
},
{
'other_skills': {$in: matchedSkillIds}
}
]
})
Finally if you do insist on doing it in one query at the very least start the pipeline from the skill collection.

MongoDB lookup with object relation instead of array

I have a collection matches like this. I'm using players object {key: ObjectId, key: ObjectID} instead of classic array [ObjectId, ObjectID] for reference players collection
{
"_id": ObjectId("5eb93f8efd259cd7fbf49d55"),
"date": "01/01/2020",
"players": {
"home": ObjectId("5eb93f8efd259cd7fbf49d59"),
"away": ObjectId("5eb93f8efd259cd7fbf49d60")
}
},
{...}
And players collection:
{
"_id": ObjectId("5eb93f8efd259cd7fbf49d59"),
"name": "Roger Federer"
"country": "Suiza"
},
{
"_id": ObjectId("5eb93f8efd259cd7fbf49d60"),
"name": "Rafa Nadal"
"country": "España"
},
{...}
What's the better way to do mongoDB lookup? something like this is correct?
const rows = await db.collection('matches').aggregate([
{
$lookup: {
from: "players",
localField: "players.home",
foreignField: "_id",
as: "players.home"
}
},
{
$lookup: {
from: "players",
localField: "players.away",
foreignField: "_id",
as: "players.away"
},
{ $unwind: "$players.home" },
{ $unwind: "$players.away" },
}]).toArray()
I want output like this:
{
_id: 5eb93f8efd259cd7fbf49d55,
date: "12/05/20",
players: {
home: {
_id: 5eb93f8efd259cd7fbf49d59,
name: "Roger Federer",
country: "Suiza"
},
away: {
_id: 5eb93f8efd259cd7fbf49d60,
name: "Rafa Nadal",
country: "España"
}
}
}
{...}
You can try below aggregation query :
db.matches.aggregate([
{
$lookup: {
from: "players",
localField: "players.home",
foreignField: "_id",
as: "home"
}
},
{
$lookup: {
from: "players",
localField: "players.away",
foreignField: "_id",
as: "away"
}
},
/** Check output of lookup is not empty array `[]` & get first doc & write it to respective field, else write the same value as original */
{
$project: {
date: 1,
"players.home": { $cond: [ { $eq: [ "$home", [] ] }, "$players.home", { $arrayElemAt: [ "$home", 0 ] } ] },
"players.away": { $cond: [ { $eq: [ "$away", [] ] }, "$players.away", { $arrayElemAt: [ "$away", 0 ] } ] }
}
}
])
Test : mongoplayground
Changes or Issues with current Query :
1) As you're using two $unwind stages one after the other, If anyone of the field either home or away doesn't have a matching document in players collection then in the result you don't even get actual match document also, But why ? It's because if you do $unwind on [] (which is returned by lookup stage) then unwind will remove that parent document from result, To overcome this you need to use preservenullandemptyarrays option in unwind stage.
2) Ok, there is another way to do this without actually using $unwind. So do not use as: "players.home" or as: "players.away" cause you're actually writing back to original field, Just in case if you don't find a matching document an empty array [] will be written to actual fields either to "home" or "away" wherever there is not match (In this case you would loose actual ObjectId() value existing in that particular field in matches doc). So write output of lookup to a new field.
Or even more efficient way, instead of two $lookup stages (Cause each lookup has to go through docs of players collection again & again), you can try one lookup with multiple-join-conditions-with-lookup :
db.matches.aggregate([
{
$lookup: {
from: "players",
let: { home: "$players.home", away: "$players.away" },
pipeline: [
{
$match: { $expr: { $or: [ { $eq: [ "$_id", "$$home" ] }, { $eq: [ "$_id", "$$away" ] } ] } }
}
],
as: "data"
}
}
])
Test : mongoplayground
Note : Here all the matching docs from players which match with irrespective of away or home field will be pushed to data array. So to keep DB operation simple you can get that array from DB along with actual matches document & Offload some work to code which is to map respective objects from data array to players.home & players.away fields.

grouping result from multiple queries and aggregating result mongo

I'm new to mongo and I have a document that has an array with the ids of all it's related documents. I need to fetch the document with all it's relateds in a single query. For the moment I fetch the document and I query separatly each of it's related document with there ids.
all my documents are on the same collection documents_nodes and look like so:
{
"id": "document_1",
"name": "flask",
"relateds": [
"document_2",
"document_3",
"document_4"
],
"parents": [
"document1_1"
]
}
The first query is to get the initial document
db.documents_nodes.find({id: document_1})
And then I query it's relateds with a second query
db.documents_nodes.aggregate([{
$match: {
$and: [{
id: {
$in: ["document_2", "document_3", "document_2"]
}
}]
}
}])
is there a way to combine the two queries, I tried this but it doesn't work
db.documents_nodes.aggregate([
{
$match: {
uri: "https://opus.adeo.com/LMFR_prod/3206"
}
},
{
$addFields: {
newRelateds:
{
$match: {
id: {
$in: [ "$relateds" ]
}
}
}
}
}
])
"errmsg" : "Unrecognized expression '$match'",
"code" : 168,
"codeName" : "InvalidPipelineOperator"
I have found a way to do it, in case someone has the same need.
I used the $unwind to flatten the array of documents and then used the $lookup to fetch the documents by their ids and finally I group the result on a new key.
[{
$match: {
id: "document_1"
}
}, {
$unwind: {
path: '$relateds',
}
}, {
$lookup: {
from: 'documents_nodes',
localField: 'relateds',
foreignField: 'id',
as: 'newRelateds'
}
}, {
$group: {
_id: '$id',
relateds: {
'$push': '$newRelateds'
}
}
}]

Mongo Aggregate Objects with $lookup using non matching values

I've got an Object Mission referring to another object Position with a key _p_position.
Mission objects look like:
{
_id: "ijjn97678",
_p_position: "Position$qwerty123",
...
}
Position objects look like:
{
_id: "qwerty123",
...
}
I don't know if it is Mongo or Parse convention but as one can see a Position$ is added on relational position attribute in missions.
I'd like to aggregate both into a single Object to get a results similar to the following:
{
_id: "ijjn97678",
_p_position: "Position$qwerty123",
positions: [
{
_id: "qwerty123"
}
]
}
using:
missions.aggregate([
{
$lookup: {
as: "position",
from: "Position",
foreignField: "_id",
localField: "_p_position",
},
},
])
But I need to remove Position$ from _p_position. Is there a way I can compute "_p_position" before it is used to find a matching Position's id ?
PS: I only have reading rights on DB
You can use $addFields to add another field which will be then passed to $lookup stage. To get the part that's following the dollar sign you need: $indexOfBytes and $substr operators. Additionally dollar sign itself is a special character in Aggregation Framework (represents a field reference) so you need $literal to force it to be considered as regular field
db.missions.aggregate([
{
$addFields: {
value: {
$let: {
vars: { index: { $indexOfBytes: [ "$_p_position", { $literal: "$" } ] } },
in: { $substr: [ "$_p_position", { $add: [ "$$index", 1 ] } , { $strLenBytes: "$_p_position" } ] }
}
}
}
},
{
$lookup: {
from: "Position",
localField: "value",
foreignField: "_id",
as: "position"
}
}
])

Find or forEach Inside Aggregate MongoDB

Let's say I have two collection in my database call rumahsakit.
First collection called dim_dokter:
[{
"_id": ObjectId("58b22c79e8c1c52bf3fad997"),
"nama_dokter": "Dr. Basuki Hamzah",
"spesialisasi": "Spesialis Farmakologi Klinik",
"alamat": " Jalan Lingkar Ring Road Utara, Yogyakarta "
}, {
"_id": ObjectId("58b22c79e8c1c52bf3fad998"),
"nama_dokter": "Dr. Danie Nukman",
"spesialisasi": "Spesialis Anak",
"alamat": " Jalan Sudirman, Yogyakarta "
}, {
"_id": ObjectId("58b22c79e8c1c52bf3fad999"),
"nama_dokter": "Dr. Bambang Kurnia",
"spesialisasi": "Spesialis Mikrobiologi Klinik",
"alamat": " Jalan Ahmad Yani, Yogyakarta "
}]
Second collection called fact_perawatan:
[{
"_id": ObjectId("58b22d13e8c1c52bf3fad99a"),
"nama_pasien": "Clark",
"detail_perawatan": [{
"id_dokter": ObjectId("58b22c79e8c1c52bf3fad997"),
"jumlah_obat": 1
}, {
"id_dokter": ObjectId("58b22c79e8c1c52bf3fad998"),
"jumlah_obat": 1
}]
}]
Collection fact_perawatan have the id_dokter that is actually point to the dim_dokter._id . I want to do aggregation to show this data in fact_perawatan collection but instead showing just the id_dokter, I want to use nama_dokter from dim_dokter.
This is my code so far:
db.fact_perawatan.aggregate([
{
$match:
{
'_id': db.fact_perawatan.find({"_id" : ObjectId("58b22d13e8c1c52bf3fad99a")})[0]._id
}
},
{
$project:
{
nama_pasien: db.fact_perawatan.find({"_id": ObjectId("58b22d13e8c1c52bf3fad99a")})[0].nama_pasien,
perawatan: [
{
dokter: db.dim_dokter.find({"_id" : db.fact_perawatan.find({"_id": ObjectId("58b22d13e8c1c52bf3fad99a")})[0].detail_perawatan[0].id_dokter})[0].nama_dokter,
},
]
}
}
])
result:
{ "_id" : ObjectId("58b22d13e8c1c52bf3fad99a"), "nama_pasien" : "Clark", "perawatan" : [ { "dokter" : "Dr. Basuki Hamzah" } ] }
Those code can get the nama_dokter from dim_dokter, but only one data. In my case, the data can be up to 5. do the detail_perawatan[0] to [5] is not solution.
So, this code:
db.dim_dokter.find({"_id" : db.fact_perawatan.find({"_id": ObjectId("58b22d13e8c1c52bf3fad99a")})[0].detail_perawatan[0].id_dokter})[0].nama_dokter,
How make the code above to loop as many as the data in there ? so I can get all the data.
Thanks
You can use a $lookup to left join with dim_dokter following by a $group to regroup your data in perawatan field :
db.fact_perawatan.aggregate([{
$match: {
'_id': ObjectId("58b22d13e8c1c52bf3fad99a")
}
}, {
$unwind: "$detail_perawatan"
}, {
$lookup: {
from: "dim_dokter",
localField: "detail_perawatan.id_dokter",
foreignField: "_id",
as: "dim_dokter"
}
}, {
$unwind: "$dim_dokter"
}, {
$group: {
_id: "$_id",
nama_pasien: {
$first: "$nama_pasien"
},
perawatan: {
$push: {
"dokter": "$dim_dokter.nama_dokter"
}
}
}
}])
The $unwind is used to deconstruct the array field detail_perawatan to be able to $lookup afterwards