Create mongodb view for sub-collections - mongodb

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 }

Related

Size of nested array in aggregation projection

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

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.

Find percent in mongo

I have a collection with 2 docs like below.
{
_id:1,
Score: 30,
Class:A,
School:X
}
{
Score:40,
Class:A,
School:Y
}
I need help in writing query to find out percentage of score like below
{
School:X,
Percent:30/70
}
{
School:Y
Percent:40/70
}
This input:
var r =
[
{"school":"X", "class":"A", "score": 30}
,{"school":"Y", "class":"A", "score": 40}
,{"school":"Z", "class":"A", "score": 20}
,{"school":"Y", "class":"B", "score": 50}
,{"school":"Z", "class":"B", "score": 17}
];
run through this pipeline:
db.foo.aggregate([
// Use $group to gather up the class and save the inputs via $push
{$group: {_id: "$class", tot: {$sum: "$score"}, items: {$push: {score:"$score",school:"$school"}}} }
// Now we have total by class, so just "reproject" that array and do some nice
// formatting as requested:
,{$project: {
items: {$map: { // overwrite input array $items; this is OK
input: "$items",
as: "z",
in: {
school: "$$z.school",
pct: {$concat: [ {$toString: "$$z.score"}, "/", {$toString:"$tot"} ]}
}
}}
}}
]);
produces this output, where _id is the Class:
{
"_id" : "A",
"items" : [
{"school" : "X", "pct" : "30/90"},
{"school" : "Y", "pct" : "40/90"},
{"school" : "Z", "pct" : "20/90"}
]
}
{
"_id" : "B",
{"school" : "Y", "pct" : "50/67"},
{"school" : "Z", "pct" : "17/67"}
]
}
From here you can $unwind if you wish.

find documents having a specific count of matches array

I've searched high and low but not been able to find what i'm looking for so apologies if this has already been asked.
Consider the following documents
{
_id: 1,
items: [
{
category: "A"
},
{
category: "A"
},
{
category: "B"
},
{
category: "C"
}]
},
{
_id: 2,
items: [
{
category: "A"
},
{
category: "B"
}]
},
{
_id: 3,
items: [
{
category: "A"
},
{
category: "A"
},
{
category: "A"
}]
}
I'd like to be able to find those documents which have more than 1 category "A" item in the items array. So this should find documents 1 and 3.
Is this possible?
Using aggregation
> db.spam.aggregate([
{$unwind: "$items"},
{$match: {"items.category" :"A"}},
{$group: {
_id: "$_id",
item: {$push: "$items.category"}, count: {$sum: 1}}
},
{$match: {count: {$gt: 1}}}
])
Output
{ "_id" : 3, "item" : [ "A", "A", "A" ], "count" : 3 }
{ "_id" : 1, "item" : [ "A", "A" ], "count" : 2 }