How to $concat two fields inside a subdocument in mongodb - mongodb

WHAT I WANT TO ACHIEVE
Let us say I have this object:
{
"_id" : ObjectId("5aec063380a7490014e88792"),
"personal_info" : {
"dialing_code": "+44",
"phone_number": "67467885664"
}
}
I need to concat the two values personal_info.dialing_code (+44) and phone_number (67467885664) into one. +4467467885664 and compare it to a value. I need to retrieve a specific record from the database that will match the said value.
PROBLEM
I am having trouble concatinating two fields inside a subdocument and I am receiving this error:
{
"name": "MongoError",
"message": "$concat only supports strings, not object",
"ok": 0,
"errmsg": "$concat only supports strings, not object",
"code": 16702,
"codeName": "Location16702"
}
ATTEMPT #1
I have tried this:
UserModel.aggregate([
{ $unwind: '$personal_info' },
{$project: {
concat_p: {$concat: [
'$personal_info.dialing_code',
'$personal_info.phone_number'
]}
}}
])
It is giving me an error as mentioned above and in result I cannot do a $match right after.
ATTEMPT #2
I also tried this:
UserModel.aggregate([
{ $unwind: '$personal_info' },
{$project: {
p_dialing_code: '$personal_info.dialing_code',
p_phone_number: '$personal_info.phone_number',
concat_p: {$concat: [
'$p_dialing_code',
'$p_phone_number'
]}
}}
])
I have successfully took out the subdocument values one level however when I tried concatinating, it is producing me null values. This is the result I am getting:
{
"_id": "5af0998036daa90014129d6e",
"p_dialing_code": "+44",
"p_phone_number": "13231213213244",
"concat_p": null
}
I know how to do it on the $match pipeline but I have no luck concatinating the values inside the subdocument. Clearly, I need to do this first before I can compare. Thanks

It seems like you have different types under personal_info.dialing_code and personal_info.phone_number fields. In your example $concat is applied to every document in your collection and that's why you're getting an exception since $concat strictly expects its parameters to be strings.
So it will be working fine for document posted in your question but will throw an exception for something like this:
{
"_id" : ObjectId("5aec063380a7490014e88792"),
"personal_info" : {
"dialing_code": {},
"phone_number": "67467885664"
}
}
One way to fix this is to add $match condition before $project and use $type operator to get only documents having strings on those fields you want to concatenate.
db.UserModel.aggregate([
{
$match: {
$expr: {
$and: [
{ $eq: [ { $type: "$personal_info.dialing_code" }, "string" ] },
{ $eq: [ { $type: "$personal_info.phone_number" }, "string" ] }
]
}
}
},
{$project: {
concat_p: {$concat: [
"$personal_info.dialing_code",
"$personal_info.phone_number"
]}
}}
])

Related

MongoDB: FieldPath field names may not contain ‘.’

I'm trying to do a $lookup from a collection whose documents look this way:
{
"_id": {"$oid":"5f1957c7cdf25116937ed3ef"},
"idSensor": {"$numberLong":"3"},
...
"data":
{
"inicio":"2019-11-28T16:09:08+01:00",
"fin":"2019-11-28T16:09:18+01:00",
...
},
...
}
I deliver them to the $unwind stage just as "array", and then I try a $match:
[
...
{
"$match":
{
"$expr":
{
"$and":
[
{"array.idSensor": 3},
{"$gt": ["array.data.inicio", "ISODate('2021-02-28T23:59:59+01:00')"]},
{"$lt": ["array.data.fin", "ISODate('2021-03-15T00:00:01+01:00')"]}
]
}
}
},
...
]
Here's where I'm getting an error message:
"FieldPath field names may not contain '.'."
Filtering just by "array.idSensor" is OK, I checked it, so the problem lies within the dates.
Thanks in advance for your help.

MongoDB aggregate query return document based on match query and priority values

In a mongodb collection , i have following documents :
{"id":"1234","name":"John","stateCode":"CA"}
{"id":"1234","name":"Smith","stateCode":"CA"}
{"id":"1234","name":"Tony","stateCode":"GA"}
{"id":"3323", "name":"Neo","stateCode":"OH"}
{"id":"3323", "name":"Sam","stateCode":"US"}
{"id":"4343","name":"Bruce","stateCode":"NV"}
I am trying to write a mongo aggregate query which do following things:
match based on id field
Give more priority to document having values other than "NV" or "GA" in stateCode field.
If all the document have values either "NV" or "GA" then ignore the priority.
If any of the document have stateCode other than "NV" or "GA" , then return those document.
Example 1:
id = "1234"
then return
{"id":"1234","name":"John","stateCode":"CA"}
{"id":"1234","name":"Smith","stateCode":"CA"}
Example 2:
id = "4343"
then return
{"id":"4343","name":"Bruce","stateCode":"NV"}
Could you please help with a query to achieve this.
I tried with a query , but i am stuck with error:
Failed to execute script.
Error: command failed: {
"ok" : 0,
"errmsg" : "input to $filter must be an array not string",
"code" : 28651,
"codeName" : "Location28651"
} : aggregate failed
Query :
db.getCollection('emp').aggregate([{$match:{
'id': "1234"
}
},
{
$project: {
"data": {
$filter: {
input: "$stateCode",
as: "data",
cond: { $ne: [ "$data", "GA" ],$ne: [ "$data", "NV" ] }
}
}
}
}
])
I actually recommend you split this into 2 queries, first try to find documents with a different status code and if that fails then retrieve the rest.
With that said here is a working pipeline that does it in one go, Due to the fact we cant know in advance whether the condition is true or not we need to iterate all the documents who match the id, this fact makes it VERY inefficient in the case the id is shared by many documents, if this is not possible then using this pipeline is fine.
db.getCollection('emp').aggregate([
{
$match: {
'id': "1234"
}
},
{ //we have to group so we can check
$group: {
_id: null,
docs: {$push: "$$ROOT"}
}
},
{
$addFields: {
highPriorityDocs: {
$filter: {
input: "$docs",
as: "doc",
cond: {$and: [{$ne: ["$$doc.stateCode", "NV"]}, {$ne: ["$$doc.stateCode", "GA"]}]}
}
}
}
},
{
$project: {
finalDocs: {
$cond: [ // if size of high priority docs gt 0 return them.
{$gt: [{$ize: "$highPriorityDocs"}, 0]},
"$highPriorityDocs",
"$docs"
]
}
}
},
{
$unwind: "$finalDocs"
},
{
$replaceRoot: {newRoot: "$finalDocs"}
}
])
The last two stages are just to restore the original structure, you can drop them if you don't care about it.

Remove https/https from documents of mongodb data

How to remove http:// or https:// from the beginning and '/' from the end of the tags.Domain in MongoDB aggregation?
Sample document:
{
"_id" : ObjectId("5d9f074f5833c8cd1f685e05"),
"tags" : [
{
"Domain" : "http://www.google.com",
"rank" : 1
},
{
"Domain" : "https://www.stackoverflow.com/",
"rank" : 2
}
]
}
Taking an assumption that the Domain field in tags would contain valid URLs with valid appends and prepends of (https, http, //, /, com/, org/, /in)
The $trim operator is used to remove https://, http://, and / from tags.Domain
NOTE: This would not work for a URL that is already formatted and doesn't contain those characters at the beginning/end. Example: 'hello.com' would become 'ello.com', 'xyz.ins' would become 'xyz.in' etc.
Aggregation Query:
db.collection.aggregate([
{
$addFields:{
"tags":{
$map:{
"input":"$tags",
"as":"tag",
"in":{
$mergeObjects:[
"$$tag",
{
"Domain":{
$trim: {
"input": "$$tag.Domain",
"chars": "https://"
}
}
}
]
}
}
}
}
}
]).pretty()
Output:(demo)
{
"_id" : 2, //ObjectId
"tags" : [
{
"rank" : 1,
"Domain" : "www.google.com"
},
{
"rank" : 2,
"Domain" : "www.stackoverflow.com"
}
]
}
The solution ended up being longer than I expected it to be (I hope someone can find a more concise solution), but here you go:
db.test.aggregate([
{$unwind:"$tags"}, //unwind tags so that we can separately deal with http and https
{
$facet: {
"https": [{ // the first stage will...
$match: { // only contain documents...
"tags.Domain": /^https.*/ // that are allowed by the match the regex /^https.*/
}
}, {
$addFields: { // for all matching documents...
"tags.Domain": {"$substr": ["$tags.Domain",8,-1]} // we change the tags.Domain field to required substring (skip 8 characters and go on till the last character)
}
}],
"http": [{ // similar as above except we're doing the inverse filter using $not
$match: {
"tags.Domain": { $not: /^https.*/ }
}
}, {
$addFields: { // for all matching documents...
"tags.Domain": {"$substr": ["$tags.Domain",7,-1]} // we change the tags.Domain field to required substring (skip 7 characters and go on till the last character)
}
}
]
}
},
{ $project: { all: { $concatArrays: [ "$https", "$http" ] } } }, //we have two arrays at this point, so we just concatenate them both to have one array called "all"
//unwind and group the array by _id to get the document back in the original format
{$unwind: "$all"},
{$group: {
_id: "$all._id",
tags: {$push: "$all.tags"}
}}
])
For removing the / from the end, you can have another facet with a regex that matches the url (something like /.*\/$/ should work), and use that facet in the concat as well.
With help from: https://stackoverflow.com/a/49660098/5530229 and https://stackoverflow.com/a/44729563/5530229
As dnickless said in the first answered referred above, as always with the aggregation framework, it may help to remove individual stages from the end of the pipeline and run the partial query in order to get an understanding of what each individual stage does.

$elemMatch with distinct

I have some problems with distinct queries.
db.sessions.distinct("tests.device_serial")
[
"",
"5b34f4bf9854a",
"5b34f4bf98664",
"5b34f4bf98712",
"5b34f4bf9876b",
"5b34f4bf987c6"
]
I don't want to get the result with empty strings. I tried to run query:
db.sessions.distinct("tests.device_serial", {"tests.device_serial" : {$ne: ""}})
[ ]
Why I got empty array? Where is my mistake?
Guessing tests.device_serial is an array, here's your mistake :
db.sessions.distinct("tests.device_serial", {"tests.device_serial" : {$ne: ""}})
Query in your distinct command is filtering documents where the array 'tests' contains a field named device_serial with a value of "", and not only the fields in array.
To achieve what you want, you can use aggregation framework, unwind array to multiple documents, filter and group by null with an $addToSet command to get distinct values.
Here's the query :
db.sessions.aggregate(
[
{
$unwind: {
path : "$tests"
}
},
{
$match: {
"tests.device_serial":{$ne:""}
}
},
{
$group: {
"_id":null,
"device_serials":{$addToSet:"$tests.device_serial"}
}
},
]
);

Mongodb find inside sub array

I have a document that's setup like this:
{
_id : ObjectId(),
info : [
[
1399583281000,
20.13
],
[
1399583282000,
20.13
],
[
1399583283000,
20.13
],
[
1399583285000,
20.13
],
[
1399583286000,
20.13
]
]
}
This data could be spread across multiple documents. In general, each document contains data in the info for 59 periods (seconds).
What I would like to do is get all of the info data where the timestamp is greater than a specific time.
Any ideas how I would go about doing this?
Thank you
EDIT:
So, I've found that this seems to return all of the documents:
db.infos.find({
info:{
$elemMatch:{
0:{
$gt:1399583306000
}
}
}
})
But maybe I need to use this in an aggregate query? so that it will return just all the values?
Your on the right track, but there are a few things to note here, aside from the part that nested arrays ( and especially with anonymous keys) are not exactly a great way to store things, but as long as you consistently know the position then that should be reasonably okay.
There is a distinct difference between matching documents and matching "elements of an array". Though your current value would actually not match (your search value is not within the bounds of the document), if the value actually was valid your query correctly matches the "document" here, which contains a matching element in the array.
The "document" contains all of the array elements, even those that do not match, but the condition says the "document" does match, so it is returned. If you just want the matching "elements" then use .aggregate() instead:
db.infos.aggregate([
// Still match the document
{ "$match": {
"info": {
"$elemMatch": { "0": {"$gte": 1399583285000} }
}
}},
// unwind the array for the matched documents
{ "$unwind": "$info" },
// Match only the elements
{ "$match": { "info.0": { "$gte": 1399583285000 } } },
// Group back to the original form if you want
{ "$group": {
"_id": "$_id",
"info": { "$push": "$info" }
}}
])
And that returns just the elements that matched the condition:
{
"_id" : ObjectId("536c1145e99dc11e65ed07ce"),
"info" : [
[
1399583285000,
20.13
],
[
1399583286000,
20.13
]
]
}
Or course if you only ever expected one element to match, then you could simply use projection with .find()**:
db.infos.find(
{
"info":{
"$elemMatch":{
"0": {
"$gt": 1399583285000
}
}
}
},
{
"info.$": 1
}
)
But with a term like $gt you are likely to get multiple hits within a document so the aggregate approach is going to be safer considering that the positional $ operator is only going to return the first match.