MongoDB Closest Match on properties - mongodb

Let says I have a Users collection in MongoDB whose schema looks like this:
{
name: String,
sport: String,
favoriteColor: String
}
And lets say I passed in values like this to match a user on:
{ name: "Thomas", sport: "Tennis", favoriteColor:"blue" }
What I would like to do is match the user based off all those properties. However, if no user comes back, I would like to match a user on just these properties:
{sport: "Tennis", favoriteColor:"blue" }
And if no user comes back, I would like to match a user on just this property:
{ favoriteColor: "blue" }
Is it possible to do something like this in one query with Mongo? I saw the $switch condition in Mongo that will match on a case and then immediately return, but the problem is that I can't access the document it would have retrieved in the then block. It looks like you can only write strings in there.
Any suggestions on how to accomplish what I'm looking for?
Is the best thing (and only way) to just execute multiple User.find({...}) queries?

This is a good case to use MongoDB text index:
First you need to create text index on those fields:
db.users.ensureIndex({ name: "text", sport: "text", favoriteColor: "text" });
Then you can search the best match with "$text" limited by a number to show:
db.users.find( { $text: { $search: "Tennis blue Thomas" } } ).limit(10)

Try adding rank to all documents with weightage in aggregation pipeline, and sum the rank, $sort descending to get most matched documents on top
name -> 1
sport -> 2
favoriteColor -> 4
by doing this matching favoriteColor will always have higher weightage then sport and name or combination of both
aggregate pipeline
db.col.aggregate([
{$match : {
$or : [
{"name" : {$eq :"Thomas"}},
{"sport" : {$eq : "Tennis"}},
{"favoriteColor" : {$eq : "blue"}}
]
}},
{$addFields : {
rank : {$sum : [
{$cond : [{$eq : ["$name", "Thomas"]}, 1, 0]},
{$cond : [{$eq : ["$sport", "Tennis"]}, 2, 0]},
{$cond : [{$eq : ["$favoriteColor", "blue"]}, 4, 0]}
]}
}},
{$match : {rank :{$gt : 0}}},
{$sort : {rank : -1}}
])

Hope this query will satisfy your require condition, you can get relevant result in single db hit. Just create a query in aggregate pipeline
db.collection.aggregate([
{$match : {
$or : [
{"name" : {$eq :"Thomas"}},
{"sport" : {$eq : "Tennis"}},
{"favoriteColor" : {$eq : "blue"}}
]
}},
{$addFields : {
rank : {$sum : [
{$cond : [{$and:[{$eq : ["$name", "Thomas"]},{$eq : ["$sport", "Tennis"]},{$eq : ["$favoriteColor", "blue"]}] } , 1, 0]},
{$cond : [{$and:[{$eq : ["$name", "Thomas"]},{$eq : ["$sport", "Tennis"]}] } , 2, 0]},
{$cond : [{$and:[{$eq : ["$name", "Thomas"]}] } , 3, 0]},
]}
}},
{$group:{
_id:null,
doc:{$push:'$$ROOT'},
rank:{$max:'$rank'}
}},
{$unwind:'$doc'},
{$redact: {
$cond: {
if: { $eq: [ "$doc.rank", '$rank' ] },
then: "$$KEEP",
else: "$$PRUNE"
}
}},
{
$project:{
name:'$doc.name',
sport:'$doc.sport',
favoriteColor:'$doc.favoriteColor',
}}
])

Simply create a query builder for $match pipe in mongoDB aggregate pipeline or use it for find also, create JavaScript object variable and build your query dynamically.
var query={};
if(name!=null){
query['name']={ '$eq': name};
}
if(sport!=null){
query['sport']={ '$eq': sport};
}
if(favoriteColor!=null){
query['favoriteColor']={ '$eq': favoriteColor};
}
db.collection.find(query)
It will give exactly matched result on dynamic basis

Did you try with $or:https://docs.mongodb.com/manual/reference/operator/query/or/
I used it when I wanted to check if username or email exists..

Related

get additional data on same collection in Mongodb C#

I have a collection called "User". I'm passing userid to get the record. In addition to that i also need additional 10 last updatedAt(DateTime) record excluding the userid record but added together. So, total returned result will be 11 in this case. Is that possible using same query? I tried using Or and lookup but can't make it work as expected.
Any help is appreciated.
User collection:
[{
"id" : "123456",
"name" : "foo",
"addressIds" : [ObjectId(234567)]
} ,
"id" : "345678",
"name" : "bar",
"addressIds" : [ObjectId(678565), ObjectId(567456)]
}]
Address collection:
[{
"_id":"234567",
"district" : "district1",
"pincode" : "568923",
},
{
"_id":"678565",
"district" : "district2",
"pincode" : "568924",
},
{
"_id":"567456",
"district" : "district3",
"pincode" : "568925",
}]
Using facets, i have the User and the addressIds. Can i have the actual documents for AddressIds in User?
Edit:
You can use $facet, like this:
db.collection.aggregate([
{$sort: {date: -1}},
{$facet: {
byTate: [{$limit: 10}],
byUser: [{$match: {userId: 455845}}]
}
},
{$project: {
byDate: {
$filter: {
input: "$byDate",
as: "item",
cond: {$ne: ["$$item",{"$arrayElemAt": ["$byUser", 0]}]}
}
},
byUser: 1,
}
},
{
$project: {
byDate: {$slice: ["$byDate", 10]},
byUser: 1
}
}
])
You can see it works on the playground .
Switch between userId: 455845 / 455845 to see both cases.

MongoDB get only matching elements from nested array

I have a collection like this:
{
_id : 123,
username : "xy",
comments : [
{
text : "hi",
postdate : 123456789
},
{
text : "hi1",
postdate : 555555555
},
{
text : "hi2",
postdate : 666666666
},
{
text : "hi3",
postdate : 987654321
}
]}
Now I want only the comments that have postdate 555555555 or higher and 987654321 or lower. I have this query, but it doesn't work:
db.post.aggregate([
{$match : {$and : [
{"_id": ObjectId("123")},
{"comments.posttime" : {$lte : 987654321}},
{"comments.posttime" : {$gte : 555555555}}
]}}
,{$unwind: "$comments"}]).pretty();
But when I try this it gets me all of the array elements. How should this be done?
Thank you!
Use $redact to Restricts the contents of the document,
db.an.aggregate([{
$redact: {
"$cond": [{
$and: [{
"$gte": [{
"$ifNull": ["$postdate", 555555555]
},
555555555
]
}, {
"$lte": [{
"$ifNull": ["$postdate", 987654321]
},
987654321
]
}]
}, "$$DESCEND", "$$PRUNE"]
}
}]).pretty()
you have to unwind the comments first and then do the match. so that comments array will be flattened and match condition can filter it properly.
[{$unwind: "$comments"},{$match : {$and : [
{"_id": ObjectId("123")},
{"comments.posttime" : {$lte : 987654321}},
{"comments.posttime" : {$gte : 555555555}}
]}}]
this will give one row for each comment, if you want the matching comments inside the array, use aggregate on _id and $push the comments

Sorting aggregation addToSet result

Is there a way to get result of $addToSet as sorted array ?
I tried to expand the pipeline and $unwind the array, sort it and group it again ,
but still the result isn't sorted.
The arrays are pretty big and i try to avoid sort them in the the application.
Document Example :
{
"_id" : ObjectId("52a84825cc8391ab188b4567"),
"id" : 129624
"message" : "Sample",
"date" : "12-09-2013,17:34:34",
"dt" : ISODate("2013-12-09T17:34:34.000Z"),
}
Query :
db.uEvents.aggregate(
[
{$match : {dt : {$gte : new Date(2014,01,01) , $lt : new Date(2015,01,17)}}}
,{$sort : {dt : 1}}
, {$group : {
_id : {
id : "$id"
, year : {'$year' : "$dt"}
, month : {'$month' : "$dt"}
, day : {'$dayOfMonth' : "$dt"}
}
,dt : {$addToSet : "$dt"}
}}
]
);
Yes it is possible, but approach it differently. I'm just provide my own data for this, but you'll get the concept.
My Sample:
{ "array" : [ 2, 4, 3, 5, 2, 6, 8, 1, 2, 1, 3, 5, 9, 5 ] }
I'm going to "semi-quote" the CTO on this and state that Sets are considered to be unordered.
There is an actual JIRA, Google groups statement that goes something like that. So let's take it from "Elliot" and accept that this will be the case.
So if you want an ordered result, you have to massage that way with stages like this
db.collection.aggregate([
// Initial unwind
{"$unwind": "$array"},
// Do your $addToSet part
{"$group": {"_id": null, "array": {"$addToSet": "$array" }}},
// Unwind it again
{"$unwind": "$array"},
// Sort how you want to
{"$sort": { "array": 1} },
// Use $push for a regular array
{"$group": { "_id": null, "array": {"$push": "$array" }}}
])
And then do whatever. But now your array is sorted.
Since mongoDB version 5.2 you can do it without $unwind:
The $setIntersection create a set out of the array and the $sortArray sorts it:
db.collection.aggregate([
{$set: {array: {$setIntersection: ["$array"]}}},
{$set: {$sortArray: {input: "$array", sortBy: 1}}}
])

MongoDB. Find all document where element array have on of the needed element

I have mongodb documents
{
"_id" : ObjectId("4e8ae86d08101908e1000001"),
"name" : ["Some Name","Another"],
"zipcode" : ["2223"]
}
{
"_id" : ObjectId("4e8ae86d08101908e1000002"),
"name" : ["Another", "Name"],
"zipcode" : ["2224"]
}
{
"_id" : ObjectId("4e8ae86d08101908e1000003"),
"name" : ["Yet", "Another", "Name"],
"zipcode" : ["2225"]
}
I need to find elements where "name array" have " Another ", "Name" values.
I tried to use $in, but if there more values, like in
{
"_id" : ObjectId("4e8ae86d08101908e1000003"),
"name" : ["Yet", "Another", "Name"],
"zipcode" : ["2225"]
}
It doesn't return it =(
The use of $in should be fine (as explained below) as long as your data is correct and the values do not contain any whitespace. But if they are not exactly the same you are going to need to do some $regex matching, also using the $and form:
db.collection.find({
$and: [
{name: {$regex: 'Another'} },
{name: {$regex: 'Name'}}
]
})
If you just want to get the one document that has both of, and only the fields values you want, and in the same order just supply the argument as an array:
db.collection.find({ name: ["Another", "Name" ] })
The usage of $in is to match any of the elements you supply as a list. In this case you would match all of your documents
db.collection.find({ name: {$in: [ "Another", "Name" ] } })
For the $all operator, it's function is to match all of elements contained in your argument list, so if an argument was not present in the array being searched it would not be included. So just the last two would match:
db.collection.find({ name: {$all: [ "Another", "Name" ] } })
{
"_id" : ObjectId("4e8ae86d08101908e1000002"),
"name" : ["Another", "Name"],
"zipcode" : ["2224"]
}
{
"_id" : ObjectId("4e8ae86d08101908e1000003"),
"name" : ["Yet", "Another", "Name"],
"zipcode" : ["2225"]
}
Finally, if you don't know the order of the elements you are matching, while a little contrived, aggregate gives you a way to get out of jail:
db.collection.aggregate([
// Match using $all to reduce the list
{$match: {name: {$all: ["Name","Another"] }}},
// Keep the original document in the _id
{$project:{
_id: {
_id: "$_id",
name: "$name",
zipcode: "$zipcode"
},
name: 1
}},
// Unwind the "name" array
{$unwind: "$name"},
// Count up the entries per _id
{$group: { _id: "$_id", count: {$sum: 1}}},
// Only match *2* as there were two elements we were expecting
{$match: {count: 2} },
// Project back to the original form
{$project: {
_id:0,
_id: "$_id._id",
name: "$_id.name",
zipcode: "$_id.zipcode"
}}
])
And that's all the forms I can think of.
Have you looked at the MongoDB documentation for $in?
Find it here
You'll need to do this:
find({name: { $in: [ "Another", "Name"] }})

group operations over arrays using Mongo aggregation framework

I'm using mongodb 2.2. I would like to use the new Aggregation Framework to do queries over my documents, but the elements are arrays.
Here an example of my $project result:
{
"type" : [
"ads-get-yyy",
"ads-get-zzz"
],
"count" : [
NumberLong(0),
NumberLong(10)
],
"latency" : [
0.9790918827056885,
0.9790918827056885
]
}
I want to group by type, so for "ads-get-yyy" to know how much is the average of count and how much is the average of the latency.
I would like to have something similar to the next query, but that works inside of the elements of every array:
db.test.aggregate(
{
$project : {
"type" : 1,
"count" : 1,
"latency" : 1
}
},{
$group : {
_id: {type : "$type"},
count: {$avg: "$count"},
latency: {$avg: "$latency"}
}
});
I'm just learning the new AF too, but I think you need to first $unwind the types so that you can group by them. So something like:
db.test.aggregate({
$project : {
"type" : 1,
"count" : 1,
"latency" : 1
}
},{
$unwind : "$type"
},{
$group : {
_id: {type : "$type"},
count: {$avg: "$count"},
latency: {$avg: "$latency"}
}
});