Apply an expression to each array element in document - javascript

I've got a sample document that I'm trying to project within a MongoDB aggregate pipeline. I'm testing with a single document that looks roughly like this:
{
"_id" : "",
"title" : "Questions",
"sortIndex" : 0,
"topics" : [
{
"_id" : "",
"title" : "Creating a Question",
"sortIndex" : 1,
"thumbnail" : "CreatingAQuestion.jpg",
"seenBy" : [ "user101", "user202" ],
"pages" : [
{
"visual" : "SelectPlanets.gif",
"text" : "Some Markdown"
}
]
},
{
"_id" : "",
"title" : "Deleting a Question",
"sortIndex" : 0,
"thumbnail" : "DeletingAQuestion.jpg",
"seenBy" : [ "user101" ],
"pages" : [
{
"visual" : "SelectCard.gif",
"text" : "Some Markdown"
}
]
}
]
}
The output I'm trying to obtain is something along these lines:
{
"_id" : "",
"title" : "Questions",
"topics" : [
{
"title" : "Creating a Question",
"thumbnail" : "CreatingAQuestion.jpg",
"seen" : true
},
{
"title" : "Deleting a Question",
"thumbnail" : "DeletingAQuestion.jpg",
"seen" : false
}
]
}
Specifically the bit I'm struggling with is the seen flag.
I've read the docs which state:
When projecting or adding/resetting a field within an embedded document...
... Or you can nest the fields:
contact: { address: { country: <1 or 0 or expression> } }
I wish to use an expression and I took note of the following:
When nesting the fields, you cannot use dot notation inside the embedded document to specify the field, e.g. contact: { "address.country": <1 or 0 or expression> } is invalid.
So I'm trying to work out how to "reference" a subdocument field within an expression. That quote suggests I can't use dot notation but when I can't seem to get it working with nested notation either. Here's what I've got so far:
db
.getCollection('chapters')
.aggregate([
{
$project: {
title: 1,
topics: {
title: 1,
thumbnail: 1,
publishedAt: 1,
test: "$seenBy",
seen: { $in: ["user202", "$seenBy"] },
}
}
}
])
So I've hard coded user202 into my query for now, and expected to see true and false for the 2 documents. I've also put in a test field to map out the seenBy field from the sub-document. What this produces is:
{
"_id" : "",
"title" : "Questions",
"topics" : [
{
"title" : "Creating a Question",
"thumbnail" : "CreatingAQuestion.jpg",
"test" : [
"user101",
"user202"
],
"seen" : true
},
{
"title" : "Deleting a Question",
"thumbnail" : "DeletingAQuestion.jpg",
"test" : [
"user101",
"user202"
],
"seen" : true
}
]
}
So obviously my "$seenBy" isn't accessing the correct topic because the test field contains the data from the 1st document.
So ultimately my question is, how can I access the seenBy field within a subdocument, referring to the current subdocument so I can create an expression?
Note: I have got this working with multiple $project and an $unwind but wanted to try compress/clean it up a bit.

You really need to use $map here. Simply notating the array in projection ( which is a bit of a boon since MongoDB 3.2 ) does not really cut it when you need a localized value for the current element. That is what you need and it's what $map provides:
db.getCollection('chapters').aggregate([
{ $project: {
title: 1,
topics: {
$map: {
input: "$topics",
as: "t",
in: {
title: "$$t.title",
thumbnail: "$$t.thumbnail",
publishedAt: "$$t.publishedAt",
test: "$$t.seenBy",
seen: { $in: ["user202", "$$t.seenBy"] },
}
}
}}
])
So for each element the current value of "seenBy" as a property is being tested by the expression. Without the $map that is not possible, and you can only really notate the "whole" array. Which is really not what you want to test here.

Related

Mongodb query combining $text search with $regex

My dilemma is that mongodb $text searches must match an exact word: e.g. if trying to match a post with 'testing123' a search for 'test' will not match, but using $regex will match. Now, I want to make use of indexes too, but I also want partial matches.
My thinking is that I could combine them with an $or operator, but it is not working. Is this possible? Each query alone inside the $or work, but when combining them I get no matches.
If this is not possible, I have found a pretty good solution, here , but I would like the combined $or to work if possible, but any other suggestions are welcome.
const posts = await Post.find({
name: { $regex: 'foo', $options: 'i' },
$or: [
{ $text: { $search: text, $caseSensitive: false } },
{ text: { $regex: text, $options: 'i' } },
],
});
One way of doing this is to downcase the text into another field, then use $regex search on that field.
You have text that you want to search for any substring case insensitively:
MongoDB Enterprise ruby-driver-rs:PRIMARY> db.foo.insert({foo:'hello world TESTING123'})
WriteResult({ "nInserted" : 1 })
Step 1: add another field which stores the text in lower case.
MongoDB Enterprise ruby-driver-rs:PRIMARY> db.foo.insert({foo:'hello world TESTING123',foo_lower:'hello world testing123'})
Step 2: add index.
MongoDB Enterprise ruby-driver-rs:PRIMARY> db.foo.createIndex({foo_lower:1})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 2,
"numIndexesAfter" : 3,
"commitQuorum" : "votingMembers",
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1597711723, 7),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1597711723, 7)
}
Step 3: downcase the query to "testing123"
Step 4: use $regex.
MongoDB Enterprise ruby-driver-rs:PRIMARY> db.foo.find({foo_lower:{$regex:'testing123'}})
{ "_id" : ObjectId("5f3b2498f885e53d90f30979"), "foo" : "hello world TESTING123", "foo_lower" : "hello world testing123" }
MongoDB Enterprise ruby-driver-rs:PRIMARY> db.foo.find({foo_lower:{$regex:'testing123'}}).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "test.foo",
"indexFilterSet" : false,
"parsedQuery" : {
"foo_lower" : {
"$regex" : "testing123"
}
},
"queryHash" : "0D14CC56",
"planCacheKey" : "1974A2D4",
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"filter" : {
"foo_lower" : {
"$regex" : "testing123"
}
},
"keyPattern" : {
"foo_lower" : 1
},
"indexName" : "foo_lower_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"foo_lower" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"foo_lower" : [
"[\"\", {})",
"[/testing123/, /testing123/]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "serene",
"port" : 14420,
"version" : "4.4.0",
"gitVersion" : "563487e100c4215e2dce98d0af2a6a5a2d67c5cf"
},
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1597711761, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1597711761, 1)
}

Remove array from objects field in MongoDB

I am trying to remove an element of Array just by finding it via the ObjectId in the array element. Following MongoDB opposite of $addToSet to '$removeFromSet', I should've been able to do:
db.collection.update(
{name: 'nameOfNode'},
{$pull: { activities: ["5e7d16e9736bb64bdd158c13"]}})
or
db.collection.update(
{name: 'nameOfNode'},
{$pull: { activities: ["5e7d16e9736bb64bdd158c13", "Materi"]}})
Yet both of these codes above doesn't work, does if there is another way to remove them by just one single value, or if that isn't possible, remove them with the two values?
Note: I have removed some confidential information from the screenshot, hence the supposed gaps between the letters are not spaces, but covered texts. The strings I've tried to use to refer to them are identical though, no difference.
I see 2 problems with the code:
- The value is not a member of the activities array, it is a member of an array contain in the activities array
- Searching for a string like activities: ["5e7d16e9736bb64bdd158c13"] will not match a value of type ObjectId.
I ran a quick demonstration of matching an ObjectId:
MongoDB Enterprise replset:PRIMARY> db.updtest.find()
{ "_id" : ObjectId("5e7d354ccede22b56b5335bc"), "name" : "nameOfNode", "activities" : [ [ ObjectId("5e7d354ccede22b56b5335b9"), "Check-in" ], [ ObjectId("5e7d354ccede22b56b5335ba"), "Materi" ], [ ObjectId("5e7d354ccede22b56b5335bb"), "Materi" ] ] }
MongoDB Enterprise replset:PRIMARY> db.updtest.update({"name" : "nameOfNode"},{$pull:{"activities.0":ObjectId("5e7d354ccede22b56b5335b9")}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
MongoDB Enterprise replset:PRIMARY> db.updtest.find()
{ "_id" : ObjectId("5e7d354ccede22b56b5335bc"), "name" : "nameOfNode", "activities" : [ [ "Check-in" ], [ ObjectId("5e7d354ccede22b56b5335ba"), "Materi" ], [ ObjectId("5e7d354ccede22b56b5335bb"), "Materi" ] ] }
Note that while this successfully removes the ObjectId value, it does not remove the containing array, which is what I think you wanted.
If you are using MongoDB 4.2, you could use $filter to remove the entire subelement, like:
db.updtest.update(
{"name" : "nameOfNode"},
[ {$set:{
activities:{
$filter:{
input:"$activities",
cond:{$ne:["$this.0",ObjectId("5e7d16e9736bb64bdd158c13")]}
}
}
}}]
)
If you can modify the schema, insert activities as objects instead of as sub-arrays. For example, if each activity were of the form {id:... type: ...}, you could match the specific id using $pull:
MongoDB Enterprise replset:PRIMARY> db.updtest.find()
{ "_id" : ObjectId("5e7d3939cede22b56b5335c4"), "name" : "nameOfNode", "activities" : [ { "id" : ObjectId("5e7d3939cede22b56b5335c1"), "type" : "Check-in" }, { "id" : ObjectId("5e7d3939cede22b56b5335c2"), "type" : "Materi" }, { "id" : ObjectId("5e7d3939cede22b56b5335c3"), "type" : "Materi" } ] }
MongoDB Enterprise replset:PRIMARY> db.updtest.update({"name" : "nameOfNode"},{$pull:{"activities":{id:ObjectId("5e7d3939cede22b56b5335c2")}}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
MongoDB Enterprise replset:PRIMARY> db.updtest.find()
{ "_id" : ObjectId("5e7d3939cede22b56b5335c4"), "name" : "nameOfNode", "activities" : [ { "id" : ObjectId("5e7d3939cede22b56b5335c1"), "type" : "Check-in" }, { "id" : ObjectId("5e7d3939cede22b56b5335c3"), "type" : "Materi" } ] }

Mongodb: project to return the field in array of object without using unwind

I need get value of field without using $unwind
because $unwind and $group takes much longer time.
My document (looks like):
{
"_id" : ObjectId("512e28984815cbfcb21646a7"),
"providers" : [
{
"list" : [
{
"code" : "ATT",
"descr" : "Attending"
}
],
"Name" : "John Doe",
"prvdId" : "1"
},
{
"list" : [
{
"code" : "RFR",
"descr" : "Referring"
},
{
"code" : "TRT",
"descr" : "Treating"
}
],
"Name" : "Smith William",
"prvdId" : "2"
}
]
}
cond is if "code" : "TRT", than get "prvdId"
Expected result is
{"prvdId" : "2"}
Use $filter with $in to look for a match in nested array followed by $let with $arrayElemAt to output prvdId in 3.4.
db.col.aggregate([
{"$match":{"providers.list.code":"TRT"}},
{"$project":{
"_id":0,
"prvdId":{
"$let":{
"vars":{
"providersl":{
"$filter":{
"input":"$providers",
"as":"providerf",
"cond":{"$in":["TRT","$$providerf.list.code"]}
}
}
},
"in":{"$arrayElemAt":["$$providersl.prvdId",0]}
}
}
}}
])
According to above mentioned description as a solution expected result can be obtained by using $elemMatch operator used to search an array element into find operation.
db.collection.find({
providers: {
$elemMatch: {
list: {
$elemMatch: {
code: "TRT"
}
}
}
}
}, {
'providers.$.prvdId': 1
})

How to get element from collection in Meteor if it multi-dimensional array or object or both?

I have collection "groups". like this:
{
"_id" : "e9sc7ogDp8pwY2uSX",
"groupName" : "one",
"creator" : "KPi9JwvEohKJsFyL4",
"eventDate" : "",
"isEvent" : true,
"eventStatus" : "Event announced",
"user" : [
{
"id" : "xfaAjgcSpSeGdmBuv",
"username" : "1#gmail.com",
"email" : "1#gmail.com",
"order" : [ ],
"price" : [ ],
"confirm" : false,
"complete" : false,
"emailText" : ""
},
...
],
...
"buyingStatus" : false,
"emailTextConfirmOrder" : " With love, your Pizzaday!! "
}
How can I get a value of specific element? For example i need to get value of "Groups.user.confirm" of specific group and specific user.
I tried to do so in methods.js
'pizzaDay.user.confirm': function(thisGroupeId, thisUser){
return Groups.find({ _id: thisGroupeId },{"user": ""},{"id": thisUser}).confirm
},
but it returns nothing.
Even in mongo console I can get just users array using
db.groups.findOne({ _id: "e9sc7ogDp8pwY2uSX"},{"user": ""})
The whole code is github
http://github.com/sysstas/pizzaday2
Try the following query:-
db.groups.aggregate(
[
{
$match:
{
_id: thisGroupeId,
"user.id": thisUser
}
},
{
$project:
{
groupName : 1,
//Add other fields of `user` level, if want to project those as well.
user:
{
"$setDifference":
[{
"$map":
{
"input": "$user",
"as": "o",
"in":
{
$eq : ["$$o.id" , thisUser] //Updated here
}
}
},[false]
]
}
}
}
]);
The above query will give the object(s) matching the query in $match inside user array. Now you can access any field you want of that particular object.
'pizzaDay.user.confirm': function(){
return Groups.findOne({ _id: thisGroupeId }).user.confirm;
I resolved it using this:
Template.Pizzaday.helpers({
confirm: function(){
let isConfirm = Groups.findOne(
{_id: Session.get("idgroupe")}).user.filter(
function(v){
return v.id === Meteor.userId();
})[0].confirm;
return isConfirm;
},
});
But I still think that there is some much elegant way to do that.

updating mongodb using $push operator in angular-meteor

I am trying to push arrays from one collection to another.
this is the code I used in my server js
updateSettings: function(generalValue) {
let userId = Meteor.userId();
let settingsDetails = GeneralSettings.findOne({
"_id": generalValue
});
Meteor.users.update({
_id: userId
}, {
$push: {
"settings._id": generalValue,
"settings.name": settingsDetails.name,
"settings.description": settingsDetails.description,
"settings.tag": settingsDetails.tag,
"settings.type": settingsDetails.type,
"settings.status": settingsDetails.status
}
})
}
updateSettings is the meteor method. GeneralSettings is the first collection and user is the second collection. I want to push arrays from GeneralSettings to users collection. While I try this the result i got is like
"settings" : {
"_id" : [
"GdfaHPoT5FXW78aw5",
"GdfaHPoT5FXW78awi"
],
"name" : [
"URL",
"TEXT"
],
"description" : [
"https://docs.mongodb.org/manual/reference/method/db.collection.update/",
"this is a random text"
],
"tag" : [
"This is a demo of an Url selected",
"demo for random text"
],
"type" : [
"url",
"text"
],
"status" : [
false,
false
]
}
But the output I want is
"settings" : {
"_id" : "GdfaHPoT5FXW78aw5",
"name" : "URL",
"description" :
"https://docs.mongodb.org/manual/reference/method/db.collection.update/",
"tag" :"This is a demo of an Url selected",
"type" : "url",
"status" : false
},
What changes to be made in my server.js inorder to get this output
This is one case where you "do not want" to use "dot notation". The $push operator expects and Object or is basically going to add the "right side" to the array named in the "left side key":
// Make your life easy and just update this object
settingDetails._id = generalValue;
// Then you are just "pushing" the whole thing into the "settings" array
Meteor.users.update(
{ _id: userId },
{ "$push": { "settings": settingDetails } }
)
When you used "dot notation" on each item, then that is asking to create "arrays" for "each" of the individual "keys" provided. So it's just "one" array key, and the object to add as the argument.

Categories