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

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.

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 - 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 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.

Mongo Sort Aggregation not working on Sub Document

Trying to sort the sub-key on the document.
Example of data from the pipeline. Incorrect sort order for subkey availability.startIso
{
"_id": "60e458d7b896de9c8e44d6c9",
"uid": "6233ed1d8b154aa79d1435b5",
"name": "Pale",
"phoneNumber": "+19999813917",
"profileMedia": {
"url": "https://storage.googleapis.com/refresh-me-dev.appspot.com/dummy_photos/dummy_1.jpg",
"type": "photo"
},
"createdIso": "2021-07-06T13:21:27.513Z",
"isDeleted": false,
"isFlagged": false,
"isBanned": false,
"isAdmin": false,
"isVendor": true,
"lastOpenedAppIso": "2021-07-06T13:21:27.513Z",
"vendorMeta": {
"servicesOffered": [
"swedish"
],
"location": [
0,
0
]
},
"distanceFromPoint": 0,
"availability": [
{
"_id": "60e458d7b896de9c8e44d6cc",
"uid": "dec97d4b1dea44f7b2fa45a5",
"vendorUid": "6233ed1d8b154aa79d1435b5",
"startIso": "2021-07-12T04:07:21.349Z",
"endIso": "2021-07-12T05:07:21.360Z"
},
{
"_id": "60e458d7b896de9c8e44d6ce",
"uid": "a5928ea5c18c4321bd6a9a9b",
"vendorUid": "6233ed1d8b154aa79d1435b5",
"startIso": "2021-07-11T01:52:18.323Z",
"endIso": "2021-07-11T02:52:18.335Z"
}
]
}
Example of the aggregation
let vendors = await mongoDb
.collection<User>(collectionNames.users)
.aggregate([
{
$geoNear: {
near: { type: "Point", coordinates: [lat, lng] },
spherical: true,
maxDistance: 7500,
distanceField: "distanceFromPoint",
},
},
{
$match: { isVendor: true },
},
{
$match: { "vendorMeta.servicesOffered": { $in: services } },
},
{
$lookup: {
from: "vendor.availability",
localField: "uid",
foreignField: "vendorUid",
as: "availability",
},
},
{
$addFields: {
availability: {
$filter: {
input: "$availability",
as: "availability",
cond: { $and: [{ $gte: ["$$availability.startIso", nowIso] }, { $lte: ["$$availability.endIso", nDaysIso] }] },
},
},
},
},
{ $sort: { "availability.startIso": 1 } },
{ $match: { availability: { $ne: [] } } },
])
.toArray();
This is working as intended, $sort does not work on arrays and can't be used like this. What you can do is $unwind, then $sort and end by $grouping to restore the structure, like so:
[
// ...,
{
$unwind: "$availability"
},
{ $sort: { "availability.startIso": 1 } },
{
$group: {
_id: '$_id',
root: {$first: "$$ROOT"},
availability: {$push: '$availability'}
}
},
{
$replaceRoot: {
newRoot: {
$mergeObjects: [
'$root',
{ availability: '$availability'}
]
}
}
}
]
Note that i removed the :
{ $match: { availability: { $ne: [] } } },
As it's no longer required because $unwind will remove those documents for you.

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.