MongoDB aggregation, combining 2 arrays in round-robin fashion - mongodb

I have data in a MongoDB collection that looks something like this:
[
{
"_id": 1,
"type": "big",
"fields": [11, 12, 13],
"items": [21, 22, 23]
},
{
"_id": 2,
"type": "small",
"fields": [14, 15],
"items": [24, 25]
},
{
"_id": 3,
"type": "small",
"fields": [],
"items": [41, 42]
},
{
"_id": 4,
"type": "small",
"fields": [31, 32, 33],
"items": []
}
]
I have been tasked with returning data according to a procedure like this:
For each document in the collection, obtain 1 value from its fields (if there are any), and 1 value its items (if there are any). Concatenate all of the results in a single array.
One might summarize this as selecting data in "round robin" fashion from two arrays held in each document.
How would I achieve this in a MongoDB aggregation query? This logic is not hard to implement in the client that connects to the Mongo server, but I would like to let Mongo take care of pagination (with $skip and $limit). I am using MongoDB 4.4.
The resulting data would look something like this:
[
{
"value": 11,
"type": "field",
"fromId": 1
},
{
"value": 21,
"type": "item",
"fromId": 1
},
{
"value": 14,
"type": "field",
"fromId": 2
},
{
"value": 24,
"type": "item",
"fromId": 2
},
{
"value": 41,
"type": "item",
"fromId": 3
},
{
"value": 31,
"type": "field",
"fromId": 4
},
]

If I understand your question right; this should be a workable pipe. To implement a random functionality, you would simply adjust the index passed to $arrayElemAt
https://mongoplayground.net/p/PPXV6fTSwHP
db.collection.aggregate([
{$project: {
types: [
{type: "field", values: "$fields"},
{type: "item", values: "$items"}
]
}},
{$unwind: '$types'},
{$project: {
_id: 0,
value: {$arrayElemAt: ['$types.values', 0]},
type: '$types.type',
fromId: '$_id'
}},
{$match: {
value: {$exists: true}
}}
])
Randomizing would look something like this:
https://mongoplayground.net/p/qi1Ud53J6yv
db.collection.aggregate([
{$project: {
types: [
{type: "field", values: "$fields"},
{type: "item", values: "$items"}
]
}},
{$unwind: '$types'},
{$project: {
_id: 0,
value: {$arrayElemAt: [
'$types.values',
{$floor: {$multiply: [{$rand: {}}, {$size: '$types.values'}]}}
]},
type: '$types.type',
fromId: '$_id'
}},
{$match: {
value: {$exists: true}
}}
])

Related

MongoDB Aggregate - Match documents that exist on collection A but not on collection B

How do I match documents that exist on collection A but not on collection B, using mongoDB aggregation?
Collection A:
[{
"_id": 1,
"operation":"SEC",
"name":"x"
},{
"_id": 2,
"operation": "SEC",
"name": "y"
},
{
"_id": 3,
"operation": "SEC",
"name": "z"
}]
Collection B:
[
{
"_id": 1,
"operation": "SEC",
"name": "x"
},
{
"_id": 2,
"operation": "SEC",
"name": "y"
}
]
expected output:
[
{
"_id": 3,
"operation": "SEC",
"name": "z"
}
]
One option is using a $lookup pipeline with the $$ROOT and $match unmatched documents:
db.CollectionA.aggregate([
{$lookup: {
from: "CollectionB",
let: {root: "$$ROOT"},
pipeline: [{$match: {$expr: {$eq: ["$$ROOT", "$$root"]}}}],
as: "collectionB"
}},
{$match: {"collectionB.0": {$exists: false}}},
{$unset: "collectionB"}
])
See how it works on the playground example

Finding ranges of continuous values

I have the following Mongo collection:
[
{
"key": 1,
"user": "A",
"comment": "commentA1"
},
{
"key": 2,
"user": "A",
"comment": "commentA2"
},
{
"key": 5,
"user": "A",
"comment": "commentA5"
},
{
"key": 2,
"user": "B",
"comment": "commentB2"
},
{
"key": 3,
"user": "B",
"comment": "commentB3"
},
{
"key": 6,
"user": "B",
"comment": "commentB6"
}
]
and I need to find the first continuous keys, with no gaps, per user.
So, for user A I should get the first 2 documents, and for user B the first two also.
The collection might contain more than 2M documents, so the query should work fast.
I have found SQL solutions for this problem (http://www.silota.com/docs/recipes/sql-gap-analysis-missing-values-sequence.html in section number 3), but I am looking for a Mongo solution.
How can I do it in Mongo 4.0 (DocumentDB) ?
EDIT: according to further elaboration on the comments,
One option is:
db.collection.aggregate([
{$sort: {key: 1}},
{$group: {
_id: "$user",
data: {$push: {key: "$key", comment: "$comment"}},
shadow: {$push: {$add: ["$key", 1]}}
}},
{$project: {
data: 1,
shadow: {$filter: {input: "$shadow", cond: {$in: ["$$this", "$data.key"]}}}
}},
{$project: {data: 1, shadow: 1, firstItem: {$subtract: [{$first: "$shadow"}, 1]}}},
{$project: {data: 1, firstItem: 1, shadow: {$concatArrays: [["$firstItem"], "$shadow"]}}},
{$project: {
data: 1,
shadow: {$reduce: {
input: {$range: [0, {$size: "$shadow"}]},
initialValue: [],
in: {
$concatArrays: [
"$$value",
{$cond: [
{$eq: [
{$arrayElemAt: ["$shadow", "$$this"]},
{$add: ["$$this", "$firstItem"]}
]},
[{$arrayElemAt: ["$shadow", "$$this"]}],
[]
]},
]
}
}
}
}
},
{$project: {data: {$filter: {input: "$data", cond: {$in: ["$$this.key", "$shadow"]}}}}},
{$unwind: "$data"},
{$project: {comment: "$data.comment", key: "$data.key"}}
])
See how it works on the playground example

How to merge an array object's fields with aggregate in mongodb

I am trying to aggregate an object with arrays. Would appreciate help.
here is my sample object.
[
{
"tipo": "A",
"prices": [
{
"min_pax": 1,
"max_pax": 3,
"type": "One Way",
"price": 35
},
{
"min_pax": 1,
"max_pax": 3,
"type": "Round Trip",
"price": 63
},
{
"min_pax": 4,
"max_pax": 6,
"type": "One Way",
"price": 40
},
{
"min_pax": 4,
"max_pax": 6,
"type": "Round Trip",
"price": 65
},
{
"min_pax": 7,
"max_pax": 10,
"type": "One Way",
"price": 50
},
{
"min_pax": 7,
"max_pax": 10,
"type": "Round Trip",
"price": 80
}
],
}
]
I want to merge objects with same number of min & max pax, for example:
Between 1 and 3 passengers for one way trip the cost is 35, for round trip the cost is 63.
I think looks better is I can merge both objects.
I would like something like this:
[
{
"tipo": "A",
"prices": [
{
"min_pax": 1,
"max_pax": 3,
"one_way": 35,
"round_trip": 63
},
{
"min_pax": 4,
"max_pax": 6,
"one_way": 40,
"round_trip":65
},
{
"min_pax": 7,
"max_pax": 10,
"one_way": 50,
"round_trip": 80
},
],
}
]
I would really appreciate your help
One option is to $unwind and $group again according to the pax:
db.collection.aggregate([
{$unwind: "$prices"},
{$group: {
_id: {max_pax: "$prices.max_pax", min_pax: "$prices.min_pax"},
tipo: {$first: "$tipo"},
data: {$push: {k: "$prices.type", v: "$prices.price"}}
}
},
{$project: {
_id: 0,
tipo: 1,
max_pax: "$_id.max_pax",
min_pax: "$_id.min_pax",
data: {$arrayToObject: "$data"}
}
},
{$set: {"data.max_pax": "$max_pax", "data.min_pax": "$min_pax"}},
{$group: {_id: "$tipo", prices: {$push: "$data"}}},
{$project: {_id: 0, tipo: "$_id", prices: 1}}
])
See how it works on the playground example

$addFields is not adding value in document

Query is as follows and result is given below:
What I want is I am adding field called name, in which I want categoryObj[0].categoryName but it is empty.
Tried categoryObj.$.categoryName but giving error.
Once name is obtained as I want i will exclude categoryObj with project opertator.
Thanks for help in advance
let itemsByCategory = await VendorItem.aggregate([
{$match: {vendor: vendorId}},
{$lookup: {
from: "vendorcategories",
localField: "category",
foreignField: "_id",
as: 'categoryDetails'
}},
{$group:{
"_id":"$category",
"count":{"$sum":1},
"items":{"$push":"$$ROOT"},
"categoryObj":{"$addToSet":"$categoryDetails"}
}},
{$project: {"items.categoryDetails":0}},
{$addFields: {"categoryName" : "$categoryObj.categoryName"}},
//{$project: {"categoryObj":0}},
]);
and the result is as follows
{
"itemsByCategory": [
{
"_id": "62296d612a1462a7d5e4b86b",
"count": 1,
"menuItems": [
{
"_id": "622971fa4fda7b4c792a7812",
"category": "62296d612a1462a7d5e4b86b",
"vendor": "62296c6f2a1462a7d5e4b863",
"item": "Dahi Chaat",
"price": 30,
"inStock": true,
"variants": [
{
"variantName": "With Sev",
"variantPrice": 40,
"_id": "622975b9f7bdf6c2a3b7703c"
}
],
"toppings": [
{
"name": "cheese",
"price": 10,
"inStock": true,
"_id": "62297766ff9f01d236c60736"
}
],
"categoryDetails": [
{
"_id": "62296d612a1462a7d5e4b86b",
"categoryName": "Snacks",
"categoryDescription": "Desciption changed!",
"vendor": "621c6c944d6d79e83219e59a",
"__v": 0
}
]
}
],
"categoryObj": [
[
{
"_id": "62296d612a1462a7d5e4b86b",
"categoryName": "Snacks",
"categoryDescription": "Desciption changed!",
"vendor": "621c6c944d6d79e83219e59a",
"__v": 0
}
]
],
"name": []
}
]
}
You can add an $unwind phase in order to "loop" all objects inside "categoryObj", but you will need to group it back afterwards:
{"$addFields": {orig_id: "$_id"}},
{"$unwind": "$categoryObj"},
{"$addFields": {"name": {"$arrayElemAt": ["$categoryObj", 0]}}},
{"$group": {_id: "$orig_id", name: {$push: "$name.categoryName"},
menuItems: {$first: "$menuItems"}, count: {$first: "count"},
}
}
See playground here:
https://mongoplayground.net/p/wsH2Y0UZ_FH

Mongo Filter Similar Documents

Let's say I have this collection:
Cards:
[
{orientation: "portrait", size: "45", columns: 2, type: 1, _id: "c5ea1968-6ab5-4b05-80a9-db9dabe29dde"},
{orientation: "landscape", size: "45", columns: 3, type: 1, _id: "37186b8e-4033-46c6-8b2e-82ee45f96904"},
{orientation: "portrait", size: "45", columns: 2, type: 1, _id: "8f49a3ff-2859-4027-b644-88ac0949808d"},
{orientation: "portrait", size: "45", columns: 2, type: 2, _id: "862717f4-2ef7-4839-947a-02771338c38c"},
{orientation: "portrait", size: "45", columns: 2, type: 3, _id: "e1a93b10-dfcb-42a2-955a-ee14d539624f"}
]
And I need a result like this:
CardList:
[
{ type: 1,
list: [
{orientation: "landscape", size: "45", columns: 3, type: 1, _id: "1_landscape_2"},
{orientation: "portrait", size: "45", columns: 2, type: 1, _id: "1_portrait_2"}
]
},
{ type: 2,
list: [
{orientation: "portrait", size: "45", columns: 2, type: 2, _id: "2_portrait_2"}
],
{ type: 3,
list: [
{orientation: "portrait", size: "45", columns: 2, type: 3, _id: "3_portrait_2"}
]
}
];
So, group by common attribute type, and not repeating documents with same attributes orientation and columns, for example.
I was able to group by type:
{ _id : "$type", list: { $push: "$$ROOT" } }
But, how would be the next stages?
You can use two $group stages like this:
First group to get the elements where orientation, columns and type are the same. So, that is, create the "list" objects.
With that objects, another $group by the type and add result to an array
db.collection.aggregate([
{
"$group": {
"_id": {
"orientation": "$orientation",
"columns": "$columns",
"type": "$type"
},
"list": {
"$first": "$$ROOT"
}
}
},
{
"$group": {
"_id": "$_id.type",
"list": {
"$push": "$list"
}
}
}
])
Example here
Note how the first $group uses $first to avoid repeated values (each type has one single elements with the same orientation and columns. And the second $group uses $push to generate the list.
Also, to get your desired _id output (and assuming is compound by type_orientation_columns) in the list field you can do this:
Add orientation and columns in the second $group to keep as auxiliar values.
Use $project to get desired values
db.collection.aggregate([
{
"$group": {
"_id": {
"orientation": "$orientation",
"columns": "$columns",
"type": "$type"
},
"list": {
"$first": "$$ROOT"
}
}
},
{
"$group": {
"_id": "$_id.type",
"list": {
"$push": "$list"
},
"orientation": {
"$first": "$_id.orientation"
},
"columns": {
"$first": "$_id.columns"
}
}
},
{
"$project": {
"_id": 0,
"type": "$_id",
"list": {
"orientation": 1,
"size": 1,
"columns": 1,
"type": 1,
"_id": {
"$concat": [
{"$toString": "$_id"},
"_",
{"$toString": "$orientation"},
"_",
{"$toString": "$columns"}
]
}
}
}
}
])
Example here