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

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

Related

mongodb - Merge object arrays based on key

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

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.

Mongodb - Add geoNear to an existing query to sort by distance

There are 3 collections for this application
users - all user details
partnership - user relation to each other
location - user geolocation
I have 1 and 2 working. The 3rd task is to sort the resuling users list by distance from a given coordinate.
Here is the mongo playground:
https://mongoplayground.net/p/XELySm8KGpM
db={
users: [
{
_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: [
{
_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"
},
],
locations: [
{
_id: "123",
location: {
type: "Point",
coordinates: [
54.23,
67.12
]
}
},
{
_id: "rrr",
location: {
type: "Point",
coordinates: [
51.23,
64.12
]
}
},
{
_id: "eee",
location: {
type: "Point",
coordinates: [
55.23,
62.12
]
}
},
{
_id: "abc",
location: {
type: "Point",
coordinates: [
53.23,
67.12
]
}
},
]
}
The following query by itself works. How to integrate it to the query in the playground?
{
$geoNear: {
near: { type: "Point", coordinates: [ 41.99279 , -81.719296 ] },
distanceField: "dist.calculated",
spherical: true
}
},
$geoNear can only be the first step in an aggregation pipeline, so you need to first sort by distance and then do all the other things. This means your schema is not very efficient for this.
One option is:
db.locations.aggregate([
{$geoNear: {
near: { type: "Point", coordinates: [ 41.99279 , -81.719296 ] },
distanceField: "calculatedDist",
spherical: true
}
},
{$lookup: {
from: "partnership",
let: {user_id: "$_id"},
pipeline: [
{$match: {$expr: {
$or: [
{$and: [{$eq: ["$fromUser", "rrr"]}, {$eq: ["$toUser", "$$user_id"]}]},
{$and: [{$eq: ["$toUser", "rrr"]}, {$eq: ["$fromUser",
"$$user_id"]}]},
]
}}}
],
as: "valid"
}
},
{$match: {"valid.0": {$exists: true}}},
{$lookup: {
from: "users",
localField: "_id",
foreignField: "_id",
as: "user"
}},
{$project: {user: {$first: "$user"}, calculatedDist: 1}},
{$sort: {calculatedDist: 1}},
{$group: {_id: 0, users: {$push: "$user"}, count: {$sum: 1}}}
])
See how it works on the playground example

Mongodb: Populate based on condition

I have some collections and I am trying to transform a log object into its details (using populate).
Companies (company with its users):
[
{
_id: "comp123",
companyId: "compName123",
users: [
{ user: "user111", status: "active"},
{ user: "user222", status: "active"},
]
},
{
_id: "comp456",
name: "compName456",
users: [
{ user: "user333", status: "active"}
]
},
{
_id: "comp789",
name: "compName789",
users: [
{ user: "user444", status: "inactive"}
]
},
]
Users:
[
{_id: "user111", firstName: "userName111"},
{_id: "user222", firstName: "userName222"},
{_id: "user333", firstName: "userName333"},
{_id: "user444", firstName: "userName444"},
]
I am trying to transform log collection into data.
examples:
For the first object of the log:
{
companyId: "comp123",
actionDetails: [
entities: [
{ id: "user111", entityType: "User"}
]
]
},
I want it to return:
{
companyId: {_id: "comp123", name: "compName123"}, // taken from companies
userId: { _id: "user111", firstName: "userName111"}, // taken from users
// Does company=comp123 that has a user with user=user111 and status=active exist?
isUserActiveInCompany: true
}
Another example of log:
{
companyId: "comp456",
actionDetails: [
entities: [
{ id: "user444", entityType: "User"}
]
]
}
Output is:
{
companyId: {_id: "comp456", name: "compName456"}, // taken from companies
userId: { _id: "user444", firstName: "userName444"}, // taken from users
isUserActiveInCompany: false // Does company=comp456 that has a user with user=user444 and status=active exist?
}
last important example of log:
{
companyId: "comp789",
actionDetails: [
entities: [
{ id: "attr333", entityType: "Attribute"}
]
]
}
Output:
{
companyId: {_id: "comp789", name: "compName789"}, // taken from companies
userId: {}, // taken from users (entityType is Attribute so we ignore it)
isUserActiveInCompany: null // entityType is Attribute so we ignore it
}
If there will be a log of comp789 with user444, isUserActiveInCompany should be false (cause the user is inactive in his company).
Currently, I do:
populate([
{
path: "actionDetails.entities.id",
select: "id firstName",
},
{
path: "companyId",
select: "name",
},
]
Any help appreciated!
Convert the below Aggregation Pipeline code to Mongoose Equivalent to get the output you desire.
db.log.aggregate([
{
'$match': {
// <-- I highly recommend that you use a `$match` condition since there are 2 lookup operators in the aggregation which will significantly increase execution time.
}
},
{
'$lookup': {
'from': 'Companies',
'let': {'cId': '$companyId'},
'pipeline': [
{
'$match': {
'$expr': {
'$eq': ['$_id', '$$cId']
}
}
},
{
"$project": {
'company': {
"_id": "$_id",
"companyName": "$companyId"
},
'users': {
"$filter": {
'input': "$users",
'as': "usr",
'cond': {
"$eq": ["$$usr.status", "active"],
},
},
},
}
},
],
'as': 'companyDetails'
}
},
{
'$unwind': {
'path': "$actionDetails",
}
},
{
'$unwind': {
'path': "$actionDetails.entities",
}
},
{
'$lookup': {
'from': 'Users',
'let': {"uId": "$actionDetails.entities.id"},
'pipeline': [
{
"$match": {
"$expr": {
"$eq": ["$_id", "$$uId"],
},
},
},
{
"$project": {
"firstName": 1,
},
},
],
'as': "userDetails",
}
},
{
'$project': {
"companyId": {"$arrayElemAt": ["$companyDetails.company", 0]},
"userId": {
"_id": "$actionDetails.entities.id",
"firstName": {"$arrayElemAt": ["$userDetails.firstName", 0]},
},
"isUserActiveInCompany": {
"$switch": {
"branches": [
{
'case': {
"$ne": ["$actionDetails.entities.entityType", "User"]
},
'then': null,
},
{
'case': {
"$in": [
"$actionDetails.entities.id",
{
"$map": {
'input': {"$arrayElemAt": ["$companyDetails.users", 0]},
'as': "elem",
'in': "$$elem.user"
}
}
]
},
'then': true,
},
],
'default': false,
}
}
}
}
], {
'allowDiskUse': true,
});
Let me know if you want a complete explanation and logic of each stage.

mongodb query nested array with date field

this is my document .
"calendar": {
"_id": "5cd26a886458720f7a66a3b8",
"hotel": "5cd02fe495be1a4f48150447",
"calendar": [
{
"_id": "5cd26a886458720f7a66a413",
"date": "1970-01-01T00:00:00.001Z",
"rooms": [
{
"_id": "5cd26a886458720f7a66a415",
"room": "5cd17d82ca56fe43e24ae5d3",
"price": 10,
"remaining": 8,
"reserved": 0
},
{
"_id": "5cd26a886458720f7a66a414",
"room": "5cd17db6ca56fe43e24ae5d4",
"price": 12,
"remaining": 8,
"reserved": 0
},
{
"_id": "5cd26a886458720f7a66a34",
"room": "5cd17db6ca45fe43e24ae5e7",
"price": 0,
"remaining": 0,
"reserved": 0
}
]
},
}
and this is my shema:
const calendarSchema = mongoose.Schema({
hotel: {
type: mongoose.Schema.ObjectId,
ref: "Hotel",
required: true
},
city: {
type: mongoose.Schema.ObjectId,
ref: "City"
},
calendar: [
{
date: Date,
rooms: [
{
room: {
type: mongoose.Schema.ObjectId,
ref: "Room",
required: true
},
price: {
type: Number
},
remaining: {
type: Number
},
reserved: {
type: Number
}
}
]
}
]
});
First of all, as you can see my calendar stores hotelId and CityId and included another calendar that contains some objects. There is nothing fancy here. The query has two conditions as below:
1.Our specific filter is located whole dates between startDate and endDate
2.Mentioned filter only shows the room's prices and remaining ( Not included zero num ).
And after injecting this conditions, query must return only the rooms that are matched with my filter.
I tried some query but the outcome is not my result .
db.calendars.find({
'calendar': {
'$elemMatch': {
date: {
'$lt': ISODate("2019-05-09T09:37:24.005Z"),
'$lt': ISODate("2019-06-05T09:37:24.005Z")
},
"rooms.$.price": { '$gt': 0 },
"rooms.$.remaining": { '$gt': 0 }
}
}
})
Unfortunately this is not THAT easy as you describe, this cannot be done with just a find assuming you want to project ONLY (and all) the rooms that match.
However with an aggregate this is possible, it would look like this:
db.calendars.aggregate([
{
$project:
{
"rooms": {
$filter: {
input: {
"$map": {
"input": "$calendar",
"as": "cal",
"in": {
"$cond": [
{
$and: [{$gt: ["$$cal.date", ISODate("2019-05-09T09:37:24.005Z")]},
{$lt: ["$$cal.date", ISODate("2019-06-05T09:37:24.005Z")]},]
},
{
"rooms": {
"$filter": {
"input": "$$cal.rooms",
"as": "room",
"cond": {
$and: [{"$gt": ["$$room.price", 0]},
{"$gt": ["$$room.remaining", 0]}]
}
}
},
date: "$$cal.date"
},
null
]
}
},
},
as: 'final',
cond: {$size: {$ifNull: ["$$final.rooms", []]}}
}
},
}
},
{
$match: {
"rooms.0": {$exists: true}
}
}
])