MongoDB Query on multiple field condition - mongodb

I have a MongoDB model that is currently like this (this is the stripped version):
{
title: String,
type: {
type: String,
lowercase: true,
enum: ['event', 'regular', 'project'],
},
project_start_time: Date,
project_end_time: Date,
regular_start_date: Date,
regular_end_date: Date,
events: [{
id: Number,
date: Date
}]
}
Now, I want to query something like this:
Find data where the regular_end_date, project_end_time, and events at the last index are lower than the date provided
The catch is, not every data has the three criteria above because it is available according to the types (Sorry for the messy data, it is already there). Below is an example:
If the data type is an event, then there are events
If the data type is regular, then there are regular_start_date and regular_end_date
If the data type is a project, then there are project_start_date and project_end_date
So far, I've tried to use this:
db.data.find({
"$or": [
{
"project_end_time": {
"$lt": ISODate("2022-12-27T10:09:49.753Z")
},
},
{
"regular_end_date": {
"$lt": ISODate("2022-12-27T10:09:49.753Z")
}
},
{
"$expr": {
"$lt": [
{
"$getField": {
"field": "date",
"input": {
"$last": "$events"
}
}
},
ISODate("2022-12-27T10:09:49.753Z")
]
}
}
]
})
Also with aggregation pipeline:
db.data.aggregate([
{
$match: {
"$or": [{
"project_end_time": {
"$lt": ISODate("2022-12-27T10:09:49.753Z")
},
},
{
"regular_end_date": {
"$lt": ISODate("2022-12-27T10:09:49.753Z")
}
},
{
"$expr": {
"$lt": [{
"$getField": {
"field": "date",
"input": {
"$last": "$events"
}
}
},
ISODate("2022-12-27T10:09:49.753Z")
]}
}]
}
}
])
But it shows all data as if it wasn't filtered according to the criteria. Any idea where did I do wrong?
FYI I am using MongoDB 5.0.2

One option is to check if the relevant field exists before checking its value, otherwise its value is null which is less than your requested date:
db.collection.find({
$or: [
{$and: [
{project_end_time: {$exists: true}},
{project_end_time: {$lt: ISODate("2022-12-27T10:09:49.753Z")}}
]},
{$and: [
{regular_end_date: {$exists: true}},
{regular_end_date: {$lt: ISODate("2022-12-27T10:09:49.753Z")}}
]},
{$and: [
{"events.0": {$exists: true}},
{$expr: {
$lt: [
{$last: "$events.date"},
ISODate("2022-12-27T10:09:49.753Z")
]
}}
]}
]
})
See how it works on the playground example

Related

MongoDB Aggregation Pipeline - get oldest document, but debounced

I have documents that look like:
[
{
value: 'Apple',
createdAt: '2021-12-09T20:15:26.421+00:00',
},
{
value: 'Blueberry',
createdAt: '2021-12-09T20:45:26.421+00:00',
},
{
value: 'Cranberry',
createdAt: '2021-12-09T21:30:26.421+00:00',
},
{
value: 'Durian',
createdAt: '2022-01-24T20:15:26.421+00:00',
},
{
value: 'Elderberry',
createdAt: '2022-01-24T20:45:26.421+00:00',
},
]
I'd like to do an aggregation where I get the oldest document, with the caveat that if another document was created within an hour, it invalidates the first document and I actually want to grab that one instead. For example, in the above I would like to return Cranberry. Initially pick Apple, but since Blueberry comes within an hour, move to that one, and since Cranberry comes within an hour of Blueberry, select Cranberry.
You can do the followings in an aggregation pipeline:
$sort by createdAt
$limit to get the oldest document
$lookup to get all the documents with createdAt behind the current document
$reduce to loop the result array; update the accumulator/result only if the current entry is within 1 hour
db.collection.aggregate([
{
$sort: {
createdAt: 1
}
},
{
$limit: 1
},
{
"$lookup": {
"from": "collection",
let: {
current: "$createdAt"
},
pipeline: [
{
$match: {
$expr: {
$gte: [
"$createdAt",
"$$current"
]
}
}
}
],
"as": "within"
}
},
{
"$addFields": {
"within": {
"$reduce": {
"input": "$within",
"initialValue": null,
"in": {
"$cond": {
"if": {
$or: [
{
$eq: [
"$$value",
null
]
},
{
$lte: [
{
"$subtract": [
"$$this.createdAt",
"$$value.createdAt"
]
},
3600000
]
}
]
},
"then": "$$this",
"else": "$$value"
}
}
}
}
}
}
])
Here is the Mongo playground for your reference.

Mongo DB Join on Primary/Foreign Key

I have two collections, viz: clib and mp.
The schema for clib is : {name: String, type: Number} and that for mp is: {clibId: String}.
Sample Document for clib:
{_id: ObjectId("6178008397be0747443a2a92"), name: "c1", type: 1}
{_id: ObjectId("6178008397be0747443a2a91"), name: "c2", type: 0}
Sample Document for mp:
{clibId: "6178008397be0747443a2a92"}
{clibId:"6178008397be0747443a2a91"}
While Querying mp, I want those clibId's that have type = 0 in clib collection.
Any ideas how this can be achieved?
One approach that I can think of was to use $lookUp, but that doesnt seem to be working. Also, I m not sure if this is anti-pattern for mongodb, another approach is to copy the type from clib to mp while saving mp document.
If I've understood correctly you can use a pipeline like this:
This query get the values from clib where its _id is the same as clibId and also has type = 0. Also I've added a $match stage to not output values where there is not any coincidence.
db.mp.aggregate([
{
"$lookup": {
"from": "clib",
"let": {
"id": "$clibId"
},
"pipeline": [
{
"$match": {
"$expr": {
"$and": [
{
"$eq": [
{
"$toObjectId": "$$id"
},
"$_id"
]
},
{
"$eq": [
"$type",
0
]
}
]
}
}
}
],
"as": "result"
}
},
{
"$match": {
"result": {
"$ne": []
}
}
}
])
Example here
db.mp.aggregate([
{
$lookup: {
from: "clib",
let: {
clibId: "$clibId"
},
pipeline: [
{
$match: {
$expr: {
$and: [
{
$eq: [ "$_id", "$$clibId" ],
}
]
}
}
},
{
$project: { type: 1, _id: 0 }
}
],
as: "clib"
}
},
{
"$unwind": "$clib"
},
{
"$match": {
"clib.type": 0
}
}
])
Test Here

mongodb aggregate apply a function to a field

As part of an aggregate I need to run this transformation:
let inheritances = await db.collection('inheritance').aggregate([
{ $match: { status: 1 }}, // inheritance active
{ $project: { "_id":1, "name": 1, "time_trigger": 1, "signers": 1, "tree": 1, "creatorId": 1, "redeem": 1, "p2sh": 1 } },
{ $lookup:
{
from: "user",
let: { creatorId: { $concat: [ "secretkey", { $toString: "$creatorId" } ] }, time_trigger: "$time_trigger"},
pipeline: [
{ $match:
{ $expr:
{ $and:
[
{ $eq: [ "$_id", sha256( { $toString: "$$creatorId" } ) ] },
{ $gt: [ new Date(), { $add: [ { $multiply: [ "$$time_trigger", 24*60*60*1000 ] }, "$last_access" ] } ] },
]
}
}
},
],
as: "user"
},
},
{ $unwind: "$user" }
]).toArray()
creatorId comes from a lookup, and in order to compare it to _id I first need to do a sha256.
How can I do it?
Thanks.
External functions will not work with the aggregation framework. Everything is parsed to BSON by default. It is all basically processed from BSON operators to native C++ code implementation, This is by design for performance.
Basically in short, you can't do this. I recommend just storing the hashed value on every document as a new field, otherwise you'll have to do it in code just before the pipeline.

Mongo Query to Return only a subset of SubDocuments

Using the example from the Mongo docs:
{ _id: 1, results: [ { product: "abc", score: 10 }, { product: "xyz", score: 5 } ] }
{ _id: 2, results: [ { product: "abc", score: 8 }, { product: "xyz", score: 7 } ] }
{ _id: 3, results: [ { product: "abc", score: 7 }, { product: "xyz", score: 8 } ] }
db.survey.find(
{ id: 12345, results: { $elemMatch: { product: "xyz", score: { $gte: 6 } } } }
)
How do I return survey 12345 (regardless of even if it HAS surveys or not) but only return surveys with a score greater than 6? In other words I don't want the document disqualified from the results based on the subdocument, I want the document but only a subset of subdocuments.
What you are asking for is not so much a "query" but is basically just a filtering of content from the array in each document.
You do this with .aggregate() and $project:
db.survey.aggregate([
{ "$project": {
"results": {
"$setDifference": [
{ "$map": {
"input": "$results",
"as": "el",
"in": {
"$cond": [
{ "$and": [
{ "$eq": [ "$$el.product", "xyz" ] },
{ "$gte": [ "$$el.score", 6 ] }
]}
]
}
}},
[false]
]
}
}}
])
So rather than "contrain" results to documents that have an array member matching the condition, all this is doing is "filtering" the array members out that do not match the condition, but returns the document with an empty array if need be.
The fastest present way to do this is with $map to inspect all elements and $setDifference to filter out any values of false returned from that inspection. The possible downside is a "set" must contain unique elements, so this is fine as long as the elements themselves are unique.
Future releases will have a $filter method, which is similar to $map in structure, but directly removes non-matching results where as $map just returns them ( via the $cond and either the matching element or false ) and is then better suited.
Otherwise if not unique or the MongoDB server version is less than 2.6, you are doing this using $unwind, in a non performant way:
db.survey.aggregate([
{ "$unwind": "$results" },
{ "$group": {
"_id": "$_id",
"results": { "$push": "$results" },
"matched": {
"$sum": {
"$cond": [
{ "$and": [
{ "$eq": [ "$results.product", "xyz" ] },
{ "$gte": [ "$results.score", 6 ] }
]},
1,
0
]
}
}
}},
{ "$unwind": "$results" },
{ "$match": {
"$or": [
{
"results.product": "xyz",
"results.score": { "$gte": 6 }
},
{ "matched": 0 }
}},
{ "$group": {
"_id": "$_id",
"results": { "$push": "$results" },
"matched": { "$first": "$matched" }
}},
{ "$project": {
"results": {
"$cond": [
{ "$ne": [ "$matched", 0 ] },
"$results",
[]
]
}
}}
])
Which is pretty horrible in both design and perfomance. As such you are probably better off doing the filtering per document in client code instead.
You can use $filter in mongoDB 3.2
db.survey.aggregate([{
$match: {
{ id: 12345}
}
}, {
$project: {
results: {
$filter: {
input: "$results",
as: "results",
cond:{$gt: ['$$results.score', 6]}
}
}
}
}]);
It will return all the sub document that have score greater than 6. If you want to return only first matched document than you can use '$' operator.
You can use $redact in this way:
db.survey.aggregate( [
{ $match : { _id : 12345 }},
{ $redact: {
$cond: {
if: {
$or: [
{ $eq: [ "$_id", 12345 ] },
{ $and: [
{ $eq: [ "$product", "xyz" ] },
{ $gte: [ "$score", 6 ] }
]}
]
},
then: "$$DESCEND",
else: "$$PRUNE"
}
}
}
] );
It will $match by _id: 12345 first and then it will "$$PRUNE" all the subdocuments that don't have "product":"xyz" and don't have score greater or equal 6. I added the condition ($cond) { $eq: [ "$_id", 12345 ] } so that it wouldn't prune the whole document before it reaches the subdocuments.

MongoDB how to push multiple objects in a collection into an array

If I have a multiple documents in a mongodb collection that look like this:
// document 1
{
_id: '123',
date: '5/10/15',
charges: [{
amount: 500,
description: 'foo',
},{
amount: 400,
description: 'bar',
}],
}
// document 2
{
_id: '456',
date: '5/11/15',
charges: [{
amount: 500,
description: 'foo',
},{
amount: 300,
description: 'foo',
}],
}
I want to create and array of all charges that have an amount of 500. The result should look like this:
[{
amount: 500,
description: 'foo'
}, {
amount: 500,
description: 'foo'
}]
What is the most efficient way to accomplish this?
Try this:
db.collection.aggregate(
[
{
$unwind: "$charges"
},
{
$match: {
amount: 500
}
}
]
);
Across documents you use the aggregation framework with $unwind and $group:
db.collection.aggregate([
// Match documents with the required criteria
{ "$match": { "charges.amount": 500 } },
// Unwind to de-normalize the content
{ "$unwind": "$charges" },
// Filter the de-normalized documents
{ "$match": { "charges.amount": 500 } },
// Group back the result
{ "$group": {
"_id": null,
"charges": { "$push": "$charges" }
}}
])
Or a bit more efficient in modern versions is to filter the array first:
db.collection.aggregate([
// Match documents with the required criteria
{ "$match": { "charges.amount": 500 } },
// Pre filter the array
{ "$redact": {
"$cond": {
"if": { "$eq": [{ "$ifNull": [ "$amount", 500 ] }, 500 ]},
"then": "$$DESCEND",
"else": "$$PRUNE"
}
}},
// Unwind to de-normalize the content
{ "$unwind": "$charges" },
// Group back the result
{ "$group": {
"_id": null,
"charges": { "$push": "$charges" }
}}
])
Future versions ( working in current development releases ) will have a more helpful $filter method:
db.collection.aggregate([
// Match documents with the required criteria
{ "$match": { "charges.amount": 500 } },
// Filter the array
{ "$project": {
"charges": {
"$filter": {
"input": "$charges",
"as": "charge",
"cond": {
"$eq": [ "$$charge.amount", 500 ]
}
}
}
}},
// Unwind to de-normalize the content
{ "$unwind": "$charges" },
// Group back the result
{ "$group": {
"_id": null,
"charges": { "$push": "$charges" }
}}
])
All result in:
{
"_id": null,
"charges": [
{
amount: 500,
description: 'foo'
}, {
amount: 500,
description: 'foo'
}
]
}