Convert array of objects, to object - mongodb

I have a mongo db record like this
items: [
0: {
key: name,
value: y
},
1: {
key: module,
value: z
}
]
And per record my expected output is this
{
name : y,
module : z
}
What is the best mongo db aggregation i should apply to achieve this. I have tried few ways but i am unable to produce output.
We can't use unwind otherwise it will break the relationship between name and module.

Query
the items array is almost ready to become document {}
$map to rename key->k and value->v (those are the field names mongo needs)
$arrayToObject to make the items [] => {}
and then merge items document with the root document
and project the items {} (we don't need it anymore the fields are part of the root document now)
*you can combine the 2 $set in 1 stage, i wrote it in 2 to be simpler
Test code here
aggregate(
[{"$set":
{"items":
{"$map":
{"input": "$items", "in": {"k": "$$this.key", "v": "$$this.value"}}}}},
{"$set": {"items": {"$arrayToObject": ["$items"]}}},
{"$replaceRoot": {"newRoot": {"$mergeObjects": ["$items", "$$ROOT"]}}},
{"$project": {"items": 0}}])

aggregate(
[{
$project: {
items: {
$map: {
input: "$items",
"in": {
"k": "$$this.key",
"v": "$$this.value"
}
}
}
}
}, {
$project: {
items: {
$arrayToObject: "$items"
}
}
}])
Test Code

Related

MongoDB filter and get count of all nested values

I have a MongoDB collection with a schema like this:
{
"_id":"0e5101cb8c39356305830f36",
"person1":{
"name":"Test",
"status":"valid"
},
"person2":{
"name":"Test2",
"status":"invalid"
},
"person3":{
"name":"Test3",
"status":"valid"
}
}
The status field can be either valid, invalid or pending. I want to query this collection and then count, for the whole collection, how many valid, invalid and pending persons it has. I am using a no-code tool to run a query so I think I need to use something like Aggregation Framework to process this. I am new to Aggregation framework, I tried a bunch of stages but couldn't make this work. Any help is appreciated.
The data model is awkward (the first two stages below are just to get the data in a "reasonable" format), but here's one way you could do it.
db.collection.aggregate([
{
"$project": {
"rootArray": {"$objectToArray": "$$ROOT"}
}
},
{
"$project": {
"persons": {
"$map": {
"input": {
"$filter": {
"input": "$rootArray",
"cond": {
"$regexMatch": {
"input": "$$this.k",
"regex": "person"
}
}
}
},
"as": "person",
"in": "$$person.v"
}
}
}
},
{"$unwind": "$persons"},
{
"$group": {
"_id": "$persons.status",
"count": {"$count": {} }
}
}
])
Try it on mongoplayground.net.

Set and concat to string field with arrayFilters in MongoDB

My MongoDB document looks like this.
{
_id: '556ebf103a8b4c3d067bb6466df7c0651c53a1ff79fa348edff656586c24ad0c',
Log: [
{
ScriptName: 'UpdateScript',
ScriptLogs: [
{
ID: 'f5bd1fefb2a5c4712ea1d35eb5bd309074984c5932db3723cc29f8d92258a3fa',
StdOut: 'Hello'
}
]
}
]}
I have the MongoDb Query
db.getCollection("ServerEntitys").update(
{_id: '556ebf103a8b4c3d067bb6466df7c0651c53a1ff79fa348edff656586c24ad0c'},
{
$set: {
'Log.$[i].ScriptLogs.$[j].StdOut': { "$concat": ["$Log.$[i].ScriptLogs.$[j].StdOut", "World"]}
}
},
{ arrayFilters: [ { "i.ScriptName": "UpdateScript" } , { "j.ID": 'f5bd1fefb2a5c4712ea1d35eb5bd309074984c5932db3723cc29f8d92258a3fa'} ] }
)
After running the query I get
StdOut: {
$concat: [
'$Log.$[i].ScriptLogs.$[j].StdOut',
'World'
]
},
instead of
StdOut: 'HelloWorld'
Can someone tell me why mongoDB uses $concat as value and not as a $concat instruction
When you are using normal update, you should use ONLY update operators
After MongoDB 4.2, we can also update using a pipeline update, using ONLY aggregate operators (no update ones, no arrayfilters etc)
We dont have an update operator that concat strings, so looks like you need pipeline update like the bellow.
Query
nested $map
outer to match the updateScript
inner to match the ID
in both cases if no match is found we keep the old array member
Playmongo
coll.updateMany({},
[{"$set":
{"Log":
{"$map":
{"input": "$Log",
"as": "outer",
"in":
{"$cond":
[{"$ne": ["$$outer.ScriptName", "UpdateScript"]}, "$$outer",
{"$mergeObjects":
["$$outer",
{"ScriptLogs":
{"$map":
{"input": "$$outer.ScriptLogs",
"as": "inner",
"in":
{"$cond":
[{"$ne": ["$$inner.ID", "2"]}, "$$inner",
{"$mergeObjects":
["$$inner",
{"StdOut":
{"$concat":
["$$inner.StdOut"," World"]}}]}]}}}}]}]}}}}}])

mongo - accessing a key-value field while filtering

I have these data:
myMap = {
"2": "facing",
"3": "not-facing"
"1": "hidden"
}
stages [
{
"k": 1,
"v": "hidden"
},
{
"k": 2,
"v": "facing"
},
{
"k": 3,
"v": "not-facing"
}
]
and a aggregate query but, I'm missing a syntax to dynamically fetch the map data:
db.MyCollection.aggregate()
.addFields({
myMaps: myMap
})
.addFields({
stages: stages
})
.addFields({
process: {
$filter: {
input: stages,
as: stageData,
cond: {$eq: [$$stageData.v, $myMaps[$$stageData.k]]}
}
}
})
As you may already note, this syntax: $myMaps[$$stageData.k] doesn't work, how should I access the myMaps based on the value of the k in stageData ?
Query
like your query set mymap and stages as extra field
mymapKeys is an extra field added (you can $unset is in the end)
filter the stages, and if stage.k is contained in mymapKeys we keep that member
*not sure if this is what you need, but looks like from your query
in mongodb query language we dont have getField(doc,$$k) we only have getField(doc,constant_string) which cant be used in your case,
so it costs here more than a hashmap lookup, here its like linear cost (check if member in the array). For arrays we have $getElementAt(array,$$k) if those numbers are always in sequence, 1,2,3 etc you might be able to use arrays instead of objects
Playmongo
aggregate(
[{"$set": {"mymap": {"2": "facing", "3": "not-facing", "1": "hidden"}}},
{"$set":
{"mymapKeys":
{"$map": {"input": {"$objectToArray": "$mymap"}, "in": "$$this.k"}}}},
{"$set":
{"stages":
[{"k": 1, "v": "hidden"}, {"k": 2, "v": "facing"},
{"k": 3, "v": "not-facing"}]}},
{"$set":
{"process":
{"$filter":
{"input": "$stages",
"cond": {"$in": [{"$toString": "$$this.k"}, "$mymapKeys"]}}}}}])

Remove Duplicate character from string in Mongodb

I want to remove duplicate characters from strings in MongoDB.
Example:
Input string: xxxyzzxcdv
Output string: xyzcdv
Query
reduce on range(count string)
keep 2 values {"previous": [], "string": ""} (initial value of reduce)
get the cur-char {"$substrCP": ["$mystring", "$$this", 1]}
this is the current index on the string, and i take the next char
if it is in previous kep "string" as it is, else concat to add the new character
heelo
reduce on (0 1 2 3 4) `{"$range": [0, {"$strLenCP": "$mystring"}]}`
we start from `{"previous": [], "string": ""}`
- get 1 character start from index 0
`{"$substrCP": ["$mystring", "$$this", 1]}}` = "h"
- if this character is on previous don't add it
`{"$in": ["$$cur_char", "$$value.previous"]}`
- else add it on previous and on the string the 2 concats in code
Repeat for `index($$this)`= 1
- get 1 character start from index 1
`{"$substrCP": ["$mystring", "$$this", 1]}}` = "e"
.....
PlayMongo
aggregate(
[{"$set":
{"mystring":
{"$getField":
{"field": "string",
"input":
{"$reduce":
{"input": {"$range": [0, {"$strLenCP": "$mystring"}]},
"initialValue": {"previous": [], "string": ""},
"in":
{"$let":
{"vars": {"cur_char": {"$substrCP": ["$mystring", "$$this", 1]}},
"in":
{"$cond":
[{"$in": ["$$cur_char", "$$value.previous"]},
"$$value",
{"previous":
{"$concatArrays": ["$$value.previous", ["$$cur_char"]]},
"string":
{"$concat": ["$$value.string", "$$cur_char"]}}]}}}}}}}}}])
Edit
The second query removed only the duplicates we choose.
Query
removes only the characters in the array, here only ["x"]
i removed the $getField because its only for MongoDB 5 +
aggregate(
[{"$set":
{"mystring":
{"$reduce":
{"input": {"$range": [0, {"$strLenCP": "$mystring"}]},
"initialValue": {"previous": [], "string": ""},
"in":
{"$let":
{"vars": {"cur_char": {"$substrCP": ["$mystring", "$$this", 1]}},
"in":
{"$cond":
[{"$and":
[{"$in": ["$$cur_char", "$$value.previous"]},
{"$in": ["$$cur_char", ["x"]]}]},
"$$value",
{"previous":
{"$concatArrays": ["$$value.previous", ["$$cur_char"]]},
"string":
{"$concat": ["$$value.string", "$$cur_char"]}}]}}}}}}},
{"$set": {"mystring": "$mystring.string"}}])
Edit
If you need to use this aggregation for update, you can use it as pipeline update like.
update({},
[{"$set": ......])
See your driver to find how to do update with pipeline, in Java its like above, alternative run it as database command
Let's assume we have the following collection and the records inside of it:
db.messages.insertMany([
{
"message": "heelllo theeere"
},
{
"message": "may thhhee forrrce be wiithh yyouuu"
},
{
"message": "execute orrrrdder 66"
}
])
Due to uncertainty, I am dropping solutions for both manipulating while querying and updating the records (permanently).
If you want to remove them while running your aggregation query:
In addition to #Takis's solution, using $function pipeline operator can be another option if your MongoDB version is 4.4 or higher.
Further readings on $function operator
// Query via Aggregation Framework
db.messages.aggregate([
{
$match: {
message: {
$ne: null
}
}
},
{
$addFields: {
distinctChars: {
$function: {
body: function (message) {
return message
.split("")
.filter(function (item, pos, self) {
return self.indexOf(item) == pos;
})
.join("");
},
args: ["$message"],
lang: "js"
}
}
}
},
])
If you want to remove them via an update operation:
// Update each document using cursor
db.messages.find({ message: { $ne: null } })
.forEach(doc => {
var distinctChars = doc.message
.split("")
.filter(function (item, pos, self) {
return self.indexOf(item) == pos;
})
.join("");
db.messages.updateOne({ _id: doc._id }, [{ $set: { distinctChars: distinctChars } }]);
});
A quick reminder: Above script just shows an easy way to update the records to reach the goal without focusing on other details. It can be an expensive operation depending on your real world collection's size and configurations, sharding for instance. Consider to improve it with your own way.
Result
For both way, the result should be like the following:
[
{
"_id": {
"$oid": "618d95ccdedc26d80875b75a"
},
"message": "heelllo theeere",
"distinctChars": "helo tr"
},
{
"_id": {
"$oid": "618d95ccdedc26d80875b75b"
},
"message": "may thhhee forrrce be wiithh yyouuu",
"distinctChars": "may theforcbwiu"
},
{
"_id": {
"$oid": "618d95ccdedc26d80875b75c"
},
"message": "execute orrrrdder 66",
"distinctChars": "excut ord6"
}
]

How to search embedded array

I want to get all matching values, using $elemMatch.
// create test data
db.foo.insert({values:[0,1,2,3,4,5,6,7,8,9]})
db.foo.find({},{
'values':{
'$elemMatch':{
'$gt':3
}
}
}) ;
My expecected result is {values:[3,4,5,6,7,8,9]} . but , really result is {values:[4]}.
I read mongo document , I understand this is specification.
How do I search for multi values ?
And more, I use 'skip' and 'limit'.
Any idea ?
Using Aggregation:
db.foo.aggregate([
{$unwind:"$values"},
{$match:{"values":{$gt:3}}},
{$group:{"_id":"$_id","values":{$push:"$values"}}}
])
You can add further filter condition in the $match, if you would like to.
You can't achieve this using an $elemMatch operator since, mongoDB doc says:
The $elemMatch projection operator limits the contents of an array
field that is included in the query results to contain only the array
element that matches the $elemMatch condition.
Note
The elements of the array are documents.
If you look carefully at the documentation on $elemMatch or the counterpart to query of the positional $ operator then you would see that only the "first" matched element is returned by this type of "projection".
What you are looking for is actually "manipulation" of the document contents where you want to "filter" the content of the array in the document rather than return the original or "matched" element, as there can be only one match.
For true "filtering" you need the aggregation framework, as there is more support there for document manipulation:
db.foo.aggregate([
// No point selecting documents that do not match your condition
{ "$match": { "values": { "$gt": 3 } } },
// Unwind the array to de-normalize as documents
{ "$unwind": "$values },
// Match to "filter" the array
{ "$match": { "values": { "$gt": 3 } } },
// Group by to the array form
{ "$group": {
"_id": "$_id",
"values": { "$push": "$values" }
}}
])
Or with modern versions of MongoDB from 2.6 and onwards, where the array values are "unique" you could do this:
db.foo.aggregate([
{ "$project": {
"values": {
"$setDifference": [
{ "$map": {
"input": "$values",
"as": "el",
"in": {
"$cond": [
{ "$gt": [ "$$el", 3 ] },
"$$el",
false
]
}
}},
[false]
]
}
}}
])