mongodb push values into array with $reduce - mongodb

I have an document structure like this:
_id: 123,
posts: [
{
_id: 234,
likesUser: [345, 456, 567, 678]
}
]
I want to use $reduce here so my expected output should look like this:
output: {
likes: [23, 31, 12, 32], //each element is the size of the "likesUser" array
ids: ["14312", "2342", "2312", "235234"] //each element is the "_id" in the posts array
}
I have try it like this:
let result = await Post.aggregate([
{
$match: {
_id: USER
}
},
{
$project: {
posts: {
$reduce: {
input: "$posts",
initialValue: { likes: [], ids: [] },
in: {
likes: {
$push: { $size: "$$this.likesUser" }
},
ids: {
$push: "$$this._id"
}
}
}
}
}
}
]);
In my frontend i use chart.js. The yAxsis are the likes and the xAxsis are the ID's. So for every element in my posts array i need the length of likesUser and _id
But it doenst work. Has anybody an idea how to achive this?

You are very close, you just have a minor syntax error in your $reduce, Try this:
let result = await Post.aggregate([
{
$match: {
_id: USER
}
},
{
$project: {
posts: {
$reduce: {
input: "$posts",
initialValue: {likes: [], ids: []},
in: {
likes: {
$concatArrays: ["$$value.likes", [{$size: "$$this.likesUser"}]]
},
ids: {
$concatArrays: ["$$value.ids", ["$$this._id"]]
}
}
}
}
}
}
]);

You are on the right way. You need to use $mergeObjects operator to achieve this. Following query will be helpful:
db.collection.aggregate([
{
$match: {
_id: USER
}
},
{
$project: {
post: {
$reduce: {
input: "$posts",
initialValue: {
likes: [],
ids: []
},
in: {
$mergeObjects: [
"$$value",
{
likes: {
$concatArrays: [
"$$value.likes",
[
{
$size: "$$this.likesUser"
}
]
]
},
ids: {
$concatArrays: [
"$$value.ids",
[
"$$this._id"
]
]
}
}
]
}
}
}
}
}
])
MongoPlayGroundLink
I hope the above is helpful.

Related

MongoDB Query: How can I aggregate an array of objects as a string

I have an array of objects where I want to make a string concatenating all of the same attributes from the array. Example:
{
_id: 123,
example_document: true,
people: [
{
name: "John",
age: 18
}, {
name: "Clint",
age: 20
}
]
}
And I wanna make a query where my result would be:
{
_id: 123,
example_document: true,
people: [
{
name: "John",
age: 18
}, {
name: "Clint",
age: 20
}
],
concat_names: "John, Clint"
}
I think aggregate is the path I should take, but I'm not being able to find a way of getting a string out of this, only concat array elements into another array. Anyone could help?
You can use $concat combined with $reduce to achieve this, like so:
db.collection.aggregate([
{
$addFields: {
concat_names: {
$reduce: {
input: "$people",
initialValue: "",
in: {
$concat: [
"$$value",
{
$cond: [
{
$eq: [
{
"$strLenCP": "$$value"
},
0
]
},
"",
", "
]
},
"$$this.name"
]
}
}
}
}
}
])
Mongo Playground
You can use aggregation operators $project, and $map:
db.collection.aggregate([
{
$project:
{
concat_names:
{
$map:
{
input: "$people",
as: "i",
in: "$$i.name"
}
}
}
}])

How to match string within embedded array or doc in MongoDB?

After searching for a whole day, I am doubting whether MongoDB can fulfill below requirement:
Q: How can I filter out documents that meet below conditions ?
In last array element of students_replies, there is a reply from a student whose name containing string 'ason'.
id_1: first_school, students_replies: [
{Date:20210101, replies: [
{name: jack, reply: 'I do not like this idea'},
{name: jason, reply: 'I would rather stay at home'},
{name: charles, reply: 'I have an plan to improve'},
]},
{Date:20210401, replies: [
...]},
{Date:20210801, replies: [
...]},
]
id_2: second_shool, students_replies: [..]
id_3: third_shool, students_replies: [...]
Mongoplayground
Use $slice and $regex
For your example this becomes:
db.collection.aggregate([
// project only the last reply
{
"$project": {
key: 1,
last_reply: {
"$slice": [
"$students_replies",
-1
]
}
}
},
// filter the documents
{
"$match": {
"last_reply.replies.name": {
"$regex": "ason"
}
}
}
])
https://mongoplayground.net/p/a9piw2WQ8n6
Since you need last array element of students_replies, use $arrayElemAt
db.collection.aggregate([
{
"$match": {
$expr: {
$regexMatch: {
input: {
$reduce: {
input: {
$arrayElemAt: [
"$students_replies.replies",
-1
]
},
initialValue: "",
in: {
$concat: [
"$$value",
"$$this.name",
","
]
}
}
},
regex: "ason"
}
}
}
},
{
"$project": {
"students_replies": 0
}
}
])
mongoplayground
another answer
db.collection.aggregate([
{
$match: {
$expr: {
$ne: [
{
$filter: {
input: {
$map: {
input: {
$arrayElemAt: [
"$students_replies.replies",
-1
]
},
as: "r",
in: "$$r.name"
}
},
as: "s",
cond: {
$regexMatch: {
input: "$$s",
regex: "ason"
}
}
}
},
[]
]
}
}
},
{
"$project": {
"students_replies": 0
}
}
])
mongoplayground

How to convert an array to object in mongodb query?

I am new to mongodb and wanted to convert my array to object using pipeline. For example,
{
field1: [1,2,3,4,5],
field2: [‘a’,’b’,’c’,’d’,’e’],
}
I want the above document to be converted to,
{
fields: [
{
field1: 1,
field2: ‘a’
},
......
{
field1: 5,
field2: ‘e’
}
]
}
Any idea how I can achieve this?
You can use $unwind to separate your arrays.
And then format your new list with $project without forgetting to remove the duplicates created by the $unwind.
db.collection.aggregate({
"$unwind": {
path: "$field1",
includeArrayIndex: "field1_index"
}
},
{
"$unwind": {
"path": "$field2",
"includeArrayIndex": "field2_index"
}
},
{
"$project": {
"fields": {
"field1": "$field1",
"field2": "$field2"
},
"diff": {
$cmp: [
"$field1_index",
"$field2_index"
]
}
}
},
{
"$match": {
"diff": 0
}
},
{
$group: {
_id: "$_id",
fields: {
$push: "$fields"
}
}
})
Try it here
You can use $zip and $map and $reduce to achieve this:
db.collection.aggregate([
{
"$addFields": {
fields: {
$reduce: {
input: {
$zip: {
inputs: [
{
$map: {
input: "$field1",
as: "f1",
in: {
field1: "$$f1"
}
}
},
{
$map: {
input: "$field2",
as: "f2",
in: {
field2: "$$f2"
}
}
}
]
}
},
initialValue: [],
in: {
"$concatArrays": [
[
{
"$mergeObjects": "$$this"
}
],
"$$value"
]
}
}
}
}
}
])
MongoPlayground
Make sure both field1 and field2 are of equal length or you will lose some data.

Mongoose: Get multiple distinct lists of values for multiple array fields

I need to get three distinct lists of values for three different fields, but I would like to avoid querying the database more than once. I am sure the answer it to use the mongodb aggregation framework, however, I am not sure how to use it to select a distinct list of strings?
tags: [{ type: String }],
categories: [{ type: String }],
hashtags: [{ type: String }],
...
let tags = await Model.distinct('tags');
let categories = await Model.distinct('categories');
let hashtags = await Model.distinct('hashtags');
You can $group by constant value to get an array of arrays from all the documents and then run $reduce along with $setUnion to get unique values:
await Model.aggregate([
{
$group: {
_id: null,
tags: { $push: "$tags" },
categories: { $push: "$categories" },
hashtags: { $push: "$hashtags" }
}
},
{
$project: {
tags: {
$reduce: {
input: "$tags",
initialValue: [],
in: { $setUnion: [ "$$value", "$$this" ] }
}
},
categories: {
$reduce: {
input: "$categories",
initialValue: [],
in: { $setUnion: [ "$$value", "$$this" ] }
}
},
hashtags: {
$reduce: {
input: "$hashtags",
initialValue: [],
in: { $setUnion: [ "$$value", "$$this" ] }
}
}
}
}
])
You can simplify using just $addToSet
await Model.aggregate([
{
$group: {
_id: null,
tags: { $addToSet: "$tags" },
categories: { $addToSet: "$categories" },
hashtags: { $addToSet: "$hashtags" }
}
}])

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
}