can't get data for unknown key in mongodb query - mongodb

I want to get a subarray from a document using mongodb queries but I don't know the key for it. My data looks something like this:
{ 'log file name I dont know': [Array] }
'log file name I dont know' is the unique key at that level. Is there something like "get value for the first key" in mongodb aggregation pipeline?

1 Maybe something like this:
db.collection.aggregate([
{
$group: {
_id: "$_id",
new: {
$first: "$$ROOT"
}
}
},
{
"$project": {
"map": {
"$objectToArray": "$new"
}
}
},
{
$unwind: "$map"
},
{
$match: {
"map.k": {
"$ne": "_id"
},
"map.v": 4
}
}
])
Explained:
Add the root document to new key named "new"
Convert the object named "new" to array named "map"
$unwind the array
Remove the array elements with k:"_id" , so in the final result to have only:
map.k -> the unknown keys
map.v -> the array values for the unknown keys
Finally you can add in the $match stage the searched element , in the example:"map.v":4
playground1
2 Here is the option when you want to preserve the strange structure with the unknown key after the match stage:
db.collection.aggregate([
{
$group: {
_id: "$_id",
new: {
$first: "$$ROOT"
}
}
},
{
"$project": {
"map": {
"$objectToArray": "$new"
}
}
},
{
$match: {
"map.v": 1
}
},
{
"$project": {
"map": {
"$arrayToObject": "$map"
}
}
},
{
$replaceRoot: {
newRoot: "$map"
}
}
])
Explained:
Add the root document to new key named "new"
Convert the object named "new" to array named "map"
Match the necessary documents(in the example this is array element 1 ( "map.v":1 )
Convert back the array to object.
Replace the root tocument with the "map docunent" so it looks as original collection
playground2
Few words: File name value is not recommended to be a key , best practice is the filename to be key/value so if you want to search on filename value to be possible to create index on that filename key field , so your json structure to looke something like:
{ "filename":"The unknown file name" , "theArray":[1,2,3] }

Related

Project array elements as individual fields

I have a collection with a field in each document that looks like:
{
person = [{"name":"Alex"},{"name":"Betsy"},{"name":"Chauncy"}]
}
Documents can have different length for this array
I'm trying to project the individual element of this array as fields within the same document with the key as Name {index} and the corresponding value for the name. I can't seem to generate the projected fields given an array.
Expected output:
[{
"name1": "Alex",
"name2": "Betsy",
"name3": "Chauncy"
}]
You can use aggregation
$unwind to deconstruct the array and it helps to get the index too
$group to reconstruct the array with key (k): value(v) pair which helps in next stage
$arrayToObject since group already made k:v pair, this helps to make array to opbect
$replaceRoot helps to make the object as root
Here is the code
db.collection.aggregate([
{
$unwind: { path: "$person", includeArrayIndex: "index" }
},
{
$group: {
_id: null,
person: {
$push: {
k: { "$concat": [ "name", { $toString: "$index" } ] },
v: "$person.name"
}
}
}
},
{
$project: {
person: { "$arrayToObject": "$person" }
}
},
{
"$replaceRoot": { "newRoot": "$person" }
}
])
Working Mongo playground

Is there a way to give order field to the result of MongoDB aggregation?

Is there any way to give order or rankings to MongoDB aggregation results?
My result is:
{
"score":100
"name": "John"
},
{
"score":80
"name": "Jane"
},
{
"score":60
"name": "Lee"
}
My wanted result is:
{
"score":100
"name": "John",
"rank": 1
},
{
"score":80
"name": "Jane"
"rank": 2
},
{
"score":60
"name": "Lee"
"rank": 3
}
I know there is a operator called $includeArrayIndex but this only works with $unwind operator.
Is there any way to give rank without using $unwind?
Using $unwind requires grouping on my collection, and I'm afraid grouping pipeline would be too huge to process.
The other way is to use $map and add rank in document using its index, and don't use $unwind stage because it would be single field array you can directly access using its key name as mention in last line of code,
$group by null and make array of documents in root array,
$map to iterate loop of root array, get the index of current object from root array using $indexOfArray and increment that returned index number using $add because index start from 0, and that is how we are creating rank field, merge object with current element object and rank field using $mergeObjects
let result = await db.collection.aggregate([
{
$group: {
_id: null,
root: {
$push: "$$ROOT"
}
}
},
{
$project: {
_id: 0,
root: {
$map: {
input: "$root",
in: {
$mergeObjects: [
"$$this",
{
rank: { $add: [{ $indexOfArray: ["$root", "$$this"] }, 1] }
}
]
}
}
}
}
}
]);
// you can access result using root key
let finalResult = result[0]['root'];
Playground

Renaming Field Within an Array in MongoDB

I need to update the name of field in a collection. The problem is that the field in question is within an array. So I'm trying to determine the correct way do this. I tried this to accomplish renaming the field that exists within the "plans" array: :
db.customers.updateMany( {}, { $rename: { "plans" : { "subscriptionType": "membershipType" } } } );
But this won't work. What's the correct way to handle this kind of transformation of a field within an array?
The data looks like this:
{
_id: 123,
prop1: value,
prop2: value,
prop3: value,
plans: [
subscriptionType: value,
otherProp: value,
otherProp: value
]
}
You can use Aggregation Framework's $addFields to override plans field and $map operator to rename field inside an array. Then you can use $out to override existing collection:
db.customers.aggregate([
{
$addFields: {
plans: {
$map:{
input: "$plans",
as: "plan",
in: {
membershipType: "$$plan.subscriptionType",
otherField: "$$plan.otherField",
otherField2: "$$plan.otherField2"
}
}
}
}
},
{
$out: "customers"
}
])
Alternatively you can do that dynamically. In this solution you don't have to explicitly specify other field names:
db.customers.aggregate([
{
$addFields: {
plans: {
$map:{
input: "$plans",
as: "plan",
in: {
$mergeObjects: [
{ membershipType: "$$plan.subscriptionType" },
{
$arrayToObject: {
$filter: {
input: { $objectToArray: "$$plan" },
as: "plan",
cond: { $ne: [ "$$plan.k", "subscriptionType" ] }
}
}
}
]
}
}
}
}
},
{
$out: "customers"
}
])
Using $objectToArray to $filter out old key-value pair and the using $mergeObjects to combine that filtered object with new renamed field.

MongoDB query or aggregation to skip sub-documents

I'd like to create a query or aggregation where the returned documents do not include sub-documents. I do not know that a given field will be a sub-document ahead of time (or I would just use the projection to skip them). So for example, if I have a document like this:
{
_id: 1,
field1: "a",
field2: "b",
field3: {
subfield1: "c",
subfield2: "d"
}
}
When my query returns this document, it either skips field3, or replaces field3's value with something else (e.g. a string = "field_is_an_object").
As I said, I don't know ahead of time which fields will be sub-documents (or "object" types). The $redact operator was the closest I could find, but I couldn't figure out a syntax to get it to work.
There are at least two ways you can achieve what you want:
The first one is pretty concise and requires just one aggregation stage which, however, is a little bit more complex and harder to understand:
db.collection.aggregate({
$replaceRoot: { // create a new top level document
"newRoot": { // ...which shall be
$arrayToObject: { // ...created from an array
$filter: { // ...that again should contain only those elements
input: { // ...from our input array
$objectToArray: "$$ROOT" // ...which is our respective top level document transformed into an array of key-value pairs
},
cond: { // ...where
$ne: [ { $type: "$$this.v" }, "object" ] // ...the "v" (as in "value" field is not an object)
}
}
}
}
}
})
The second one I can think of is way more verbose but pretty easy to understand by adding the stages step-by-step (as always with the aggregation framework).
db.collection.aggregate({
$project: {
"tmp": { // we create a temporary field
$objectToArray: "$$ROOT" // that contains our respective root document represented as an array of key-value pairs
}
}
}, {
$unwind: "$tmp" // flatten the temporary array into multiple documents
}, {
$match: {
"tmp.v": { $not: { $type: "object" } } // filter all documents out that we do not want in our result
}
}, {
$group: { // group all documents together again
"_id": "$_id", // into one bucket per original document ("_id")
"tmp": {
$push: "$tmp" // and create an array with all the key-value pairs that have survived our $match stage
}
}
}, {
$replaceRoot: { // create a new top level document...
"newRoot": {
$arrayToObject: "$tmp" // ...out of the data we have left in our array
}
}
})

How do I query a mongo document containing subset of nested array

Here is a doc I have:
var docIHave = {
_id: "someId",
things: [
{
name: "thing1",
stuff: [1,2,3,4,5,6,7,8,9]
},
{
name: "thing2",
stuff: [4,5,6,7,8,9,10,11,12,13,14]
},
{
name: "thing3",
stuff: [1,4,6,8,11,21,23,30]
}
]
}
This is the doc I want:
var docIWant = {
_id: "someId",
things: [
{
name: "thing1",
stuff: [5,6,7,8,9]
},
{
name: "thing2",
stuff: [5,6,7,8,9,10,11]
},
{
name: "thing3",
stuff: [6,8,11]
}
]
}
stuff´s of docIWant should only contain items greater than min=4
and smaller than max=12.
Background:
I have a meteor app and I subscribe to a collection giving me docIHave. Based on parameters min and max I need the docIWant "on the fly". The original document should not be modified. I need a query or procedure that returns me docIWant with the subset of stuff.
A practical code example would be greatly appreciated.
Use the aggregation framework for this. In the aggregation pipeline, consider the $match operator as your first pipeline stage. This is quite necessary to optimize your aggregation as you would need to filter documents that match the given criteria first before passing them on further down the pipeline.
Next use the $unwind operator. This deconstructs the things array field from the input documents to output a document for each element. Each output document is the input document with the value of the array field replaced by the element.
Another $unwind operation would be needed on the things.stuff array as well.
The next pipeline stage would then filter dopcuments where the deconstructed things.stuff match the given min and max criteria. Use a $match operator for this.
A $group operator is then required to group the input documents by a specified identifier expression and applies the accumulator expression $push to each group. This creates an array expression to each group.
Typically your aggregation should end up like this (although I haven't actually tested it but this should get you going in the right direction):
db.collection.aggregate([
{
"$match": {
"things.stuff": { "$gt": 4, "$lte": 11 }
}
},
{
"$unwind": "$things"
},
{
"$unwind": "$things.stuff"
},
{
"$match": {
"things.stuff": { "$gt": 4, "$lte": 11 }
}
},
{
"$group": {
"_id": {
"_id": "$_id",
"things": "$things"
},
"stuff": {
"$push": "$things.stuff"
}
}
},
{
"$group": {
"_id": "$_id._id",
"things": {
"$push": {
"name": "$_id.things.name",
"stuff": "$stuff"
}
}
}
}
])
If you need to transform the document on the client for display purposes, you could do something like this:
Template.myTemplate.helpers({
transformedDoc: function() {
// get the bounds - maybe these are stored in session vars
var min = Session.get('min');
var max = Session.get('max');
// fetch the doc somehow that needs to be transformed
var doc = SomeCollection.findOne();
// transform the thing.stuff arrays
_.each(doc.things, function(thing) {
thing.stuff = _.reject(thing.stuff, function(n) {
return (n < min) || (n > max);
});
});
// return the transformed doc
return doc;
}
});
Then in your template: {{#each transformedDoc.things}}...{{/each}}
Use mongo aggregation like following :
First use $unwind this will unwind stuff and then use $match to find elements greater than 4. After that $group data based on things.name and add required fields in $project.
The query will be as following:
db.collection.aggregate([
{
$unwind: "$things"
}, {
$unwind: "$things.stuff"
}, {
$match: {
"things.stuff": {
$gt: 4,
$lt:12
}
}
}, {
$group: {
"_id": "$things.name",
"stuff": {
$push: "$things.stuff"
}
}
}, {
$project: {
"thingName": "$_id",
"stuff": 1
}
}])