How to get the items around a specific item? - mongodb

I'd like to get x items before and y items after (the neighbors of) the record with "lastseen":true:
// (_id fields omitted)
{ "msg": "hello 1" }
{ "msg": "hello 2" }
{ "msg": "hello 3", "lastseen": true }
{ "msg": "hello 4" }
{ "msg": "hello 5" }
For example, if I query with x=1 and y=1 the result should be:
// (_id fields omitted)
{ "msg": "hello 2" }
{ "msg": "hello 3", "lastseen": true }
{ "msg": "hello 4" }
What are my options in mongodb to achieve that?

It must be simpler to implement the logic on client side with several queries. Assuming you documents are ordered by _id:
findOne({"lastseen":true})
find({_id: {$lt: <_id from the previous query>}}).sort({_id:-1}).limit(1)
find({_id: {$gt: <_id from the first query>}}).sort({_id:1}).limit(1)
The only way to do it in a single query I can imagine is to group the documents into array, and then use $indexOfArray in combination with $slice:
db.collection.aggregate([
// ensure lastseen is present to calculate index properly
{ $addFields: {lastseen: { $ifNull: [ "$lastseen", false ] } } },
// get all documents into array
{ $group: { _id:null, docs: { $push:"$$ROOT" } } },
// get index of first matched document
{ $project: { docs:1, match: { $indexOfArray: [ "$docs.lastseen", true ] } } },
// slice the array
{ $project: { docs: { $slice: [ "$docs", { $subtract: [ "$match", 1 ] } , 3 ] } } },
// remove added lastseen
{ $project: { docs:
{ $map: {
input: "$docs",
as: "doc",
in: { $cond: {
if: "$$doc.lastseen",
then: "$$doc",
else: { $arrayToObject: { $filter: {
input: { $objectToArray: "$$doc" },
as: "field",
cond: { $ne: [ "$$field.k", "lastseen" ] }
} } }
} }
} }
} },
// un-group documents from the array
{ $unwind: "$docs" },
{ $replaceRoot: {newRoot:"$docs"}}
]);
but I doubt efficiency of such query.

Answer is very short you can use skip() to skip how many you want
// (_id fields omitted)
{ "msg": "hello 1" }
{ "msg": "hello 2" }`
{ "msg": "hello 3", "lastseen": true }
{ "msg": "hello 4" }
{ "msg": "hello 5" }
Command:
db.collection.find({},{_id:0}).skip(1)

Related

How to convert time string to ISO format in mongodb aggregation?

All document in my collection are same as this:
{
"_id": {
"$oid": "6396c58284bfad036f960288"
},
"title": "This is a nice title.",
"time": "3266 sec"
}
But I need to convert time field like this:
{
"_id": {
"$oid": "6396c58284bfad036f960288"
},
"title": "This is a nice title.",
"time": "PT3266S"
}
$set - Set the time field. With $regexFind to capture the matched group.
$set - Set the time field. With $concat to concat string with "PT", the first element of time.captures array and "S".
db.collection.aggregate([
{
$set: {
time: {
$regexFind: {
input: "$time",
regex: "(\\d+) sec"
}
}
}
},
{
$set: {
time: {
$concat: [
"PT",
{
$arrayElemAt: [
"$time.captures",
0
]
},
"S"
]
}
}
}
])
Demo # Mongo Playground
Or you can combine both $set stages into one:
db.collection.aggregate([
{
$set: {
time: {
$concat: [
"PT",
{
$arrayElemAt: [
{
$getField: {
field: "captures",
input: {
$regexFind: {
input: "$time",
regex: "(\\d+) sec"
}
}
}
},
0
]
},
"S"
]
}
}
}
])

MongoDB Aggregation - Show all sub documents with some information from parent document

My invoices collection looks something like this:
[
{
inv_name: "Client 1",
inv_date: 2022-12-20T05:09:09.803Z,
inv_ready: false,
inv_payments: [
{
id: "123",
pay_amount: 32.45
},
{
id: "456",
pay_amount: 55.60
}
]
},
{
inv_name: "Client 2",
inv_date: 2022-12-19T05:09:09.803Z,
inv_ready: true,
inv_payments: [
{
id: "459",
pay_amount: 67.45
},
{
id: "556",
pay_amount: 30.60
}
]
}
]
I know how to create an array of just the sub docs using $project and $unwind. The goal is to just add some data from the parent document to each object like this.
[
{
"client": "Client 1",
"pay_amount": 32.45,
"pay_date": "123"
},
{
"client": "Client 1",
"pay_amount": 55.6,
"pay_date": "456"
},
{
"client": "Client 2",
"pay_amount": 67.45,
"pay_date": "459"
},
{
"client": "Client 2",
"pay_amount": 30.6,
"pay_date": "556"
}
]
Here is what I tried:
db.invoices.aggregate([
{
"$project": {
_id: 0,
"inv_payments": {
$reduce: {
input: "$inv_payments",
initialValue: [],
in: {
$concatArrays: [
[
{
client: "$this.name",
pay_id: "$$this.id",
pay_amount: "$$this.pay_amount"
}
],
"$inv_payments"
]
}
}
}
}
},
{
$unwind: "$inv_payments"
},
{
$replaceRoot: {
newRoot: "$inv_payments"
}
}
])
I think I am close. I am not sure where the bug is. Any ideas would be greatly appreciated!
Besides the typo error mentioned in the comment, the way you used the $reduce operator is incorrect. You didn't work with the accumulator variable ($$value).
Rather than using $reduce, use the $map operator which is simpler in your scenario:
Solution 1: $map
{
"$project": {
_id: 0,
"inv_payments": {
$map: {
input: "$inv_payments",
in: {
client: "$inv_name",
pay_date: "$$this.id",
pay_amount: "$$this.pay_amount"
}
}
}
}
}
Demo ($map) # Mongo Playground
Solution 2: $reduce
For $reduce operator, your query should be:
{
"$project": {
_id: 0,
"inv_payments": {
$reduce: {
input: "$inv_payments",
initialValue: [],
in: {
$concatArrays: [
[
{
client: "$inv_name",
pay_date: "$$this.id",
pay_amount: "$$this.pay_amount"
}
],
"$$value"
]
}
}
}
}
}
Demo ($reduce) # Mongo Playground

MongoDB aggregate filter returns null

In MongoDB, I have a messages' collection (find it below):
I'm interested in querying the parent document by id, and say filtering contactedNumberMessages to include only incoming messages (those having direction "in") so I wrote the following code with Mongoose, however contactedNumberMessages is null in the returned data, any clue as to why I'm getting null? Thank you
Messages.aggregate([
{
$match: {
_id: id
}
},
{
$project: {
messaging: {
ourNumber: 1,
messages: {
contact: 1,
contactedNumberMessages: {
$filter: {
input: "$contactedNumberMessages",
as: "message",
cond: {
$eq: ["$$message.direction", "out"]
}
}
}
}
}
}
}
]);
{
"_id": {
"$oid": "612f4e32aa56064f1608c2eb"
},
"messaging": [
{
"ourNumber": "+15123568549",
"messages": [
{
"contact": "+21629000111",
"contactedNumberMessages": [
{
"direction": "out",
"content": "Hello!",
"when": {
"$date": "2021-09-23T23:00:00.000Z"
},
"nature": "SMS"
},
{
"direction": "in",
"content": "Hi!",
"when": {
"$date": "2021-09-23T23:00:00.000Z"
},
"nature": "SMS"
}
]
}
]
}
]
}
pls refer to example here: https://mongoplayground.net/p/9toRoa_5IE9
you should use something like below in aggregation:
[{$match: {
_id: ObjectId('612f4e32aa56064f1608c2eb')
}}, {$unwind: {
path: '$messaging',
}}, {$unwind: {
path: '$messaging.messages',
}}, {$project: {
messaging: {
ourNumber: 1,
messages: {
contact: 1,
contactedNumberMessages: {
$filter: {
input: "$messaging.messages.contactedNumberMessages",
as: "message",
cond: {
$eq: ["$$message.direction", "out"]
}
}
}
}
}
}}]
As you have nested array within array and sub array that filter stage was not getting correct output, i have added unwind to get the normal array for field:messaging.messages.contactedNumberMessages
if needed you can again do groupby to ensure you get document in expected format as after unwind it will create multiple documents in aggregation for each documents in array which in unwinded.

How to use $addFields in mongo to add elements to just existing documents?

I am trying to add a new field to an existing document by using a combination of both $ifnull and $cond but an empty document is always added at the end.
Configuration:
[
{
line: "car",
number: "1",
category: {
FERRARI: {
color: "blue"
},
LAMBORGHINI: {
color: "red"
}
}
},
{
line: "car",
number: "2",
category: {
FERRARI: {
color: "blue"
}
}
}
]
Query approach:
db.collection.aggregate([
{
$match: {
$and: [
{ line: "car" },
{ number: { $in: ["1", "2"] } }
]
}
},
{
"$addFields": {
"category.LAMBORGHINI.number": {
$cond: [
{ "$ifNull": ["$category.LAMBORGHINI", false] },
"$number",
"$$REMOVE"
]
}
}
},
{
$group: {
_id: null,
CATEGORIES: {
$addToSet: "$category.LAMBORGHINI"
}
}
}
])
Here is the link to the mongo play ground:
https://mongoplayground.net/p/RUnu5BNdnrR
I tried the mentioned query but I still get that ugly empty set added at the end.
$$REMOVE will remove last field/key, from your field category.LAMBORGHINI.number the last field is number that is why it is removing number from the end, you can try another approach,
specify just category.LAMBORGHINI, if condition match then it will return object of current category.LAMBORGHINI and number object after merging using $mergeObjects
{
"$addFields": {
"category.LAMBORGHINI": {
$cond: [
{ "$ifNull": ["$category.LAMBORGHINI", false] },
{
$mergeObjects: [
"$category.LAMBORGHINI",
{ number: "$number" }
]
},
"$$REMOVE"
]
}
}
}
Playground

MongoDB compute fields from 2 subdocuments and produce new document

I have collection that hold 2 subdocuments fsgeneral and balancesheet. In order to compute the Book Value for different fye, I will have take the value balancesheet.shareholderFund / fsgeneral.dilutedShares.
The question is it possible to use aggregate to do this and how can this be achieved?
My input collection as below:
{
"counter": "APPLE",
"fsgeneral": {
"0": {
"fye": "Mar-01",
"dilutedShares": {
"value": 10
}
},
"1": {
"fye": "Mar-02",
"dilutedShares": {
"value": 10
}
}
},
"balancesheet": {
"0": {
"fye": "Mar-01",
"shareholderFund": {
"value": 200
}
},
"1": {
"fye": "Mar-02",
"shareholderFund": {
"value": 400
}
}
}
Expected result:
{
"counter": "APPLE",
"output": {
"0": {
"fye": "Mar-01",
"bookvalue": {
"value": 20
}
},
"1": {
"fye": "Mar-02",
"bookvalue": {
"value": 40
}
}
}
}
I have tried a few aggregates but failed to come to how 2 subdocuments can be used at the same time.
You can try,
$objectToArray convert object to array and input in $map, $map will iterate loop,
$reduce to iterate loop of fsgeneral and check key matches with balancesheet then divide using $divide
$arrayToObject, $map will return array and this will convert to again object
db.collection.aggregate([
{
$project: {
counter: 1
output: {
$arrayToObject: {
$map: {
input: { $objectToArray: "$balancesheet" },
as: "a",
in: {
k: "$$a.k",
v: {
fye: "$$a.v.fye",
bookvalue: {
value: {
$reduce: {
input: { $objectToArray: "$fsgeneral" },
initialValue: 0,
in: {
$cond: [
{ $eq: ["$$this.k", "$$a.k"] },
{ $divide: ["$$a.v.shareholderFund.value", "$$this.v.dilutedShares.value"] },
"$$value"
]
}
}
}
}
}
}
}
}
}
}
}
])
Playground