MongoDB: Find items where two specified key values are equal - node.js

I have a mongoDB collection and an item in the collection looks like below:
{
"_id": "52f535b56268a019bd11cc2a",
"description": "Some description",
"entry_date": "2014-02-07T19:36:21.430Z",
"last_update": "2014-02-07T19:36:21.430Z",
"r": "samestring",
"s": "samestring"
}
Dates are ISODate objects.
This query returns items correctly
db.myCollection.find({$where : "this.entry_date < this.last_update"});
Below query returns nothing (I expect that it returns the above item):
db.myCollection.find({$where : "this.entry_date == this.last_update"});
And this query returns all items (I expected again it returns the above item):
db.myCollection.find({$where :"this.r == this.s"});
What am I doing wrong?
Thanks!!
---EDIT----
So I tried to test with a small data like below:
> db.myCollection.find({},{ _id: 0, start_date: 1, end_date: 1});
{
"start_date" : ISODate("2014-02-07T19:36:21.430Z"),
"end_date" : ISODate("2014-02-07T19:36:21.430Z")
}
{
"start_date" : ISODate("2014-02-07T19:36:21.430Z"),
"end_date" : ISODate("2014-02-07T22:39:02.114Z")
}
It didn't work for Date as you can see:
> db.myCollection.find(
{$where: "Date(this.start_date) == Date(this.end_date)"},
{ _id: 0, start_date: 1, end_date: 1 }
);
{
"start_date" : ISODate("2014-02-07T19:36:21.430Z"),
"end_date" : ISODate("2014-02-07T19:36:21.430Z")
}
{
"start_date" : ISODate("2014-02-07T19:36:21.430Z"),
"end_date" : ISODate("2014-02-07T22:39:02.114Z")
}
Works for string values:
> db.myCollection.find({$where: "this.title == this.description"},{ _id: 0, title: 1 });
{ "title" : "samedescription" }

You have to be really careful when comparing dates in JavaScript - use valueOf() or getTime():
> db.myCollection.find({$where: "this.start_date.getTime() == this.end_date.getTime()"});
{ "_id" : ObjectId("52f5b7e316d795f0a076fbdf"), "description" : "a description", "title" : "a title", "start_date" : ISODate("2014-02-07T19:36:21.430Z"), "end_date" : ISODate("2014-02-07T19:36:21.430Z") }
Here is why your other queries didn't work.
db.myCollection.find({$where: "Date(this.start_date) == Date(this.end_date)"});
This didn't work because you didn't use new when initializing the dates. This generally has hilarious results with all dates being equal to each other:
> Date(2014,2,8) == Date(1941,12,7)
true
> Date(2000,1,1) == Date(1995,2,8)
true
But even if you properly instantiate the date using new, you still get hilarious results when comparing dates using ==, as demonstrated in this gist:
var dateValue = 504001800000; // Saturday, December 21st, 1985 at 3:30am EST
var date1 = new Date(dateValue);
var date2 = new Date(dateValue);
console.log(date1 == date2); // false (different instances)
console.log(date1 === date2); // false (different instances)
console.log(date1 > date2); // false (date1 is not later than date2)
console.log(date1 < date2); // false (date1 is not earlier than date2)
console.log(date1 >= date2); // true (rofl)
console.log(date1 <= date2); // true (ahahahaha)
As for your other query:
It didn't work if I consider them as strings either:
db.myCollection.find({$where: "this.start_date == this.end_date"});
You're not actually comparing them as strings, you're comparing ISODate objects, which is how they're stored. For ISODate, similar to Date, the == operator will return false unless you're comparing the exact same instance. Using getTime should work, however, which is what I did up above.
Hopefully, none of this makes any sense whatsoever, because if it does, I'm worried about your sanity.

--EDITED--
You were looking for the $where operator. Your query must be in a valid JSON notation and outside of this operator there is no other access to that kind of raw JavaScript notation.
Dates
finally:
{$where: "this.start_date.valueOf() == this.end_date.valueOf()"}
Also be careful not to run into reserved words and other traps.
Be very careful to read the documentation on this operator and make sure you absolutely need it. It will slow things down considerably as your find will scan the entire collection. It cannot use an index.

Related

ARANGODB AQL MATCHES() with nested objects not possible

I use MATCHES() AQL function to search for entries in the arango database matching an example. This feature works nice for plain examples, but I cannot get it work properly with the nested features. See example:
RETURN MATCHES(
{ "a" : { "c" : 1 }, "b" : 1 },
{ "a" : { "c" : 1 } },
false
)
This returns true, however if I try:
RETURN MATCHES(
{ "a" : { "c" : 1, "b" : 1 }},
{ "a" : { "c" : 1 } },
false
)
It returns false !! (I expected to return true)
I have read that this is known in the Query by example section
https://www.arangodb.com/docs/stable/data-modeling-documents-document-methods.html#query-by-example
Their solution is to use dot notation, but it does not work in AQL
Following their example:
RETURN MATCHES(
{ "a" : { "c" : 1, "b" : 1 } },
{ "a.c" : 1 },
false
)
returns false (and I would expect to return true)
How can I then, use the MATCHES() for nested attributes?
FYI: I use arangodb v3.5.5-1
Clarification:
I want to get a match of { "a" : { "c" : 1, "b" : 1 } } by giving { "a" : { "c" : 1 } } as example
I've posted the Issue in ArangoDB repository: https://github.com/arangodb/arangodb/issues/12541
MATCHES compares attributes - it doesn't care what type the attributes have, so if you are trying to match nested objects, they have to have the same attributes/values; it follows that you can not have arbitrarily deep structures overlayed and checked for correspondence.
In the given example you can pick out the substructure with LET and use MATCHES against that
LET doc = { "a" : { "c" : 1, "b" : 1 }}
LET a = doc.a
RETURN MATCHES(
a,
{ "c": 1}
)
To leverage the arangojs capability to use paths to peek into the structure, you can write a user function that uses query by example and call that from AQL (https://www.arangodb.com/docs/stable/aql/extending.html).
Nota Bene: The arangodb client library of your language should provide convenient access to registering user functions (e.g. class AqlUserFunctions in arangodb-php).

Check if date range conflicts with current documents MongoDB

This problem has a layer of complexity and I'm not sure best practices to work around.
Attempting to create a booking calendar that serves the next availability date for their selected range. We have had cancellations that open up gaps.
Current functionality just takes the furthest end date and serves the next day, this has become very inaccurate and is leaving gaps due to cancellations.
Ex. User is looking to book for 10 days at the nearest available date. In the database there is a gap between 2 documents that would suit this user, how do I query to verify this availability?
Database document example
{
"start_date" : "2020-02-06T19:58:25.430Z",
"end_date" : "2020-02-16T19:58:25.430Z"
},
{
"start_date" : "2020-02-17T19:58:25.430Z",
"end_date" : "2020-02-27T19:58:25.430Z"
},
{
"start_date" : "2020-03-21T19:58:25.430Z",
"end_date" : "2020-03-31T19:58:25.430Z"
},
In between the second and third document there is a 2 week period that the user could claim. Currently this user would receive 2020-04-01 as the next availability. I would like to give them the option to claim a cancellation date by sending 2020-02-28 as the next availability.
How do I query mongodb to see if there is an x day amount availability in between document ranges before defaulting to the furthest end_date?
One idea: create a separate cancelled collection, and move any cancelled appointments into this collection.
If a new appointment is to be made, search this cancelled collection first for any open spots due to cancellations. If nothing suitable is found, then proceed to the furthest end date as per your current method.
An illustration:
> db.appointments.find()
{ "_id" : 0, "start" : 1, "end" : 2, "duration" : 1, "name": "John" }
{ "_id" : 1, "start" : 2, "end" : 4, "duration" : 2, "name": "Jane" }
{ "_id" : 2, "start" : 4, "end" : 5, "duration" : 1, "name": "Jack" }
// Jane cancelled the appt with _id 1
> c = db.appointments.findOne({_id: 1})
> db.cancelled.insert(c)
> db.appointments.deleteOne({_id: 1})
// current state of appointments
> db.appointments.find()
{ "_id" : 0, "start" : 1, "end" : 2, "duration" : 1, "name": "John" }
{ "_id" : 2, "start" : 4, "end" : 5, "duration" : 1, "name": "Jack" }
// Bob wanted to make an appointment spanning two "time units". Is it available?
> db.cancelled.findOne({duration: 2})
{ "_id" : 1, "start" : 2, "end" : 4, "duration" : 2, "name" : "Jane" }
// If Bob agreed to take over Jane's time slot
// reinsert the document from "cancelled" to "appointments", changing the name
> a = db.cancelled.findOne({duration: 2})
> a.name = "Bob"
> db.appointments.insert(a)
> db.cancelled.deleteOne({_id:1})
// end state of appointments
> db.appointments.find().sort({start: 1})
{ "_id" : 0, "start" : 1, "end" : 2, "duration" : 1, "name": "John" }
{ "_id" : 1, "start" : 2, "end" : 4, "duration" : 2, "name": "Bob" }
{ "_id" : 2, "start" : 4, "end" : 5, "duration" : 1, "name": "Jack" }
Note in this illustration, I also recorded the duration of each appointment so they can be searched efficiently. The end state is _id: 1 from Jane was replaced by Bob, with nothing in the cancelled collection.
To make sure that both appointments and cancelled collections are both updated correctly, you might want to use Transactions that's available since MongoDB 4.0.
I figured out a solution after letting it ferment in my brain for a week. I couldn't get the wanted results out of querying mongodb. I had to use javascript to compare some of the documents. Not sure about how the run time will hold up, for me that's a problem for a different day.
Thought I would add my solution as I came across my own question in google searches for the last week.
//Find all the documents that have an end date greater than today, sort them based // on the start date to order documents
let sort = await db.collection(COLLECTION)
.find({end_date : { $gte:new Date() }
})
.sort({start_date: 1 })
.toArray();
//Set the nextDate variable
let nextDate;
if(sort.length >= 2){
//Javascript iteration if there are more than 2 documents returned from sorting
for (let i = 0; i < sort.length; i++) {
var startDate = Date.parse(sort[i].end_date);
//endDate is ternary incase the amount of documents is odd number
var endDate = sort[i + 1] ? Date.parse(sort[i + 1].start_date) : new Date()
var timeDiff = endDate - startDate;
var daysDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24))
//Get the day difference between the date if the difference is greater than the //days alloted by user return the end date
if(daysDiff > days) {
return nextDate = sort[i].end_date
}
}
//If no days have room for the decided days, return the last document in array
return nextDate = sort[sort.length - 1].end_date
//If only one document, return end date
} else if (sort.length === 1) {
nextDate = sort[0].endDate
//No documents found, return todays date as it is available.
} else {
nextDate = new Date()
}
This actually works perfect for what I need it to do. Hopefully will get a good refactor and can be simplified.

Select all object have date between two date on fields value of mongodb

I have a bit of trouble getting data from mongodb. I have a collection like this:
Activities = [
... some another activity object item ... ,
{
type: "special"
dates: {
begin: ISODate("2019-07-07T17:00:00.000Z"),
end: ISODate("2019-07-20T17:00:00.000Z"),
note: ""
},
status: "pending"
} ,
,
{
type: "event"
dates: {
times: {
from: "12:00",
to: "15:00"
},
begin: ISODate("2019-07-21T17:00:00.000Z"),
end: ISODate("2019-08-29T17:00:00.000Z"),
note: ""
},
status: "pending"
} ,
... some another activity object item ...
]
I want to get all activity data has : dates.begin < X (my params date) < dates.end . And this is my way i have tried :
_Activities.find({
"dates.end" : {
$lte : "2019-08-31T17:00:00.000Z"
},
"dates.begin" : {
$gte : "2019-07-15T17:00:00.000Z"
}
}, callback);
// return [];
_Activities.find({
"dates.end" : {
$lte : "2019-08-30T17:00:00.000Z"
}
}, callback);
// return [] too;
I don't know where I was wrong, anyone has a solution to help me :((( Thanks for taking the time .
Because your fields have type Date so you need to pass a Date to compare. Something like this may work:
$lte : new Date("2019-08-31T17:00:00.000Z")
You can pass dates as javascript Date object it will work
dates: {
end: { $lte: new Date('2019-08-31T17:00:00.000Z') } ,
begin: { $gte: new Date('2019-07-15T17:00:00.000Z') }
}

Remove duplicate array objects mongodb

I have an array and it contains duplicate values in BOTH the ID's, is there a way to remove one of the duplicate array item?
userName: "abc",
_id: 10239201141,
rounds:
[{
"roundId": "foo",
"money": "123
},// Keep one of these
{// Keep one of these
"roundId": "foo",
"money": "123
},
{
"roundId": "foo",
"money": "321 // Not a duplicate.
}]
I'd like to remove one of the first two, and keep the third because the id and money are not duplicated in the array.
Thank you in advance!
Edit I found:
db.users.ensureIndex({'rounds.roundId':1, 'rounds.money':1}, {unique:true, dropDups:true})
This doesn't help me. Can someone help me? I spent hours trying to figure this out.
The thing is, I ran my node.js website on two machines so it was pushing the same data twice. Knowing this, the duplicate data should be 1 index away. I made a simple for loop that can detect if there is duplicate data in my situation, how could I implement this with mongodb so it removes an array object AT that array index?
for (var i in data){
var tempRounds = data[i]['rounds'];
for (var ii in data[i]['rounds']){
var currentArrayItem = data[i]['rounds'][ii - 1];
if (tempRounds[ii - 1]) {
if (currentArrayItem.roundId == tempRounds[ii - 1].roundId && currentArrayItem.money == tempRounds[ii - 1].money) {
console.log("Found a match");
}
}
}
}
Use an aggregation framework to compute a deduplicated version of each document:
db.test.aggregate([
{ "$unwind" : "$stats" },
{ "$group" : { "_id" : "$_id", "stats" : { "$addToSet" : "$stats" } } }, // use $first to add in other document fields here
{ "$out" : "some_other_collection_name" }
])
Use $out to put the results in another collection, since aggregation cannot update documents. You can use db.collection.renameCollection with dropTarget to replace the old collection with the new deduplicated one. Be sure you're doing the right thing before you scrap the old data, though.
Warnings:
1: This does not preserve the order of elements in the stats array. If you need to preserve order, you will have retrieve each document from the database, manually deduplicate the array client-side, then update the document in the database.
2: The following two objects won't be considered duplicates of each other:
{ "id" : "foo", "price" : 123 }
{ "price" : 123, "id" : foo" }
If you think you have mixed key orders, use a $project to enforce a key order between the $unwind stage and the $group stage:
{ "$project" : { "stats" : { "id_" : "$stats.id", "price_" : "$stats.price" } } }
Make sure to change id -> id_ and price -> price_ in the rest of the pipeline and rename them back to id and price at the end, or rename them in another $project after the swap. I discovered that, if you do not give different names to the fields in the project, it doesn't reorder them, even though key order is meaningful in an object in MongoDB:
> db.test.drop()
> db.test.insert({ "a" : { "x" : 1, "y" : 2 } })
> db.test.aggregate([
{ "$project" : { "_id" : 0, "a" : { "y" : "$a.y", "x" : "$a.x" } } }
])
{ "a" : { "x" : 1, "y" : 2 } }
> db.test.aggregate([
{ "$project" : { "_id" : 0, "a" : { "y_" : "$a.y", "x_" : "$a.x" } } }
])
{ "a" : { "y_" : 2, "x_" : 1 } }
Since the key order is meaningful, I'd consider this a bug, but it's easy to work around.

Mongo custom multikey sorting

Mongo docs state:
The Mongo multikey feature can automatically index arrays of values.
That's nice. But how about sorting based on multikeys? More specifically, how to sort a collection according to array match percentage?
For example, I have a pattern [ 'fruit', 'citrus' ] and a collection, that looks like this:
{
title: 'Apples',
tags: [ 'fruit' ]
},
{
title: 'Oranges',
tags: [ 'fruit', 'citrus' ]
},
{
title: 'Potato',
tags: [ 'vegetable' ]
}
Now, I want to sort the collection according to match percentage of each entry to the tags pattern. Oranges must come first, apples second and potatoes last.
What's the most efficient and easy way to do it?
As of MongoDB 2.1 a similar computation can be done using the aggregation framework. The syntax is something like
db.fruits.aggregate(
{$match : {tags : {$in : ["fruit", "citrus"]}}},
{$unwind : "$tags"},
{$group : {_id : "$title", numTagMatches : {$sum : 1}}},
{$sort : {numTagMatches : -1}} )
which returns
{
"_id" : "Oranges",
"numTagMatches" : 2
},
{
"_id" : "Apples",
"numTagMatches" : 1
}
This should be much faster than the map-reduce method for two reasons. First because the implementation is native C++ rather than javascript. Second, because "$match" will filter out the items which don't match at all (if this is not what you want, you can leave out the "$match" part, and change the "$sum" part to be either 1 or 0 depending on if the tag is equal to "fruit" or "citrus" or neither).
The only caveat here is that mongo 2.1 isn't recommended for production yet. If you're running in production you'll need to wait for 2.2. But if you're just experimenting on your own you can play around with 2.1, as the aggregation framework should be more performant.
Note: The following explanation is required for Mongo 2.0 and earlier. For later versions you should consider the new aggregation framework.
We do something similar while trying to fuzzy-match input sentence which we index. You can use map reduce to emit the object ID every time you get a match and them sum them up. You'll then need to load the results into your client and sort by the highest value first.
db.plants.mapReduce(
function () {
var matches = 0;
for (var i = 0; i < targetTerms.length; i++) {
var term = targetTerms[i];
for (var j = 0; j < this.tags.length; j++) {
matches += Number(term === this.tags[j]);
}
}
emit(this._id, matches);
},
function (prev, curr) {
var result = 0;
for (var i = 0; i < curr.length; i++) {
result += curr[i];
}
return result;
},
{
out: { inline: 1 },
scope: {
targetTerms: [ 'fruit', 'oranges' ],
}
}
);
You would have you pass your ['fruit', 'citrus' ] input values using the scope parameter in the map reduce call as {targetTerms: ['fruit', 'citrus' ]} so that they are available in the map function above.

Resources