Query to find connected components in mongodb graph collection? - mongodb

I want to group the connected component in the mongodb collection.
Example:
{ '_id': 1, 'data': '...', 'similar_id': [2,3,4] }
{ '_id': 2, 'data': '...', 'similar_id': [1] }
{ '_id': 3, 'data': '...', 'similar_id': [1,4] }
{ '_id': 4, 'data': '...', 'similar_id': [1,3] }
{ '_id': 7, 'data': '...', 'similar_id': [2,3,4] }
{ '_id': 5, 'data': '...', 'similar_id': [6] }
{ '_id': 6, 'data': '...', 'similar_id': [5] }
Diagram for above network.
So I want a query which can find connected components.
{ '_id': ..., 'groups': {[1,2,3,4], [5,6], [7]} }
The result may not need to look like above but only in some form such that they are separated in different groups.

It ain't pretty but this is what I got, a brief description of my strategy was initially creating two groups of nodes. one contains node that are "connected" (i.e both x=>y and y=>x edges exist). and the other are potential single nodes. meaning they had one or zero of the x=>y or y=>x edges.
Once achieving this all we have to do is reducing the array by connecting connected nodes.
Mind you I fully believe this is not the "best" way to achieve the result you want as I just focused on getting it done without over thinking about performance or redundancy. with that said I'm define myself as a Mongo enthusiast and I would definitely say I struggled with this a little. For me this is usually a red flag that says my schema or db solution is wrong (maybe use a graph db?). Again these are just my opinions and it's entirely possible I just tangled myself with this pipeline.
It's worth mentioning I considered an approach using $graphLookup however on a fully connected or nearly fully connected graph this has a required depth usage of n where n=number of node, eventually I decided against it although thi approach might viable if you have any prior knowledge that can limit the depth to a certain constant.
db.collection.aggregate([
{
$unwind: {
path: "$similar_id",
preserveNullAndEmptyArrays: true
}
},
{
$addFields: {
similar_id: {
$ifNull: [
"$similar_id",
"$_id"
]
}
}
},
{
$sort: {
_id: 1,
similar_id: -1
}
},
{
$addFields: {
tmpId: {
$cond: [
{
$gt: [
"$similar_id",
"$_id"
]
},
[
"$_id",
"$similar_id"
],
[
"$similar_id",
"$_id"
]
]
}
}
},
{
$group: {
_id: "$tmpId",
sum: {
$sum: 1
}
}
},
{
$facet: {
single: [
{
$match: {
sum: 1
}
},
{
$unwind: "$_id"
},
{
$group: {
_id: null,
potentionals: {
$addToSet: "$_id"
}
}
}
],
clusters: [
{
$match: {
sum: 2
}
},
{
$group: {
_id: null,
edges: {
$addToSet: "$_id"
},
}
},
{
$project: {
all: {
$reduce: {
input: "$edges",
initialValue: [],
in: {
$setUnion: [
"$$this",
"$$value"
]
}
}
},
groups: {
$reduce: {
input: "$edges",
initialValue: [],
in: {
$cond: [
{
$gt: [
{
$size: {
$filter: {
input: "$$value",
as: "subgroup",
cond: {
$gt: [
{
$size: {
$setIntersection: [
"$$subgroup",
"$$this"
]
}
},
0
]
}
}
}
},
0
]
},
{
$map: {
input: "$$value",
as: "subgroup",
in: {
$cond: [
{
$gt: [
{
$size: {
$setIntersection: [
"$$subgroup",
"$$this"
]
}
},
0
]
},
{
"$setUnion": [
"$$this",
"$$subgroup"
]
},
"$$subgroup"
]
}
}
},
{
$concatArrays: [
"$$value",
[
"$$this"
]
]
}
]
}
}
}
}
}
]
}
},
{
$unwind: {
path: "$single",
preserveNullAndEmptyArrays: true
}
},
{
$unwind: {
path: "$clusters",
preserveNullAndEmptyArrays: true
}
},
{
$project: {
groups: {
$concatArrays: [
"$clusters.groups",
{
$map: {
input: {
$filter: {
input: "$single.potentionals",
as: "pot",
cond: {
$eq: [
{
$size: {
$setIntersection: [
[
"$$pot"
],
"$clusters.all"
]
}
},
0
]
}
}
},
as: "single",
in: [
"$$single"
]
}
}
]
}
}
}
])
MongoPlayground

Sorry for such late reply, but maybe other will find this useful.
You can try using NetworkX library in Python.
1st unwind similar_id to have documents with pairs {'_id':1,'similar_id':2}
import networkx as nx
unwind={'$unwind':'$similar_id'}
pipeline=[unwind]
cursor=db.collection.aggregate(pipeline)
G=nx.Graph()
for c in cursor:
G.add_edge(c['_id'],c['similar_id'])
all_clusters=list(nx.connected_components(G)) # a list of all connected components
len(all_clusters) # number of connected components

Related

How can i aggregate filter nested documents and get value from other field

I have a collection like this:
{
'_id' : ObjectId('6251f8556e75125f9260f333'),
'name': 'jojo',
'profile': 'jojo profile',
'date': ISODate("2022-04-09T21:18:40.473Z"),
'look': [
{ 'art': 'group-id', 'data': 'alma', 'dt': '1'},
{ 'art': 'called', 'data': 'central', 'dt': '1'},
{ 'art': 'access-time', 'data': 108000, 'dt': '1'}
]
'answer': [
{ 'art': 'rate-id', 'data': 'limit1', 'dt': '1'},
{ 'art': 'protocol', 'data': 'tcp', 'dt': '1'}
]
},
{
'_id' : ObjectId('6251f8306e75125f9260f332'),
'name': 'dodo',
'profile': 'dodo profile',
'date': ISODate("2022-04-09T15:20:58.562Z"),
'look': [
{ 'art': 'group-id', 'data': 'alma', 'dt': '1'},
{ 'art': 'called', 'data': 'central', 'dt': '1'},
]
'answer': [
{ 'art': 'rate-id', 'data': 'limit1', 'dt': '1'},
]
},
{
'_id' : ObjectId('6251a5113700ba4a0a59c48f'),
'name': 'kaka',
'profile': 'kaka profile',
'date': ISODate("2022-04-09T15:22:25.816Z"),
'look': [
{ 'art': 'access-time', 'data': 50400, 'dt': '1'}
]
'answer': [
{ 'art': 'protocol', 'data': 'tcp', 'dt': '1'}
]
}
and I was expecting an output like this:
{
'_id' : ObjectId('6251f8556e75125f9260f333'),
'name': 'jojo',
'profile': 'jojo profile',
'date': ISODate("2022-04-09T21:18:40.473Z"),
'goup': 'alma', // filter by 'group-id' and put value of data field
'called': 'central', // filter by 'called' and put value of data field
'accessTime': 108000, // filter by 'access-time' and put value of data field
'rate': 'limi1', // filter by 'rate-id' and put value of data field
'protocol': 'tcp', // filter by 'protocol' and put value of data field
},
{
'_id' : ObjectId('6251f8306e75125f9260f332'),
'name': 'dodo',
'profile': 'dodo profile',
'date': ISODate("2022-04-09T15:20:58.562Z"),
'goup': 'alma',
'called': 'central',
'accessTime': '', // set blank data if not exist
'rate': 'limi1',
'protocol': '', // set blank data if not exist
},
{
'_id' : ObjectId('6251a5113700ba4a0a59c48f'),
'name': 'kaka',
'profile': 'kaka profile',
'date': ISODate("2022-04-09T15:22:25.816Z"),
'goup': '', // set blank data if not exist
'called': '', // set blank data if not exist
'accessTime': 50400,
'rate': '', // set blank data if not exist
'protocol': 'tcp',
}
I've searched here but couldn't find an answer that matches the problem I'm facing, probably because of the wrong keywords.
Since I'm new to mongodb, I'm confused about how to solve the query I want. How can I achieve this? Please help me...
You would require an aggregate operation that has a pipeline with the following key operators and stages:
$map: an operator to transform the look and answer arrays into documents with just mapped k and v fields, crucial for obtaining a hash map with the following operator
$arrayToObject: this allows the above to be possible i.e. converting an array into a single document
$mergeObjects: combine top level fields i.e. _id, date, name, profile together with the converted documents above
$replaceWith: pipeline stage to replace the root document with the specified document from above
Overall, your pipeline should follow:
const first = {
$first: {
$split: ['$$this.art', '-']
}
};
const keyExpression = {
$cond: [
{ $eq: [first, 'access'] },
'accessTime',
first
]
};
const pipeline = [
{ $replaceWith: {
$mergeObjects: [
{
_id: '$_id',
date: '$date',
name: '$name',
profile: '$profile',
protocol: '',
group: '',
called: '',
rate: '',
accessTime: '',
},
{ $arrayToObject: {
$map: {
input: '$look',
in: { k: keyExpression, v: '$$this.data' }
}
} },
{ $arrayToObject: {
$map: {
input: '$answer',
in: { k: keyExpression, v: '$$this.data' }
}
} }
]
} }
]
Mongo Playground
For this you should use the aggregation framework of mongo db, because will require complex operations to get the data in the shape that you want.
https://www.mongodb.com/docs/manual/aggregation/
Every aggregation is an array of stages and every stage does something specific.
I used the next stages:
addFields: Allows you to add new fields to the response of every document, so if you don't have group in the document, that will add or replace it.
project: Allows you remove some fields of a document. In a projection stage if you set an attribute as 0 that will remove that attribute from the response.
Also I used some operators:
filter: this allows you to filter data of an element that is an array
arrayElemenAt: receives an array and return the position specified
The pipeline:
[
{
"$addFields":{
"group":{
"$arrayElemAt":[
{
"$filter":{
"input":"$look",
"as":"item",
"cond":{
"$eq":[
"$$item.art",
"group-id"
]
}
}
},
0
]
},
"called":{
"$arrayElemAt":[
{
"$filter":{
"input":"$look",
"as":"item",
"cond":{
"$eq":[
"$$item.art",
"called"
]
}
}
},
0
]
},
"accessTime":{
"$arrayElemAt":[
{
"$filter":{
"input":"$look",
"as":"item",
"cond":{
"$eq":[
"$$item.art",
"access-time"
]
}
}
},
0
]
},
"rate":{
"$arrayElemAt":[
{
"$filter":{
"input":"$answer",
"as":"item",
"cond":{
"$eq":[
"$$item.art",
"rate-id"
]
}
}
},
0
]
},
"protocol":{
"$arrayElemAt":[
{
"$filter":{
"input":"$answer",
"as":"item",
"cond":{
"$eq":[
"$$item.art",
"protocol"
]
}
}
},
0
]
}
}
},
{
"$addFields":{
"group":"$group.data",
"called":"$called.data",
"accessTime":"$accessTime.data",
"rate":"$rate.data",
"protocol":"$protocol.data"
}
},
{
"$project":{
"look":0,
"answer":0
}
}
]
This is quite cumbersome with the current structure, as for each field you have to convert the object to an array, filter it then convert it back, here's how it looks:
db.collection.aggregate([
{
$replaceRoot: {
newRoot: {
"$mergeObjects": [
{
_id: "$_id",
name: "$name",
profile: "$profile",
date: "$date",
},
{
"$arrayToObject": {
$map: {
input: {
$filter: {
input: {
$objectToArray: {
$ifNull: [
{
"$arrayElemAt": [
{
$filter: {
input: {
$ifNull: [
"$look",
[]
]
},
cond: {
$eq: [
"$$this.art",
"group-id"
]
}
}
},
0
]
},
{
art: ""
}
]
}
},
cond: {
$eq: [
"$$this.k",
"data"
]
}
}
},
in: {
k: "goup",
v: "$$this.v"
}
}
}
},
{
"$arrayToObject": {
$map: {
input: {
$filter: {
input: {
$objectToArray: {
$ifNull: [
{
"$arrayElemAt": [
{
$filter: {
input: {
$ifNull: [
"$look",
[]
]
},
cond: {
$eq: [
"$$this.art",
"called"
]
}
}
},
0
]
},
{
art: ""
}
]
}
},
cond: {
$eq: [
"$$this.k",
"data"
]
}
}
},
in: {
k: "called",
v: "$$this.v"
}
}
}
},
{
"$arrayToObject": {
$map: {
input: {
$filter: {
input: {
$objectToArray: {
$ifNull: [
{
"$arrayElemAt": [
{
$filter: {
input: {
$ifNull: [
"$look",
[]
]
},
cond: {
$eq: [
"$$this.art",
"access-time"
]
}
}
},
0
]
},
{
art: ""
}
]
}
},
cond: {
$eq: [
"$$this.k",
"data"
]
}
}
},
in: {
k: "access-time",
v: "$$this.v"
}
}
}
},
{
"$arrayToObject": {
$map: {
input: {
$filter: {
input: {
$objectToArray: {
$ifNull: [
{
"$arrayElemAt": [
{
$filter: {
input: {
$ifNull: [
"$answer",
[]
]
},
cond: {
$eq: [
"$$this.art",
"rate-id"
]
}
}
},
0
]
},
{
art: ""
}
]
}
},
cond: {
$eq: [
"$$this.k",
"data"
]
}
}
},
in: {
k: "rate",
v: "$$this.v"
}
}
}
},
{
"$arrayToObject": {
$map: {
input: {
$filter: {
input: {
$objectToArray: {
$ifNull: [
{
"$arrayElemAt": [
{
$filter: {
input: {
$ifNull: [
"$answer",
[]
]
},
cond: {
$eq: [
"$$this.art",
"protocol"
]
}
}
},
0
]
},
{
art: ""
}
]
}
},
cond: {
$eq: [
"$$this.k",
"data"
]
}
}
},
in: {
k: "protocol",
v: "$$this.v"
}
}
}
}
]
}
}
}
])
Mongo Playground
If you're using Mongo version 5+, then you can use $getField to simplify the syntax a little bit, here's how one field would look like in this syntax:
goup: {
$getField: {
field: 'data',
input: {
'$arrayElemAt': [
{
$filter: {
input: {
$ifNull: [
'$look',
[],
],
},
cond: {
$eq: [
'$$this.art',
'group-id',
],
},
},
},
0,
],
},
},
},

Mongodb aggregation, get expected result on groupBy without hard-coding categories

My objective is to write an efficient query, that with the given input, gives me the expected output. I have some working solution, but all "types" are "manually" written, so I guess I'm looking for help to get the same output but in a different way.
input
reportId
type
weight
A
"fish"
4
A
"fish"
2
A
"cow"
0
B
"fish"
2
B
"tuna"
1
B
"bird"
Expected output
[
{
reportId: "A",
totalCount: 3,
totalWeight: 6,
fishCount: 2,
tunaCount: 0,
cowCount: 1,
birdCount: 0
},
{
reportId: "A",
totalCount: 3,
totalWeight: 2,
fishCount: 1,
tunaCount: 1,
cowCount: 0,
birdCount: 1
},
]
Partial "hard-coded" solution
What I have been doing so far is to create 2 group-by steps: It kind of get's the job done, but in my real use-case there are a lot of types, and therefore the group-stages are very long.
[
{
$group: {
_id: { reportId: "$reportId", type: $type },
count: { $sum: 1 },
totalWeight: { $sum: "$weight" }
}
},
{
$group: {
_id: "$_id.reportId",
totalCount: { $sum: "$totalCount" },
totalWeight: { $sum: "$totalWeight" },
fishCount: {
$sum: {
$cond: {
"if": { $eq: ["$_id.type", "fish"] },
then: "$count",
else: 0
}
}
},
tunaCount: {
$sum: {
$cond: {
"if": { $eq: ["$_id.type", "tuna"] },
then: "$count",
else: 0
}
}
},
// <== And here I have a count blog for each type. Can I get the same result in a better way?
}
}
]
I will focus to the second part, which is the difficult one. I don't know whether there is a shorter and better solution, but this one should work:
db.collection.aggregate([
{
$unset: "_id"
},
{
$set: {
data: {
"$objectToArray": "$$ROOT"
}
}
},
{
$group: {
_id: "$reportId",
data: {
$push: "$data"
}
}
},
{
$set: {
data: {
$reduce: {
input: "$data",
initialValue: [],
in: {
$concatArrays: [
"$$value",
"$$this"
]
}
}
}
}
},
{
$set: {
data: {
$filter: {
input: "$data",
cond: {
$not: {
$in: [
"$$this.k",
[
"totalCount",
"totalWeight"
]
]
}
}
}
}
}
},
{
$unwind: "$data"
},
{
$group: {
_id: "$_id",
data: {
$push: "$data"
}
}
},
{
$replaceRoot: {
newRoot: {
$arrayToObject: "$data"
}
}
}
])
See Mongo playground

How to find prev/next document after sort in MongoDB

I want to find prev/next blog documents whose publish date is closest to the input document.
Below is the document structure.
Collection Examples (blog)
{
blogCode: "B0001",
publishDate: "2020-09-21"
},
{
blogCode: "B0002",
publishDate: "2020-09-22"
},
{
blogCode: "B0003",
publishDate: "2020-09-13"
},
{
blogCode: "B0004",
publishDate: "2020-09-24"
},
{
blogCode: "B0005",
publishDate: "2020-09-05"
}
If the input is blogCode = B0003
Expected output
{
blogCode: "B0005",
publishDate: "2020-09-05"
},
{
blogCode: "B0001",
publishDate: "2020-09-21"
}
How could I get the output result? In sql, it seems using ROW_NUMBER can solve my problem, however I can't find a solution to achieve the feature in MongoDB. The alternate solution may be reference to this answer (But, it seems inefficient). Maybe using mapReduce is another better solutions? I'm confused at the moment, please give me some help.
You can go like following.
We need to compare existing date with given date. So I used $facet to categorize both dates
The original data should be one Eg : B0003. So that I just get the first element of the origin[] array to compare with rest[] array
used $unwind to flat the rest[]
Substract to get the different between both dates
Again used $facet to find previous and next dates.
Then combined both to get your expected result
NOTE : The final array may have 0<elements<=2. The expected result given by you will not find out whether its a prev or next date if there is a one element. So my suggestion is add another field to say which date it is as the mongo playground shows
[{
$facet: {
origin: [{
$match: { blogCode: 'B0001' }
}],
rest: [{
$match: {
$expr: {
$ne: ['$blogCode','B0001']
}
}
}]
}
}, {
$project: {
origin: {
$arrayElemAt: ['$origin',0]
},
rest: 1
}
}, {
$unwind: {path: '$rest'}
}, {
$project: {
diff: {
$subtract: [{ $toDate: '$rest.publishDate' },{ $toDate: '$origin.publishDate'}]
},
rest: 1,
origin: 1
}
}, {
$facet: {
prev: [{
$sort: {diff: -1}
},
{
$match: {
diff: {$lt: 0 }
}
},
{
$limit: 1
},
{
$addFields:{"rest.type":"PREV"}
}
],
next: [{
$sort: { diff: 1 }
},
{
$match: {
diff: { $gt: 0 }
}
},
{
$limit: 1
},
{
$addFields:{"rest.type":"NEXT"}
}
]
}
}, {
$project: {
combined: {
$concatArrays: ["$prev", "$next"]
}
}
}, {
$unwind: {
path: "$combined"
}
}, {
$replaceRoot: {
newRoot: "$combined.rest"
}
}]
Working Mongo playground
Inspire for the solution of varman proposed. I also find another way to solve my problem by using includeArrayIndex.
[
{
$sort: {
"publishDate": 1
},
},
{
$group: {
_id: 1,
root: {
$push: "$$ROOT"
}
},
},
{
$unwind: {
path: "$root",
includeArrayIndex: "rownum"
}
},
{
$replaceRoot: {
newRoot: {
$mergeObjects: [
"$root",
{
rownum: "$rownum"
}
]
}
}
},
{
$facet: {
currRow: [
{
$match: {
blogCode: "B0004"
},
},
{
$project: {
rownum: 1
}
}
],
root: [
{
$match: {
blogCode: {
$exists: true
}
}
},
]
}
},
{
$project: {
currRow: {
$arrayElemAt: [
"$currRow",
0
]
},
root: 1
}
},
{
$project: {
rownum: {
prev: {
$add: [
"$currRow.rownum",
-1
]
},
next: {
$add: [
"$currRow.rownum",
1
]
}
},
root: 1
}
},
{
$unwind: "$root"
},
{
$facet: {
prev: [
{
$match: {
$expr: {
$eq: [
"$root.rownum",
"$rownum.prev"
]
}
}
},
{
$replaceRoot: {
newRoot: "$root"
}
}
],
next: [
{
$match: {
$expr: {
$eq: [
"$root.rownum",
"$rownum.next"
]
}
}
},
{
$replaceRoot: {
newRoot: "$root"
}
}
],
}
},
{
$project: {
prev: {
$arrayElemAt: [
"$prev",
0
]
},
next: {
$arrayElemAt: [
"$next",
0
]
},
}
},
]
Working Mongo playground

Splitting an alphanumeric string like "3a" or "32ab" in mongodb aggregation pipeline

I would like to split an alphanumeric string like 3a into "3" and "a". Please help if any one has an idea. I can't use the $split in mongodb aggregation.
I'm not sure that this is efficient, but this answer may give you a solution.
Since we can't use regex in $split,
First stage - divide the sentence into words and store in char[]
Flat the char[] using $unwind
Categorize all string into strings[] and all numbers into numbers[] using $facet. Here we use $match with regex
Then combined as what you need.
Assume this is your string.
{
char:"32ab"
}
The mongo script might be,
db.collection.aggregate([{$addFields: {
'char': {
$map: {
input: {
$range: [
0,
{
$strLenCP: '$char'
}
]
},
'in': {
$substrCP: [
'$char',
'$$this',
1
]
}
}
}
}}, {$unwind: {
path: '$char',
preserveNullAndEmptyArrays: false
}}, {$facet: {
strings: [
{
$match: {
'char': RegExp('^[A-Za-z]+$')
}
},
{
$group: {
_id: null,
arr: {
$push: '$char'
}
}
},
{
$project: {
combined: {
$reduce: {
input: '$arr',
initialValue: '',
'in': {
$concat: [
'$$value',
'$$this'
]
}
}
}
}
}
],
numbers: [
{
$match: {
'char': {
$not: RegExp('^[A-Za-z]+$')
}
}
},
{
$group: {
_id: null,
arr: {
$push: '$char'
}
}
},
{
$project: {
combined: {
$reduce: {
input: '$arr',
initialValue: '',
'in': {
$concat: [
'$$value',
'$$this'
]
}
}
}
}
}
]
}}, {$project: {
string: {
$arrayElemAt: [
{
$ifNull: [
'$strings.combined',
''
]
},
0
]
},
number: {
$toInt:{
$arrayElemAt: [
{
$ifNull: [
'$numbers.combined',
''
]
},
0
]
}
}
}}])
And the output is
{
string : "ab",
numbers: 32
}

Returning a document with two fields from the same array in MongoDB

Given documents such as
{
_id: 'abcd',
userId: '12345',
activities: [
{ status: 'login', timestamp: '10000001' },
{ status: 'logout', timestamp: '10000002' },
{ status: 'login', timestamp: '10000003' },
{ status: 'logout', timestamp: '10000004' },
]
}
I am trying to create a pipeline such as all users that have their latest login/logout activities recorded between two timestamps will be returned. For example, if the two timestamp values are between 10000002 and 10000003, the expected document should be
{
_id: 'abcd',
userId: '12345',
login: '10000003',
logout: '10000002'
}
Of if the two timestamp values are between -1 and 10000001, the expected document should be :
{
_id: 'abcd',
userId: '12345',
login: '10000001',
logout: null
}
Etc.
I know it has to do with aggregations, and I need to $unwind, etc., but I'm not sure about the rest, namely evaluating two fields from the same document array
You can try below aggregation:
db.col.aggregate([
{
$unwind: "$activities"
},
{
$match: {
$and: [
{ "activities.timestamp": { $gte: "10000001" } },
{ "activities.timestamp": { $lte: "10000002" } }
]
}
},
{
$sort: {
"activities.timestamp": -1
}
},
{
$group: {
_id: "$_id",
userId: { $first: "$userId" },
activities: { $push: "$activities" }
}
},
{
$addFields: {
login: { $arrayElemAt: [ { $filter: { input: "$activities", as: "a", cond: { $eq: [ "$$a.status", "login" ] } } } , 0 ] },
logout: { $arrayElemAt: [ { $filter: { input: "$activities", as: "a", cond: { $eq: [ "$$a.status", "logout" ] } } } , 0 ] }
}
},
{
$project: {
_id: 1,
userId: 1,
login: { $ifNull: [ "$login.timestamp", null ] },
logout: { $ifNull: [ "$logout.timestamp", null ] }
}
}
])
We need to use $unwind + $sort + $group to make sure that our activities will be sorted by timestamp. After $unwind you can use $match to apply filtering condition. Then you can use $filter with $arrayElemAt to get first (latest) value of filtered array. In the last $project you can explicitly use $ifNull (otherwise JSON key will be skipped if there's no value)
You can use below aggregation
Instead of $unwind use $lte and $gte with the $fitler aggregation.
db.collection.aggregate([
{ "$project": {
"userId": 1,
"login": {
"$max": {
"$filter": {
"input": "$activities",
"cond": {
"$and": [
{ "$gte": ["$$this.timestamp", "10000001"] },
{ "$lte": ["$$this.timestamp", "10000004"] },
{ "$lte": ["$$this.status", "login"] }
]
}
}
}
},
"logout": {
"$max": {
"$filter": {
"input": "$activities",
"cond": {
"$and": [
{ "$gte": ["$$this.timestamp", "10000001"] },
{ "$lte": ["$$this.timestamp", "10000004"] },
{ "$lte": ["$$this.status", "logout"] }
]
}
}
}
}
}}
])