Accessing a random field using other field value - mongodb

I have a document like this:
{
value: "field2",
field1: [ ... ],
field2: [ ... ],
...
}
Where value will be the value of one of the fields in the document. and many different fields are possible for one document.
I want to match a document. fetch the relevant field only and them do some calculations on it.
For example I want to do:
{
$unwind: "$value"
}
And get the results of field2 unwinded.
How can I do this?

It's a little bit "hacky" but you can achieve this using operators like $objectToArray and $filter like so:
db.collection.aggregate([
{
$addFields: {
"values": {
$arrayElemAt: [
{
$filter: {
input: {
$objectToArray: "$$ROOT"
},
as: "field",
cond: {
$eq: [
"$$field.k",
"$value"
]
}
}
},
0
]
}
}
},
{
$unwind: "$values.v"
},
{
$replaceRoot: {
newRoot: "$values.v"
}
},
])
MongoPlayground

Related

MongoDB find records with reverse match

I have the following structure to store user's like. For example, _id1 user likes user _id2 & _id3. Where the _id1 user is having a match with user _id2 only.
I need only records with a match.
Document Structure --
[
{'from':'_id1', 'to':'_id2'},
{'from':'_id1', 'to':'_id3'},
{'from':'_id2', 'to':'_id1'},
]
Expected Output --
[
{'from':'_id1', 'to':'_id2'},
{'from':'_id2', 'to':'_id1'},
]
You can $group on the "couple" and only then match those that were summed to be 2. The trick is to make sure the _id your grouping on is the same - I just did it with $cond and create the tuple [ _id1, _id2 ] where the smaller id is always first, like so:
db.collection.aggregate([
{
$group: {
_id: {
$cond: [
{
$gt: [
"$from",
"$to"
]
},
[
"$to",
"$from"
],
[
"$from",
"$to"
]
]
},
sum: {
$sum: 1
},
roots: {
$push: "$$ROOT"
}
}
},
{
$match: {
sum: {
$gt: 1
}
}
},
{
$unwind: "$roots"
},
{
$replaceRoot: {
newRoot: "$roots"
}
}
])

Use $lookup on a double nested array

I'm trying to use an _id saved in a double nested array to find and attach data from a different document. It essentially looks like this:
doc1 = {
_id: ObjectId,
...other stuff...,
firstArray: [
...other stuff...,
secondArray: [
other_id: ObjectId
]
]
}
doc2 = {
_id: ObjectId,
...the stuff I want...
}
Not every entry in secondArray is going to contain an other_id field, only some. This has gotten me close to the result I want except that the firstArray field contains an entry for the total number of entries in the secondArray field. I think I'm just missing one step to essentially undo the one of the two $unwinds.
doc1.aggregate([
{ $match: { _id: req.params._id }},
{ $unwind: "$firstArray" },
{ $unwind: "$firstArray.secondArray" },
{ $lookup: {
from: "doc2",
localField: "firstArray.secondArray.other_id",
foreignField: "_id",
as: "firstArray.secondArray.other",
}},
{ $addFields: {
"firstArray.secondArray.other": {
$arrayElemAt: ["$firstArray.secondArray.other", 0]
}
}},
{ $group: {
_id: "$_id",
...other stuff...,
firstArray: { $push: "$firstArray" },
}},
]);
You need to add another $group stage to reconstruct the "secondArray" structure, in general it's easy to remember that for each $unwind action you do, you'll need to counter that with another $group in order to restore the initial structure.
In order to do that you'll need some kind of a unique identifier for firstArray so you can group by it, if it doesn't exist you can add an _id that is the index of the element in the array like so:
{
"$addFields": {
"firstArray": {
$map: {
input: {
$zip: {
inputs: [
"$firstArray",
{
$range: [
0,
{
$size: "$firstArray"
}
]
}
]
}
},
as: "item",
in: {
"$mergeObjects": [
{
"$arrayElemAt": [
"$$item",
0
]
},
{
_id: {
"$arrayElemAt": [
"$$item",
1
]
}
}
]
}
}
}
}
}
Now firstArray has a unique field _id that we can later use to group on like so:
... the pipeline ...
{
$group: {
_id: {
id: "$_id",
first: "$firstArray._id"
},
secondArray: {
$push: "$firstArray.secondArray"
}
}
},
{
$group: {
_id: "$id._id",
firstArray: {
$push: {
secondArray: "$secondArray"
}
},
}
}
You will need to add support for all the "other stuff" you defined in your own pipeline, but this pipeline flow is the way to go.
Mongo Playground

Dynamic key in MongoDB

Im trying to create a dynamic group by (with sum agg) in MongoDB. But don't know how to right syntax that.
Lets imaging 2 documents:
{
"_id": {"$oid":"5f69f6a360c8479d0908a649"},
"key":"key1",
"data":{
"key1":"value1",
"key2":"value2",
"key3":"value3",
"key4":"value4"
},
"count":10
}
{
"_id": {"$oid":"5f69f6a360c8479d0908a649"},
"key":"key2",
"data":{
"key1":"value5",
"key2":"value6",
"key3":"value7",
"key4":"value8"
},
"count":15
}
With the key attribute, I want to control, which is the groupby attribute.
A pseudo query could look like:
[{
$group: {
_id: {
'$key': data[$key]
},
sum: {
'$sum': '$count'
}
}
}]
Output should look like:
value1 : 10
value6 : 15
Somebody knows how to do that?
I don't understand the purpose of $sum and $group, there are no arrays in your documents.
This aggregation pipeline give desired result:
db.collection.aggregate([
{ $set: { data: { $objectToArray: "$data" } } },
{ $set: { data: { $filter: { input: "$data", cond: { $eq: ["$$this.k", "$key"] } } } } },
{ $set: { data: { k: { $arrayElemAt: ["$data.v", 0] }, v: "$count" } } },
{ $set: { data: { $arrayToObject: "$data" } } },
{ $replaceRoot: { newRoot: { $mergeObjects: ["$$ROOT", "$data"] } } },
{ $unset: ["key", "count", "data"] }
])
You can try,
$reduce input data as array using $objectToArray, check condition if key matches with data key then return key as value and value as count field
convert that returned key and value object array to exact object using $arrayToObject
replace field using $replaceWith
db.collection.aggregate([
{
$replaceWith: {
$arrayToObject: [
[
{
$reduce: {
input: { $objectToArray: "$data" },
initialValue: {},
in: {
$cond: [
{ $eq: ["$$this.k", "$key"] },
{
k: "$$this.v",
v: "$count"
},
"$$value"
]
}
}
}
]
]
}
}
])
Playground

MongoDB - match multiple fields same value

So I am trying to do something where I can group MongoDB fields for a check.
Given I have following data structure:
{
//Some other data fields
created: date,
lastLogin: date,
someSubObject: {
anotherDate: date,
evenAnotherDate: date
}
On these I want to do a check like this:
collection.aggregate([
{
$match: {
"created": {
$lt: lastWeekDate
},
"someSubObject.anotherDate": {
$lt: lastWeekDate
},
"lastLogin": {
$lt ...
is there a possibility to group the fields and do something like
$match: {
[field1, field2, field3]: {
$lt: lastWeekDate
}
}
You need $expr to use $map to generate an array of boolean values and then $allElementsTrue to apply AND condition
db.collection.find({
$expr: {
$allElementsTrue: {
$map: {
input: [ "$field1", "$field2", "$field3" ],
in: { $lt: [ "$$this", lastWeekDate ] }
}
}
}
})
EDIT: if you need that logic as a part of aggregation you can use $match which is an equivalent of find
db.collection.aggregate([
{
$match: {
$expr: {
$allElementsTrue: {
$map: {
input: [ "$field1", "$field2", "$field3" ],
in: { $lt: [ "$$this", lastWeekDate ] }
}
}
}
}
}
])

How to select Specific attributes in an embedded mongo document

I have a mongo document similar to following structure:
{
id: '111eef8b94d3e91f4c7d22a37deb4aad',
description: 'Secret Project',
title: 'secret project',
students: [
{ _id: '123', name: 'Alex', primary_subject: 'Math', address: 'xxxxx', dob: '1989-10-10', gender: 'F', nationality: 'German' },
{ _id: '124', name: 'Emanuel', primary_subject: 'Physics', address: 'yyyyyy', dob: '1988-05-07', gender: 'M', nationality: 'French' },
{ _id: '242', name: 'Mike', primary_subject: 'Chemistry', address: 'zzzz', dob: '1990-02-02', gender: 'M', nationality: 'English' }
]
}
I need to fetch specific attributes. For example want to fetch only name, primary_subject, nationality attributes.
Using the below mongo query, I am able to fetch all attributes.
db.student_projects.aggregate({
$project: {
"students": {
$filter: {
input: "$students",
as: "st",
cond: {
$eq: [ "$$st._id", "242" ]
}
},
}
}
},
{ $unwind: { path: "$students", preserveNullAndEmptyArrays: false } }
).pretty();
Above query fetches all attributes of the matching student. But in my case I need just 3 attributes.
Use $map to reshape the output array:
db.student_projects.aggregate({
$project: {
"students": {
$map: {
input: {
$filter: {
input: "$students",
as: "st",
cond: {
$eq: [ "$$st._id", "242" ]
}
}
},
in: {
name: "$$this.name",
primary_subject: "$$this.primary_subject",
nationality: "$$this.nationality"
}
}
}
}
},
{ $unwind: { path: "$students", preserveNullAndEmptyArrays: false } }
).pretty();
Just like it's other language counterparts, "reshaphing" arrays is what $map does.
If you want to get "fancy" with a longer list of "included" fields than "excluded", then there are some modern operators from later releases of MongoDB 3.6 and above which can help here:
db.student_projects.aggregate({
$project: {
"students": {
$map: {
input: {
$filter: {
input: "$students",
as: "st",
cond: {
$eq: [ "$$st._id", "242" ]
}
}
},
in: {
$arrayToObject: {
$filter: {
input: { $objectToArray: "$$this" },
cond: {
"$not": {
"$in": [ "$$this.k", [ "_id", "address", "dob" ] ]
}
}
}
}
}
}
}
}
},
{ $unwind: "$students" }
).pretty();
The $objectToArray transforms to "key/value" pairs of k and v representing the object keys and values. From this "array" you can $filter the results on the k values for those you don't want. The $in allows comparison to a "list", and the $not negates the comparison value.
Finally you can transform the "array" back into the object form via $arrayToObject.
And of course you could always simply $project after the $unwind:
db.student_projects.aggregate({
$project: {
"students": {
$filter: {
input: "$students",
as: "st",
cond: {
$eq: [ "$$st._id", "242" ]
}
},
}
}
},
{ $unwind: "$students" },
{ $project: {
"students": {
"name": "$students.name",
"primary_subject": "$students.primary_subject",
"nationality": "$students.nationality"
}
}
).pretty();
And if you don't want the "students" key, then just remove it:
{ $project: {
"name": "$students.name",
"primary_subject": "$students.primary_subject",
"nationality": "$students.nationality"
}
Or use $replaceRoot from the original $map version:
db.student_projects.aggregate({
$project: {
"students": {
$map: {
input: {
$filter: {
input: "$students",
as: "st",
cond: {
$eq: [ "$$st._id", "242" ]
}
}
},
in: {
name: "$$this.name",
primary_subject: "$$this.primary_subject",
nationality: "$$this.nationality"
}
}
}
}
},
{ $unwind: "$students" },
{ $replaceRoot: { newRoot: "$students" } }
).pretty();
But for that matter you could have also done a $match after the $unwind instead of even using $filter. It's generally more efficient to work with arrays in place though, and a lot of the time you don't need the $unwind at all, so it's good practice to get used to the methods that manipulate arrays.
Of course if the result with $replaceRoot or similar was really what you were looking for, then it would be far more advisable to not use embedded documents in an array at all. If your intended access pattern uses those embedded documents "separately" to the main document most of the time, then you really should consider keeping them in their own collection instead. This avoids the "aggregation overhead" and is a simple query and projection to return the data.
N.B The $unwind operator "defaults" to preserveNullAndEmptyArrays: false. So the original form does not need that specified, nor the path key. It's shorter to write this way unless you specifically intend to preserve those null and empty results.