MongoDB: How to match on the elements of an array? - mongodb

I have two collections as follows:
db.qnames.find()
{ "_id" : ObjectId("5a4da53f97a9ca769a15d49e"), "domain" : "mail.google.com", "tldOne" : "google.com", "clients" : 10, "date" : "2016-12-30" }
{ "_id" : ObjectId("5a4da55497a9ca769a15d49f"), "domain" : "mail.google.com", "tldOne" : "google.com", "clients" : 9, "date" : "2017-01-30” }
and
db.dropped.find()
{ "_id" : ObjectId("5a4da4ac97a9ca769a15d49c"), "domain" : "google.com", "dropDate" : "2017-01-01", "regStatus" : 1 }
I would like to join the two collections and choose the documents for which 'dropDate' field (from dropped collection) is larger than the 'date' filed (from qnames field). So I used the following query:
db.dropped.aggregate( [{$lookup:{ from:"qnames", localField:"domain",foreignField:"tldOne",as:"droppedTraffic"}},
{$match: {"droppedTraffic":{$ne:[]} }},
{$unwind: "$droppedTraffic" } ,
{$match: {dropDate:{$gt:"$droppedTraffic.date"}}} ])
but this query does not filter the records where dropDate < date. Anyone can give me a clue of why it happens?

The reason why you are not getting the record is
Date is used as a String in your collections, to make use of the comparison operators to get the desired result modify your collection documents using new ISODate("your existing date in the collection")
Please note even after modifying both the collections you need to modify your aggregate query, since in the final $match query two values from the same document is been compared.
Sample query to get the desired documents
db.dropped.aggregate([
{$lookup: {
from:"qnames",
localField:"domain",
foreignField:"tldOne",
as:"droppedTraffic"}
},
{$project: {
_id:1, domain:1,
regStatus:1,
droppedTraffic: {
$filter: {
input: "$droppedTraffic",
as:"droppedTraffic",
cond:{ $gt: ["$$droppedTraffic.date", "$dropDate"]}
}
}
}}
])
In this approach given above we have used $filter which avoids the $unwind operation

You should use $redact to compare two fields of the same document. Following example should work:
db.dropped.aggregate( [
{$lookup:{ from:"qnames", localField:"domain",foreignField:"tldOne",as:"droppedTraffic"}},
{$match: {"droppedTraffic":{$ne:[]} }},
{$unwind: "$droppedTraffic" },
{
"$redact": {
"$cond": [
{ "$lte": [ "$dropDate", "$droppedTraffic.date" ] },
"$$KEEP",
"$$PRUNE"
]
}
}
])

Related

How do I join collections based on a substring in MongoDB

Just trying to wrap my head around how to reference a collection based on a substring match.
Say I have one collection with chat information (including cell_number: 10 digits). I have another collection in which every document includes a field for area code (3 digits) and some other information about that area code, like so:
Area codes collection excerpt:
{
"_id" : ObjectId("62cf3d56580efcedf7c3b845"),
"code" : "206",
"region" : "WA",
"abbr" : "WA"
}
{
"_id" : ObjectId("62cf3d56580efcedf7c3b846"),
"code" : "220",
"region" : "OH",
"abbr" : "OH"
}
{
"_id" : ObjectId("62cf3d56580efcedf7c3b847"),
"code" : "226",
"region" : "Ontario",
"abbr" : "ON"
}
What I want to do is be able to grab out just the document from the area codes collection which has a "code" field value matching the first 3 characters of a cell number. A simple example is below. Note that in this example I have hardcoded the cell number for simplicity, but in reality it would be coming from the chat_conversations "parent" collection:
db.chat_conversations.aggregate([
{$match: {wait_start: {$gte: new Date("2020-07-01"), $lt: new Date("2022-07-12")}}},
{$lookup: {
from: "area_codes",
let: {
areaCode: "$code",
cellNumber: "2065551234"
},
pipeline: [
{$match: {
$expr: {
$eq: ["$$areaCode", {$substr: ["$$cellNumber", 0, 3]}]
}
}}
],
as: "area_code"
}},
]).pretty()
Unfortunately nothing I try here seems to work and I just get back an empty array from the area codes collection.
What am I missing here?
you should do a let on chat_converstion
or you should use let with vars and in expressions
this query should work for you
db.chat_conversations.aggregate([
{$match: {wait_start: {$gte: new Date("2020-07-01"), $lt: new Date("2022-07-12")}}},
{
$lookup: {
from: "area_codes",
let:{
"cell_Number":{$substr:["$cellNumber",0,3]}
},
pipeline:[
{
$match:{
$expr:{
$eq: [ "$code", "$$cell_Number" ]
}
}
}],
as: "data"
}
},
])

mongodb: how to compare String from document to the String constructed by expression in aggregation pipeline

I have a trouble with creating a view in MongoDB. I'm just starting with Mongo, so the question could look pretty silly.
I have the following document:
{
"_id" : NumberInt(12213054),
...
"_dt" : {
"_doc" : "time",
"time" : "01:30",
"date" : "04/02/18",
"tz" : "CET",
"tzoffset" : NumberInt(3600),
"uts" : NumberInt(1517704200)
},
...
}
I'm trying to create a view for documents that have a date '04/02/18'.
The type of "_dt.date" field is String. And I'm trying to compare it using $eq operator.
I have the following query to select the data:
{$match: {
"_dt.date": {
$eq: {
$concat: [
{$dateToString: {
format: "%d/%m/",
date: new Date("2018-02-04")
}},
{$substr: [
{$year : new Date("2018-02-04")},
2,
2
]}
]
}
}
}
}
But after I execute the query the result is empty.
The date string constructed by the subquery seems to be correct:
db.matches.aggregate({ $project: { date:
{
$concat: [
{$dateToString: {
format: "%d/%m/",
date: new Date("2018-02-04")
}},
{$substr: [
{$year : new Date("2018-02-08")},
2,
2
]}
]
}
}
}
)
Output is:
{
"_id" : NumberInt(12213054),
"date" : "04/02/18"
}
Does anyone have any idea why the first query doesn't work?
$match by design compares a field against a constant value with regular query operators.
For comparing two fields in mongodb you've to use $match with $expr with aggregation query operators in 3.6.
Compare query operators vs aggregation comparison operators.
{"$match":{
"$expr":{
"$eq":[
"$_dt.date",
{"$concat":[
{"$dateToString":{"format":"%d/%m/","date":new Date("2018-02-04")}},
{"$substr":[{"$year":new Date("2018-02-04")},2,2]}
]}
}
}}
Or
You can simply pass the date string created on client side and compare directly
{"$match":{"_dt.date":"04/02/18"}}

How to project single array field

starting from this document schema
{
"_id" : ObjectId("5a5cfde58c8a4a35a89726b4"),
"Names" : [
{ "Code" : "en", "Name" : "ITALY" },
{ "Code" : "it", "Name" : "ITALIA" }
],
"TenantID" : ObjectId("5a5cfde58c8a4a35a89726b2"),
...extra irrelevant fields
}
I need to get an object like this
{
"ID" : ObjectId("5a5cfde58c8a4a35a89726b4"),
"Name" : "ITALY"
}
Filtering by array's Code field (in the sample by 'en').
I wrote an aggregate query, this
db.Countries.aggregate([{$project: {_id:0, 'ID': '$_id', 'Names': {$filter: {input: '$Names', as: 'item', cond: {$eq: ['$$item.Code', 'en']}}}}},{$skip: 10},{$limit: 5}]);
that correctly return only documents with 'en' Names values, returning only the sub array matching elements.
Now I cannot find the way to return only the Name field value into a new 'Name' field.
Which is the best and most performing method to do this ?
Actually I'm trying on Mongo shell, but later I need to replicate this behavior with .NET driver.
Using MongoDB 3.6
Thanks
You can try below aggregation query.
$match to only consider documents where there is atleast one element in array matching input criteria.
$filter array on the input criteria and $arrayElemAt to project the single matched element.
$let to output name from matched object.
db.Countries.aggregate([
{"$match":{"Names.Code":"en"}},
{"$project":{
"_id":0,
"ID": "$_id",
"Name":{
"$let":{
"vars":{
"obj":{
"$arrayElemAt":[
{"$filter":{
"input":"$Names",
"as":"name",
"cond":{"$eq":["$$name.Code","en"]}
}},
0]
}
},
"in":"$$obj.Name"
}
}
}},
{"$skip": 10},
{"$limit": 5}
])

(mongo) How could i get the documents that have a value in array along with size

I have a mongo collection with something like the below:
{
"_id" : ObjectId("59e013e83260c739f029ee21"),
"createdAt" : ISODate("2017-10-13T01:16:24.653+0000"),
"updatedAt" : ISODate("2017-11-11T17:13:52.956+0000"),
"age" : NumberInt(34),
"attributes" : [
{
"year" : "2017",
"contest" : [
{
"name" : "Category1",
"division" : "Department1"
},
{
"name" : "Category2",
"division" : "Department1"
}
]
},
{
"year" : "2016",
"contest" : [
{
"name" : "Category2",
"division" : "Department1"
}
]
},
{
"year" : "2015",
"contest" : [
{
"name" : "Category1",
"division" : "Department1"
}
]
}
],
"name" : {
"id" : NumberInt(9850214),
"first" : "john",
"last" : "afham"
}
}
now how could i get the number of documents who have contest with name category1 more than one time or more than 2 times ... and so on
I tried to use size and $gt but couldn't form a correct result
Assuming that a single contest will never contain the same name (e.g. "Category1") value more than once, here is what you can do.
The absence of any unwinds will result in improved performance in particular on big collections or data sets with loads of entries in your attributes arrays.
db.collection.aggregate({
$project: {
"numberOfOccurrences": {
$size: { // count the number of matching contest elements
$filter: { // get rid of all contest entries that do not contain at least one entry with name "Category1"
input: "$attributes",
cond: { $in: [ "Category1", "$$this.contest.name" ] }
}
}
}
}
}, {
$match: { // filter the number of documents
"numberOfOccurrences": {
$gt: 1 // put your desired min. number of matching contest entries here
}
}
}, {
$count: "numberOfDocuments" // count the number of matching documents
})
Try this on for size.
db.foo.aggregate([
// Start with breaking down attributes:
{$unwind: "$attributes"}
// Next, extract only name = Category1 from the contest array. This will yield
// an array of 0 or 1 because I am assuming that contest names WITHIN
// the contest array are unique. If found and we get an array of 1, turn that
// into a single doc instead of an array of a single doc by taking arrayElemAt 0.
// Otherwise, "x" is not set into the doc AT ALL. All other vars in the doc
// will go away after $project; if you want to keep them, change this to
// $addFields:
,{$project: {x: {$arrayElemAt: [ {$filter: {
input: "$attributes.contest",
as: "z",
cond: {$eq: [ "$$z.name", "Category1" ]}
}}, 0 ]}
}}
// We split up attributes before, creating multiple docs with the same _id. We
// must now "recombine" these _id (OP said he wants # of docs with name).
// We now have to capture all the single "x" that we created above; docs without
// Category1 will have NO "x" and we don't want to include them in the count.
// Also, we KNOW that name can only be Category 1 but division could vary, so
// let's capture that in the $push in case we might want it:
,{$group: {_id: "$_id", x: {$push: "$x.division"}}}
// One more pass to compute length of array:
,{$addFields: {len: {$size: "$x"}} }
// And lastly, the filter for one time or two times or n times:
,{$match: {len: {$gt: 2} }}
]);
First, we need to flatten the document by the attributes and contest fields. Then to group by the document initial _id and a contest names counting different contests along the way. Finally, we filter the result.
db.person.aggregate([
{ $unwind: "$attributes" },
{ $unwind: "$attributes.contest" },
{$group: {
_id: {initial_id: "$_id", contest: "$attributes.contest.name"},
count: {$sum: 1}
}
},
{$match: {$and: [{"_id.contest": "Category1"}, {"count": {$gt: 1}}]}}]);

Mongo order by length of array

Lets say I have mongo documents like this:
Question 1
{
answers:[
{content: 'answer1'},
{content: '2nd answer'}
]
}
Question 2
{
answers:[
{content: 'answer1'},
{content: '2nd answer'}
{content: 'The third answer'}
]
}
Is there a way to order the collection by size of answers?
After a little research I saw suggestions of adding another field, that would contain number of answers and use it as a reference but may be there is native way to do it?
I thought you might be able to use $size, but that's only to find arrays of a certain size, not ordering.
From the mongo documentation:
http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24size
You cannot use $size to find a range of sizes (for example: arrays with more than 1 element). If you need to query for a range, create an extra size field that you increment when you add elements. Indexes cannot be used for the $size portion of a query, although if other query expressions are included indexes may be used to search for matches on that portion of the query expression.
Looks like you can probably fairly easily do this with the new aggregation framework, edit: which isn't out yet.
http://www.mongodb.org/display/DOCS/Aggregation+Framework
Update Now the Aggregation Framework is out...
> db.test.aggregate([
{$unwind: "$answers"},
{$group: {_id:"$_id", answers: {$push:"$answers"}, size: {$sum:1}}},
{$sort:{size:1}}]);
{
"result" : [
{
"_id" : ObjectId("5053b4547d820880c3469365"),
"answers" : [
{
"content" : "answer1"
},
{
"content" : "2nd answer"
}
],
"size" : 2
},
{
"_id" : ObjectId("5053b46d7d820880c3469366"),
"answers" : [
{
"content" : "answer1"
},
{
"content" : "2nd answer"
},
{
"content" : "The third answer"
}
],
"size" : 3
}
],
"ok" : 1
}
I use $project for this:
db.test.aggregate([
{
$project : { answers_count: {$size: { "$ifNull": [ "$answers", [] ] } } }
},
{
$sort: {"answers_count":1}
}
])
It also allows to include documents with empty answers.
But also has a disadvantage (or sometimes advantage): you should manually add all needed fields in $project step.
you can use mongodb aggregation stage $addFields which will add extra field to store count and then followed by $sort stage.
db.test.aggregate([
{
$addFields: { answers_count: {$size: { "$ifNull": [ "$answers", [] ] } } }
},
{
$sort: {"answers_count":1}
}
])
You can use $size attribute to order by array length.
db.getCollection('test').aggregate([
{$project: { "answers": 1, "answer_count": { $size: "$answers" } }},
{$sort: {"answer_count": -1}}])