Mongoose get all documents matching array intersection - javascript

By array intersection I mean, the inventory has a lot more elements than each document ingredients array, and the result I want to get from the query is all documents which all array elements are contained within the inventory. $all will get me zero results since the inventory has more elements than can be found in ingredients even if all ingredients are found within the inventory,
I have thousands of docs that have an array field of strings
{
...
recipe: "recipe1",
ingredients: [ "1 cup cooked quinoa", "6 tbsp butter", "1 large egg" ]
...
},
{
...
recipe: "recipe2",
ingredients: [ "2 lemons", "2 tbsp butter", "1 large egg" ]
...
}
{
...
recipe: "recipe3",
ingredients: [ "1lb salmon", "1 pinch pepper", "4 spears asparagus" ]
...
}
and I'm trying to find all documents where all elements in the ingredients array are contained in a sample array that contains lots of elements, lets say for the case this only contains this:
inventory = [ "lemons", "butter", "egg", "milk", "bread", "salmon", "asparagus", "pepper" ]
With this inventory array, I want to get recipe2 and recipe3.
Right now I have this inventory array and query (thanks to turivishal):
let inventory = ["lemons", "butter", "egg", "milk", "bread", "salmon", "asparagus", "pepper"];
inventory = inventory.map((i) => new RegExp(i, "i"));
query:
Recipe.find({
ingredients: { $all: inventory }
})
Expected result:
{
...
recipe: "recipe2",
ingredients: [ "2 lemons", "2 tbsp butter", "1 large egg" ]
...
}
{
...
recipe: "recipe3",
ingredients: [ "1lb salmon", "1 pinch pepper", "4 spears asparagus" ]
...
}
But I'm getting zero results

You can try aggregation operator in mquery using $expr expression condition,
first of all you can join the array of string by | order symbol and make a string, and use it in $regex search,
$filter to iterate loop of ingredients
$regexMatch to match element has any matching word
$size to get the total size of filtered elements
$eq to match filtered result and actual ingredients is equal
let inventory = ["lemons", "butter", "egg", "milk", "bread", "salmon", "asparagus", "pepper"];
let inventoryStr = inventory.join("|");
// "lemons|butter|egg|milk|bread|salmon|asparagus|pepper"
Recipe.find({
$expr: {
$eq: [
{
$size: {
$filter: {
input: "$ingredients",
cond: {
$regexMatch: {
input: "$$this",
regex: inventoryStr,
options: "i"
}
}
}
}
},
{ $size: "$ingredients" }
]
}
})
Playground

Related

How to get the json object with only 2 or more bananas in the fruits array?

I have this mongoose schema.
const storeSchema = mongoose.Schema({
name: {
type: String,
required: true,
},
fruits: {
type: [String],
required: true,
},
});
And I need to get all the objects that have 2 or more bananas in the fruits array, like this.
{
"name": "Beautiful Store",
"fruits": ["banana", "apple", "banana", "pear"]
}
I was trying something like this, but with no success...
const query = await StoreModel.find({ fruits: { $gd: 2 } })
Somethign like this:
Option 1: ( less efective but intuitive )
db.collection.aggregate([
{
$addFields: {
numBananas: {
$size: {
$filter: {
input: "$fruits",
as: "item",
cond: {
$eq: [
"$$item",
"banana"
]
}
}
}
}
}
},
{
$match: {
numBananas: {
$gte: 2
}
}
},
{
$project: {
name: 1,
fruits: 1
}
}
])
Explained:
Add field "numBananas" with the number of bananas
Filter only the document having >=2 banas
Project only the necessary output
playground
Option 2: ( Best , inspired by Buzz below :) )
db.collection.aggregate([
{
$match: {
$expr: {
$gte: [
{
$size: {
$filter: {
input: "$fruits",
as: "item",
cond: {
$eq: [
"$$item",
"banana"
]
}
}
}
},
2
]
}
}
},
{$sort:{name:1}}
])
Explained:
Just match those having >=2 bananas , best if index created on fruits
( I think this solution is in 50/50 performance competition with the $reduce option from Buzz below , I would bet on this one )
playground
Here are two variations on the theme.
The first uses $reduce to directly count the number of bananas (essentially, the {$size:{$filter...}} as above. For extra performance, we bring the calc and the $match together into a single stage:
db.foo.aggregate([
{$match: {$expr: {$gte: [
{$reduce: {
input: '$fruits',
initialValue: 0,
in: {$cond:[{$eq: ['$$this','banana']}, {$add:['$$value',1]}, '$$value']}
}}, 2]}
}}
]);
The second is a general purpose fruit counter in case you want to have more complex expressions, e.g. >=2 bananas or (1 pear and 1 apple). It does come at a cost of $unwind/$group.
c=db.foo.aggregate([
{$unwind: '$fruits'}
,{$group: {_id: {id: '$_id', f: '$fruits'}, N: {$sum:1}}}
,{$match: {'_id.f': 'banana', N:{$gte:2}} }
]);

Mongoose array filtering and returning filtered array

My document have field roomname and field users which is an array:
['name1', 'name2', 'name3', 'name4' ,'name5' ,'name6' ,'name7']
How can I get filtered array of users from 'name2' to 'name5'?
I get from 'name1' to 'name7' array by coding :
roommodel.find({roomname:'room1'},'users').then(res=>{
console.log(res)
})
When there is less number of users like this one then there is a way:
let filteredusers=res.slice(1,4).map(i=>return i)
If there is huge amount of arrays it may slowdown server. I want to know if there is a direct method.
You can use it $nin Condition in Mongodb Query Like this,
roommodel.find({
roomname:'room1',
username: { $nin: [ 'name1', 'name7' ] }},'users')
.then(res=>{
console.log(res)
})
You can use Aggregation framework. Note that you will have to pass the input of all the indexes of users array that you want to be returned.
$match to filter relevant document
$map to iterate over the input array of indexes
$arrayElemAt to return the element from users array by the index
roommodel.aggregate([
{
"$match": {
"roomname": "room1"
}
},
{
"$set": {
"users": {
"$map": {
"input": [
2,
3,
4
],
"in": {
"$arrayElemAt": [
"$users",
"$$this"
]
}
}
}
}
}
])
Working example

Is it possible to find random documents in collection, without same fields? (monogdb\node.js)

For example, I have a collection users with the following structure:
{
_id: 1,
name: "John",
from: "Amsterdam"
},
{
_id: 2,
name: "John",
from: "Boston"
},
{
_id: 3,
name: "Mia",
from: "Paris"
},
{
_id: 4,
name: "Kate",
from: "London"
},
{
_id: 5,
name: "Kate",
from: "Moscow"
}
How can I get 3 random documents in which names will not be repeated?
Using the function getFourNumbers(1, 5), I get array with 3 non-repeating numbers and search by _id
var random_nums = getThreeNumbersnumbers(1, 5); // [2,3,1]
users.find({_id: {$in: random_nums}, function (err, data) {...} //[John, Mia, John]
But it can consist two Johns or two Kates, what is unwanted behavior. How can I get three random documents ( [John, Mia, Kate]. Not [John, Kate, Kate] or [John, Mia, John]) with 1 or maximum 2 queries? Kate or John (duplicated names) should be random, but should not be repeated.
There you go - see the comments in the code for further explanation of what the stages do:
users.aggregate(
[
{ // eliminate duplicates based on "name" field and keep track of first document of each group
$group: {
"_id": "$name",
"doc": { $first: "$$ROOT" }
}
},
{
// restore the original document structure
$replaceRoot: {
newRoot: "$doc"
}
},
{
// select 3 random documents from the result
$sample: {
size:3
}
}
])
As always with the aggrgation framework you can run the query with more or less stages added in order to see the transformations step by step.
I think what you are looking for is the $group aggregator, which will give you the distinct value of the collection. It can be used as:
db.users.aggregate( [ { $group : { name : "$name" } } ] );
MongoDB docs: Retrieve Distinct Values

Cannot get correct result when using MongoDB aggregation in meteor

I am using MongoDB aggregation in meteor.
The items in database look like this:
// item1
{
products: {
aaa: 100,
bbb: 200
}
}
// item2
{
products: {
aaa: 300,
bbb: 400
}
}
My pipeline looks like this
let pipeline = [{
$limit: 10
}, {
$group: {
_id: {
// …
},
total: {
$sum: "$products.aaa"
}
}
}];
And it is working perfect. But when I change my database structure to this
// item1
{
products: [
{code: "aaa", num: 100},
{code: "bbb", num: 200}
]
}
// item2
{
products: [
{code: "aaa", num: 300},
{code: "bbb", num: 400}
]
}
The results I got for total is always 0, I think my pipeline is wrong. Please see the comment inside:
let pipeline = [{
$limit: 10
}, {
$group: {
_id: {
// …
},
total: {
$sum: "$products.0.num" // Neither this nor "$products[0].num" works
}
}
}];
So how can I write it correctly? Thanks
With MongoDB 3.2 ( which won't be the bundled server with meteor, but there is noting stopping you using a seperate server instance. And actually would be recommended ) you can use $arrayElemAt with $map:
let pipeline = [
{ "$limit": 10 },
{ "$group": {
"_id": {
// …
},
"total": {
"$sum": { "$arrayElemAt": [
{ "$map": {
"input": "$products",
"as": "product",
"in": "$$product.num"
}},
0
]}
}
}}
];
With older versions, use "two" $group stages and the $first operator after processing with $unwind. And that's just for the "first" index value:
let pipeline = [
{ "$limit": 10 },
{ "$unwind": "$products" },
{ "$group": {
"_id": "$_id", // The document _id
"otherField": { "$first": "$eachOtherFieldForGroupingId" },
"productNum": { "$first": "$products.num" }
}},
{ "$group": {
"_id": {
// …
},
"total": {
"$sum": "$productNum"
}
}}
];
So in the latter case, after you $unwind you just want to use $first to get the "first" index from the array, and it would also be used to get every field you want to use as part of the grouping key from the original document. All elements would be copied for each array member after $unwind.
In the former case, $map just extracts the "num" values for each array member, then $arrayElemAt just retrieves the wanted index position.
Naturally the newer method for MongoDB 3.2 is better. If you wanted another array index then you would need to repeatedly get the $first element from the array and keep filtering it out from the array results until you reached the required index.
So whilst it's possible in earlier versions, it's a lot of work to get there.

How could I aggregate among multiple collections at once with loop

Suppose Each collection has these common fields birthday, gender
How could I get the grouped birthday with year by group
expected result
group r01 {id: 1987, count: 21121}, {id: 1988, count: 22}, ...
The output should count for user_vip_r01 and user_general_r01
group r15 {id: 1986, count: 2121}, {id: 1985, count: 220}, ...
The output should count for user_vip_r15 and user_general_r15
I know how to write the group by year query,
But don't know how to write an loop to iterate all my collection with javascript.
And what if the collection name is part of irregular,
Something like user_old_r01, user_new_r01, user_bad_r01, should all be processed in group r01,
Is it possible to use regex to get it ?
group by year
pipeline = [
{ '$group':
'_id': '$year': '$birthday'
'count': '$sum': 1
}
{ '$sort': '_id': 1 }
]
cur = db[source_collection].runCommand('aggregate',
pipeline: pipeline_work.concat([{ '$out': output_collection_name}])
allowDiskUse: true)
collection list
"user_vip_r01",
"user_vip_r15",
"user_vip_r16",
"user_vip_r17",
"user_vip_r18",
"user_vip_r19",
"user_vip_r20",
"user_vip_r201",
....
"user_general_r01",
"user_general_r15",
"user_general_r16",
"user_general_r17",
"user_general_r18",
"user_general_r19",
"user_general_r20",
"user_general_r201",
...

Categories