Querying a multi-nested array in MongoDb 3.4.2 - mongodb

MongoDB Version - 3.4.2
I'm trying to query within the Sitecore Analytics database, trying to retrieve all users that are associated with a given List Id.
The example dataset I have follows the default Sitecore Analytics setup:
"Tags" : {
"Entries" : {
"ContactLists" : {
"Values" : {
"0" : {
"Value" : "{1E2D1AB7-72A0-4FF7-906B-DCDC020B87D2}",
"DateTime" : ISODate("2020-10-23T17:38:13.891Z")
},
"1" : {
"Value" : "{28BECCD3-476B-4B1D-9A75-02E59EF21286}",
"DateTime" : ISODate("2018-04-18T14:22:41.763Z")
},
"2" : {
"Value" : "{2C2BB0C3-483D-490E-B93A-9155BFBBE5DC}",
"DateTime" : ISODate("2018-05-10T14:26:08.494Z")
},
"3" : {
"Value" : "{DBE480F6-E305-4B35-9E6D-CBED64F4E44F}",
"DateTime" : ISODate("2018-10-27T02:41:28.776Z")
},
}
}
}
},
I want to iterate through all the entries within Values without having to specify 0/1/2/3, avoiding the following:
db.getCollection('Contacts').find({"Tags.Entries.ContactLists.Values.1.Value": "{28BECCD3-476B-4B1D-9A75-02E59EF21286}"})
I've tried the following:
db.getCollection('Contacts').find({"Tags.Entries.ContactLists.Values": {$elemMatch : {"Value":"{28BECCD3-476B-4B1D-9A75-02E59EF21286}"}}})
db.getCollection('Contacts').find({'Tags' : {$elemMatch : {$all : ['{28BECCD3-476B-4B1D-9A75-02E59EF21286}']}}})
db.getCollection('Contacts').forEach(function (doc) {
for(var i in doc.Tags.Entries.ContactLists.Values)
{
doc.Tags.Entries.ContactLists.Values[i].Value = "{28BECCD3-476B-4B1D-9A75-02E59EF21286}";
}
})
And a few other variations which I cannot recall now. And none work.
Any ideas if this is possible or on how to do this?
I want the outcome to just show filter out the results showing only the entries containing the matching GUID
Many thanks!

Demo - https://mongoplayground.net/p/upgYxgzPwJQ
It can be done using aggregation pipeline
Use $objectToArray to convert array
Use $filter to filter the array
db.collection.aggregate([
{
$addFields: {
filteredValue: {
$filter: {
input: {
$objectToArray: "$Tags.Entries.ContactLists.Values"
},
as: "val",
cond: {
$eq: [ // filter condition
"$$val.v.Value",
"{28BECCD3-476B-4B1D-9A75-02E59EF21286}"
]
}
}
}
}
}
])
Output -
[
{
"Tags": {
"Entries": {
"ContactLists": {
"Values": {
"0": {
"DateTime": ISODate("2020-10-23T17:38:13.891Z"),
"Value": "{1E2D1AB7-72A0-4FF7-906B-DCDC020B87D2}"
},
"1": {
"DateTime": ISODate("2018-04-18T14:22:41.763Z"),
"Value": "{28BECCD3-476B-4B1D-9A75-02E59EF21286}"
},
"2": {
"DateTime": ISODate("2018-05-10T14:26:08.494Z"),
"Value": "{2C2BB0C3-483D-490E-B93A-9155BFBBE5DC}"
},
"3": {
"DateTime": ISODate("2018-10-27T02:41:28.776Z"),
"Value": "{DBE480F6-E305-4B35-9E6D-CBED64F4E44F}"
}
}
}
}
},
"_id": ObjectId("5a934e000102030405000000"),
"filteredValue": [
{
"k": "1",
"v": {
"DateTime": ISODate("2018-04-18T14:22:41.763Z"),
"Value": "{28BECCD3-476B-4B1D-9A75-02E59EF21286}"
}
}
]
}
]

You can not use $elemMatch because Values is not array, but object. You can solve the problem with Aggregation Pipeline:
$addFields to add new field Values_Array that will be array representation of Values object.
$objectToArray to transform Values object to array
$match to find all documents that has requested value in new Values_Array field
$project to specify which properties to return from the result
db.getCollection('Contacts').aggregate([
{
"$addFields": {
"Values_Array": {
"$objectToArray": "$Tags.Entries.ContactLists.Values"
}
}
},
{
"$match": {
"Values_Array.v.Value": "{28BECCD3-476B-4B1D-9A75-02E59EF21286}"
}
},
{
"$project": {
"Tags": 1
}
}
])
Here is the working example: https://mongoplayground.net/p/2gY-vu3Qrvz

Related

MongoDB group by array subfield

Hello I am new to mongoDB, please I hope you can help me with this question.
My collection will look like this:
{
"_id": { "$oid": "5f1fd47..." },
"email":"c#c.com",
"materials": [
{
"_id": { "$oid": "5f1fda2..." },
"title": "MDF 18mm Blanco",
"id": "mdf18blanco",
"thickness": "18",
"family": "MDF",
"color": ""
}, ...
//others materials with different family
],
}
I did an aggregate like this:
{ "$match" : { "email" : "c#c.com" } },
{ "$unwind" : "$materials" },
{ "$group" : { "_id" : "$_id", "list" : { "$push" : "$materials.family" } } }
and I return this:
{
"_id" : ObjectId("5f1fd47d502e00051c673dd1"),
"list" : [
"MDF",
"MDF",
"MDF",
"Melamina",
"Melamina",
"Melamina",
"Melamina",
"MDF",
"Melamina",
"Aglomerado",
"Aglomerado"
]
}
but i need get this
{
"_id" : ObjectId("5f1fd47d502e00051c673dd1"),
"list" : [
"MDF",
"Melamina",
"Aglomerado"
]
}
I hope you understand my question and can help me, thank you very much.
All you need to do is use $addToSet instead of $push in your group stage:
{ "$group" : { "_id" : "$_id", "list" : { "$addToSet" : "$materials.family" } } }
One thing to note is that $addToSet does not guarantee a specific order as opposed to $push in case it matters to you.
You only need change $push to $addToSet.
A set not contains repeat values so it works.
db.collection.aggregate([
{
"$match": {
"email": "c#c.com"
}
},
{
"$unwind": "$materials"
},
{
"$group": {
"_id": "$_id",
"list": {
"$addToSet": "$materials.family"
}
}
}
])
Mongo Playground example

How to find not in values in mongodb query

Here i am having n-number of documents, below are the examples of the documents.
{
"_id" : "ABC",
"languagesknow" : {
"lng" : [
"1",
"2"
]
}
},
{
"_id" : "CDF",
"languagesknow" : {
"lng" : [
"3",
"4"
]
}
}
My question is suppose i will pass input as a one array.suppose in that array i am passing 2 & 5 & 6 means 5 & 6 not there inside lng, so want to print the values , how can achieve this?
You need $setDifference operator, try:
db.collection.aggregate([
{
$match: {
"languagesknow.lng": { $type: "array" }
}
},
{
$project: {
languagesknow: {
$setDifference: [ [ "2", "5", "6" ], "$languagesknow.lng" ]
}
}
}
])
EDIT: you can put $match in front of $setDifference to filter out all the documents where languagesknow.lng is not an array (using $type operator).
EDIT(2): if you want to get single array for entire collection then you need to add $group with $unwind to gather all the languagesknow into single array and then run $setDifference on it, try:
db.collection.aggregate([
{
$match: {
"languagesknow.lng": { $type: "array" }
}
},
{
$unwind: "$languagesknow.lng"
},
{
$group: {
_id: null,
languagesknow: { $addToSet: "$languagesknow.lng" }
}
},
{
$project: {
languagesknow: {
$setDifference: [ [ "2", "5", "6" ], "$languagesknow" ]
}
}
}
])

Find in All subkey of Object MongoDB

I have this Document in mongodb
{
"name": "test2",
"_id": "1502609098801598ffeca615f5d3dd09087c6",
"events": {
"0": {
"delay": "0",
"actionid": "2"
},
"1": {
"delay": "0",
"actionid": "3"
}
}
}
I want to find documents that contain event with specific actionid
i tried something like these but i can't find what i want
db.mycollection.find({ "events.$.actionid":"2" })
db.mycollection.find({ "events.$**.actionid":"2" })
db.mycollection.find({ "events.$": { $elemMatch: { "actionid":"2"} })
attention please: i can't change document structure and mongodb version is 3.0.6
We can take use of the aggregation feature within mongodb and project the object to an array
db.test.aggregate( [ { "$project" : { "events" : { "$objectToArray" : "$events" } } } ] )
after this we can just use the normal array filtering with a $match
db.test.aggregate([
{ "$project":
{ "events": {"$objectToArray": "$events"} } },
{ "$match":
{ "events.v.actionid" : "2" } } ] )
This will output the following:
{
"_id" : "1502609098801598ffeca615f5d3dd09087c6",
"events" : [
{
"k" : "0",
"v" : {
"delay" : "0",
"actionid" : "2"
}
},
{
"k" : "1",
"v" : {
"delay" : "0",
"actionid" : "3"
}
}
]
}
>
So you might want to project the document back to its orignial structure

Pymongo Advanced query (embeded objects and arrays)

I have been working on this for about a week, have learnt a lot about pymongo but still can't crack it.
I have the following JSON in mongo
{
"_id" : ObjectId("5845e25xxxxxxxxxx"),
"timestamp" : ISODate("2016-08-24T14:59:04.000+0000"),
"new_key" : "cambiar",
"Records" : [
{
"RecordType" : "WFD",
"Properties" : [
{
"Property" : {
"IsReadOnly" : "False",
"ValueType" : "System",
"Name" : "LastWrite"
},
"value" : "42"
},
{
"Property" : {
"IsReadOnly" : "True",
"ValueType" : "String",
"Name" : "Time_as_String"
},
"value" : "24-08-2016 14:59:08"
},
{
"Property" : {
"IsReadOnly" : "False",
"ValueType" : "32",
"Name" : "YES"
},
"value" : "1472065148"
there are many more properties below. I am trying to return just the "value" : 1472065148 and nothing else. I have written this query
x = dataset.find_one({"Records.Properties.Property.Name":'YES'},{"Records.Properties.Property.value":1})
but beacuase the 'value' is on the same level as all the other values, I get all of the value results, not just the one I am hoping for.
Is there a way to print only the result object after the object that matches the query???? "Name" : "YES" being the object i'm querying and "value" : "1472065148" being the object I want to print.
------------------------ ADDITIONAL PART ---------------
Above is one document which has 'name' : 'Yes' values that I want to retreive. However every Document has the same 'Name : YES' inside it. What i would like to do, is first select the document based on a different 'Name' : 'xxxx' by its value. For example above - look up 'name' : 'lastwrite' check that its value is 42 (thus selection this document and not the others) before retrieving the 'name' : 'YES' value (as in the answer you have given me).
(if this counts as a new question please let me know and I will remove it and post a new question)
The only option you have with the existing structure is to use aggregation.
db.dataset.aggregate([{
$unwind: "$Records"
}, {
$unwind: "$Records.Properties"
}, {
$match: {
"Records.Properties.Property.Name": 'YES'
}
}, {
$project: {
"_id": 0,
"value": "$Records.Properties.value"
}
}]).pretty()
Sample Response
{ "value" : "1472065148" }
Assume you were able to update your structure as follows (Removed some fields for brevity). The change here is the Records is no longer an array but just an embedded document.
db.dataset.insertMany([{
"timestamp": ISODate("2016-08-24T14:59:04.000+0000"),
"Records": {
"RecordType": "WFD",
"Properties": [{
"Property": {
"Name": "LastWrite"
},
"value": "42"
}, {
"Property": {
"Name": "YES"
},
"value": "1472065148"
}]
}
}])
Query
db.dataset.find({"Records.Properties.Property.Name":'YES'},{"Records.Properties.$":1}).pretty()
Response
{
"_id": ObjectId("5859e80591c255c059a3da50"),
"Records": {
"Properties": [{
"Property": {
"Name": "YES"
},
"value": "1472065148"
}]
}
}
Update for the additional part:
There are couple of ways you can take care of this. Things can be optimized a bit, but I'll leave that up to you.
Option 1 :
$map applies an equals comparison between the criteria passed and fields in each result element and generates an array with true and false values. $anyElementTrue inspects this array and returns true only if there is at least one true value in the array. Match stage to include only elements with matched value of true.
data field to keep the data using system variable $$ROOT.
Complete Query
db.dataset.aggregate([{
$unwind: "$Records"
}, {
$project: {
"_id": 0,
"matched": {
"$anyElementTrue": {
"$map": {
"input": "$Records.Properties",
"as": "result",
"in": {
"$and": [{
$eq: ["$$result.Property.Name", "LastWrite"]
}, {
$eq: ["$$result.value", "42"]
}]
}
}
}
},
"data": "$$ROOT"
}
}, {
"$match": {
"matched": true
}
}, {
$unwind: "$data.Records"
}, {
$unwind: "$data.Records.Properties"
}, {
$match: {
"data.Records.Properties.Property.Name": 'YES'
}
}, {
$project: {
"_id": 0,
"value": "$data.Records.Properties.value"
}
}]).pretty()
Option 2:
Better option (superior in performance) so use this if you the driver that supports $redact.
Similar to the above version but this one combines both the project and match stage into one. The $cond with $redact accounts for match and when match is found it keeps the complete tree or else discards it.
Complete Query
db.dataset.aggregate([{
$unwind: "$Records"
}, {
"$redact": {
"$cond": [{
"$anyElementTrue": {
"$map": {
"input": "$Records.Properties",
"as": "result",
"in": {
"$and": [{
$eq: ["$$result.Property.Name", "LastWrite"]
}, {
$eq: ["$$result.value", "42"]
}]
}
}
}
},
"$$KEEP",
"$$PRUNE"
]
}
}, {
$unwind: "$Records.Properties"
}, {
$match: {
"Records.Properties.Property.Name": 'YES'
}
}, {
$project: {
"_id": 0,
"value": "$Records.Properties.value"
}
}]).pretty()

MongoDb aggregate and group by two fields depending on values

I want to aggregate over a collection where a type is given. If the type is foo I want to group by the field author, if the type is bar I want to group by user.
All this should happen in one query.
Example Data:
{
"_id": 1,
"author": {
"someField": "abc",
},
"type": "foo"
}
{
"_id": 2,
"author": {
"someField": "abc",
},
"type": "foo"
}
{
"_id": 3,
"user": {
"someField": "abc",
},
"type": "bar"
}
This user field is only existing if the type is bar.
So basically something like that... tried to express it with an $or.
function () {
var results = db.vote.aggregate( [
{ $or: [ {
{ $match : { type : "foo" } },
{ $group : { _id : "$author", sumAuthor : {$sum : 1} } } },
{ { $match : { type : "bar" } },
{ $group : { _id : "$user", sumUser : {$sum : 1} } }
} ] }
] );
return results;
}
Does someone have a good solution for this?
I think it can be done by
db.c.aggregate([{
$group : {
_id : {
$cond : [{
$eq : [ "$type", "foo"]
}, "author", "user"]
},
sum : {
$sum : 1
}
}
}]);
The solution below can be cleaned up a bit...
For "bar" (note: for "foo", you have to change a bit)
db.vote.aggregate(
{
$project:{
user:{ $ifNull: ["$user", "notbar"]},
type:1
}
},
{
$group:{
_id:{_id:"$user.someField"},
sumUser:{$sum:1}
}
}
)
Also note: In you final answer, anything that is not of type "bar" will have an _id=null
What you want here is the $cond operator, which is a ternary operator returning a specific value where the condition is true or false.
db.vote.aggregate([
{ "$group": {
"_id": null,
"sumUser": {
"$sum": {
"$cond": [ { "$eq": [ "$type", "user" ] }, 1, 0 ]
}
},
"sumAuhtor": {
"$sum": {
"$cond": [ { "$eq": [ "$type", "auhtor" ] }, 1, 0 ]
}
}
}}
])
This basically tests the "type" of the current document and decides whether to pass either 1 or 0 to the $sum operation.
This also avoids errant grouping should the "user" and "author" fields contain the same values as they do in your example. The end result is a single document with the count of both types.