Query firestore DB with array-contains and in (firebase 9) - javascript

In Firebase (v. 9), I have this firestore DB collection named "users". Each user has these fields: "gender" (string: 'male' or 'female'), "age" (number) and "language" (string: 'en-EN' or 'fr-FR' or 'es-ES' or 'de-DE' ).
From a filter checkbox menu, a user can select the languages then execute a query, get the result and than applied another filter and get another result.
For example:
I check, from the language menu "English" and "French" --> get the result, for example 3 users (2 female and 1 male). Then, from the gender menu, I check "male" --> get the result: just that one male user from the previous query result.
But a user can also do the first query for the language and then, in the second one, check both 'male' and 'female'.
I'm trying to do the query combining 'array-contains' and 'in' operator but I have no luck.
The query
const q = query(
collection(db, 'users'),
where('gender', 'array-contains', ['male', 'female']),
where('language', 'in', ['en-EN', 'es-ES']),
where('age', '>', 14),
where('age', '<', 40)
);
EDIT: For that query I changed my DB structure: gender has become an array but with 'array-contains' I can't do:
where('gender', 'array-contains', ['male', 'female'])
It must be something like that:
where('gender', 'array-contains', 'male')
but I want to check for both gender.
What could solve my problem is doing two queries with 'in' operator but I can't do that. (Firebase allows me to have only 1 'in' operator in the query).
My goal is, for example, to get every users in the DB that speak English or French, both male or female and with an age between 14 and 40. Is that the correct way to do this? How can I do the first query for the language and then, do another query starting from that result in order to avoid redoing the first query (the language) when I query for the gender and then when I query for the age? I also create indexes as Firebase suggested me to do, but I still get an empty array.
I was reading the firebase 'Query limitation' from the doc:
Cloud Firestore provides limited support for logical OR queries. The in, and array-contains-any operators support a logical OR of up to 10 equality (==) or array-contains conditions on a single field. For other cases, create a separate query for each OR condition and merge the query results in your app.
In a compound query, range (<, <=, >, >=) and not equals (!=, not-in) comparisons must all filter on the same field.
You can use at most one array-contains clause per query. You can't combine array-contains with array-contains-any.
You can use at most one in, not-in, or array-contains-any clause per query. You can't combine in , not-in, and array-contains-any in the same query.
You can't order your query by a field included in an equality (==) or in clause.
The sum of filters, sort orders, and parent document path (1 for a subcollection, 0 for a root collection) in a query cannot exceed 100.
That says I can combine the 'in' operator only with 'array-contains'. It also says, that "for other cases, create a separate query for each OR condition and merge the query results in your app" but I can't find any example on how to do that.
I read an answer here > Firebase Firestore - Multiple array-contains in a compound query where someone suggest to change the structure of the data to query, from an array to a map and then query with the equal operator:
where('field.name1', '==', true),
where('field.name2', '==', true)
I still have no luck with this.
Edit2: I guess the only thing I could do, is to execute 2 different queries, get the results in two different arrays and than do whatever logic I need to implement using js..I mean, not with firebase query operator. Can someone guide me through the process?
Any help is appreciated.
Thank you

Google Cloud Firestore only allows one in condition per query. You'll need to do the second one in JavaScript processing the results. Probably, the best way you can do is to get the result from the original query and process the result using Javascript. See sample code below:
const q = query(
collection(db, 'users'),
where('language', 'in', ['en-EN', 'es-ES']),
where('age', '>', 14),
where('age', '<', 40)
);
// Pass the data from the checkboxes.
// Can be 'male', 'female', or ('male' and 'female')
const gender = ['male', 'female'];
let array = [];
const snapshot = await getDocs(q);
snapshot.forEach((doc) => {
if (gender.includes(doc.data().gender)) {
array.push(doc.data());
}
});
console.log(array);
The above code will return the processed result whatever you pass on the gender variable. You could do it vice-versa, if you want to query the gender first then just interchange the query and variables.
Another option is to have a compound string, for example:
Checked:
Male
Female
en_EN
en_ES
Compound strings will be ['en_male', 'en_female', 'es_male', 'es_female']. You can query this by only one in statement. See sample code below:
// Combined data passed from the checkboxes.
// Can only be one and up to 10 comparison values.
const compound = ['en_male', 'en_female', 'es_male', 'es_female'];
const q = query(
collection(db, 'users'),
where('compound', 'in', compound),
where('age', '>', 14),
where('age', '<', 40)
);
const snapshot = await getDocs(q);
snapshot.forEach((doc) => {
console.log(doc.id, doc.data());
});
The downside of this approach is you can only have up to 10 comparison values for the in operator.
For more relevant information, you may check this documentation.

Related

Finding index of a firestore document for a given query

I'm using Firestore to build a game and I'd like to show a list of high scores.
I'm currently getting the 10 highest scores with the following query:
const q = query(doc(db, "scores", title), orderBy("score", "desc"), limit(10));
In addition to this, I'd like to let the player know how they fared compared to global high scores. For example, if they got the 956th-highest score, I'd like them to know their relative position is 956.
I've seen that, for cursors, one can provide an offset with a given document, ie:
const q = query(doc(db, "scores", title), orderBy("score", "desc"), limit(10), startAt(myScoreDocRef));
Is there any way from this to get the score's logical index in the sorted result set?
Firestore recently added getCountFromServer() (API reference) function that is perfect for this use case. You can create a query that matches documents with score greater than current user's score and fetch the count as shown below:
const currentUserScore = 24;
const q = query(
collection(db, 'users'),
orderBy('score', 'desc'),
where('score', '>', currentUserScore)
)
// number of users with higher score
const snapshot = await getCountFromServer(q)
console.log(`Your rank: ${snapshot.data().count + 1}`)
However, in case multiple users have same score, they will all see same rank with this query. As a workaround you can add another parameter to the ranking apart from score, like querying users with same score and checking user age or so.
The count query costs only 1 read for each batch of up to 1000 index entries matched by the query (as per the documentation) so it's much for efficient than querying users with some offset and manually calculating the rank.

Sequelize how to return result as a 2D array instead of array of objects?

I am using Sequelize query() method as follows:
const sequelize = new Sequelize(...);
...
// IMPORTANT: No changed allowed on this query
const queryFromUser = "SELECT table1.colname, table2.colname FROM table1 JOIN table2 ON/*...*/";
const result = await sequelize.query(queryFromUser);
Because I am selecting two columns with identical names (colname), in the result, I am getting something like:
[{ "colname": "val1" }, { "colname": "val2" }...], and this array contains values only from the column table2.colname, as it is overwriting the table1.colname values.
I know that there is an option to use aliases in the SQL query with AS, but I don't have control over this query.
I think it would solve the issue, if there was a way to return the result as a 2D array, instead of the array of objects? Are there any ways to configure the Sequelize query that way?
Im afraid this will not be possible without changes in the library directly connecting to the database and parsing its response.
The reason is:
database returns BOTH values
then in javascript, there is mapping of received rows values to objects
This mapping would looks something like that
// RETURNED VALUE FROM DB: row1 -> fieldName:value&fieldName:value2
// and then javascript code for parsing values from database would look similar to that:
const row = {};
row.fieldName = value;
row.fieldName = value2;
return row;
As you see - unless you change the inner mechanism in the libraries, its impossible to change this (javascript object) behaviour.
UNLESS You are using mysql... If you are using mysql, you might use this https://github.com/mysqljs/mysql#joins-with-overlapping-column-names but there is one catch... Sequelize is not supporting this option, and because of that, you would be forced to maintain usage of both libraries at ones (and both connected)
Behind this line, is older answer (before ,,no change in query'' was added)
Because you use direct sql query (not build by sequelize, but written by hand) you need to alias the columns properly.
So as you saw, one the the colname would be overwritten by the other.
SELECT table1.colname, table2.colname FROM table1 JOIN table2 ON/*...*/
But if you alias then, then that collision will not occur
SELECT table1.colname as colName1, table2.colname as colName2 FROM table1 JOIN table2 ON/*...*/
and you will end up with rows like: {colName1: ..., colName2: ...}
If you use sequelize build in query builder with models - sequelize would alias everything and then return everything with names you wanted.
PS: Here is a link for some basics about aliasing in sql, as you may aliast more than just a column names https://www.w3schools.com/sql/sql_alias.asp
In my case I was using:
const newVal = await sequelize.query(query, {
replacements: [null],
type: QueryTypes.SELECT,
})
I removed type: QueryTypes.SELECT, and it worked fine for me.

Searching within collection collectionGroup query

So I came across this answer which allows limiting the collectionGroup query to a specific document: CollectionGroupQuery but limit search to subcollections under a particular document
However I also want to further filter results based on a specific field using where, which required an index. The query works without errors but it always returns empty snapshot:
const cityRef = firebase.firestore().doc('cities/cityId');
firebase.firestore().collectionGroup('attractions')
.where('name', '>=', keywords),
.where('name', '<=', keywords + '\uf8ff')
.orderBy('name')
.orderBy(firebase.firestore.FieldPath.documentId())
.startAt(cityRef.path),
.endAt(cityRef.path + "\uf8ff")
.get()
.then((querySnapshot) => {
console.log("Found " + querySnapshot.size + " docs");
querySnapshot.forEach((doc) => console.log("> " + doc.ref.path))
})
.catch((err) => {
console.error("Failed to execute query", err);
})
firebaser here
The problem is almost certainly that your query has range checks on two different fields (name and the document path), which isn't possible in Firestore's query model. As the documentation on query limitations says:
In a compound query, range (<, <=, >, >=) and not equals (!=, not-in) comparisons must all filter on the same field.
Your startAt and endAt clauses are just different ways of writing > and < as far as this limitation is concerned.
To understand why the SDK allow you to write this query, but doesn't give you the result you want, we'll have to dive a bit deeper into it, so... 👇
What is possible, is to pass all relevant fields to startAt and endAt so that it can determine the correct slice across all those field values.
Doing that would also remove the need to even have the where, so it'd be:
firebase.firestore().collectionGroup('attractions')
.orderBy('name')
.orderBy(firebase.firestore.FieldPath.documentId())
.startAt(keywords, cityRef.path),
.endAt(keywords + '\uf8ff', cityRef.path + "\uf8ff")
.get()
...
But this query now first looks for the documents starting at keywords and then if necessary for cityRef.path in there to disambiguate between multiple results.
What you want is the equivalent of this query:
const docId = firebase.firestore.FieldPath.documentId()l
firebase.firestore().collectionGroup('attractions')
.orderBy('name')
.where('name', '>=', keywords),
.where('name', '<=', keywords + '\uf8ff')
.orderBy(firebase.firestore.FieldPath.documentId())
.where(docId, '>=', cityRef.path),
.where(docId, '<=', cityRef.path + '\uf8ff')
Now it's immediately clear why this isn't possible, because we have range conditions on two fields.
I've been trying to get that working in this jsbin (https://jsbin.com/yiyifin/edit?js,console), so far without success, but I'll post back if I get it working or have a final verdict on why it doesn't work.

Cloud Firestore query and limit doesn't query all data

I have a list with tags that I want to search in and retrieve data that matches the search.
I can retrieve all data correct with this query
searchTags(search: string): AngularFirestoreCollection<Tag> {
return this.afs.collection(this.dbPathTags, ref => ref.where('tag', "<=", search))
}
But the problem is that i want to limit the results to 5, since I'm planning to show them in a dropdown/autocomplete (like tags in SO)
After adding the limit, the query is just searching for the top 5 items from db and ONLY returns any of them if they matches. Wanted result is query filter ALL items then returns 5 items
searchTags(search: string): AngularFirestoreCollection<Tag> {
return this.afs.collection(this.dbPathTags, ref => ref.where('tag', "<=", search).limit(5))
}
When searching for "a" I get this result
But when searching "m" or "math" I don't get anything, note that "math" is also in the db and is displayed when the limit is off
Limit off
Have considered using startAt with the Limit rather than the where.
Firebase Pagination docs https://firebase.google.com/docs/firestore/query-data/query-cursors.
edit: .orderBy('field', 'order') when order is not present ascending order is default
searchTags(search: string): AngularFirestoreCollection<Tag> {
return this.afs.collection(this.dbPathTags, ref => ref.orderby('field', 'order').startAt(search).limit(5))
}
Maybe something like that?

Firestore select where is not null

I'm using firebase to manage my project and I cannot get to create a query with a where clause where some value is not null.
Example: I have a collection of employees. Each have a list of equipments as an object where the key is the equipment id and the value a color.
user = {
firstName: 'blabla',
lastName: 'bloblo',
equipments: {
123: 'blue',
124: 'red'
}
}
I would like to get all the employees who has a certain equipment in the equipments. Lets say 123.
It comes to Select * from Employees where equipments.123 is not null. I've tried:
firestore.collection('employees').where(`equipments.${equipmentId}`, '!=', null)
but it's not working.
I can't seems to make it work. Can you help me.
Update Sep 2020: v7.21.0 introduces support for not equals (!=) queries!
That means you can now use the code bellow:
firestore.collection('employees').where(`equipments.${equipm‌​entId}`, '!=', null)
Previous answer:
Firestore has no "not equal" operator. But looking at the logic, what you're trying to do is query for values which are String, and not null. Firestore can do that if you pass a String to the where() function.
So what you can do is query for values lower than \uf8ff. That's a very high code point in the Unicode range. Since it is after most regular characters in Unicode, this query will return everything that is of type String:
firestore.collection('employees').where(`equipments.${equipm‌​entId}`, '<', '\uf8ff')
Or you can simply query for values higher than "" (empty String):
firestore.collection('employees').where(`equipments.${equipm‌​entId}`, '>', '')
FWIW since Firestore indexes are sparse [1] you can do something like:
firestore.collection('employees').orderBy(`equipments.${equipm‌​entId}`)
And you'll only get documents where that field is set. If you're explicitly setting the fields to null in your database, however, you'll get null values first, so if you want to avoid explicit null values you can do:
firestore.collection('employees').orderBy('equipments.${equipm‌​entId}').startAfter(null);
[1]: Sparse in the sense that if the field is not present in the document, Cloud Firestore does not create an index entry in the index for that document/field.

Categories

Resources