Search in array child firestore - node.js

How can I retrieve all the documents who match a child in a data structure like this:
{
[
id: {
name: "name",
products: {
items: [
productName: "this is the product Name"
]
}
}
]
}
The parameter i try to compare is the one inside products.items[0].productName.
this is how i tried but it does not retrieve anything:
try{
var data = [];
const byName = await dbRef.where('producto.items[0].producto', '==', req.params.nombre).get();
console.log(byName);
if (byName.empty) {
console.log('No matching documents.');
res.send('No matching documents.');
return;
}
byName.forEach(doc => {
console.log(doc.id, '=>', doc.data());
data.push(doc.data());
});
res.send(data);
}catch(err){
res.send(err);
}

If you want to search across all items in the items array for one that matches the value you have, you can use the array-contains operator:
dbRef.where('producto.items', 'array-contains', { producto: req.params.nombre})
But note that this only works if the array only contains the producto field in each item. The reason is that array-contains (and other array-level operators) work on complete items only.
So if the items in producto.items have multiple subfields, and you want to match on one/some of them, you can't use array-contains. In that case, you're options are:
Store the items names in a separate/additional array field product-names and then query on that with array-contains.
Store the array items in a subcollection and query that.
Use a map instead of an array to store these values. This will generate many extra indexes though, which both adds to your storage cost, and may get you to the limit on the number of indexes.

Related

Firestore Query Nested Map

Looking to construct a query against a firestore collection ('parent') where the documents have a nested map (2 logical levels deep). Specifically when the first map has dynamic keys which are not known at the time of running the query. As an example:
Document 1
{
codes: {
abc: {
id: 'hi'
},
def: {
id: 'there'
}
}
}
Document 2
{
codes: {
ghi: {
id: 'you'
},
zmp: {
id: 'guys'
}
}
}
What I would like to do is have a WHERE clause that takes a wildcard for a key in the document. ie.
firestore.collection('parent').WHERE('codes.*.id', '==', 'there')
// Results in Document 1
or
firestore.collection('parent').WHERE('codes.*.id', '==', 'you')
// Results in Document 2
Is there any way to achieve this behavior without having to resort to generating subcollection documents to be used for indexing, or polluting the document itself with a second map that maps ids to codes.
== Not ideal solution 1 (subcollections) ==
Build out the server so that when these documents are filed, a subcollection ('child') is maintained with documents that contain the related information. As an example filing Document 1 above would require filing two documents in the child subcollection:
{
id: 'hi'
code: 'abc'
}
{
id: 'there'
code: 'def'
}
Now we can query for the id we want, and get the parent reference, and follow that all the way back to the parent...
firestore.collectionGroup('child').where('id', '==', 'there')
.get()
.then(snapshot => {
for(const doc of snapshot.docs) {
return doc.ref.parent.parent
}
return Promise.reject('no parents, how sad.')
})
.then(ref => ref.get())
.then(snapshot => snapshot.data())
.then(parent => {
// Thank goodness, the parent is Document 1!
}
The downside to this is maintenance of the sub collections, as well as a number of extra operations against firestore.
== Not ideal solution 2 (model pollution) ==
Another way to achieve this is to implement another map or an array in the document itself which simply contains the ids which would then let us query on those values. ie
{
codes: {
abc: {
id: 'hi'
},
def: {
id: 'there'
}
},
codeids:['hi','there']
}
Although this is easy to query:
.WHERE('codeids', 'ARRAY CONTAINS', 'hi')
I don't like the idea of adding fields that are not meaningful to the consumer of the document (the purpose of the field only being to facilitate a documents ability to be queried due to system constraints)
Open to suggestions!

MongoDB search results are slow and inaccurate

On https://cbbanalytics.com/, after logging in with email: stackacct#gmail.com, password: pass123, a search bar appears in the top-right corner. When text is input, the following route fires off:
router.get('/live-search/text/:text', function (req, res) {
try {
let text = req.params.text;
// Use $regex
let queryFilters = { label: { $regex: `${text}`, $options: 'i' } };
// Use $search (text-index)
// let queryFilters = { $text: { $search: text } };
// Return Top 20
db.gs__ptgc_selects
.find(queryFilters)
.limit(20)
.then(data => res.json(data))
.catch(err => res.status(400).json('Error: ' + err));
} catch (error) {
res.status(500).json({ statusCode: 500, message: error.message });
}
});
gs__ptgc_selects is a mongodb collection with 180K documents, and we are searching on the label field present in each document. label is set as a text index in MongoDB Atlas:
The primary issue with the regex implementation is:
each fetch takes ~150ms which is noticeable in the search performance
regex isn't returning the best search results. searching Zio returns Alanya DeFazio before Zion Young. Optimal order of search return would be (i) all 1st names starting with Zio, sorted alphabetically, (ii) all 2nd words starting with Zio, (iii) other words with Zio nested inside the word.
using regex doesn't leverage the text index at all. as a result, Query Targeting: Scanned Objects / Returned has gone above 1000 warnings are returned when the search is used.
If we uncomment let queryFilters = { $text: { $search: text } }; and use this instead of regex:
only exact matches are returned
fetches are still at ~150ms
Is it possible to improve search within our current stack (Node JS, mongoDB, and mongoose)? Or are these limitations unavoidable?
Edit: We had recently created a search-index for the entire gs__ptgc_selects collection, however this doesn't appear to be improving search.

followers and following sytem

I am working on a social network web application I have established a system of following followers with firebase and node js , so I created a collection users and in it two following followers array, I managed to add them
Now I want to issue a condition to check if the user has already made a follow up not to add it a second time to the table how can i access to the tables (following, followers)in order to verify if the user is in
exports.onFollow = (req, res) => {
const followDocument = db.doc(`/users/${req.body.email}`);
const followerDocument = db.doc(`/users/${req.user.email}`);
let followData;
let followerData;
followDocument
.get()
.then((doc) => {
if (doc.exists) {
followData = doc.data();
if ('req.user.email', 'in', followData.followers.docs) {
return res.status(200).json({
error: 'user already follow'
});
} else {
followData.followers.push(req.user.email);
return followDocument.update({
followers: followData.followers
});
}
}
})
.catch((err) => {
console.error(err);
res.status(500).json({
error: err.code
});
});
It sounds like you want followers to be an array with unique values, so that each email address can only occurs once. Firestore has special arrayUnion operation for adding values to such a field.
From the documentation on updating elements in an array:
If your document contains an array field, you can use arrayUnion() and arrayRemove() to add and remove elements. arrayUnion() adds elements to an array but only elements not already present. arrayRemove() removes all instances of each given element.
var washingtonRef = db.collection("cities").doc("DC");
// Atomically add a new region to the "regions" array field.
washingtonRef.update({
regions: admin.firestore.FieldValue.arrayUnion("greater_virginia")
});
// Atomically remove a region from the "regions" array field.
washingtonRef.update({
regions: admin.firestore.FieldValue.arrayRemove("east_coast")
});
I'd recommend switching to using arrayUnion() for your use-case, as it prevents having to do the query to detect if the email address is already in the array.

How do I count the documents that include a value within an array?

I have a Mongoose abTest document that has two fields:
status. This is a string enum and can be of type active, inactive or draft.
validCountryCodes. This is an array of strings enums (GB, EU, AU etc). By default, it will be empty.
In the DB, at any one time, I only want there to be one active abTest for each validCountryCode so I'm performing some validation prior to creating or editing a new abTest.
To do this, I've written a function that attempts to count the number of documents that have a status of active and that contain one of the countryCodes.
The function will then return if the count is more than one. If so, I will throw a validation error.
if (params.status === 'active') {
const activeTestForCountryExists = await checkIfActiveAbTestForCountry(
validCountryCodes,
);
if (params.activeTestForCountryExists) {
throw new ValidationError({
message: 'There can only be one active test for each country code.',
});
}
}
const abTest = await AbTest.create(params);
checkIfActiveAbTestForCountry() looks like this:
const checkIfActiveAbTestForCountry = async countryCodes => {
const query = {
status: 'active',
};
if (
!countryCodes ||
(Array.isArray(countryCodes) && countryCodes.length === 0)
) {
query.validCountryCodes = {
$eq: [],
};
} else {
query.validCountryCodes = { $in: [countryCodes] };
}
const count = await AbTest.countDocuments(query);
return count > 0;
};
The count query should count not only exact array matches, but for any partial matches.
If in the DB there is an active abTest with a validCountryCodes array of ['GB', 'AU',], the attempting to create a new abTest with ['GB' should fail. As there is already a test with GB as a validCountryCode.
Similarly, if there is a test with a validCountryCodes array of ['AU'], then creating a test with validCountryCodes of ['AU,'NZ'] should also fail.
Neither is enforced right now.
How can I do this? Is this possible write a query that checks for this?
I considered iterating over params.validCountryCodes and counting the docs that include each, but this seems like bad practice.
take a look at this MongoDB documantation.
As I understood what you need is to find out if there is any document that contains at least one of the specified countryCodes and it has active status. then your query should look like this:
{
status: 'active',
$or: [
{ validCountryCodes: countryCodes[0] },
{ validCountryCodes: countryCodes[1] },
// ...
]
}
note that counting documents is not an efficient manner to check if a document exists or not, instead use findOne with only one field being projected.
You are using the correct mongo-query for your requirement. Can you verify the actual queries executed from your application is the same? Check here
{ status: 'active', validCountryCodes: { $in: [ countryCodes ] } }
For eg; below query :
{ status: 'active', validCountryCodes: { $in: ['GB' ] } }
should match document :
{ status: 'active', validCountryCodes: ['AU','GB'] }

Push Array Items to Array type Column in mongoDb

This is a Controller in which I'm trying to catch multiple candidates id(ObjectId) and try to store it in the database in the array Candidates. But data is not getting pushed in Candidates column of Array type.
routes.post('/Job/:id',checkAuthenticated,function(req,res){
var candidates=req.body.candidate;
console.log(candidates);
Job.update({_id:req.params.id},{$push:{Appliedby : req.user.username}},{$push:{Candidates:{$each:
candidates}}}
});
Console screens output
[ '5eb257119f2b2f0b4883558b', '5eb2ae1cff3ae7106019ad7e' ] //candidates
you have to do all the update operations ($set, $push, $pull, ...) in one object, and this object should be the second argument passed to the update method after the filter object
{$push:{Appliedby : req.user.username}},{$push:{Candidates:{$each: candidates}}
this will update the Appliedby array only, as the third object in update is reserved for the options (like upsert, new, ....)
you have to do something like that
{ $push: { Appliedby: req.user.username, Candidates: { $each: candidates } } }
then the whole query should be something like that
routes.post('/Job/:id', checkAuthenticated, function (req, res) {
var candidates = req.body.candidate;
console.log(candidates);
Job.update(
{ _id: req.params.id }, // filter part
{ $push: { Appliedby: req.user.username, Candidates: { $each: candidates } } } // update part in one object
)
});
this could do the trick I guess, hope it helps

Resources