Projecting a field in MongoDB based of the value of another field - mongodb

I have the following document in a MongoDB 4 collection:
{
"_id": <ObjectID>,
"active_version": "3",
"versions": {
"1": "The first very long document",
"2": "The second very long document",
"3": "The third very long document"
}
}
There are many documents like this in the collection, and each document may have many versions.
When querying for this document, I would like to retrieve only the text of the active version, e.g. "The third very long document". How can I accomplish that?

You can use $objectToArray to transform versions into an array of k-v pairs and then use $let and $filter with $arrayElemAt to get first matching version:
db.col.aggregate([
{
$project: {
current_version: {
$let: {
vars: {
version: {
$arrayElemAt: [
{
$filter: {
input: { $objectToArray: "$versions" },
as: "v",
cond: { $eq: [ "$$v.k", "$active_version" ] }
}
}
,0]
}
},
in: "$$version.v"
}
}
}
}
])
Outputs:
{ "_id" : ..., "current_version" : "The third very long document" }

Related

MongoDB - How to match the value of a field with nested document field value

I have a structure where I want to match the value of a field on root level with the value of a field inside another object in the same document, I got to his structure by unwinding on the nested field. So I have a structure like this:
{
"name": "somename",
"level": "123",
"nested":[
{
"somefield": "test",
"file": {
level:"123"
}
},
{
"somefield": "test2",
"file": {
level:"124"
}
}
]
}
After unwinding I got the structure like:
{
"name": "somename",
"level": "123",
"nested": {
"somefield": "test",
"file": {
level:"123"
}
}
}
So I want to match on level = nested.file.level and return only documents which satisfy this condition.
I tried using
$match: {
"nested.file.level": '$level'
}
also
$project: {
nested: {
$cond: [{
$eq: [
'nested.file.level',
'$level'
]
},
'$nested',
null
]
}
}
Nothing seems to work. Any idea on how I can match based on the mentioned criteria?
Solution 1: With $unwind stage
After $unwind stage, in the $match stage you need to use the $expr operator.
{
$match: {
$expr: {
$eq: [
"$nested.file.level",
"$level"
]
}
}
}
Demo Solution 1 # Mongo Playground
Solution 2: Without $unwind stage
Without $unwind stage, you may work with $filter operator.
db.collection.aggregate([
{
$match: {
$expr: {
$in: [
"$level",
"$nested.file.level"
]
}
}
},
{
$project: {
nested: {
$filter: {
input: "$nested",
cond: {
$eq: [
"$$this.file.level",
"$level"
]
}
}
}
}
}
])
Demo Solution 2 # Mongo Playground

Mongodb: Get value from $[<identifier>] in update

I want to rename a field inside a object itself inside a nested array.
As example, I want to rename the all tags m2 to m6 in this document:
{
"_id": 1,
"tagsGroup": [
{
"id": "1234",
"tags": {
"m1": 1,
"m2": 2
}
},
{
"id": "456",
"tags": {
"m3": 1,
"m2": 2
}
},
{
"id": "1234",
"tags": {
"m4": 2,
"m5": 2
}
},
]
}
This is my current state of work:
db.collection.update({},
{
"$set": {"tagsGroup.$[tGp].tags.m6": "$tagsGroup.$[tGp].tags.m2"},
"$unset": {"tagsGroup.$[tGp].tags.m2": ""}
},
{
arrayFilters: [{"tGp.tags.m2": {$exists: 1}}],
multi: true}
)
Unfortunately, the $tagsGroup.$[tGp].tags.m6 is not interpreted.
Do you guys, have a way to do this?
Thanks!
Probably similar to this question MongoDB rename database field within array,
There is no straight way to rename fields within arrays with a single command. You can try update with aggregation pipeline starting from MongoDB v4.2,
$map to iterate loop of tagsGroup array
$map to iterate loop of tags object after converting to array using $objectToArray, it will return in k and v format
$replaceOne will replace specific string on find field, this is starting from MongoDB v4.4
$arrayToObject convert tags array returned by second $map back to object format
$mergeObjects to merge current object with updated tags object
db.collection.update(
{ "tagsGroup.tags.m2": { $exists: true } },
[{
$set: {
tagsGroup: {
$map: {
input: "$tagsGroup",
in: {
$mergeObjects: [
"$$this",
{
tags: {
$arrayToObject: {
$map: {
input: { $objectToArray: "$$this.tags" },
in: {
k: {
$replaceOne: {
input: "$$this.k",
find: "m2",
replacement: "m6"
}
},
v: "$$this.v"
}
}
}
}
}
]
}
}
}
}
}],
{ multi: true }
)
Playground

Match in an array within the current document

In an aggregation pipeline operating on documents like
{
"availablePackages": [
{
"title": "Silver",
"code": "001",
},
{
"title": "Gold",
"code": "002",
},
{
"title": "Platinum",
"code": "003",
},
"selectedPackageCode": "002"
}
I need to replace everything in the above document with the title of the package whose code matches the selectedPackageCode. So I want to the pipeline to end up with
{
"packageTitle": "Gold"
}
This is not a lookup, because it's in the current document. I thought I might be able to use $let to create a variable and then a $match to find the right array element, but I have not found a syntax that works.
You need $filter to match availablePackages with selectedPackageCode and $arrayElemAt to get first matching element. In order to make it in one aggregation stage you can use $let to define temporary variable:
db.col.aggregate([
// ... other stages
{
$project: {
packageTitle: {
$let: {
vars: {
selectedPackage: {
$arrayElemAt: [
{ $filter: { input: "$availablePackages", cond: { $eq: [ "$$this.code", "$selectedPackageCode" ] } } }, 0
]
}
},
in: "$$selectedPackage.title"
}
}
}
}
])

MONGODB redact array

i just want to understand $redact in mongodb
suppose i have a collection like .
db.tab12.find()
{ "_id" : "1", "name" : "jan", "passport" : [ "usa" ] }
{ "_id" : "2", "name" : "jaan", "passport" : [ "usa", "canada" ] }
{ "_id" : "3", "name" : "jon", "passport" : [ "germany" ] }
and i run the following command
db.tab12.aggregate({$match:{"name":{$regex:"a"}}},{$redact:{$cond:{if:{$in:["$country",["canada"]]},then:"$$DESCEND",else:"$$PRUNE" }} } )
I get no result, mongodb lacks examples on net i think.
Let's imagine that you have collections PC and events. The PC is generating a different type of events and you want just to take the events which are from type:1
PC {
"_id": ObjectId,
"version": "SomeVersion",
"location": "Sofia",
"price": 220,
events: [1,2,3,4,5]
}
events{
"_id":1,
"type": 1,
"ts": "1999-01-01"
}
events{
"_id":2,
"securiy:" true,
"type": 2,
"ts": "2015-01-01"
}
PCs.aggregate([
{
$match: {
"location": "Sofia"
}
},
{
$unwind: {
path: '$events'
}
},
{
$project: {
events: '$pcs.events'
}
},
{
$unwind: {
path: '$events'
}
},
{
$lookup: {
from: 'events',
localField: 'event',
foreignField: '_id',
as: 'events'
}
},
{
$match: {
$and: [{
'events.ts': {
$gt: new Date("1970-01-01")
}
},
{
'events.ts': {
$lt: new Date("2000-02-02")
}
}
]
}
},
{
$redact: {
$cond: {
if: { $eq: [ '$events.type', 1 ] },
then: '$$KEEP',
else: '$$PRUNE'
}
}
},
this will remove all the other type of events, which are not passing the condition. After this step you can make some kind of grouping and for example if the events have some data (for example price), you can sum it.
{
$group: {
_id: {
isSecurityEvent: '$events.security',
},
totalEvents: { $sum: 1 },
}
}
}
])
From the mongo documentation you have 3 types of operators:
System Variable Description
$$DESCEND $redact returns the fields at
the current document level, excluding embedded documents. To include
embedded documents and embedded documents within arrays, apply the
$cond expression to the embedded documents to determine access for
these embedded documents.
$$PRUNE $redact excludes all fields at this current
document/embedded document level, without further inspection of any of
the excluded fields. This applies even if the excluded field contains
embedded documents that may have different access levels.
$$KEEP $redact returns or keeps all fields at this current
document/embedded document level, without further inspection of the
fields at this level. This applies even if the included field contains
embedded documents that may have different access levels.

select documents with sub arrays that match some critieria

I have a collections with documents such as:
{
_id: "1234",
_class: "com.acme.classA",
a_collection: [
{
otherdata: 'somedata',
type: 'a'
},
{
otherdata: 'bar',
type: 'a'
},
{
otherdata: 'foo',
type: 'b'
}
],
lastChange: ISODate("2014-08-17T22:25:48.918Z")
}
I want to find all document by id and a subset of the sub array. for example I want to find all documents with id "1234" and a_collection.type is 'a' giving this result:
{
_id: "1234",
_class: "com.acme.classA",
a_collection: [
{
otherdata: 'somedata',
type: 'a'
},
{
otherdata: 'bar',
type: 'a'
}
],
lastChange: ISODate("2014-08-17T22:25:48.918Z")
}
I have tried this :
db.collection_name.aggregate({
$match: {
'a_collection.type': 'a'
}
},
{
$unwind: "$a_collection"
},
{
$match: {
"a_collection.type": 'a'
}
},
{
$group: {
_id: "$_id",
a_collection: {
$addToSet: "$a_collection"
},
}
}).pretty()
but this doesnt return other properties ( such as 'lastChange' )
what is the correct way to do this ?
Are you using PHP?
And is this the only way you can get the "text"?
maybe you can rewrite it that it is like an JSON element.
something like that:
{
"_id": "1234",
"_class": "com.acme.classA",
"a_collection": [
{
"otherdata": "somedata",
"type": "a"
},
{
"otherdata": "bar",
"type": "a"
},
{
"otherdata": "foo",
"type": "b"
}
]
}
Then you can use the json_decode() function from PHP to make an array and then you can search and return only the needed data.
Edit: I read read false. do you search for a funktion like this?
db.inventory.find( {
$or: [ { _id: "1234" }, { 'a_collection.type': 'a' }]
} )
[Here][1] I found the code ;) [1]: http://docs.mongodb.org/manual/tutorial/query-documents/
this is the correct query:
db.collection_name.aggregate({
$match: {
'a_collection.type': 'a'
}
},
{
$unwind: "$a_collection"
},
{
$match: {
"a_collection.type": 'a'
}
},
{
$group: {
_id: "$_id",
a_collection: {
$addToSet: "$a_collection"
},
lastChange : { $first : "$lastChange" }
}
}).pretty()
Something is very strange about your desired query (and your pipelines). First of all, _id is a reserved field with a unique index on it. The result of finding all documents with _id = "1234" can only be 0 or 1 documents. Second, to find documents with a_collection.type = "a" for some element of the array a_collection, you don't need the aggregation framework. You just need a find query:
> db.test.find({ "a_collection.type" : "a" })
So all the work here appears to be winnowing the subarray of one document down to just those elements with a_collection.type = "a". Why do you have these objects in the same document if most of what you do is split them up and eliminate some to find a result set? How common and how truly necessary is it to harvest just the array elements with a_collection.type = "a"? Perhaps you want to model your data differently so a query like
> db.test.find({ <some condition>, "a_collection.type" : "a" })
returns you the correct documents. I can't say how you can do it best with the given information, but I can say that your current approach strongly suggests revision is needed (and I'm happy to help with suggestions if you include further information or post a new question).
I would agree with the answer you have submitted yourself, but for that in MongoDB 2.6 and greater there is a better way to do this with $map and $setDifference. Which wer both introduced at that version. But where available, this is much faster in the approach:
db.collection.aggregate([
{ "$match": { "a_collection.type": "a" } },
{ "$project": {
"$setDifference": [
{ "$map": [
"input": "$a_collection",
"as": "el",
"in": {
"$cond": [
{ "$eq": [ "$$el.type", "a" ] },
"$$el",
false
]
}
]},
[false]
]
}}
])
So that has no "group" or initial "unwind" which both can be costly options, along with the $match stage. So MongoDB 2.6 does it better.