Remove 3 similar items from an array using mongodb - arrays

I have this document and i need remove only 3 items with name "Test" using only 1 request.
{ "_id" : 1, "items" : ["Test, "Test", "Test", "Test", "Test", "Sword", "Sword]}
It must become { "_id" : 1, "items" : ["Test", "Test", "Sword", "Sword]} after request.

Maybe something like this:
db.collection.aggregate([
{
"$unwind": "$items"
},
{
$group: {
_id: "$items",
cnt: {
$sum: 1
},
it: {
$push: "$items"
},
origid: {
"$first": "$_id"
}
}
},
{
$project: {
_id: "$origid",
items: {
$slice: [
"$it",
0,
2
]
}
}
},
{
$group: {
_id: "$_id",
items: {
$push: "$items"
}
}
},
{
$addFields: {
items: {
"$reduce": {
"input": "$items",
"initialValue": [],
"in": {
"$concatArrays": [
"$$this",
"$$value"
]
}
}
}
}
}
])
Explained:
unwind the items array so you can group after
group by item so you get the repeating values
project with slice to remove more then 2x items in array
group to form back the document but without the >2 repeating values
project with concatArrays to concat arrays of items.
Playground

You can use this aggregation to get the desired result. The query uses the $reduce Aggregate Array Operator to iterate over the items array, and discards the first 3 matching "Test" items.
db.collection.aggregate([
{
$set: {
items: {
$reduce: {
input: "$items",
initialValue: { result: [], count: 0 },
in: {
$cond: [ { $and: [ { $eq: [ "$$this", "Test" ] }, { $lt: [ "$$value.count", 3 ] } ] },
{ result: "$$value.result", count: { $add: [ "$$value.count", 1 ] } },
{ result: { $concatArrays: [ "$$value.result", [ "$$this" ] ] }, count: "$$value.count" }
]
}
}
}
}
},
{
$set: {
items: "$items.result"
}
}
])

Related

How to remove fields with empty values in objects nested in an array - mongodb

Need help with removing fields with empty values in array. So far, code is removing both values if one of the field is having empty value.
Example docment:
{
"_id": ObjectId("62ed3cfbeadf50344d622dd0"),
"Status": 1,
"AnswerList": [
{
"Question1": "some question1",
"Question2": "some question2",
"Answer": "",
"Comment": "Some comment"
},
{
"Question1": "some question1",
"Question2": "some question2",
"Answer": "some answer",
"Comment": ""
}
]
}
My query so far:
db.collection.aggregate([
{
$project: {
_id: 0,
Survey: 1,
Status: 1,
AnswerList: {
$map: {
input: "$AnswerList",
in: {
$cond: {
if: {
$in: [
"",
[
"$this.Comment",
"$this.Answer"
]
]
},
then: {
$arrayToObject: {
$filter: {
input: {
$map: {
input: {
$objectToArray: "$$this"
},
as: "element",
in: {
$cond: [
{
$in: [
"$$element.k",
[
"Comment",
"Answer"
]
]
},
null,
"$$element"
]
}
}
},
as: "filter",
cond: "$$filter"
}
}
},
else: "$$this"
}
}
}
}
}
}
])
One option is using $map and $filter with $arrayToObject and $objectToArray:
db.collection.aggregate([
{
$project: {
_id: 0,
Survey: 1,
Status: 1,
AnswerList: {
$map: {
input: "$AnswerList",
as: "item",
in: {$arrayToObject: {
$filter: {
input: {$objectToArray: "$$item"},
as: "pair",
cond: {$ne: ["$$pair.v", ""]}
}
}
}
}
}
}
}
])
See how it works on the playground example

Mongodb transform array using aggregation

I have an array in Mongodb collection:
some_array:
0: title: "Community"
model: "A"
1: title: "News"
model: "B"
How can I get only titles of array and list them as bellow:
titles: "Community, News"
You can group by null means group all titles and then project titles array only
Working Playground
db.collection.aggregate([
{
"$group": {
"_id": null,
"titles": {
"$push": "$title"
}
}
},
{
"$project": {
_id: 0,
titles: {
"$reduce": {
"input": "$titles",
"initialValue": "",
"in": {
"$cond": {
"if": { "$eq": [ { "$indexOfArray": [ "$titles", "$$this" ] }, 0 ] },
"then": { "$concat": [ "$$value", "$$this" ] },
"else": { "$concat": [ "$$value", ", ", "$$this" ] }
}
}
}
}
}
}
])
db.collection.aggregate([
{
$project: {
titles: "$somearray.title"
}
}
])
Working Mongo playground

How to divide documents values into same document, however key name of the divisor is same in MongoDB

Explanation: want to divide the name: Alex , name: petr value by name: hr value.
name: Alex , name: petr and name: hr are my parameters names.
also want to see the value of name: hr in the output document.
[
{
"name": "Alex",
"value": 65
},
{
"name": "petr",
"value": 8
},
{
"name": "hr",
"value": 20
}
]
Expected Output :
[
{
"name": "Alex/hr",
"value": 3.25
},
{
"name": "petr/hr",
"value": 0.4
},
{
"name": "hr",
"value": 20
}
]
Demo - https://mongoplayground.net/p/38G_Loo8V86
Use $facet
db.collection.aggregate([
{
$facet: {
hrVal: [
{ $match: { name: "hr" } }, // filter
{ $project: { _id: 0, value: 1 } } // take only value
],
allValues: [] // all documents or you can add match pipeline to filter here
}
},
{ $unwind: "$hrVal" }, // break into individual documents - now every document will have hrVal.value
{ $unwind: "$allValues" }, // break into individual documents
{
$set: { //
"allValues": {
"$cond": [
{ $eq: [ "$allValues.name", "hr" ] }, // condition
"$allValues", // true
{ // false
_id: "$allValues._id",
name: { "$concat": [ "$allValues.name", "/hr" ] }, // set name
value: { "$divide": [ "$allValues.value", "$hrVal.value" ] } // divide by hr value
}
]
}
}
},
{ $replaceRoot: { "newRoot": "$allValues" } } // reset to orignal document shape
])
$lookup with same collection and match for name: hr and return single result
$unwind deconstruct hr array
$project to check condition if name is hr then return current name and value of not then concat name using $concat and divide value by $divide
$project to move doc object to root
db.collection.aggregate([
{
$lookup: {
from: "collection",
pipeline: [
{ $match: { name: "hr" } },
{ $limit: 1 }
],
as: "hr"
}
},
{ $unwind: "$hr" },
{
$project: {
doc: {
$cond: [
{ $eq: ["$name", "hr"] },
{
name: "$name",
value: "$value"
},
{
name: { $concat: ["$name","/","$hr.name"] },
value: { $divide: ["$value", "$hr.value"] }
}
]
}
}
},
{
$project: {
name: "$doc.name",
value: "$doc.value"
}
}
])
Playground
Second option without lookup,
$facet to separate both result
$map to iterate loop of result array and check name is hr then concat name and divide value otherwise return same
$unwind deconstruct result array
$project to show fields
db.collection.aggregate([
{
$facet: {
result: [{ $match: {} }],
hr: [{ $match: { name: "hr" } }]
}
},
{
$project: {
result: {
$map: {
input: "$result",
in: {
$cond: [
{ $eq: ["$$this.name", "hr"] },
"$$this",
{
name: { $concat: ["$$this.name", "/", { $first: "$hr.name" }] },
value: { $divide: ["$$this.value", { $first: "$hr.value" }] }
}
]
}
}
}
}
},
{ $unwind: "$result" },
{
$project: {
name: "$result.name",
value: "$result.value"
}
}
])
Playground

How to group an array of subdocuments by multiple fields?

I have spent the better part of the day on this and am now out of ideas. Here is my collection:
[
{
"_id": "ID_XXX",
"logs": [
{
"lead_id": 123,
"list_id": "list_44",
"order_id": "order_1"
},
{
"lead_id": 124,
"list_id": "list_44",
"order_id": "order_2"
}
]
},
{
"_id": "ID_YYY",
"logs": [
{
"lead_id": 125,
"list_id": "list_44",
"order_id": "order_2"
},
{
"lead_id": 126,
"list_id": "list_44",
"order_id": "order_2"
},
{
"lead_id": 127,
"list_id": "list_44",
"order_id": "order_3"
},
{
"lead_id": 128,
"list_id": "list_66",
"order_id": "order_3"
}
]
}
]
I'm just trying to get the counts for list_id and order_id while preserving the _id of the document they are in. Here is my desired output:
[
{
"_id": "ID_XXX",
"counts": [
{
"lists": {"list_44": 2},
},
{
"orders": {"order_1": 1, "order_2": 1}
}
]
},
{
"_id": "ID_YYY",
"counts": [
{
"lists": {"list_44": 3, "list_66": 1},
},
{
"orders": {"order_2": 2, "order_3": 2}
}
]
}
]
I have tried way too many aggregate variations to list here, but the latest is this:
db.collection.aggregate([
{
$unwind: "$logs"
},
{
$group: {
_id: "$_id",
lists: {
$push: "$logs.list_id"
},
orders: {
$push: "$logs.order_id"
}
}
}
])
Which does not give me what I want. Can anyone point me in the right direction? Here is the playground link: https://mongoplayground.net/p/f-jk7lbSrJ0
$reduce to iterate loop of logs, convert logs object to array in k(key) v(value) format using $objectToArray, $concatArrays with initialValue in $reduce,
$filter above reduce result as input and filter required fields from logs
$unwind deconstruct logs array
db.collection.aggregate([
{
$addFields: {
logs: {
$filter: {
input: {
$reduce: {
input: "$logs",
initialValue: [],
in: { $concatArrays: ["$$value", { $objectToArray: "$$this" }] }
}
},
cond: { $in: ["$$this.k", ["list_id", "order_id"]] }
}
}
}
},
{ $unwind: "$logs" },
$group by _id and logs object and get the total count using $sum
{
$group: {
_id: {
_id: "$_id",
logs: "$logs"
},
counts: { $sum: 1 }
}
},
$group by _id and make lists array if logs.k is list_id and return in k and v format otherwise $$REMOVE, same as for orders make an array of order on the base of order_id
$addFields to convert lists array from k and v format to object format using $arrayToObjectand same as fororders` array
{
$group: {
_id: "$_id._id",
lists: {
$push: {
$cond: [
{ $eq: ["$_id.logs.k", "list_id"] },
{
k: "$_id.logs.v",
v: "$counts"
},
"$$REMOVE"
]
}
},
orders: {
$push: {
$cond: [
{ $eq: ["$_id.logs.k", "order_id"] },
{
k: "$_id.logs.v",
v: "$counts"
},
"$$REMOVE"
]
}
}
}
},
{
$addFields: {
lists: { $arrayToObject: "$lists" },
orders: { $arrayToObject: "$orders" }
}
}
])
Playground

MongoDB get results where date is equal to max date

Assume I have the following document:
[
{
"callId": "17dac51e-125e-499e-9064-f20bd3b1a9d8",
"caller": {
"firstName": "Test",
"lastName": "Testing",
"phoneNumber": "1231231234"
},
"routeHistory": [
{
"assignedUserId": "cfa0ffe9-c77d-4eec-87d7-4430f7772e81",
"routeDate": "2020-01-01T06:00:00.000Z",
"status": "routed"
},
{
"assignedUserId": "cfa0ffe9-c77d-4eec-87d7-4430f7772e81",
"routeDate": "2020-01-03T06:00:00.000Z",
"status": "ended"
}
]
}
]
I want to get results where routeHistory.routeDate is equal to the $max routeDate value in routeHistory. I would expect my results to look like the following:
[
{
"callId": "17dac51e-125e-499e-9064-f20bd3b1a9d8",
"caller": {
"firstName": "Test",
"lastName": "Testing",
"phoneNumber": "1231231234"
},
"routeHistory": [
{
"assignedUserId": "cfa0ffe9-c77d-4eec-87d7-4430f7772e81",
"routeDate": "2020-01-03T06:00:00.000Z",
"status": "ended"
}
]
}
]
Is there a clean way to do this in a single aggregate, so that additional $match criteria can be applied?
You can use $let to define temporary variable being $max date and the use $filter along with $arrayElemAt to get first matching element:
db.collection.aggregate([
{
$addFields: {
routeHistory: {
$let: {
vars: {
maxDate: { $max: "$routeHistory.routeDate" }
},
in: {
$arrayElemAt: [
{ $filter: { input: "$routeHistory", cond: { $eq: [ "$$maxDate", "$$this.routeDate" ] } } },
0
]
}
}
}
}
}
])
Mongo Playground
EDIT:
version without $let:
db.collection.aggregate([
{
$addFields: {
maxDate: {
$max: "$routeHistory.routeDate"
}
}
},
{
$addFields: {
routeHistory: {
$arrayElemAt: [
{
$filter: { input: "$routeHistory", cond: { $eq: [ "$$maxDate", "$$this.routeDate" ] } }
},
0
]
}
}
}
}
])

Resources