mongodb - Merge object arrays based on key - mongodb

In a mongodb database, I have the following data:
// db.people
[
{
_id: ObjectId("..."),
id: 111111111,
name: "George",
relatedPeople: [{ id: 222222222, relation: "child" }],
// A bunch of other data I don't care about
},
{
_id: ObjectId("..."),
id: 222222222,
name: "Jacob",
relatedPeople: [{ id: 111111111, relation: "father" }],
// A bunch of other data I don't care about
},
{
_id: ObjectId("..."),
id: 333333333,
name: "some guy",
relatedPeople: [],
// A bunch of other data I don't care about
},
]
I would like to query the people, and select only the fields I've shown, but have extra data in relatedPeople (id + relation + name)
So the desired output would be:
[
{
_id: ObjectId("..."),
id: 111111111,
name: "George",
relatedPeople: [{ id: 222222222, relation: "child", name: "Jacob" }],
},
{
_id: ObjectId("..."),
id: 222222222,
name: "Jacob",
relatedPeople: [{ id: 111111111, relation: "father", name: "George" }],
},
{
_id: ObjectId("..."),
id: 333333333,
name: "some guy",
relatedPeople: [],
},
]
I can get something close, with this query:
db.people.aggregate([
// { $match: { /** ... */ }, },
{
$lookup: {
from: "people",
let: { relatedPeopleIds: "$relatedPeople.id" },
pipeline: [
{ $match: { $expr: { $in: ["$id", "$$relatedPeopleIds"] } } },
{
$project: {
id: 1,
name: 1,
},
},
],
as: "relatedPeople2",
},
},
{
$project: {
id: 1,
name: 1,
relatedPeople: 1,
relatedPeople2: 1,
}
}
]);
But the data is split between two fields. I want to merge each object in the arrays by their id, and place the result array in relatedPeople
I found this question, but that merge is done over a range and uses $arrayElementAt which I can't use
I also tried looking at this question, but I couldn't get the answer to work (Kept getting empty results)

You can add one step using $arrayElementAt with $indexOfArray:
db.people.aggregate([
// { $match: { /** ... */ }, },
{$project: {id: 1, name: 1, relatedPeople: 1}},
{$lookup: {
from: "people",
let: { relatedPeopleIds: "$relatedPeople.id" },
pipeline: [
{ $match: { $expr: { $in: ["$id", "$$relatedPeopleIds"] } } },
{
$project: {
id: 1,
name: 1,
},
},
],
as: "relatedPeople2",
},
},
{$set: {
relatedPeople: {$map: {
input: "$relatedPeople",
in: {$mergeObjects: [
"$$this",
{$arrayElemAt: [
"$relatedPeople2",
{$indexOfArray: ["$relatedPeople2.id", "$$this.id"]}
]}
]}
}}
}},
{$unset: "relatedPeople2"}
])
See how it works on the playground example

Related

mongodb - How to get sorted list of mutual likes from a collection?

I have two collections:
users - All the user info
'partnership` - Users can partner/follow each other.
Goal: To get all partnerships sorted by last partnered date.
When do users become partners: Assuming user_1 liked user_2 AND they become partners only when user_2 also liked user_1 (mutual like)
What is partnered date: The date/time when second like (mutual like) happened.
I started a mongo playground but please ignore my query -
Mongo Playground
Here is my sample data
db={
partnership: [
{
_id: "xyz_rrr",
updated: "2022-10-23T12:35:24.772+00:00",
users: [
"xyz",
"rrr"
]
},
{
_id: "rrr_eee",
updated: "2022-12-23T12:35:24.772+00:00",
users: [
"rrr",
"eee"
]
},
{
_id: "eee_rrr",
updated: "2023-01-21T12:35:24.772+00:00",
users: [
"eee",
"rrr"
]
},
{
_id: "mmm_rrr",
updated: "2023-02-19T12:35:24.772+00:00",
users: [
"mmm",
"rrr"
]
},
{
_id: "rrr_mmm",
updated: "2023-02-21T12:35:24.772+00:00",
users: [
"rrr",
"mmm"
]
},
],
users: [
{
_id: "abc",
name: "abc",
group: 1,
location: {
type: "Point",
coordinates: [
53.23,
67.12
]
},
calculatedDist: 112
},
{
_id: "xyz",
name: "xyyy",
group: 1,
location: {
type: "Point",
coordinates: [
54.23,
67.12
]
},
calculatedDist: 13
},
{
_id: "123",
name: "yyy",
group: 1,
location: {
type: "Point",
coordinates: [
54.23,
67.12
]
},
calculatedDist: 13
},
{
_id: "rrr",
name: "rrrrrrr",
group: 1,
location: {
type: "Point",
coordinates: [
51.23,
64.12
]
},
calculatedDist: 14
},
{
_id: "mmm",
name: "mmmm",
group: 1,
location: {
type: "Point",
coordinates: [
51.23,
64.12
]
},
calculatedDist: 14
},
{
_id: "eee",
name: "eeeee",
group: 1,
location: {
type: "Point",
coordinates: [
55.23,
62.12
]
},
calculatedDist: 143
}
],
}
Expected result
{
partneredUsers:
{ firstUser :
{
_id: "mmm",
name: "mmmm",
},
},
secondUser :
{
_id: "rrr",
name: "rrrrrrr",
},
},
partneredDate: "2023-02-21T12:35:24.772+00:00",
},
{
partneredUsers:
{ firstUser :
{
_id: "rrr",
name: "rrrrrrr",
},
},
secondUser :
{
_id: "eee",
name: "eeeee",
}
},
partneredDate: "2023-01-23T12:35:24.772+00:00",
}
}
Starting from partnership collection, create a partition key by $sortArray and $concat to identify mutually liked relationship. (They will share the same key). Use the partition key in $setWindowFields to compute count and rank.
count: pair of users mutually like each other when count > 1
rank: sort by updated: -1; the latest like will have rank: 1
Finally $lookup from users to get the details of users and $sort by partneredDate.
db.partnership.aggregate([
{
$set: {
partitionKey: {
"$reduce": {
"input": {
$sortArray: {
input: "$users",
sortBy: 1
}
},
"initialValue": "",
"in": {
"$concat": [
"$$value",
"$$this"
]
}
}
}
}
},
{
"$setWindowFields": {
"partitionBy": "$partitionKey",
"sortBy": {
"updated": -1
},
"output": {
"count": {
$sum: 1
},
rank: {
$rank: {}
}
}
}
},
{
$match: {
count: {
$gt: 1
},
rank: 1
}
},
{
"$lookup": {
"from": "users",
"localField": "users",
"foreignField": "_id",
"as": "userLookup"
}
},
{
"$project": {
_id: 0,
partneredUsers: {
firstUser: {
$first: "$userLookup"
},
secondUser: {
$last: "$userLookup"
}
},
partneredDate: "$updated"
}
},
{
$sort: {
partneredDate: -1
}
}
])
Mongo Playground

mongodb - How to sort by distance using geoNear in addition to looking up another collection

I have two functionalities working individually but want to combine them.
Functionality 1 - Sort users by their geoNear distance.
Functionality 2 - The users should not have already been liked by the
current user (look up partnership collection)
How to update this query to start from the user's collection so I can do geoNear?
The output in the below mongoplayground is correct except that the resulting users are not sorted by calculatedDist which is a field calculated by geoNear.
$geoNear: {
near: { type: "Point", coordinates: [x,y },
distanceField: "calculatedDist",
spherical: true
}
geoNear needs location which is only available in users collection hence I think below query needs to be modified to start in user's collection.
https://mongoplayground.net/p/7H_NxciKezB
db={
users: [
{
_id: "abc",
name: "abc",
group: 1,
location: {
type: "Point",
coordinates: [
54.23,
67.12
]
},
calculatedDist: 13
},
{
_id: "xyz",
name: "xyyy",
group: 1,
location: {
type: "Point",
coordinates: [
54.23,
67.12
]
},
calculatedDist: 11
},
{
_id: "123",
name: "yyy",
group: 1,
location: {
type: "Point",
coordinates: [
54.23,
67.12
]
},
calculatedDist: 2
},
{
_id: "rrr",
name: "tttt",
group: 1,
location: {
type: "Point",
coordinates: [
54.23,
67.12
]
},
calculatedDist: 11
},
{
_id: "eee",
name: "uuu",
group: 1,
location: {
type: "Point",
coordinates: [
54.23,
67.12
]
},
calculatedDist: 7
},
],
partnership: [
{
_id: "abc_123",
fromUser: "abc",
toUser: "123"
},
{
_id: "eee_rrr",
fromUser: "eee",
toUser: "rrr"
},
{
_id: "rrr_abc",
fromUser: "rrr",
toUser: "abc"
},
{
_id: "abc_rrr",
fromUser: "abc",
toUser: "rrr"
},
{
_id: "xyz_rrr",
fromUser: "xyz",
toUser: "rrr"
},
{
_id: "rrr_eee",
fromUser: "rrr",
toUser: "eee"
},
]
}
geoNear as far as I know has to be the first thing to be done so my query should start with the users collection. This breaks my partnership check because for that to work, I start at partnership collection.
In the playground above, the user eee has a lesser calculated distance as a result of geoNear but it shows after user abc.
Try this out:
db.partnership.aggregate([
// $geoNear
{
$match: {
$or: [
{
fromUser: "rrr"
},
{
toUser: "rrr"
}
]
}
},
{
$group: {
_id: 0,
from: {
$addToSet: "$fromUser"
},
to: {
$addToSet: "$toUser"
}
}
},
{
$project: {
_id: 0,
users: {
$filter: {
input: {
$setIntersection: [
"$from",
"$to"
]
},
cond: {
$ne: [
"$$this",
"rrr"
]
}
}
}
}
},
{
$lookup: {
from: "users",
let: {
userId: "$users"
},
pipeline: [
{
"$geoNear": {
"near": {
"type": "Point",
"coordinates": [
31.4998,
-61.4065
]
},
"distanceField": "calculatedDist",
"spherical": true
}
},
{
"$match": {
"$expr": {
"$in": [
"$_id",
"$$userId"
]
}
}
}
],
as: "users"
}
},
{
$project: {
users: 1,
count: {
$size: "$users"
}
}
}
])
Here, we use the pipelined form of lookup.
The lookup is on the user's collection, in which we specify a pipeline with the $geoNear stage as the first stage.
And finally filter out and only keep the users belonging to a partnership.
This is the playground link. Let me know if it works, on the playground I can't test it because $geoNear requires a 2d index.
While using calculatedDist, it looks like this:
db.partnership.aggregate([
// $geoNear
{
$match: {
$or: [
{
fromUser: "rrr"
},
{
toUser: "rrr"
}
]
}
},
{
$group: {
_id: 0,
from: {
$addToSet: "$fromUser"
},
to: {
$addToSet: "$toUser"
}
}
},
{
$project: {
_id: 0,
users: {
$filter: {
input: {
$setIntersection: [
"$from",
"$to"
]
},
cond: {
$ne: [
"$$this",
"rrr"
]
}
}
}
}
},
{
$lookup: {
from: "users",
let: {
userId: "$users"
},
pipeline: [
{
$sort: {
calculatedDist: 1
}
},
{
"$match": {
"$expr": {
"$in": [
"$_id",
"$$userId"
]
}
}
}
],
as: "users"
}
},
{
$project: {
users: 1,
count: {
$size: "$users"
}
}
}
])
Playground.

Filter on collection and merge results in another collection in MongoDB

I am using MongoDB 4.2.9 and have the following requirements:
Collection 'A' has multiple documents with a string field 'status' that I need to filter on
Collection 'B' has multiple documents
Collection A
{ _id: "1",
status: "Report",
type: "Academy",
rating: "Excellent",
ReportNo: "A1"
},
{ _id: "2",
status: "Open",
type: "Academy",
rating: "",
ReportNo: ""
},
{ _id: "3",
status: "Draft",
type: "Academy",
rating: "",
ReportNo: ""
},
{ _id: "4",
status: "Report",
type: "Academy",
rating: "Great",
ReportNo: "A4"
}
Collection B
{ _id: "98",
status: "Archived",
type: "Academy",
rating: "So So",
ReportNo: "X2"
},
{ _id: "99",
status: "Archived",
type: "Academy",
rating: "Great",
ReportNo: "X1"
}
Resulting View
{ _id: "1",
status: "Report",
type: "Academy",
rating: "Excellent",
ReportNo: "A1"
},
{ _id: "4",
status: "Report",
type: "Academy",
rating: "Great",
ReportNo: "A4"
},
{ _id: "98",
status: "Archived",
type: "Academy",
rating: "So So",
ReportNo: "X2"
},
{ _id: "99",
status: "Archived",
type: "Academy",
rating: "Great",
ReportNo: "X1"
}
My goal is to create an aggregation view so that I can filter on a status value in Collection 'A' and then merge those results with Collection 'B' and show in the view ?
I can filter on Collection 'A' using the match call, just can't see how to merge resulting documents into Collection 'B'
From my understandings, your "merge" behaviour is actually a union view of filtered view of collection A and collection B.
With MongoDB v4.2, you can use $facet to handle collection A and collection B separately.
simply perform filtering on A
perform uncorrelated $lookup on B
wrangle the result and merge them together to get the union view that you are looking for.
db.createCollection(
"unionView",
{
"viewOn" : "A",
"pipeline" : [
{
"$facet": {
"A": [
{
"$match": {
status: "Report"
}
}
],
"B": [
{
$limit: 1
},
{
"$lookup": {
"from": "B",
"pipeline": [],
"as": "B"
}
},
{
$unwind: "$B"
},
{
"$replaceRoot": {
"newRoot": "$B"
}
}
]
}
},
{
$project: {
all: {
"$setUnion": [
"$A",
"$B"
]
}
}
},
{
$unwind: "$all"
},
{
"$replaceRoot": {
"newRoot": "$all"
}
}
]
}
)
Here is the Mongo Playground for your reference.
With MongoDB v4.4+, you can create a view with $unionWith
db.createCollection(
"unionView",
{
"viewOn" : "A",
"pipeline" : [
{
"$match": {
status: "Report"
}
},
{
"$unionWith": {
"coll": "B"
}
}
]
}
)
Here is the Mongo playground for your reference.

mongo nested aggregation with join

I've following tenant collection:
{id: 1, name: "T1", type: "DEFAULT", state: "ACTIVE"},
{id: 2, name: "T2", type: "DEFAULT", state: "DISABLED"},
{id: 3, name: "T3", type: "STANDARD", state: "ACTIVE"},
{id: 4, name: "T4", type: "TRIAL", state: "DELETED"},
{id: 5, name: "T5", type: "DEFAULT", state: "DISABLED"}
and then second collection with options:
{id:1, tenantId: 1, opt: "OPERATING"},
{id:2, tenantId: 2, opt: "OPERATING"},
{id:3, tenantId: 3, opt: "POSTPONED"},
{id:4, tenantId: 4, opt: "DELETED"},
{id:5, tenantId: 5, opt: "POSTPONED"}
Id' like to aggregate this collections to get umber of tenant types grouped with number of operations, but I'd like to remove all DELETED tenants and all DELETED options from search. Something like this:
{type: "DEFAULT", count: 3, opts: {operating: 2, postponed: 1}}
{type: "STANDARD", count: 1, opts: {postponed: 1}}
Grouping the tenants is fine, but I don't know what should I use for that next grouping of options.
db.tenant.aggregate([
{$match: { state: {$ne: "DELETED"}}},
{$lookup: {
from: "option",
localField: "_id",
foreignField: "tenantId",
as: "options"
}},
{$group {
_id: "$type",
count: {$sum: 1}
}}
])
$group by type and get group of ids
$lookup with pipeline match $in condition for tenantId
$group by opt and get count of option
$project to show fields in k and v format
$project to show required fields, $size to count total tenant and $arrayToObject convert opts array to object
db.tenant.aggregate([
{ $match: { state: { $ne: "DELETED" } } },
{
$group: {
_id: "$type",
ids: { $push: "$id" }
}
},
{
$lookup: {
from: "options",
let: { ids: "$ids" },
pipeline: [
{ $match: { opt: { $ne: "DELETED" }, $expr: { $in: ["$tenantId", "$$ids"] } } },
{
$group: {
_id: "$opt",
count: { $sum: 1 }
}
},
{
$project: {
_id: 0,
k: "$_id",
v: "$count"
}
}
],
as: "opts"
}
},
{
$project: {
_id: 0,
type: "$_id",
count: { $size: "$ids" },
opts: { $arrayToObject: "$opts" }
}
}
])
Playground

MongoDB : create a view which is the union of several collections

I currently have a collection that i need to split in several smaller collections. Is there a way to make a View containing the union of all my smaller collections ?
According to the MongoDB Manual, i could use the $lookup operator in the pipeline, but it ends up being more like a "join" than an "union".
Here is an example of what i want to do :
Current collection :
{ _id: 1, name: "abc", country: "us" }
{ _id: 2, name: "def", country: "us" }
{ _id: 3, name: "123", country: "de" }
{ _id: 4, name: "456", country: "de" }
Splitting into :
Collection_US
{ _id: 1, name: "abc", country: "us" }
{ _id: 2, name: "def", country: "us" }
Collection_DE
{ _id: 3, name: "123", country: "de" }
{ _id: 4, name: "456", country: "de" }
And then, make a view :
View
{ _id: 1, name: "abc", country: "us" }
{ _id: 2, name: "def", country: "us" }
{ _id: 3, name: "123", country: "de" }
{ _id: 4, name: "456", country: "de" }
Is it possible to do this ?
This is the same modified of taminov's code.
db.createView('union_view', 'us', [
{
$facet: {
us: [
{$match: {}}
],
de: [
{$limit: 1},
{
$lookup: {
from: 'de',
localField: '__unexistingfield',
foreignField: '__unexistingfield',
as: '__col2'
}
},
{$unwind: '$__col2'},
{$replaceRoot: {newRoot: '$__col2'}}
]
},
},
{$project: {data: {$concatArrays: ['$us', '$de']}}},
{$unwind: '$data'},
{$replaceRoot: {newRoot: '$data'}}
])
its very hacky but will work for small collections. you may end up having to use a real collection if the collections are big.
db.createView('union_view', 'col1', [
{
$facet: {
col1: [
{ $match:{}}
],
col2: [
{ $limit:1},
{ $lookup:{
from: 'col2',
localField: '__unexistingfield',
foreignField: '__unexistingfield',
as: '__col2'
}},
{ $unwind:'$__col2'},
{ $replaceRoot: {newRoot: '$__col2'}}
]
},
},
{ $project: { filesFolders: {$setUnion: ['$files', '$folders']}}},
{ $unwind: '$filesFolders' },
{ $replaceRoot: {newRoot: '$filesFolders'}}
])