Search location using $near and $geometry in $lookup resulted field - mongodb

The scenario is Employee and they are working in multiple Store Locations, and combine both collections, that is working perfectly,
Problem in: Search in particular location (input lat, long) and this query will give that particular employee working on particular store and that location field is in lookup collection.
Employee
{
"_id": ObjectId("5f03064b3460ef1f10ec2f25"),
"employeeName": "Anonymous",
"stores": [
{
"storeId": ObjectId("5f03030a3460ef1f10ec2f23"),
"workTime": "09:30 to 12:30"
},
{
"storeId": ObjectId("5f03064b3460ef1f10ec2f26"),
"workTime": "01:30 to 02:30"
}
]
}
Store
{
"_id": ObjectId("5f03030a3460ef1f10ec2f23"),
"storeName": "A",
"location": {
"coordinates": ["longitude", "latitude"]
}
}
{
"_id": ObjectId("5f03064b3460ef1f10ec2f26"),
"storeName": "B",
"location": {
"coordinates": ["longitude", "latitude"]
}
}
The below is final result that i am getting successfully:
{
"_id": ObjectId("5f03064b3460ef1f10ec2f25"),
"employeeName": "Anonymous",
"stores": [
{
"storeId": ObjectId("5f03030a3460ef1f10ec2f23"),
"workTime": "09:30 to 12:30",
"storeLocation": {
"storeName": "A",
"location": {
"coordinates": ["longitude", "latitude"]
}
}
},
{
"storeId": ObjectId("5f03064b3460ef1f10ec2f26"),
"workTime": "01:30 to 02:30",
"storeLocation": {
"storeName": "B",
"location": {
"coordinates": ["longitude", "latitude"]
}
}
}
]
}
Aggregation
db.Enmployee.aggregate([
{ "$unwind": "$stores" },
{
"$lookup": {
"from": "Store",
"localField": "stores.storeId",
"foreignField": "_id",
"as": "stores.storeLocation"
}
},
{ "$unwind": "$stores.storeLocation" },
{
"$group": {
"_id": "$_id",
"root": { "$mergeObjects": "$$ROOT" },
"stores": { "$push": "$stores" }
}
},
{
"$replaceRoot": {
"newRoot": {
"$mergeObjects": ["$root", "$$ROOT"]
}
}
},
// <== below $match query will come here
{
"$project": {
"_id": 1,
"employeeName": 1,
"stores.workTime": 1,
"stores.storeLocation.storeName": 1,
"stores.storeLocation.location": 1
}
}
]
)
My goal is to search employee, they are working on particular location of stores using its latitude and longitude, i have tried below query in above aggregation.
{
"$match": {
"stores.storeLocation.location": {
"$near": {
"$maxDistance": 1000,
"$geometry": {
"type": "Point",
"coordinates": ["Input Longitude", "Input Latitude"]
}
}
}
}
}
It gives an error:
Failed to execute script.
Error: command failed: {
"ok" : 0,
"errmsg" : "$geoNear, $near, and $nearSphere are not allowed in this context",
"code" : 2,
"codeName" : "BadValue"
} : aggregate failed
Details:
_getErrorWithCode#src/mongo/shell/utils.js:25:13
doassert#src/mongo/shell/assert.js:18:14
_assertCommandWorked#src/mongo/shell/assert.js:534:17
assert.commandWorked#src/mongo/shell/assert.js:618:16
DB.prototype._runAggregate#src/mongo/shell/db.js:260:9
DBCollection.prototype.aggregate#src/mongo/shell/collection.js:1062:12
DBCollection.prototype.aggregate#:1:355
#(shell):1:1

$geoNear is a pipeline stage itself and you can not use it inside $match. Also, it should be the first stage of the pipeline, so you need to declare it in the $lookup pipeline.
Note that you also need to have a geo index for that use:
db.getCollection('Store').createIndex( { "location.coordinates" : "2dsphere" } )
After the changes mentioned above, the query will look like as below:
db.Employee.aggregate([
{ "$unwind": "$stores" },
{
"$lookup": {
from: "Store",
let: {
storeId: "$stores.storeId"
},
pipeline: [
{ $geoNear: {
includeLocs: "location",
distanceField: "distance",
near: {type: 'Point', coordinates: [57, 35]},
maxDistance: 1000,
spherical: true}},
{
$match: {
$expr: { $eq: ["$$storeId", "$_id"] }
}
}
],
as: "stores.storeLocation"
}
},
{ "$unwind": "$stores.storeLocation" },
{
"$group": {
"_id": "$_id",
"root": { "$mergeObjects": "$$ROOT" },
"stores": { "$push": "$stores" }
}
},
{
"$replaceRoot": {
"newRoot": {
"$mergeObjects": ["$root", "$$ROOT"]
}
}
},
// <== below $match query will come here
{
"$project": {
"_id": 1,
"employeeName": 1,
"stores.workTime": 1,
"stores.storeLocation.storeName": 1,
"stores.storeLocation.location": 1
}
}
]
)
You can findout more about $geoNear options here.

Related

Mongo aggregate on array objects using count

I have a collection with documents in below format: (shown below 2 sample document)
1st doc:
{
"date": 20221101,
"time":1500,
"productCode": "toycar",
"purchaseHistory": [
{
"clientid": 123,
"status": "SUCCESS"
},
{
"clientid": 456,
"status": "FAILURE"
}
]
}
2nd doc:
{
"date": 20221101,
"time": 1500,
"productCode": "toycar",
"purchaseHistory": [
{
"clientid": 890,
"status": "SUCCESS"
},
{
"clientid": 678,
"status": "SUCCESS"
}
]
}
I want to query above and print output in below format where purchaseHistory.status = 'SUCCESS' and date = 20221101:
{productCode:"toycar", "time": 1500, "docCount": 2, "purchaseHistCount":3}
How can I achieve this?
I tried below:
db.products.aggregate({
$match : {date:20221101, 'purchaseHistory.status':'SUCCESS'},
"$group": {
"_id": {
"pc": "$productCode",
"time": "$time"
},
"docCount": {$sum :1}
}
})
Something like this maybe:
db.collection.aggregate([
{
$match: {
date: 20221101,
"purchaseHistory.status": "SUCCESS"
}
},
{
"$addFields": {
"purchaseHistory": {
"$filter": {
"input": "$purchaseHistory",
"as": "ph",
"cond": {
$eq: [
"$$ph.status",
"SUCCESS"
]
}
}
}
}
},
{
$group: {
_id: {
t: "$time",
pc: "$productCode"
},
docCount: {
$sum: 1
},
purchaseHistCount: {
$sum: {
$size: "$purchaseHistory"
}
}
}
}
])
Explained:
Filter the matched documents.
Filter the purchaseHistory SUCCESS only.
Group the result to see count of matching documents & matching purchaseHistory.
Playground

MongoDB. Get every element from array in new field

I have a document with a nested array array_field:
{
"_id": {
"$oid": "1"
},
"id": "1",
"array_field": [
{
"data": [
{
"regions": [
{
"result": {
"item": [
"4",
"5",
"3"
]
}
},
{
"result": {
"item": [
"5"
]
}
},
{
"result": {
"item": [
"1"
]
}
}
]
}
]
}
]
}
I need add new field, new_added_field for example, with each array element from array_field.data.regions.result.item and remove array_field from document.
For example:
{
"_id": {
"$oid": "1"
},
"id": "1",
"new_added_field": [4,5,3,5,1]
}
I think i can do this with help of $unwind or $map but have difficulties and need dome hint, how i can do it with help op aggregation?
As you said,
db.collection.aggregate([
{
"$project": {
newField: {
"$map": {
"input": "$array_field",
"as": "m",
"in": "$$m.data.regions.result.item"
}
}
},
},
{ "$unwind": "$newField" },
{ "$unwind": "$newField" },
{ "$unwind": "$newField" },
{ "$unwind": "$newField" },
{
"$group": {
"_id": "$_id",
"newField": { "$push": "$newField" }
}
}
])
Working Mongo playground

How to group same record into multiple groups using mongodb aggregate pipeline

I have a two collections.
OrgStructure (visualise this as a tree structure)
Example Document:
{
"id": "org1",
"nodes": [
{
"nodeId": "root",
"childNodes": ["child1"]
},
{
"nodeId": "child1",
"childNodes": ["child2"]
},
{
"nodeId": "child2",
"childNodes": []
}
]
}
Activity
Example Document:
[
{
"id":"A1",
"orgUnit": "root"
},
{
"id":"A2",
"orgUnit": "child1"
},
{
"id":"A3",
"orgUnit": "child2"
}
]
Now my expectation is to group activities by orgUnit such a way that by considering the child nodes as well.
Here i don't want to do a lookup and i need to consider one OrgStructure document as an input, so that i can construct some condition using the document such a way that the query will return the below result.
Expected result
[
{
"_id": "root",
"activities": ["A1","A2","A3"]
},
{
"_id": "child1",
"activities": ["A2","A3"]
},
{
"_id": "child2",
"activities": ["A3"]
}
]
So im ecpecting an aggregate query something like this
{
"$group": {
"_id": {
"$switch": {
"branches": [
{
"case": {"$in": ["$orgUnit",["root","child1","child2"]]},
"then": "root"
},
{
"case": {"$in": ["$orgUnit",["child1","child2"]]},
"then": "child1"
},
{
"case": {"$in": ["$orgUnit",["child2"]]},
"then": "child2"
}
],
"default": null
}
}
}
}
Thanks in advance!
You will need 2 steps:
create another collection nodes for recursive lookup. The original OrgStructure is hard to perform $graphLookup
db.OrgStructure.aggregate([
{
"$unwind": "$nodes"
},
{
"$replaceRoot": {
"newRoot": "$nodes"
}
},
{
$out: "nodes"
}
])
Perform $graphLookup on nodes collection to get all child nodes. Perform $lookup to Activity and do some wrangling.
db.nodes.aggregate([
{
"$graphLookup": {
"from": "nodes",
"startWith": "$nodeId",
"connectFromField": "childNodes",
"connectToField": "nodeId",
"as": "nodesLookup"
}
},
{
"$lookup": {
"from": "Activity",
"let": {
nodeId: "$nodesLookup.nodeId"
},
"pipeline": [
{
$match: {
$expr: {
$in: [
"$orgUnit",
"$$nodeId"
]
}
}
},
{
$group: {
_id: "$id"
}
}
],
"as": "activity"
}
},
{
$project: {
_id: "$nodeId",
activities: "$activity._id"
}
}
])
Here is the Mongo playground for your reference.

How to select related events from the same table?

How do I make the next self query on MongoDB?
SELECT e.user_id AS user_id,
e.datetime AS started_at,
(SELECT MIN(datetime) ## taking the closest "end" event datetime of that userId ##
FROM events
WHERE type = "end" AND
user_id = e.user_id AND
datetime > e.datetime) AS end_at,
FROM events AS e
WHERE e.type = "start"
Over the next event data table:
{"_id" : "1", "type": "start", "datetime": "2022-02-01T10:15Z", "userId": "1"},
{"_id" : "2", "type": "end", "datetime": "2022-02-01T10:20Z", "userId": "1"},
{"_id" : "3", "type": "start", "datetime": "2022-02-01T10:16Z", "userId": "2"},
{"_id" : "4", "type": "end", "datetime": "2022-02-01T10:21Z", "userId": "2"},
{"_id" : "5", "type": "start", "datetime": "2022-02-02T11:01Z", "userId": "1"},
{"_id" : "6", "type": "end", "datetime": "2022-02-02T11:02Z", "userId": "1"}
The expected result should look like:
user_id
started_at
end_at
1
2022-02-01T10:15Z
2022-02-01T10:20Z
2
2022-02-01T10:16Z
2022-02-01T10:21Z
1
2022-02-02T11:01Z
2022-02-02T11:02Z
Maybe something like this:
db.collection.aggregate([
{
$sort: {
"datetime": 1
}
},
{
$project: {
"d": {
k: "$type",
v: "$datetime"
},
userId: 1
}
},
{
$group: {
_id: "$userId",
e: {
$push: "$d"
}
}
},
{
$addFields: {
e: {
$map: {
input: {
$range: [
0,
{
$size: "$e"
},
2
]
},
as: "index",
in: {
$slice: [
"$e",
"$$index",
2
]
}
}
}
}
},
{
$unwind: "$e"
},
{
$project: {
events: {
"$arrayToObject": "$e"
}
}
},
{
$project: {
userId: "$_id",
start_at: "$events.start",
end_at: "$events.end",
_id: 0
}
}
])
Explailed:
( The solution will work only if the user events start / end sequentially )
Sort the documents by datetime.
Rename the fields type & datetime to k,v ( suitable for $arrayToObject )
Group the documents per userId ( Note this solution has the limitation that total number of events must not exceed 16MB per userId)
Split the events per date/time pairs (start+end , considering user cannot start new event if the previous has not finished)
$unwind the events array
Convert start/end array to object.
Project the fields as per the expected output.
playground
Not sure what the exact use case is , but in general looks abit more practical if you add sessionId for every event document so if user can start paralel sessions the start/end events to be possible easier to correlate based on sessionId.
Here's a pipeline that closely (exactly?) follows your SQL. I converted the string datetime to ISODate to insure comparisons were done properly, but perhaps this is unecessary.
db.collection.aggregate([
{
// match each start
"$match": { "type": "start" }
},
{ // lookup ends for userId in collection
"$lookup": {
"from": "collection",
"localField": "userId",
"foreignField": "userId",
"let": {
"isoDate": {
"$dateFromString": {
"dateString": "$datetime",
"format": "%Y-%m-%dT%H:%MZ"
}
}
},
"pipeline": [
{
"$match": {
"type": "end",
"$expr": {
"$gt": [
{
"$dateFromString": {
"dateString": "$datetime",
"format": "%Y-%m-%dT%H:%MZ"
}
},
"$$isoDate"
]
}
}
}
],
"as": "endArray"
}
},
{ // output desired fields
"$project": {
"_id": 0,
"userId": 1,
"started_at": "$datetime",
"end_at": {
// assumes original collection was sorted
"$first": "$endArray.datetime"
}
}
}
])
Try it on mongoplayground.net.
Here's another pipeline that uses "$setWindowFields", but it's not ideal. I don't know how to filter "$setWindowFields" "output" given the allowed operators, etc., but it works. Improvement comments welcome!
db.collection.aggregate([
{
// add winField sorted array to each doc
// containing userId docs following
// current doc
"$setWindowFields": {
"partitionBy": "$userId",
"sortBy": { "datetime": 1 },
"output": {
"winField": {
"$push": "$$CURRENT",
"window": {
"documents": [ 1, "unbounded" ]
}
}
}
}
},
{
// just keep start docs
"$match": { "type": "start" }
},
{
// sorting on start datetime
"$sort": { "datetime": 1 }
},
{
// output desired fields
"$project": {
"_id": 0,
"userId": 1,
"started_at": "$datetime",
"end_at": {
// grab first end datetime
"$getField": {
"field": "datetime",
"input": {
"$first": {
"$filter": {
"input": "$winField",
"cond": { "$eq": [ "$$this.type", "end" ] }
}
}
}
}
}
}
}
])
Try it on mongoplayground.net.

MongoDB: Get all $matched elements individually from an array

I'm trying to get all matched elements individually, here is the sample data and the query.
// json
[
{
"name": "Mr Cool",
"ican": [
{
"subcategory": [
{
"id": "5bffdba824488b182ec86f8d", "name": "Cricket"
},
{
"id": "5bffdba824488b182ec86f8c", "name": "Footbal"
}
],
"category": "5bffdba824488b182ec86f88",
"name": "Sports"
}
]
}
]
// query
db.collection.aggregate([
{
"$match": {
"ican.subcategory.name": { $in: ["Cricket","Football"] }
}
},
{
"$project": { "_id": 1, "name": 1, }
}
])
I'm getting the combined result, I need the individual match record. I tried $all and $elementMatch but getting the same response. how can I get the results as below. I'm using $aggregate because I will be using $geoNear pipeline for getting the nearby users.
// current result
[
{
"_id": ObjectId("5a934e000102030405000000"),
"name": "Mr Cool"
}
]
// expected result
[
{
"_id": ObjectId("5a934e000102030405000000"),
"name": "Mr Cool",
"subcategory: "Cricket"
},
{
"_id": ObjectId("5a934e000102030405000000"),
"name": "Mr Cool",
"subcategory: "Footbal"
}
]
Thank you
Try this Mongo Playground
db.col.aggregate([
{"$unwind" : "$ican"},
{"$unwind" : "$ican.subcategory"},
{"$match" : {"ican.subcategory.name": { "$in": ["Cricket","Football"] }}},
{"$group" : {"_id" : null,"data" : {"$push" : {"_id" : "$_id","name" : "$name","subcategory" : "$ican.subcategory.name"}}}},
{"$unwind" : "$data"},
{"$replaceRoot" : {"newRoot" : "$data"}}
])
You can use below aggregation without the $unwind and for better performance
db.collection.aggregate([
{ "$match": { "ican.subcategory.name": { "$in": ["Cricket","Football"] }}},
{ "$project": {
"ican": {
"$reduce": {
"input": "$ican",
"initialValue": [],
"in": {
"$concatArrays": [
{ "$filter": {
"input": {
"$map": {
"input": "$$this.subcategory",
"as": "s",
"in": { "name": "$name", "subcategory": "$$s.name" }
}
},
"as": "fil",
"cond": { "$in": ["$$fil.subcategory", ["Football"]] }
}},
"$$value"
]
}
}
}
}},
{ "$unwind": "$ican" },
{ "$replaceRoot": { "newRoot": "$ican" }}
])