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
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
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
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.
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.
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 }