MongoDB query to find in nested schema - node.js

This query is returning the first object but it should not return. Because it has the BU but in different domain. Its doing fine in single objects in collaborators. When there is multiple Its not behaving as expected. How can we do this any suggestions?
My criteria is In the collaborator array
Only BU name or
Only Domain or
Both BU and Domain it should return.
In below situation first one has same domain <{"domain": "xyz.com"}> but still its not returning. Why?
[
{
name: "1",
collaborators: [
{
"domain": "xyz.com"
},
{
"buName": "Vignesh B"
},
{
"domain": "yz.com"
},
{
"domain": "xyz.com",
"buName": "Vignesh B"
}
]
},
{
name: "2",
collaborators: [
{
"domain": "xyz.com",
"buName": "Vignesh BU"
}
]
},
{
name: "3",
collaborators: [
{
"domain": "xyz.com"
}
]
},
{
name: "4",
collaborators: [
{
"buName": "Vignesh BU"
},
{
"domain": "xyz.com"
},
{
"domain": "xyz.com",
"buName": "Vignesh BU"
}
]
}
]
db.collection.find({
$or: [
{
"collaborators.domain": "xyz.com",
"collaborators.buName": {
"$exists": false
}
},
{
"collaborators.buName": "Vignesh BU",
"collaborators.domain": {
"$exists": false
}
},
{
"collaborators.buName": "Vignesh BU",
"collaborators.domain": "xyz.com"
}
]
})

It is not returning the first document because the buName values in this document are "Vignesh B" and not "Vignesh BU". Only add an U in Vignesh B and it works.
Link to mongodb playground

I think there was a comment at wone point that said that the name: "1" document was expected to return (as it matches the second "Only Domain" criteria) but it is not currently. This is because you will need to use the $elemMatch operator since you are querying an array with multiple conditions.
The query should look as follows, as demonstrated in this playground example (note that I've changed the name: 3 document so that it would not match):
db.collection.find({
$or: [
{
"collaborators": {
$elemMatch: {
"domain": "xyz.com",
"buName": {
"$exists": false
}
}
}
},
{
"collaborators": {
$elemMatch: {
"buName": "Vignesh BU",
"domain": {
"$exists": false
}
}
}
},
{
"collaborators": {
$elemMatch: {
"buName": "Vignesh BU",
"domain": "xyz.com"
}
}
}
]
})
Why is this change needed? It is because of the semantics of how querying an array works in MongoDB. When querying on multiple nested conditions without using $elemMatch you are telling the database that different entries in the array can each individually satisfy the requirements. As shown in this playground example, that means that when you run this query:
db.collection.find({
"arr.str": "abc",
"arr.int": 123
})
The following document will match:
{
_id: 1,
arr: [
{
str: "abc"
},
{
int: 123
}
]
}
This is because the first entry in the array satisfies one of the query predicates while the other entry in the array satisfies the second predicate. Changing the query to use $elemMatch changes the semantics to specify that a single entry in the array must successfully satisfy all query predicate conditions which prevents the document above from matching.
In your specific situation the same thing was happening with your first set of conditions of:
{
"collaborators.domain": "xyz.com",
"collaborators.buName": {
"$exists": false
}
}
The first array item in the name: "1" document was matching the collaborators.domain condition. The problem was the second condition. While that same first array entry did not have a buName field, two of the other entries in the array did. Since there is no $elemMatch present, the database checked those other entries, found that the buName existed there, and that caused the query predicates to fail to match and for the document to not get returned. Adding the $elemMatch forces both of those checks to happen against the single entry in the array hence resolving the issue.

Related

mongoDB projection on array in an object

I have this document structure in the collection:
{"_id":"890138075223711744",
"guildID":"854557773990854707",
"name":"test-lab",
"game": {
"usedWords":["akşam","elma","akım"]
}
}
What is the most efficient way to get its fields except the array (it can be really large), and at the same time, see if an item exists in the array ?
I tried this:
let query = {_id: channelID}
const options = { sort: { name: 1 }, projection: { name: 1, "game.usedWords": { $elemMatch: { word}}}}
mongoClient.db(db).collection("channels").findOne(query, options);
but I got the error: "$elemMatch can not be used on nested fields"
If I've understood correctly you can use this query:
Using positional operator $ you can return only the matched word.
db.collection.find({
"game.usedWords": "akşam"
},
{
"name": 1,
"game.usedWords.$": 1
})
Example here
The output is only name and the matched word (also _id which is returned by default)
[
{
"_id": "890138075223711744",
"game": {
"usedWords": [
"akşam"
]
},
"name": "test-lab"
}
]

Mongoose How to push to nested array with condition

Suppose I have the following schema:
{
userId: docId,
skills: [
{
_id: SkillId,
endorsers: [
{
userId: idOfUser
}
]
}
]
}
What I want is a user should not be able to endorse a specific skill in a document more than once. First should find a specific document using its docId, and inside that, a specific skill using skillId, and then check if its endorsers array is empty or a specific user not endorsed yet, then push.
What I tried so far:
{
"userId": req.params.userId,
"skills": {
"$elemMatch": {
"skillId": ObjectId(req.params.skillId),
"$or": [
{"endorsers": []},
{"endorsers.userId":{$ne: endorser._id}}
]
}
}
},
{
"$push": { "skills.$[outer].endorsers": endorserData }
},
{
"arrayFilters": [
{ "outer.skillId": ObjectId(req.params.skillId) }
]
}
But this is not working, and along update, I need the updated result.
You can use addToSet function of mongoose arrays to keep their elements unique. https://mongoosejs.com/docs/api.html#mongoosearray_MongooseArray-addToSet

Push if not present or update a nested array mongoose [duplicate]

I have documents that looks something like that, with a unique index on bars.name:
{ name: 'foo', bars: [ { name: 'qux', somefield: 1 } ] }
. I want to either update the sub-document where { name: 'foo', 'bars.name': 'qux' } and $set: { 'bars.$.somefield': 2 }, or create a new sub-document with { name: 'qux', somefield: 2 } under { name: 'foo' }.
Is it possible to do this using a single query with upsert, or will I have to issue two separate ones?
Related: 'upsert' in an embedded document (suggests to change the schema to have the sub-document identifier as the key, but this is from two years ago and I'm wondering if there are better solutions now.)
No there isn't really a better solution to this, so perhaps with an explanation.
Suppose you have a document in place that has the structure as you show:
{
"name": "foo",
"bars": [{
"name": "qux",
"somefield": 1
}]
}
If you do an update like this
db.foo.update(
{ "name": "foo", "bars.name": "qux" },
{ "$set": { "bars.$.somefield": 2 } },
{ "upsert": true }
)
Then all is fine because matching document was found. But if you change the value of "bars.name":
db.foo.update(
{ "name": "foo", "bars.name": "xyz" },
{ "$set": { "bars.$.somefield": 2 } },
{ "upsert": true }
)
Then you will get a failure. The only thing that has really changed here is that in MongoDB 2.6 and above the error is a little more succinct:
WriteResult({
"nMatched" : 0,
"nUpserted" : 0,
"nModified" : 0,
"writeError" : {
"code" : 16836,
"errmsg" : "The positional operator did not find the match needed from the query. Unexpanded update: bars.$.somefield"
}
})
That is better in some ways, but you really do not want to "upsert" anyway. What you want to do is add the element to the array where the "name" does not currently exist.
So what you really want is the "result" from the update attempt without the "upsert" flag to see if any documents were affected:
db.foo.update(
{ "name": "foo", "bars.name": "xyz" },
{ "$set": { "bars.$.somefield": 2 } }
)
Yielding in response:
WriteResult({ "nMatched" : 0, "nUpserted" : 0, "nModified" : 0 })
So when the modified documents are 0 then you know you want to issue the following update:
db.foo.update(
{ "name": "foo" },
{ "$push": { "bars": {
"name": "xyz",
"somefield": 2
}}
)
There really is no other way to do exactly what you want. As the additions to the array are not strictly a "set" type of operation, you cannot use $addToSet combined with the "bulk update" functionality there, so that you can "cascade" your update requests.
In this case it seems like you need to check the result, or otherwise accept reading the whole document and checking whether to update or insert a new array element in code.
if you dont mind changing the schema a bit and having a structure like so:
{ "name": "foo", "bars": { "qux": { "somefield": 1 },
"xyz": { "somefield": 2 },
}
}
You can perform your operations in one go.
Reiterating 'upsert' in an embedded document for completeness
I was digging for the same feature, and found that in version 4.2 or above, MongoDB provides a new feature called Update with aggregation pipeline.
This feature, if used with some other techniques, makes possible to achieve an upsert subdocument operation with a single query.
It's a very verbose query, but I believe if you know that you won't have too many records on the subCollection, it's viable. Here's an example on how to achieve this:
const documentQuery = { _id: '123' }
const subDocumentToUpsert = { name: 'xyz', id: '1' }
collection.update(documentQuery, [
{
$set: {
sub_documents: {
$cond: {
if: { $not: ['$sub_documents'] },
then: [subDocumentToUpsert],
else: {
$cond: {
if: { $in: [subDocumentToUpsert.id, '$sub_documents.id'] },
then: {
$map: {
input: '$sub_documents',
as: 'sub_document',
in: {
$cond: {
if: { $eq: ['$$sub_document.id', subDocumentToUpsert.id] },
then: subDocumentToUpsert,
else: '$$sub_document',
},
},
},
},
else: { $concatArrays: ['$sub_documents', [subDocumentToUpsert]] },
},
},
},
},
},
},
])
There's a way to do it in two queries - but it will still work in a bulkWrite.
This is relevant because in my case not being able to batch it is the biggest hangup. With this solution, you don't need to collect the result of the first query, which allows you to do bulk operations if you need to.
Here are the two successive queries to run for your example:
// Update subdocument if existing
collection.updateMany({
name: 'foo', 'bars.name': 'qux'
}, {
$set: {
'bars.$.somefield': 2
}
})
// Insert subdocument otherwise
collection.updateMany({
name: 'foo', $not: {'bars.name': 'qux' }
}, {
$push: {
bars: {
somefield: 2, name: 'qux'
}
}
})
This also has the added benefit of not having corrupted data / race conditions if multiple applications are writing to the database concurrently. You won't risk ending up with two bars: {somefield: 2, name: 'qux'} subdocuments in your document if two applications run the same queries at the same time.

node elastic search strict match

can anyone tell me how to strict match in elasticsearch-js. here is my search
client.search({{
index: 'hash_tag',
type: 'hash_tag',
lenient:false,
body: {
query: {
match: {
tag_name: 'hash tag 1'
}
}
}
}).then(function (body) {
console.log("body", JSON.stringify(body));
}, function (error) {
console.trace(error.message);
})
this query search either hash, tag ,1 i'm looking for exact whole string match.here is my example index style.
{
"_index": "hash_tag",
"_type": "hash_tag",
"_id": "3483",
"_score": 0.019691018,
"_source": {
"id": "3483",
"labels": [
"hash_tag"
],
"tag_name": "hash tag 2"
}
}
by default elasticsearch will "tokenize" your text fields to add them to the inverted index, that's why you get results for each term used.
In order to get the full match you can have different approaches, the simplest would be to use a match_frase:
GET /megacorp/employee/_search
{
"query" : {
"match_phrase" : {
"about" : "rock climbing"
}
}
}
Another option would be to add that specific field with a mapping of not_analyzed, then the text wouldn't be tokenized.

MongoDB: Query model and check if document contains object or not, then mark / group result

I have a Model called Post, witch contains an property array with user-ids for users that have liked this post.
Now, i need to query the post model, and mark the returned results with likedBySelf true/false for use in by client - is this possible?
I dont have to store the likedBySelf property in the database, just modify the results to have that property.
A temporary solution i found was to do 2 queries, one that finds the posts that is liked by user x, and the ones that have not been liked by user x, and en map (setting likedBySelf true/false) and combine the 2 arrays and return the combined array. But this gives some limitations to other query functions such as limit and skip.
So now my queries looks like this:
var notLikedByQuery = Post.find({likedBy: {$ne: req.body.user._id}})
var likedByQuery = Post.find({likedBy: req.body.user._id})
(I'm using the Mongoose lib)
PS. A typical post can look like this (JSON):
{
"_id": {
"$oid": "55fc463c83b2d2501f563544"
},
"__t": "Post",
"groupId": {
"$oid": "55fc463c83b2d2501f563545"
},
"inactiveAfter": {
"$date": "2015-09-25T17:13:32.426Z"
},
"imageUrl": "https://hootappprodstorage.blob.core.windows.net/devphotos/55fc463b83b2d2501f563543.jpeg",
"createdBy": {
"$oid": "55c49e2d40b3b5b80cbe9a03"
},
"inactive": false,
"recentComments": [],
"likes": 8,
"likedBy": [
{
"$oid": "558b2ce70553f7e807f636c7"
},
{
"$oid": "559e8573ed7c830c0a677c36"
},
{
"$oid": "559e85bced7c830c0a677c43"
},
{
"$oid": "559e854bed7c830c0a677c32"
},
{
"$oid": "559e85abed7c830c0a677c40"
},
{
"$oid": "55911104be2f86e81d0fb573"
},
{
"$oid": "559e858fed7c830c0a677c3b"
},
{
"$oid": "559e8586ed7c830c0a677c3a"
}
],
"location": {
"type": "Point",
"coordinates": [
10.01941398718396,
60.96738099591897
]
},
"updatedAt": {
"$date": "2015-09-22T08:45:41.480Z"
},
"createdAt": {
"$date": "2015-09-18T17:13:32.426Z"
},
"__v": 8
}
#tskippe you can use a method like following to process whether the post is liked by the user himself and call the function anywhere you want.
var processIsLiked = function(postId, userId, doc, next){
var q = Post.find({post_id: postId});
q.lean().exec(function(err,res){
if(err) return utils.handleErr(err, res);
else {
if(_.find(doc.post.likedBy,userId)){ //if LikedBy array contains the user
doc.post.isLiked = true;
} else {
doc.post.isLiked = false;
}
});
next(doc);
}
});
}
Because you are using q.lean() you dont need to actually persist the data. You need to just process it , add isLiked field in the post and send back the response. **note that we are manuplating doc directly. Also you chan tweek it to accept doc containing array of posts and iterating it and attach an isLiked field to each post.
I found that MongoDB's aggregation with $project tequnique was my best bet. So i wrote up an aggregation like this.
Explanation:
Since i want to keep the entire document, but $project purpose is to modify the docs, thus you have to specify the properties you want to keep. A simple way of keeping all the properties is to use "$$ROOT".
So i define a $project, set all my original properties to doc: "$$ROOT", then create a new property "likedBySelf", which is marked true / false if a specified USERID is in the $likedBy set.
I think that this is more clean and simple, than querying every single model after a query to set a likedBySelf flag. It may not be faster, but its cleaner.
Model.aggregate([
{ $project: {
doc: "$$ROOT",
likedBySelf: {
$cond: {
"if": { "$setIsSubset": [
[USERID],
"$likedBy"
]},
"then": true,
"else": false
}
}
}}
]);

Resources