Firebase query all users who are not friends with user - flutter - flutter

I'm trying to make social media app using flutter. one key feature with apps like this is looking for friends, and I would like to implement this in my app.
Here's a look in my Firestore Database:
We have a collection of "users" where we store their data and a subcollection "friends" where we store the userID of the added users:
Let's say user "4uuBry" is friends with user "5083CM", then "5083CM" should not be seen in a list of friend suggestion for "4uuBry".
So how do I query all users who are not friends with "4uuBry" and display them in a ListView?

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.
Example:
QuerySnapshot querySnap = await FirebaseFirestore.instance
.collection('chat')
.where('participant', arrayContains: myEmployeId)
.where('type', isEqualTo: '1') //you will change the query here
.get();

Related

Is there a way in Flutter with Firebase to make a query to check that array does not contain document reference

I was wondering if there is a way to make this code function another way around, so it finds all other records that do not contain this document reference?
Currently, I am building a list of records from the Users collection where I want to show only records that do not belong to some team.
I am using the ListView widget to display all records from the collection but I have a problem with filtering out users that have currently selected teams in their User document.
That User document contains a List of Document References to Teams Document Collection.
child: StreamBuilder<List<UsersRecord>>(
stream: queryUsersRecord(
queryBuilder: (usersRecord) => usersRecord.where(
'team_memberships',
arrayContains: widget.teamDocument!.reference),
),
I have tried a couple of things but I can not manage to do them so I am stuck.
Try using whereNotIn
Try this code:
child: StreamBuilder<List<UsersRecord>>(
stream: queryUsersRecord(
queryBuilder: (usersRecord) => usersRecord.whereNotIn(
'team_memberships',
[widget.teamDocument!.reference]),
),
There is no way you can query Firestore for documents that don't have a certain field or value. So you cannot query Firestore based on the absence of a particular value in an array.
Firestore queries can only return documents based on values that so exist in the document. So a value on which you need to perform the query should exist in the first place. If a document doesn't have a value for a specific field, it won't be included in an index on that field, hence the impossibility of performing that query.
Why is this not possible? This is because the number of non-existent values compared to what you already have in your database is extremely large. This means that Firestore cannot and should not create an index for all non-existing values. Querying for the non-existence of a value in an array would require a scan of all your documents, and that doesn't scale.
Perhaps you should also consider changing the array with a map:
$docId (document)
|
--- team_memberships (map)
|
--- referenceOne: true
|
--- referenceTwo: false
In this way, you'll always have an index for each reference.

How to set indexes correctly Firestore Java

I have posts collection and I want to take all the posts that its "start" field is greater that current time and user_id not equal to "1".
Query query = firebaseFirestore
.collection("posts")
.whereNotEqualTo("user_id", "1")
.whereGreaterThan("start", Timestamp.now());
I added index like on a screenshot, but still I get an error that I can’t use double condition.
What is wrong?index screenshot
An index won't help you here. The problem is that Firestore doesn't support the query you're trying to perform. Please review the documentation on query limitations to better understand:
In a compound query, range (<, <=, >, >=) and not equals (!=, not-in) comparisons must all filter on the same field.
Your query is using a range filter and an inequality filter on different fields, which is not supported.

How to make complex queries (including orderBy, whereIn and isNotEqualTo) from Cloud Firestore with flutter?

Main question
I am making a social media app which recommends posts to users. The posts have fields user, gym and grade, all of which are of type String. user refers to the user that made the post.
I want to recommend posts which have gym and grade that are found in lists of preferred gyms and grades, so I want to use whereIn on those fields. However, I do NOT want to recommend posts that were posted by the user of the app (i.e. I don't want users to see their own posts in their recommended posts), so I want to use isNotEqualTo on the user field.
Additionally, the posts have a timestamp field. I want to recommend the latest posts, so I want to use orderBy on the timestamp field.
Lastly, I only want to load 10 posts at a time, so I want to use limit(10).
However, I have to load the next 10 posts when the user scrolls past 10 posts, so I want to use startAfterDocument for that.
So this is what I attempted:
A possible searchTerms looks like this:
final searchTerms = {
'notUser': 'nathantew',
'gyms': ['gym1','gym2'],
'grades': ['grade1','grade2'],
}
Then the query using searchTerms (the actual query logic is longer in my code as the posts have more fields, and I attempted to make a generalised query function. I extracted the relevant parts):
var query =
FirebaseFirestore.instance.collection('posts').orderBy('timestamp');
if (lastDoc != null) {
query = query.startAfterDocument(lastDoc);
}
if (searchTerms['notUser'] != null) {
query = query.where('user', isNotEqualTo: searchTerms['notUser']);
}
if (searchTerms['gyms'] != null) {
query = query.where('gym', whereIn: searchTerms['gyms']);
}
if (searchTerms['grades'] != null) {
query = query.where('grade', whereIn: searchTerms['grades']);
}
return query.limit(10).get();
However, that throws an error on the isNotEqualTo filter:
_AssertionError ('package:cloud_firestore/src/query.dart': Failed assertion: line 677 pos 11: 'field == orders[0][0]': The initial orderBy() field '[[FieldPath([timestamp]), false]][0][0]' has to be the same as the where() field parameter 'FieldPath([user])' when an inequality operator is invoked.)
Extra questions and things I tried
I doubt this is how queries actually work for a non-SQL database like cloud firestore, but the way I imagine the query is that the posts are first ordered by timestamp, then we iterate through the posts from newest to oldest, then whichever post has a gym and grade in the list of preferred gyms and grades, AND doesn't have a user which is 'nathantew', will be added to the list of documents that will be returned to the app when the list reaches 10 documents.
And this way, when I request for the next 10 posts, I can pass in the last document of the previous query and start from there.
I tried searching up the error, but it seems to me that my understanding of how queries happen is completely wrong, because I quickly found a confusingly large amount of rules restricting how queries can be made.
The rules are confusing too, for example, https://firebase.google.com/docs/firestore/query-data/order-limit-data#limitations states "If you include a filter with a range comparison (<, <=, >, >=), your first ordering must be on the same field". Also, "You cannot order your query by any field included in an equality (=) or in clause." I'm using isNotEqualTo in this case, so does that fall under the first rule, or the second? It doesn't seem like isNotEqualTo is a "range comparison" filter, but the error called on the isNotEqualTo sounds like it is referring to the first rule...
I've seen suggestions to make a less complicated query omitting certain filters, and only add the filters in the local app code. I don't know how that'll work.
I can't omit the orderBy timestamp query filter or I would have to query the entire database to filter it locally.
But if I omit any of the where filters, then I need to query an unknown number of documents each time to ensure 10 documents are queried that satisfy the conditions.
And the more I think about it, the more possible errors I imagine. What if a user deletes their post as I am trying to use that document for the startAfterDocument filter...?
So, how should I make this query? Even better, where can I find best practices for such use cases? Are they all hidden from amateurs as they are used by large companies with professionals?

What is the effect of passing an array to Firestore database's orderBy clause?

Imagine the documents in my Firestore database represent recipes and each document has a 'tags' field, which is an array containing tags for that recipe (for example: spicy, French, vegetarian).
When a user searches for some tags, I use the array-contains-any operator to pull back all the recipes that contain at least one of the tags that a user has searched for. I then order the returned documents by the number of tags that have been matched. For example, if a user searched for 'spicy French vegetarian' then the documents whose 'tags' field contains all three of those tags appears first, followed by the documents whose 'tags' field contains two of those tags etc.
I am currently sorting manually using:
const userSearchedTags: string[] = [...] // array of tags searched for by user
recipes.sort((r1, r2) => {
const r1Tags: string[] = r1.get('tags');
const r2Tags: string[] = r2.get('tags');
const r1TagMatchQuant = r1Tags.filter(r1Tag => userSearchedTags.includes(r1Tag)).length;
const r2TagMatchQuant = r2Tags.filter(r2Tag => userSearchedTags.includes(r2Tag)).length;
return r2TagMatchQuant - r1TagMatchQuant;
});
But I want to paginate this query, which requires them to be ordered by Firebase so I thought I might be able to use the orderBy clause in my query so that Firebase gives me the documents back already ordered by number of matched tags. If this worked then I would be able to use the startAfter() clause to paginate my query. I attempted this:
FirebaseFirestore.instance
.collection('recipes')
.where('tags', arrayContainsAny: userSearchedTags)
.orderBy('tags')
.get();
But it does not return the documents ordered by number of matched tags. I thought it might be returning the documents ordered by the length of the 'tags' array on each document, but further testing has shown it is not doing that either. So my question is how is Firebase ordering these documents when I pass 'tags' (a field that is an array) to the orderBy clause?
Also, is there a way to get Firebase to return the documents to me ordered by the number of matched tags or is the way I am doing it now (manually sorting after pulling back all documents with at least one matching tag) the only way?
The order in which document are returned is typically the same as however the documents are present in the index that is being searched. Assuming your tags are strings, then according to the documentation on data types that'd be:
UTF-8 encoded byte order
So I expect it to be the order of that first tag that it matches, and definitely not the number of tags matched that you were hoping for. That type of results prioritization based on a calculated value is simply not something Firestore queries are made for as it would require that Firestore reorders the matches before returning them to you.

Is there an equivalent to `beginsWith` in Firestore?

The Firestore documentation on making queries includes examples where you can filter a collection of documents based on whether some field in the document either matches, is less than, or is greater than some value you pass in. For example:
db.collection("cities").whereField("population", isLessThan: 100000)
This will return every "city" whose "population" is less than 100000. This type of query can be made on fields of type String as well.
db.collection("cities").whereField("name", isGreaterThanOrEqualTo: "San Francisco")
I don't see a method to perform a substring search. For example, this is not available:
db.collection("cities").whereField("name", beginsWith: "San")
I suppose I could add something like this myself using greaterThan and lessThan but I wanted to check first:
Why doesn't this functionality exist?
My fear is that it doesn't exist because the performance would be terrible.
[Googler here] You are correct, there are no string operations like beginsWith or contains in Cloud Firestore, you will have to approximate your query using greater than and less than comparisons.
You say "it doesn't exist because the performance would be terrible" and while I won't use those exact words you are right, the reason is performance.
All Cloud Firestore queries must hit an index. This is how we can guarantee that the performance of any query scales with the size of the result set even as the data set grows. We don't currently index string data in a way that would make it easy to service the queries you want.
Full text search operations are one of the top Cloud Firestore feature requests so we're certainly looking into it.
Right now if you want to do full text search or similar operations we recommend integrating with an external service, and we provide some guidance on how to do so:
https://firebase.google.com/docs/firestore/solutions/search
It is possible now:
db.collection('cities')
.where('name', '>=', 'San')
.where('name', '<', 'Sam');
for more details see Firestore query documents startsWith a string