MongoDB Multi level $lookup sort not working - mongodb

Multi level $lookup sort not working in aggregation.
Sorting works only for country, state name. Tried applying sorting for cities, but country sort overwrites the city sort.
Query2 is working but i don't want to sort collections inside lookup pipeline.
Is there any way to achieve all level of sorting(country,state,city) in Query1
Query1(Not Working):
Country.aggregate([
{
$lookup:{
from: 'states',
localField:'_id',
foreignField:'countryId',
as:'states'
}
},
{
$unwind: {
path: "$states",
preserveNullAndEmptyArrays: true
}
},
{
$sort: {
'states.name': 1
}
},
{
$lookup:{
from: 'cities',
localField:'states._id',
foreignField:'stateId',
as:'states.cities'
}
},
{
$sort: {
'states.cities.name': 1
}
},
{
$group: {
_id: {
_id: '$_id',
name: '$name'
},
states: {
$push: '$states'
}
}
},
{
$project: {
_id: '$_id._id',
name: '$_id.name',
states: 1
}
},
{
$sort: {
name: 1
}
}
])
Query2(Working):
Execution time is 8 times higher than Query1.
[
{
$lookup : {
from : 'states',
let: { 'countryId': '$_id' },
pipeline: [
{
$match: {
$expr:
{
$eq: ['$countryId', '$$countryId']
}
}
},
{
$sort : {
name : -1
}
}
],
as : 'states'
}
},
{
$unwind: {
path: '$states',
preserveNullAndEmptyArrays: true
}
},
{
$lookup : {
from : 'cities',
let: { 'stateId': '$states._id' },
pipeline: [
{
$match: {
$expr:
{
$eq: ['$stateId', '$$stateId']
}
}
},
{
$sort : {
name : -1
}
}
],
as : 'states.cities'
}
},
{
$group: {
_id: {
_id: '$_id',
name: '$name'
},
states: {
$push: '$states'
}
}
},
{
$project: {
_id: '$_id._id',
name: '$_id.name',
states: 1
}
}
]

In the newer $lookup syntax you do not need to use $unwind to join nested fields. You can easily use $lookup inside the pipeline to join multiple level.
[
{ "$lookup": {
"from": "states",
"let": { "countryId": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$countryId", "$$countryId"] }}},
{ "$lookup": {
"from": "cities",
"let": { "stateId": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$stateId", "$$stateId"] }}},
{ "$sort": { "name": -1 }}
],
"as": "cities"
}},
{ "$sort": { "name": -1 }}
],
"as": "states"
}}
]

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.

Extract value from an array of objects in mongodb aggregate

I am new to mongodb,
I'm trying to extract expectedDeliveryTime from the following data:
{
"from": "Giza",
"delivery_rule": [
{
"to": "Giza",
"expectedDeliveryTime": 3
},
{
"to": "Riyadh",
"expectedDeliveryTime": 2
}
]
}
I am trying to fetch expectedDeliveryTime WHERE from='Giza' and to='Riyadh'
MySQL equivalent would be SELECT expectedDeliveryTime FROM delivery_rules AS d WHERE d.from='Giza' AND d.to='Giza'
Below is part of my code
{
$lookup: {
from: 'Setting',
pipeline: [
{
$match: {
$expr: { $eq: ['$name', 'delivery_rules'] },
},
}
],
as: 'delivery_rules',
}
},
{
$addFields: {
delivery_rules: "$delivery_rules.value"
}
},
{ $unwind: '$delivery_rules' },
{
$addFields: {
delivery_rules: {
$filter: {
input: "$delivery_rules",
as: "rule",
cond: {
$eq: [
"$$rule.from",
"Giza"
]
},
}
}
}
},
{
$group: {
expectedDeliveryTime: { $first: '$delivery_rules' },
}
},
{
$project: {
_id: 0,
expectedDeliveryTime: 1,
}
}
You can solve this using simply $match and $unwind
db.collection.aggregate([
{
"$match": {
"from": "Giza"
}
},
{
"$unwind": "$delivery_rule"
},
{
"$match": {
"delivery_rule.to": "Riyadh"
}
},
{
$project: {
_id: 0,
expectedDeliveryTime: "$delivery_rule.expectedDeliveryTime"
}
}
])
Here is the Mongo playground for your reference.

$match in lookup stage without removing local documents in mongoDB

Right now I have a lookup stage that matches a collection of answers with their respective questions. The problem I'm having is that on the $match if the question does not having a matching answer document in the lookup collection it is removed from the results of the aggregation. How can I avoid this?
{
$lookup: {
from: 'groupansphotos',
let: { question_id: '$questions._id' },
pipeline: [
{
$match: {
$expr: { $eq: ['$_id', '$$question_id'] },
},
},
{ $unwind: '$answers' },
{ $match: { 'answers.reported': { $lt: 1 } } },
{ $sort: { 'answers.helpful': -1 } },
{ $limit: 2 },
{
$project: {
_id: 0,
k: { $toString: '$answers._id' }, // k
v: '$$ROOT.answers', // v
},
},
],
as: 'answers',
},
},

Join multiple collections after parallel aggregation in Mongodb

I have a collection called "Reel" which has embedded Objects.
{
"_id":"reel_1",
"category":[
{
"_id" : "cat_1",
"videos": [ {"_id":"vid_1"},{"_id":"vid_2"} ] //_id is reference of Video collection
},
{
"_id" : "cat_2",
"videos": [ {"_id":"vid_3"},{"_id":"vid_4"} ]
}
]
}
Video is another collection whose _id is referred inside reel-> category -> videos -> _id
{
"_id":"vid_1",
"title":"title 1",
"groups":[{"_id":"group_1"},{"_id":"group_2"}]
},
{
"_id":"vid_2",
"title":"title 2",
"groups":[{"_id":"group_1"},{"_id":"group_4"}]
},
{
"_id":"vid_3",
"title":"title 3",
"groups":[{"_id":"group_1"},{"_id":"group_2"}]
},
{
"_id":"vid_4",
"title":"title 4",
"groups":[{"_id":"group_3"},{"_id":"group_4"}]
}
The Document collection which holds _id of Reel and _id of Category
{
"_id":"doc_1",
"title":"document title",
"assessments":[
{
"reel":"reel_1", // reel reference _id
"category":"cat_1", // category reference _id
"groups":[{"_id":"group_1"},{"_id":"group_2"}
]
}
]
}
I need to join and find all related embedded Objects which has group_1.
I have done joining between Reel collection and Video collection and working fine,
{ $unwind: { path: '$category', preserveNullAndEmptyArrays: true }},
{ $unwind: { path: '$category.videos', preserveNullAndEmptyArrays: true }},
{
$lookup: {
from: 'video',
localField: 'category.videos._id',
foreignField: '_id',
as: 'joinVideo'
}
},
{ $unwind: { path: "$joinVideo", preserveNullAndEmptyArrays: true }},
{ $unwind: { path: "$joinVideo.groups", preserveNullAndEmptyArrays: true }},
{ $match: { "joinVideo.groups._id": "group_1" }},
{ $addFields: { "category.videos": "$joinVideo" }},
{
$group: {
_id: {
_id: "$_id",
category: "$category._id"
},
videos: {
$addToSet: "$category.videos"
}
}
}, {
$group: {
_id: "$_id._id",
category: {
$addToSet: {
"_id": "$_id.category",
"videos": "$videos"
}
}
}
}
The document collection should be embedded inside the category object based on reel _id and and category _id filtered by group_1. My expected result is
{
"_id":"reel_1",
"category":[
{
"_id" : "cat_1",
"videos": [
{
"_id":"vid_1",
"title":"title 1",
"groups":[ {"_id":"group_1"},{"_id":"group_2"}]
},
{
"_id":"vid_2",
"title":"title 2",
"groups":[{"_id":"group_1"},{"_id":"group_4"}]
}
],
"documents":[
{ // this document comes by reel="reel_1", category="cat_1", filtered by "group_1"
"_id":"doc_1",
"title":"document title",
}
]
},
{
"_id" : "cat_2",
"videos": [
{
"_id":"vid_3",
"title":"title 3",
"groups":[{"_id":"group_1"},{"_id":"group_2"}]
}
]
}
]
}
I tried in many ways. Since I'm new to Mongodb, I couldn't sort this out.
Since MongoDB v3.6, $lookup allows perform uncorrelated sub-queries. This allows us perform non-standard queries to join two or more collections.
Note: Explanation why we need to use $expr inside $lookup pipeline
Explanation
We apply $unwind to flatten $category
We perform $lookup with 2 conditions:
video.groups._id == 'group_1' and video._id in reel.category.videos._id
Since $reel.category.videos._id returns an array, we need to use $in operator
Again we perform $lookup with 2 conditions. It creates documents field for every document
To remove fields dynamically, we need to use Aggregation expressions called $$REMOVE which allows us exclude conditionally a field from document
We perform $group stage to transform into desired result
db.reel.aggregate([
{
$unwind: {
path: "$category",
preserveNullAndEmptyArrays: true
}
},
{
$lookup: {
from: "video",
let: {
videos: "$category.videos._id"
},
pipeline: [
{
$match: {
"groups._id": "group_1",
$expr: {
$in: [
"$_id",
"$$videos"
]
}
}
}
],
as: "category.videos"
}
},
{
$lookup: {
from: "document",
let: {
reel_id: "$_id",
category_id: "$category._id"
},
pipeline: [
{
$match: {
$expr: {
$and: [
{
$in: [
"$$reel_id",
"$assessments.reel"
]
},
{
$in: [
"$$category_id",
"$assessments.category"
]
}
]
}
}
},
{
$project: {
_id: 1,
title: 1
}
}
],
as: "category.documents"
}
},
{
$addFields: {
"category.documents": {
$cond: [
{
$eq: [
{
$size: "$category.documents"
},
0
]
},
"$$REMOVE",
"$category.documents"
]
}
}
},
{
$group: {
_id: "$_id",
category: {
$push: "$category"
}
}
}
])
MongoPlayground

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,
}}