Size of nested array in aggregation projection - mongodb

I have a "Quiz" model, where each quiz document has some questions and an array of answers in them.
Example document structure (Quiz.findOne()):
_id: ObjectId("611478ac34dde61f28dbe4db"),
name: "Quiz 1",
questions: [
{
text: "Question 1",
answers: ["a", "b", "c"],
},
{
text: "Question 2",
answers: ["m", "n", "o", "p"],
},
...
...
{
text: "Question 1000",
answer: ["a", "c", "e", "f"],
},
]
I am selecting some particular questions by index, using MongoDB aggregation.
Aggregation code:
Quiz.aggregate([
{
$match: { _id: "611478ac34dde61f28dbe4db" },
},
{
$addFields: {
questions: {
$map: {
input: [0, 1], //Choosing the questions at index 0 and 1 (can be any other index)
as: "i",
in: {
$arrayElemAt: ["$questions", "$$i"],
},
},
},
},
},
{
$project: {
"questions.answers": 1,
},
},
])
Output comes as:
[{
_id: "611478ac34dde61f28dbe4db",
questions: [
{
answers: ["a", "b", "c"],
},
{
answers: ["m", "n", "o", "p"],
},
]
}]
How can I convert this output to show only the "$size" of the array?
Expected output:
[{
_id: "611478ac34dde61f28dbe4db",
questions: [
{
answers: 3, //Had 3 answers: a, b, c
},
{
answers: 4, //Had 4 answers: m, n, o, p
},
]
}]

You can add new aggregation stage where you can use $map and $size pipeline operators, like this:
db.collection.aggregate([
{
$project: {
"questions": {
$map: {
input: "$questions",
as: "question",
in: {
answers: {
$size: "$$question.answers"
}
}
}
}
}
}
])
Here is the working example: https://mongoplayground.net/p/0eyAlMWai0A

Related

Remove multiple objects from nested array 3

I try to clean my collection with single update query , need to remove some deeply nested objects , but without breaking other objects , here is a good solution provided by #rickhg12hs:
Remove multiple objects from deeply nested array 2
but it has small drawback , it is breaking the content of _a._p object when there is no _a._p.s object inside...
and original solution provided by #nimrod serok:
Remove multiple elements from deep nested array with single update query
but it has other issue , when there is missing "_a._p.s.c" , "_a._p.s.d" or "_a._p.s.a" object it add objects with null values instead which afcourse is not expected ...
Playground test
This are 2x example original documents:
[
{
"_id": ObjectId("5c05984246a0201286d4b57a"),
f: "x",
"_a": [
{
"_onlineStore": {}
},
{
"_p": {
"s": {
"a": {
"t": [
{
id: 1,
"dateP": "20200-09-20",
did: "x",
dst: "y",
den: "z"
},
{
id: 2,
"dateP": "20200-09-20"
}
]
},
"c": {
"t": [
{
id: 3,
"dateP": "20300-09-22"
},
{
id: 4,
"dateP": "20300-09-23",
did: "x",
dst: "y",
den: "z"
},
{
id: 5,
"dateP": "20300-09-23"
}
]
}
}
}
}
]
},
{
"_id": ObjectId("5c05984246a0201286d4b57b"),
f: "x",
"_a": [
{
"_onlineStore": {}
},
{
"_p": {
_t: "Some field",
_x: "Some other field"
}
}
]
}
]
Expected result after update:
[
{
"_a": [
{
"_onlineStore": {}
},
{
"_p": {
"s": {
"a": {
"t": [
{
"dateP": "20200-09-20",
"den": "z",
"did": "x",
"dst": "y",
"id": 1
}
]
},
"c": {
"t": [
{
"dateP": "20300-09-23",
"den": "z",
"did": "x",
"dst": "y",
"id": 4
}
]
}
}
}
}
],
"_id": ObjectId("5c05984246a0201286d4b57a"),
"f": "x"
},
{
"_a": [
{
"_onlineStore": {}
},
{
"_p": {
_t: "Some field",
_x: "Some other field"
}
}
],
"_id": ObjectId("5c05984246a0201286d4b57b"),
"f": "x"
}
]
The goal is with single update query to remove any objects under _a._p.s.[a|c|d].t where the fields did,dst and den are missing but without breaking other objects _a._p where _a._p.s do not exists ...
Looks like a small change to #rickhg12hs's answer can solve this:
db.collection.update({},
[
{$set: {
_a: {$map: {
input: "$_a",
as: "elem",
in: {$cond: [
{$or: [
{$eq: [{$type: "$$elem._p"}, "missing"]},
{$eq: [{$type: "$$elem._p.s"}, "missing"]}
]},
"$$elem",
{
_p: {s: {
$arrayToObject: {$map: {
input: {$objectToArray: "$$elem._p.s"},
as: "anyKey",
in: {
k: "$$anyKey.k",
v: {
t: {$filter: {
input: "$$anyKey.v.t",
as: "t",
cond: {$setIsSubset: [
["did", "dst", "den"],
{$map: {
input: {$objectToArray: "$$t"},
in: "$$this.k"
}}
]}
}}
}
}
}}
}
}}
]}
}}
}}
],
{
"multi": true
})
See how it works on the playground example

Display the conditionnal size of an array with the others fields of a mongodb document

I have a collection of fridges and I would like to have some fields from each fridge matching a condition plus the 'conditionnal size' of the items in this fridge.
This is an example of my DB :
db={
"fridges": [
{
_id: 1,
items: [
{
itemId: 1,
name:"beer"
},
{
itemId: 2,
name: "chicken"
}
],
brand:"Bosch",
size:195,
cooler:true,
color:"grey"
},
{
_id: 2,
items: [
{
itemId: 1,
name:"beer"
},
{
itemId: 2,
name: "chicken"
},
{
itemId: 3,
name: "lettuce"
}
],
brand:"Electrolux",
size:200,
cooler:true,
color:"white"
},
]
}
I want to get fridges with these mutuals conditions ('and' condition):
brand is $in ["Bosch","Samsung"]
color is $in ["grey","white"]
In addition :
The number of items with a name $in ["beer","lettuce"]
And finally :
Removing some fields like the size and items of the result.
In our example, the excepted output would be :
{
_id:1
itemsNumber:1,
brand:"Bosch",
cooler:true,
color:"grey"
}
Explanations :
We removed the field items and size, itemsNumber counts the number of beers and lettuce from items array. And we only keep the first fridge its brand is Bosch and it's grey.
This what I have so far :
db.fridges.aggregate([
{
"$match": {
$and: [
{
"brand": {
$in: [
"Bosch",
"Samsung"
]
}
},
{
"color": {
$in: [
"grey",
"white"
]
}
}
]
}
},
{
"$project": {
"itemsNumber": {
$size: "$items" // This is not good
},
brand: 1,
cooler: 1,
color: 1
}
}
])
Which returns me :
[
{
"_id": 1,
"brand": "Bosch",
"color": "grey",
"cooler": true,
"itemsNumber": 2
}
]
Counting the items matching with either beer or lettuce is my main problem.
This is an executable example.
Thanks in advance !
I found out how to make it work. Thank you #joe for suggesting to use filter this was indeed the solution.
Here is the complete query :
db.fridges.aggregate([
{
$match: {
$and: [
{
"brand": {
$in: [
"Bosch",
"Samsung"
]
}
},
{
"color": {
$in: [
"grey",
"white"
]
}
}
]
}
},
{
$project: {
"itemsNumber": {
"$filter": {
"input": "$items",
"as": "item",
"cond": {
$in: [
"$$item.name",
[
"beer",
"lettuce"
]
]
}
}
},
brand: 1,
cooler: 1,
color: 1
}
}
])
Runnable example.

Query for documents where match contiguous array elements

I have a MongoDB collection with documents in the following format:
{ "_id" : 1, "tokens": [ "I", "have", "a", "dream" ] },
{ "_id" : 2, "tokens": [ "dream", "a", "little", "dream" ] },
{ "_id" : 3, "tokens": [ "dream", "a", "dream" ] },
{ "_id" : 4, "tokens": [ "a" , "little", "dream" ] },
...
I need to get all doucuments which "tokens" include contiguous array elements: "a", "dream".
So, the following are matched doucuments:
{ "_id" : 1, "tokens": [ "I", "have", "a", "dream" ] },
{ "_id" : 3, "tokens": [ "dream", "a", "dream" ] },
Is there a way to get the right results?
A trick that is to have a regexp.
$match to get the all documents which has $all array input
$addFields to have a duplicate the tokens and input array
$reduce helps to concat all string joining -
$regexMatch to match both strings
$match to eliminate unwanted data
$project to get necessary fields only
The code is
[{
$match: {
tokens: { $all: ["a", "dream"] }
}
}, {
$addFields: {
duplicate: "$tokens",
inputData: ["a", "dream"]
}
}, {
$addFields: {
duplicate: {
$reduce: {
input: "$duplicate",
initialValue: "",
in: { $concat: ["$$value", "-", "$$this"] }
}
},
inputData: {
$reduce: {
input: "$inputData",
initialValue: "",
in: { $concat: ["$$value", "-", "$$this"] }
}
}
}
}, {
$addFields: {
match: {
$regexMatch: { input: "$duplicate", regex: '$inputData' }
}
}
}, {
$match: {
match: true
}
}, {
$project: { _id: 1, tokens: 1 }
}]
Working Mongo playground
Note: Do check multiple scenarios although its working for this scenario

MongoDB: Transform array of objects to array of arrays

I have a collection named "records" that contains documents in the following form:
{
"name": "a"
"items": [
{
"a": "5",
"b": "1",
"c": "2"
},
{
"a": "6",
"b": "3",
"c": "7"
}
]
}
I want to keep the data just as it is in the database (to make the data easy to read and interpret). But I'd like to run a query that returns the data in the following form:
{
"name": "a"
"items": [
["5", "1", "2"],
["6", "3", "7"],
]
}
Is this possible with pymongo? I know I can run a query and translate the documents using Python, but I'd like to avoid iterating over the query result if possible.
I have a table named "records"
Collection
Is this possible with pymongo?
Yes
Any pointers on how to approach this would be super helpful!
I'd suggest you to use a view to transform your data during a query in MongoDB.
In this way, you can get transformed data and apply find to already transformed data if you need.
db.createCollection(
"view_name",
{"viewOn": "original_collection_name",
"pipeline": [{$unwind: "$items"},
{$project: {name: 1, items: {$objectToArray: "$items"}}},
{$project: {name: 1, items: {$concatArrays: ["$items.v"]}}},
{$group: {_id: "$_id", name: {$first: "$name"},
items: {$push: "$items"}}}]
}
)
> db.view_name.find({name: "a"})
{ "_id" : ObjectId("5fc3dbb69cb76f866582620f"), "name" : "a", "items" : [ [ "5", "1", "2" ], [ "6", "3", "7" ] ] }
> db.view_name.find({"items": {$in: [["5", "1", "2"]]}})
{ "_id" : ObjectId("5fc3dbb69cb76f866582620f"), "name" : "a", "items" : [ [ "5", "1", "2" ], [ "6", "3", "7" ] ] }
> db.view_name.find()
{ "_id" : ObjectId("5fc3dbb69cb76f866582620f"), "name" : "a", "items" : [ [ "5", "1", "2" ], [ "6", "3", "7" ] ] }
Query:
db.original_collection_name.aggregate([
{$unwind: "$items"},
{$project: {name: 1, items: {$objectToArray: "$items"}}},
{$project: {name: 1, items: {$concatArrays: ["$items.v"]}}},
{$group: {_id: "$_id", name: {$first: "$name"}, items: {$push: "$items"}}}])
Using $objectToArray and $map transformations:
// { name: "a", items: [ { a: "5", b: "1", c: "2" }, { a: "6", b: "3", c: "7" } ] }
db.collection.aggregate([
{ $set: { items: { $map: { input: "$items", as: "x", in: { $objectToArray: "$$x" } } } } },
// {
// name: "a",
// items: [
// [ { k: "a", v: "5" }, { k: "b", v: "1" }, { k: "c", v: "2" } ],
// [ { k: "a", v: "6" }, { k: "b", v: "3" }, { k: "c", v: "7" } ]
// ]
// }
{ $set: { items: { $map: { input: "$items", as: "x", in: "$$x.v" } } } }
])
// { name: "a", items: [["5", "1", "2"], ["6", "3", "7"]] }
This maps items' elements as key/value arrays such that { field: "value" } becomes [ { k: "field", v: "value" } ]. This way whatever the field name, we can easily access the value using v, which is the role of the second $set stage: "$$x.v".
This has the benefit of avoiding heavy stages such as unwind/group.
Note that you can also imbricate the second $map within the first; but that's probably less readable.

Create mongodb view for sub-collections

I have some collections with sub-collections in them and I need to be able to get sub-collections as they're not sub-collections. Let's say I have collection like that:
[
{author: "aa", books: [{title:"a", pages: 100}, {title: "b", pages: 200}]},
{author: "ab", books: [{title:"c", pages: 80}, {title: "d", pages: 150}]}
]
I want to be able to view this collection like this:
[
{author: "aa", books.title: "a", books.pages: 100},
{author: "aa", books.title: "b", books.pages: 200},
{author: "ab", books.title: "c", books.pages: 80},
{author: "ab", books.title: "d", books.pages: 150}
]
Is it possible to create a view as what I need and filter it through web api?
Edit after #mickl 's question:
What I want is show every sub-collection in a new row. I have 2 records in the main collection and 2 sub-collections in every record. So I want to get 4 rows and want to be able to do it on the db side not on the api side.
So the key thing here is $unwind operator which transforms an array of n elements into n elements with single subdocument.
db.createView(
"yourview",
"yourcollection",
[ { $unwind: "$books" } ]
)
This will give you a documents in following format:
{ author: "aa", books: { title: "a", pages: 100 } },
{ author: "aa", books: { title: "b", pages: 200 } },
{ author: "ab", books: { title: "c", pages: 80 } },
{ author: "ab", books: { title: "d", pages: 150 } }
EDIT: to have keys with dots in their names you can run below command:
db.createView(
"yourview",
"yourcollection",
[
{ $unwind: "$books" },
{
$project: {
author: 1,
books2: {
$map: {
input: { $objectToArray: "$books" },
as: "book",
in: {
k: { $concat: [ "books.", "$$book.k" ] },
v: "$$book.v"
}
}
}
}
},
{
$replaceRoot: {
newRoot: { $mergeObjects: [ { author: "$author" }, { $arrayToObject: "$books2" } ] }
}
}
]
)
Basically it uses $objectToArray and $arrayToObject to "force" MongoDB to return fields with dots in their names. Outputs:
{ "author" : "aa", "books.title" : "a", "books.pages" : 100 }
{ "author" : "aa", "books.title" : "b", "books.pages" : 200 }
{ "author" : "ab", "books.title" : "c", "books.pages" : 80 }
{ "author" : "ab", "books.title" : "d", "books.pages" : 150 }