MongoDB : Retrieve Associated Value from Object in an Array of Arrays - mongodb

In mongo I have a documents that follow the below pattern :
{
name: "test",
codes: [
[
{
code: "abc",
value: 123
},
{
code: "def",
value: 456
},
],
[
{
code: "ghi",
value: 789
},
{
code: "jkl",
value: 012
},
]
]
}
I'm using an aggregate query (because of joins) and in a $project block I need to return the "name" and the value of the object that has a code of "def" if it exists and an empty string if it doesn't.
I can't simply $unwind codes and $match because the "def" code is not guaranteed to be there.
$filter seems like the right approach as $elemMatch doesn't work, but its not obvious to me how to do this on nested array of arrays.

You can try below query, instead of unwinds & filter this can give you required result with less docs to operate on :
db.collection.aggregate([
/** merge all arrays inside codes array into code array */
{
$addFields: {
codes: {
$reduce: {
input: '$codes',
initialValue: [],
in: { $concatArrays: ["$$value", "$$this"] }
}
}
}
},
/** project only needed fields & value will be either def value or '',
* if 'def' exists in any doc then we're check index of it to get value of that particular object using arrayElemAt */
{
$project: {
_id:0, name: 1, value:
{
$cond: [{ $in: ["def", '$codes.code'] }, { $arrayElemAt: ['$codes.value', { $indexOfArray: ["$codes.code", 'def'] }] }, '']
}
}
}])
Test : MongoDB-Playground

Related

MongoDB - arrayFilters and updateMany from same document

I have the following sample of data:
{
_id: 1,
seniorityDate: '2001-01-01T00:00:00Z',
assigned: [
{
groupId: 11,
system: 'Dep',
effectiveDate: null
},
{
groupId: 12,
system: 'Team',
effectiveDate: null
},
...
]
}
and I would like to update the object effectiveDate based on seniorityDate in the array of assigned where system:'Team' only:
db.collection.updateMany({},
[{
$set: {
'assigned.$[elem].effectiveDate': '$seniorityDate'
}
}], {
arrayFilters: [{
"elem.system": "Team"
}]
})
but I got the following error:
arrayFilters may not be specified for pipeline-syle updates
The expected result will be:
{
_id: 1,
seniorityDate: '2001-01-01T00:00:00Z',
assigned: [
{
groupId: 11,
system: 'Dep',
effectiveDate: null
},
{
groupId: 12,
system: 'Team',
effectiveDate: '2001-01-01T00:00:00Z'
},
...
]
}
How can I achieve it?
You can't use the arrayFilters with the aggregation pipeline at the same time. While you are updating the value from another field, hence you can only achieve with aggregation pipeline.
$set - Set assigned field.
1.1. $map - Iterate element in assigned array and return new array.
1.1.1. $mergeObjects - Merge current iterated document with the document from 1.1.1.1.
1.1.1.1. Document with effectiveDate field. With the $cond operator, if matches the condition, use the seniorityDate value, else remain the existing value.
db.collection.updateMany({},
[
{
$set: {
"assigned": {
$map: {
input: "$assigned",
in: {
$mergeObjects: [
"$$this",
{
effectiveDate: {
$cond: {
if: {
$eq: [
"$$this.system",
"Team"
]
},
then: "$seniorityDate",
else: "$$this.effectiveDate"
}
}
}
]
}
}
}
}
}
])
Thanks to #rickhg12hs' suggestion, always limit the document for better performance, as you know which document/field should be updated by condition.
Hence your update query with query condition will be as below:
db.collection.updateMany({
"assigned.system": "Team"
},
[
...
])
Demo # Mongo Playground

MongoDb 4.x Query with projection without empty entries using find

We are now using mongo-db to store data from tests. I am using Mongo-Shell
The document is structured like this:
{
static1:"abc",
static2:"xyz",
static3:"asd", [...],
nested:[
{
data1: "d1",
data2: "d2",
},
{
data1: "dx",
data4: "d4",
data5: "d5",
data6: "d6",
},
{
data1: "ds",
data8:"data8"
}, [...]
]
}
So the static-data is always in the same structure, but for every measurement the object can look different. It could be voltage with upper, lower and actual value. Or just comparison of a target value and an actual value. Highly dynamic.
Same data1-name represent same attributes.
Now e.g. I want to display only some static data and one (or several) attributes of a nested document.
I am using this query:
find({}, {_id:0, data1:1, "nested.data8":1}).pretty()
As expected, only the static-data1 is displayed, but the dynamic measurements has lots of empty objects in the shell-output
Example-Output:
{
"static1" : "123",
"nested" : [
{ },
{ },
{ }, [...] ,
{ "data8" : "OK" }
] }
The desired output would be:
{
"static1" : "123",
"nested" : [
{ "data8" : "d8" }
]
}
I also tried this query on the mongo shell:
aggregate( { $addFields: {"static":"$static1", "data8":"$nested.data8"} }, { $project:{"static1":1, "nested.data8":1} } ).pretty()
But the result is the same.
I hope there is a ways to get rid of the empty documents in the output.
Thanks
You can try $filter operator to filter result of nested array by checking condition not equal to empty object {}
db.collection.aggregate([
{
$project: {
_id: 0,
data1: 1,
"nested.data8": 1
}
},
{
$set: {
nested: {
$filter: {
input: "$nested",
cond: { $ne: ["$$this", {}] }
}
}
}
}
])
Playground

How to avoid adding duplicate objects to an array in MongoDB

this is my schema:
new Schema({
code: { type: String },
toy_array: [
{
date:{
type:Date(),
default: new Date()
}
toy:{ type:String }
]
}
this is my db:
{
"code": "Toystore A",
"toy_array": [
{
_id:"xxxxx", // automatic
"toy": "buzz"
},
{
_id:"xxxxx", // automatic
"toy": "pope"
}
]
},
{
"code": "Toystore B",
"toy_array": [
{
_id:"xxxxx", // automatic
"toy": "jessie"
}
]
}
I am trying to update an object. In this case I want to update the document with code: 'ToystoreA' and add an array of subdocuments to the array named toy_array if the toys does not exists in the array.
for example if I try to do this:
db.mydb.findOneAndUpdate({
code: 'ToystoreA,
/*toy_array: {
$not: {
$elemMatch: {
toy: [{"toy":'woddy'},{"toy":"buzz"}],
},
},
},*/
},
{
$addToSet: {
toy_array: {
$each: [{"toy":'woddy'},{"toy":"buzz"}],
},
},
},
{
new: false,
}
})
they are added and is what I want to avoid.
how can I do it?
[
{
"code": "Toystore A",
"toy_array": [
{
"toy": "buzz"
},
{
"toy": "pope"
}
]
},
{
"code": "Toystore B",
"toy_array": [
{
"toy": "jessie"
}
]
}
]
In this example [{"toy":'woddy'},{"toy":"buzz"}] it should only be added 'woddy' because 'buzz' is already in the array.
Note:when I insert a new toy an insertion date is also inserted, in addition to an _id (it is normal for me).
As you're using $addToSet on an object it's failing for your use case for a reason :
Let's say if your document look like this :
{
_id: 123, // automatically generated
"toy": "buzz"
},
{
_id: 456, // automatically generated
"toy": "pope"
}
and input is :
[{_id: 789, "toy":'woddy'},{_id: 098, "toy":"buzz"}]
Here while comparing two objects {_id: 098, "toy":"buzz"} & {_id: 123, "toy":"buzz"} - $addToSet consider these are different and you can't use $addToSet on a field (toy) in an object. So try below query on MongoDB version >= 4.2.
Query :
db.collection.updateOne({"_id" : "Toystore A"},[{
$addFields: {
toy_array: {
$reduce: {
input: inputArrayOfObjects,
initialValue: "$toy_array", // taking existing `toy_array` as initial value
in: {
$cond: [
{ $in: [ "$$this.toy", "$toy_array.toy" ] }, // check if each new toy exists in existing arrays of toys
"$$value", // If yes, just return accumulator array
{ $concatArrays: [ [ "$$this" ], "$$value" ] } // If No, push new toy object into accumulator
]
}
}
}
}
}])
Test : aggregation pipeline test url : mongoplayground
Ref : $reduce
Note :
You don't need to mention { new: false } as .findOneAndUpdate() return old doc by default, if you need new one then you've to do { new: true }. Also if anyone can get rid of _id's from schema of array objects then you can just use $addToSet as OP was doing earlier (Assume if _id is only unique field), check this stop-mongoose-from-creating-id-property-for-sub-document-array-items.

How to conditionally set a value to parent field based on another field inside an array in mongodb aggregate?

I want to set a variable at a function level from inside the aggregate map operator. Here is a snippet of what I have till now.
$project: {
collectionId: 1,
name: 1,
description: 1,
image: 1,
created: 1,
updated: 1,
isPresent: "no",
"products": {
$map: {
"input": "$products",
as: "product",
in: {
"title": "$$product.name",
"imageUrl": "$$product.mainImage",
"productId": "$$product.productId",
"$isPresent": "yes"
}
}
},
total: {
$size: "$products"
},
userId: "$user.userId",
userName: "$user.name",
}
I want to set the isPresent node value to yes inside the map operator at in based on a condition. For the time being I have kept it to just yes. But this doesn't work.
What is the way to do it?
You can use $in and $cond operators :
db.collection.aggregate([
/** `$products.productId` gives an array of 'productId''s from products array */
{
$addFields: { isPresent: { $cond: [ { $in: [ 2, "$products.productId" ] }, "Yes", "No" ] } }
}
])
Test : mongoplayground
Note :
As we're using $in each document going through this has to have products array, otherwise $in will error out, if there are any such cases you need to have to condition to skip for those docs which don't have products array.

MongoDB - Autocomplete - Get all words starting with X

I have a collection (users) with the following structure:
{
propA: {
words: ["i", "have", "an","important", "question"]
}
}
I want to get autocomplete options from the db for some input in my website.
So first, i think that i need to create an index for propA.words.
Maybe something like this(?):
db.users.createIndex({ "propA.words" : 1 })
Second, how can i query this index to get all the words starting with X?
For example, for the string "i", the query will retrieve ["i", "important"].
Thanks!
EDIT:
This is the collection:
{
propA: {
words: ["aa","bb","cc","dd"]
}
}
{
propA: {
words: ["ab"]
}
}
{
propA: {
words: []
}
}
{
propB: []
}
Now, i want a query to get all the words that starts with "a".
The query should return ["aa","ab"] on the above collection.
I want the query to use only the index so the search will be efficient.
You can use this aggregation, which iterates over the words array and matches the regex search string.
db.collection.aggregate( [
{
$addFields: {
matches: {
$filter: {
input: "$propA.words",
as: "w",
cond: {
$regexMatch: { input: "$$w" , regex: "^i" }
}
}
}
}
}
] )
The output:
{
"_id" : 1,
"propA" : {
"words" : [
"i",
"have",
"an",
"important",
"question"
]
},
"matches" : [
"i",
"important"
]
}
[ EDIT ADD ]
Now, i want a query to get all the words that starts with "a". The
query should return ["aa","ab"] on the above collection. I want the
query to use only the index so the search will be efficient.
The aggregation:
db.collection.aggregate( [
{
$match: { "propA.words": { $regex: "^a" } }
},
{
$unwind: "$propA.words"
},
{
$group: {
_id: null,
matchedWords: {
$addToSet: {
$cond: [ { $regexMatch: { input: "$propA.words", regex: "^a" } },
"$propA.words",
"$DUMMY" ]
}
}
}
},
{
$project: { _id: 0 }
}
] )
The result:
{ "matchedWords" : [ "ab", "aa" ] }
Index usage:
The index is created on the collection as follows:
db.collection.createIndex( { "propA.words": 1 } )
You can verify the index usage on the aggregation's $match stage by applying the explain and generating a query plan. For example:
db.collection.explain("executionStats").aggregate( [ ... ] )
yes you make an index on the field, which is an array. then use regex query - the symbol ^ for 'starts with'... an index on an array field can create a big load... but your query being a 'start-with' is an efficient design....