Firestore: how to perform a query with inequality / not equals - firebase

I want select from Firestore collection just articles written NOT by me.
Is it really so hard?
Every article has field "owner_uid".
Thats it: I JUST want to write equivalent to "select * from articles where uid<>request.auth.uid"
TL;DR: solution found already: usages for languages/platforms: https://firebase.google.com/docs/firestore/query-data/queries#kotlin+ktx_5

EDIT Sep 18 2020
The Firebase release notes suggest there are now not-in and != queries. (Proper documentation is now available.)
not-in finds documents where a specified field’s value is not in a specified array.
!= finds documents where a specified field's value does not equal the specified value.
Neither query operator will match documents where the specified field is not present. Be sure the see the documentation for the syntax for your language.
ORIGINAL ANSWER
Firestore doesn't provide inequality checks. According to the documentation:
The where() method takes three parameters: a field to filter on, a comparison operation, and a value. The comparison can be <, <=, ==, >, or >=.
Inequality operations don't scale like other operations that use an index. Firestore indexes are good for range queries. With this type of index, for an inequality query, the backend would still have to scan every document in the collection in order to come up with results, and that's extremely bad for performance when the number of documents grows large.
If you need to filter your results to remove particular items, you can still do that locally.
You also have the option of using multiple queries to exclude a distinct value. Something like this, if you want everything except 12. Query for value < 12, then query for value > 12, then merge the results in the client.

For android it should be easy implement with Task Api.
Newbie example:
FirebaseFirestore db = FirebaseFirestore.getInstance();
Query lessQuery = db.collection("users").whereLessThan("uid", currentUid);
Query greaterQuery = db.collection("users").whereGreaterThan("uid", currentUid);
Task lessQuery Task = firstQuery.get();
Task greaterQuery = secondQuery.get();
Task combinedTask = Tasks.whenAllSuccess(lessQuery , greaterQuery)
.addOnSuccessListener(new OnSuccessListener<List<Object>>() {
#Override
public void onSuccess(List<Object> list) {
//This is the list of "users" collection without user with currentUid
}
});
Also, with this you can combine any set of queries.
For web there is rxfire

This is an example of how I solved the problem in JavaScript:
let articlesToDisplay = await db
.collection('articles')
.get()
.then((snapshot) => {
let notMyArticles = snapshot.docs.filter( (article) =>
article.data().owner_uid !== request.auth.uid
)
return notMyArticles
})
It fetches all documents and uses Array.prototype.filter() to filter out the ones you don't want. This can be run server-side or client-side.

Updating the answer of Darren G, which caused "TypeError: Converting circular structure to JSON". When we perform the filter operation, the whole firebase object was added back to the array instead of just the data. We can solve this by chaining the filter method with the map method.
let articles = []
let articlesRefs = await db.collection('articles').get();
articles = articlesRefs.docs
.filter((article) => article.data.uid !== request.auth.uid) //Get Filtered Docs
.map((article) => article.data()); //Process Docs to Data
return articles
FYI: This is an expensive operation because you will fetching all the articles from database and then filtering them locallly.

Track all user id in a single document (or two)
filter unwanted id out
Use "where in"
var mylistofidwherenotme = // code to fetch the single document where you tracked all user id, then filter yourself out
database.collection("articles").where("blogId", "in", mylistofidwherenotme)

let query = docRef.where('role','>',user_role).where('role','<',user_role).get()
This is not functioning as the "not equal" operation in firestore with string values

You can filter the array of objects within the javascript code.
var data=[Object,Object,Object] // this is your object array
var newArray = data.filter(function(el) {
return el.gender != 'Male';
});

Related

Firestore rule to only add/remove one item of array

To optimize usage, I have a Firestore collection with only one document, consisting in a single field, which is an array of strings.
This is what the data looks like in the collection. Just one document with one field, which is an array:
On the client side, the app is simply retrieving the entire status document, picking one at random, and then sending the entire array back minus the one it picked
var all = await metaRef.doc("status").get();
List tokens=all['all'];
var r=new Random();
int numar=r.nextInt(tokens.length);
var ales=tokens[numar];
tokens.removeAt(numar);
metaRef.doc("status").set({"all":tokens});
Then it tries to do some stuff with the string, which may fail or succeed. If it succeeds, then no more writing to the database, but if it fails it fetches that array again, adds the string back and pushes it:
var all = await metaRef.doc("status").get();
List tokens=all['all'];
List<String> toate=(tokens.map((element) => element as String).toList());
toate.add(ales.toString());
metaRef.doc("status").set({"all":toate});
You can use the methods associated with the Set object.
Here is an example to check that only 1 item was removed:
allow update: if checkremoveonlyoneitem()
function checkremoveonlyoneitem() {
let set = resource.data.array.toSet();
let setafter = request.resource.data.array.toSet();
return set.size() == setafter.size() + 1
&& set.intersection(setafter).size() == 1;
}
Then you can check that only one item was added. And you should also add additional checks in case the array does not exist on your doc.
If you are not sure about how the app performs the task i.e., successfully or not, then I guess it is nice idea to implement this logic in the client code. You can just make a simple conditional block which deletes the field from the document if the operation succeeds, either due to offline condition or any other issue. You can find the following sample from the following document regarding how to do it. Like this, with just one write you can delete the field which the user picks without updating the whole document.
city_ref = db.collection(u'cities').document(u'BJ')
city_ref.update({
u'capital': firestore.DELETE_FIELD
})snippets.py

Trying to filter down chat messages using Flutter and Firestore

I'm trying to create a Blocked Users list in my chat app using the latest versions of Flutter Beta channel (1.23.0-18.1.pre) and cloud_firestore 0.14.3.
Here's my data structure:
At first, I tried something like this (Hardcoded just to test), by filtering the messages I'm querying from Firestore. Firebase doesn't like this.
query: firestore.collection('messages')
.where('userId', whereNotIn: ['123456789', '987654321'] )
.where('hashtag', isEqualTo: hashTag)
.orderBy('submittedAt', descending: true),
reverse: true,
I get this error:
E/FLTFirestoreMsgCodec(24331): java.lang.IllegalArgumentException: Invalid query. You have an inequality where filter (whereLessThan(), whereGreaterThan(), etc.) on field 'userId' and so you must also have 'userId' as your first orderBy() field, but your first orderBy() is currently on field 'submittedAt' instead.
After doing some more reading, filtering on the client-side by just hiding the messages actually better suits my needs.
Unfortunately, I'm running in circles. I'm currently thinking I would map a stream to a list, and then do something like this:
if (message.userId is in the list) {
isBlocked = true;
} else {
isBlocked = false;
}
And then filtering out the messages if isBlocked is true. I tried hardcoding the values for that and it worked. BTW, Sorry for the pseudocode, but I deviated so many times that now I'm simply lost.
I was wondering if this was the correct approach? Any suggestions would be rad. I also tried using a future list from a stream but I couldn't get that to work either.
Future<Stream<List<BlockedUser>>> getBlockedIds() async {
Stream<List<BlockedUser>> list;
Stream<QuerySnapshot> snapshot = FirebaseFirestore.instance.collection('user').doc('id').collection('blocked').snapshots();
list = snapshot.map((query) => query.docs.map(
(doc) => BlockedUser(
id: doc.data()['id'])
).toList());
return list;
}
I can't get that to work since I don't know what to do with that list.
Thanks, everyone!

How to query an array of objects in a Firebase Cloud Function, to get a matching object and then update

I am using a scheduled task in a Firebase Cloud Function to query an array which contains a number of objects that need to be updated if a matching condition exists. My current attempt is using the 'array-contains' method to get the objects, then loop over them to find a matching condition which will then batch update the items. This is my data structure:
I need to find an object that is <= the current time, and also if the 'active' value = false.
export const liveMeetingsTrigger = functions.runWith( { memory: '1GB' }).pubsub
.schedule('every 1 minutes').onRun(async context => {
const now = admin.firestore.Timestamp.now();
const liveMeetings = await admin.firestore().collection('fl_content').where('meeting', 'array-contains', 'liveMeetingDate').get();
const batch = admin.firestore().batch();
liveMeetings.forEach(doc => {
if(doc.data().liveMeetingDate <= now && doc.data().active == false){
batch.update(doc.ref,'active',true);
}
});
return await batch.commit();
});
I have also tried using an exact object in the query instead of just using 'liveMeetingDate', but still get no results back, any help would be great - thanks.
Debugging: As the array I am trying to reach is inside of the (map) object 'liveMeetings' i have tried the dot notation (liveMeetings.meeting) with no success. Also trying a new collection with the the 'meeting' array at top level has provided no success.
Simple logging in the console (liveMeetings.size) shows that nothing is being returned on the query, so therefore the logging does not even reach the loop in the code.
As explained in this anwser the following query will not work:
const liveMeetings = await admin.firestore().collection('fl_content').where('meeting', 'array-contains', 'liveMeetingDate').get();
because the meetings array contain some objects, instead of "simple" or primitive data (e.g. string, number...).
You could query it with the exact objects, like:
const obj = {active: false, liveMeetingDate: ..., meetingId: ..., ....};
const liveMeetings = await admin.firestore().collection('fl_content').where('meeting', 'array-contains', 'obj').get();
Another approach would be to create a new collection which contains the similar documents (same Document ID) but with a meeting Array that contains only the liveMeetingDate property.
Finally, note that since your Array is within a map, you need to do
await admin.firestore().collection('fl_content').where('liveMeetings.meeting', 'array-contains', ...).get();
(PS: I don't mark this question as duplicate since you expressly ask for more help in the comments of the duplicate question/answer)

Fetch collection startAfter documentID

Is there a way to fetch document after documentID like
private fun fetchCollectoionnAfterDocumentID(limit :Long){
val db = FirebaseFirestore.getInstance()
var query:Query = db.collection("questionCollection")
.startAfter("cDxXGLHlP56xnAp4RmE5") //
.orderBy("questionID", Query.Direction.DESCENDING)
.limit(limit)
query.get().addOnSuccessListener {
var questions = it.toObjects(QuestionBO::class.java)
questions.size
}
}
I want to fetch sorted questions after a given Document ID. I know I can do it using DocumentSnapShot. In order to fetch the second time or after the app is resume I have to save this DocumentSnapshot in Preference.
Can It be possible to fetch after document ID?
startAfter - > cDxXGLHlP56xnAp4RmE5
Edit
I know I can do it using lastVisible DocumentSnapshot . But I have to save lastVisible DocumentSnapshot in sharedPreference.
When app launch first time 10 question are fetched from questionCollection. Next time 10 more question have to be fetched after those lastVisible. So for fetching next 10 I have to save DocumentSnapshot object in sharedPreference. Suggest me a better approach after seeing my database structure.
And one more thing questionID is same as Document reference ID.
There is no way you can pass only the document id to the startAfter() method and simply start from that particular id, you should pass a DocumentSnapshots object, as explained in the official documentation regarding Firestore pagination:
Use the last document in a batch as the start of a cursor for the next batch.
first.get().addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot documentSnapshots) {
=// Get the last visible document
DocumentSnapshot lastVisible = documentSnapshots.getDocuments()
.get(documentSnapshots.size() -1);
// Construct a new query starting at this document,
Query next = db.collection("cities")
.orderBy("population")
.startAfter(lastVisible) //Pass the DocumentSnapshot object
.limit(25);
// Use the query for pagination
}
});
See, here the lastVisible is a DocumentSnapshot object which represents the last visible object. You cannot pass only a document id. For more information, you can check my answer from the following post:
How to paginate Firestore with Android?
It's in Java but I'm confident you can understand it and write it in Kotlin.
Edit:
Please consider defining an order of your results so that all your pages of data can exist in a predictable way. So you need to either specify a startAt()/startAfter() value to indicate where in the ordering to begin receiving ordered documents or use a DocumentSnapshot to indicate the next document to receive, as explained above.
Another solution might be to put the document id into the document itself (as a value of a property) and order on it, or you can use FieldPath.documentId() to order by the id without having to add one.
You can also check this and this out.
There is one way to let startAfter(documentID) works.
Making one more document "get", then using the result as startAfter input.
val db = FirebaseFirestore.getInstance()
// I use javascript await / async here
val afterDoc = await db.collection("questionCollection").doc("cDxXGLHlP56xnAp4RmE5").get();
var query:Query = db.collection("questionCollection")
.startAfter(afterDoc)
.orderBy("questionID", Query.Direction.DESCENDING)
.limit(limit)
A simple way to think of this: if you order on questionID you'll need to know at least the value of questionID of the document to start after. You'll often also want to know the key, to disambiguate between documents with the same values. But since it sounds like your questionID values are unique within this collection, that might not be needed here.
But just knowing the key isn't enough, as that would require Firestore to scan its entire index to find that document. Such an index scan would break the performance guarantees of Firestore, which is why it requires you to give you the information it needs to perform a direct lookup in the index.

multi query and pagination with firestore

I am trying to implement multi query and pagination with firestore, but once I add < or > to the query the cursor not working.
//working example:
the doc id i save as propery on the doc
ref.where('category','==' 'cats').where('status', '==', 1).orderBy('id').cursor('last doc id from the returned list').limit(3)
//not working exmple:
ref.where('category','==' 'cats').where('status', '==', 1).orderBy('price').where('price', '>=', 2).where('price', '<=', 55).orderBy('id').cursor('last doc id from the returned list').limit(3)
no error returned. is it bug with firestore or on my end.
There has documentation at firebase on Pagination & Query and query data. We have to use the startAt() or startAfter() methods to define the start point for a query. Similarly, use the endAt() or endBefore() methods to define an end point for your query results.
Example:
To get all cities with a population >= 1,000,000, ordered by population,
db.collection("cities")
.orderBy("population")
.startAt(1000000);
and to get all cities with a population <= 1,000,000, ordered by population,
db.collection("cities")
.orderBy("population")
.endAt(1000000);
So pagination should be done using this method like,
// Construct query for first 25 cities, ordered by population
Query first = db.collection("cities")
.orderBy("population")
.limit(25);
first.get()
.addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot documentSnapshots) {
// ...
// Get the last visible document
DocumentSnapshot lastVisible = documentSnapshots.getDocuments()
.get(documentSnapshots.size() -1);
// Construct a new query starting at this document,
// get the next 25 cities.
Query next = db.collection("cities")
.orderBy("population")
.startAfter(lastVisible)
.limit(25);
// Use the query for pagination
// ...
}
});
A Firestore query can only have a single range condition.
From the documentation on queries:
You can combine where() filters with orderBy() and limit().
However, if you have a filter with a range comparison (<, <=, >, >=), your first ordering must be on the same field:
Invalid: Range filter and first orderBy on different fields
citiesRef.where("population", ">", 100000).orderBy("country")
Check this example App using FireStore with Angular 8 app having pagination
using queries
limit()
orderBy()
startAt()
endBefore()
startAfter()
As Frank pointed out so far firestore doesn't permit combining ranges on different properties. I hope google will fix this someday, having multiple range filters seems like a pretty essential feature for any database.
Also it's a rather ugly solution, but I guess you could leave out the .where('price', '>=', 2) part and filter out the data afterwards on the client.

Resources