How to query Firebase for keys that match a string prefix - firebase

Since firebase not support any spatial indexing then I use geohash which someone advice here.
Geohash is using characters to find the nearby.
So let say I have w22qt4vhye1c w99qt4vdf4vc w22qt4vhzerct geohash store in firebase and I want to query only which geohash close to w22qt4.
How to do that?
I know firebase has startAt. I tried but not work.
UPDATE
Storing geohash to firebase
//Encode lat lng, result w22qt4vhye1c3
var geoHashLatLng = encodeGeoHash(lat,lng);
firebaseRef.child('/geohash/' + geoHashLatLng).set(id);
so in json
root
- geohash
- w22qt4vhye1c3
- id
- z99qt4vdf4vc
- w22qt4vhzerct
My question here.
I Just want to query geohash which start from characters w22qt4. Can we do that in firebase?
UPDATE 2
startAt() seems like not query with characters start with ...
Example: I have following gehash
geohash node
geohash
- a123
- a333
- b123
- c123
- d123
With the following code
var firebaseRef = new Firebase('https://test.firebaseio.com');
var query = firebaseRef.child('/geohash').startAt(null, 'a');
query.on('child_added', function(snapshot) {
console.log(snapshot.name());
});
Will get this results
startAt(null, 'a') //return a123, a333, b123, c123, d123
startAt(null, 'c123') //return c123, d123
startAt(null, 'd') //return d123
My expected results
startAt(null, 'a') //return a123, a333
startAt(null, 'c123') //return c123
startAt(null, 'd') //return d123
My guess, startAt() will query the 26 alphabet letters in sequence but not matching.
So, what can I do in firebase so I can get my expected results above?

In addition to Andrew's answer above, what you want to do to get the desired effect with geohashes (i.e. be able to do prefix queries that let you specify accuracy in the 'query', specifically, for example every 3 characters in the geohash gets you ~ +-80km) is to store the data in a more granular and discretized manner.
So, rather than how you're storing it now, you'll want to save the data by chopping the geohash string key into fragments (I've done it at the 3 character boundary, but you should choose an appropriate tokenization strategy that gives you the desired accuracy) and use each fragment as a child name, as such:
root
- geohash
- w22
- qt4
- vhy
- e1c
- w22qt4vhye1c3
- id
- vhz
- erc
- w22qt4vhzerct
- id
- z99
- qt4
- vdf
- z99qt4vdf4vc
- id
Then, as in your question, when you need to get all geohashes that start with w22qt4 you would do a query against:
firebaseRef.child('/geohash/w22/qt4/')
Which would then give you the vhy and vhz children which then have all the actual geohashes that you were interested in.

You can use a normal startAt query to find keys near the specified key. For example, in this case you'd:
var query = firebaseRef.child("geohash").startAt(null, 'c').limit(5);
query.on("child_added", ...);
This will give you the 5 elements after geoHashLatLng when sorted lexigraphically.
If you want to end your query at some specified key, you can do that as well. For example:
var query = firebaseRef.child("geohash").startAt(null, 'c').endAt('d');

Related

How do I do a first-in-first-out system in Flutter/Firestore?

I'm building an app very similar to an inventory management system. My roadblock right now is the number of queries allowed in firestore, because it doesn't allow multiple .where()'s.
So using the inventory management system concept, we create a collection:
We'll Call it 'InventoryCollection'. Let's say it has 3 fields:
- ItemID
- ItemName
- DateAdded
Each document in 'InventoryCollection' has a sub collection called 'PurchaseHistory':
It has fields:
- PurchaseID (auto generated document id)
- ItemID (from parent collection document)
- PurchaseDate
- Quantity
Dilemma: How to deduct the Quantity of an Item using the concept of first in first out?
So let's say Item1 has 4 PurchseHistory data. and let's say one of them has 0 quantity because they were already sold.
- PurchaseID: 12340
- ItemID: 00001
- PurchaseDate: 01-feb-2021
- Quantity: 0
- PurchaseID: 12341(auto generated document id)
- ItemID: 00001
- PurchaseDate: 02-feb-2021
- Quantity: 10
- PurchaseID: 12342
- ItemID: 00001
- PurchaseDate: 03-feb-2021
- Quantity: 5
- PurchaseID: 12343
- ItemID: 00001
- PurchaseDate: 03-feb-2021
- Quantity: 15
I need to find a way to retrieve the PurchaseId 12341 to get the "First In" using the parameters below. I can then figure out a way to do the "First Out" on my own:
get all PurchaseHistory Documents with the ItemID: 00001
only get documents that has 1 or more Quantity
sort the data by PurchaseDate so that the first result will be the entry from feb 2
limit query result by 1 to only get the feb 2 document
get the purchaseID of that document to be used in the method below:
void _deductQuantity(String itemId, String PurchaseID {
final itemCollection = FirebaseFirestore.instance.collection('InventoryCollection').doc(ItemId).collection('PurchaseHistory').doc(purchaseID);
itemCollection.update({
'Quantity': _quantity - int.parse(_editItemQuantityController.text.trim()),
});
}
In my mind, the query looks like this:
String purchaseIdStream;
final docRef = FirebaseFirestore.instance.collection('InventoryCollection').doc(ItemId).collection('PurchaseHistory').doc();
purchaseIdStream = await docRef
.where('ItemId', isEqualTo: ItemId)
.where('Quantity', isGreaterThan: 0)
.orderBy('PurchaseDate')
.get();
var _id = purchaseIdStream.ref.documentID;
I fully understand firebase does not allow to orderby, if you don't have the field in a .where for purchasedate. So it's definitely not possible to do it on the code above. since I already have 2 where's and a 3rd where is not possible.
I understand that I have to do server side filtering instead of multiple .where's however, I am only a frontend dev. My understanding in backend concepts are very limited. So I need terrible guidance on how to implement this in dart/flutter.
Any advice will be highly appreciated. Thank you!
First off, the .where() attribute is only for CollectionReference, not DocumentReference. Remove the .doc(ItemId) in docRef. I think it should work after that.
If it doesn't, get the data and sort it manually, like this:
final purchaseIdList =
await docRef.where('ItemId', isEqualTo: ItemId).where('Quantity', isGreaterThan: 0).get();
purchaseIdList.docs.sort((QueryDocumentSnapshot a, QueryDocumentSnapshot b) =>
(a.data()['PurchaseHistory']['PurchaseDate'] as String)
.compareTo(b.data()['PurchaseHistory']['PurchaseDate'] as String));
final fifoId = purchaseIdList.docs[0].get('PurchaseId');
If I'm missing something, do tell. I'll edit the answer.
The documentation says that if a field is used in orderBy, it also should be used in the first where.
citiesRef.where("population", ">", 100000).orderBy("population").limit(2);
The answer is to use where for itemId and quantity, then do the rest of the filters in Dart:
var listOfDocs = await docRef
.where('ItemId', isEqualTo: ItemId)
.where('Quantity', isGreaterThan: 0)
.get();
docs.sort((prev, next) => (prev.data()['PurchaseDate'] as Timestamp)
.compareTo(next.data()['PurchaseDate'] as Timestamp));
final desiredDocId = docs.first.id;
print(desiredDocId);

Flutter Firestore Geo query

WHAT I HAVE TO DO:
I'm building an application where I want to do a query in firebase and receive the users who are in a range of X kilometers.
But I want to limit this received documents and also to have a function for getting more documents on the scroll. I'm using a StreamBuilder->ListView.build in order to build my list. When I'm going to receive the end of the scrollable list I want to get more documents from Firestore (if I have any).
WHAT I'VE DONE ALREADY:
So far I found just 2 solutions for my problem:
Firestore Helpers
GeoFlutterFire
THE PROBLEM:
Both of those libraries are good with just one BIG problem, in both of this case I'm not able to limit the number of documents that I'm going to receive (I can JUST limit the original query for Firestore.instance.collection("MyColection")) but is not going to help me that much because I will have this case
Give me 10 documents from Firestore -> Give me the 5km range users ->
I will receive just 4 let's say.
What I want instead is to receive 10 documents from Firestore within a range of X.
Is there any solution for this problem? Or I need to implement my own library for this? Or is not possible even so?
GeoPoint userGeoPoint = user.geoPoint["geopoint"];
// distance in miles
double distance = 30;
double lat = 0.0144927536231884;
double lon = 0.0181818181818182;
double lowerLat = userGeoPoint.latitude - (lat * distance);
double lowerLon = userGeoPoint.longitude - (lon * distance);
double greaterLat = userGeoPint.latitude + (lat * distance);
double greaterLon = userGeoPint.longitude + (lon * distance);
GeoPoint lesserGeopoint = GeoPoint(lowerLat, lowerLon);
GeoPoint greaterGeopoint = GeoPoint(greaterLat, greaterLon);
Query query = Firestore.instance
.collection(path)
.where("geoPoint.geopoint", isGreaterThan: lesserGeopoint)
.where("geoPoint.geopoint", isLessThan: greaterGeopoint)
.limit(limit);
This worked for me. Hope it helps.
A bit late but I had the same problem and I decided to build an external server with distance ordering and limit functionality. Server stores objects that includes geopoint and Firestore document ID.
The client sends a request to the server with center point and radius.
The server returns a list of document IDs.
Fetch documents from Firestore based on provided IDs.
The best I came up with.

Cypher: how to navigate graph to find the right path

I'm new to Cypher, I'm trying to learn to navigate a graph correctly.
I hava a situation like this: 2 Users have associated the same Service, the service is accessible via an Account. So, the user 'usr01' can access to the Service 'srv01' with account 'acct01'; the user 'usr02 can access to the Service 'srv01' with account 'acct02'.
The aim is to extract 2 records like this:
usr01 - srv01 - acct01
usr02 - srv01 - acct02
So, I executed these queries:
Creation of nodes:
create (s:XService {serviceId:'srv01'}) return s;
create (u:XUser {userId:'usr01'}) return u;
create (u:XUser {userId:'usr02'}) return u;
create (u:XAccount {accountId:'acct01'}) return u;
create (u:XAccount {accountId:'acct02'}) return u;
Relationship creation:
MATCH (u:XUser{userId:'usr01'}), (s:XService {serviceId:'srv01'}), (a:XAccount {accountId:'acct01'})
CREATE (u)-[:HAS_SERVICE]->(s)-[:HAS_ACCOUNT]->(a)
MATCH (u:XUser{userId:'usr02'}), (s:XService {serviceId:'srv01'}), (a:XAccount {accountId:'acct02'})
CREATE (u)-[:HAS_SERVICE]->(s)-[:HAS_ACCOUNT]->(a)
The graph result I've received is this
If I execute this query - starting from the user usr01:
MATCH (u:XUser {userId: 'usr01'}) OPTIONAL MATCH (u)-[:HAS_SERVICE]->(s:XService) OPTIONAL MATCH (s)-[:HAS_ACCOUNT]->(a:XAccount)
RETURN u.userId, s.serviceId, a.accountId;
I obtain this result:
So, how can I do to obtain the result described above (usr01 - srv01 - acct01) and not the cartesian product that I've received?
Thanks in advance
The problem is that when you add the relationship between the service and the account you do not indicate an association relationship with the user. As a solution, you can make a smart node "Access Rule":
MERGE (s:XService {serviceId:'srv01'})
MERGE (u1:XUser {userId:'usr01'})
MERGE (ua1:XAccount {accountId:'acct01'})
MERGE (u1)-[:can_access]->(ca1:AccessRule)-[:to_service]->(s)
MERGE (ca1)-[:with_account]->(ua1)
MERGE (u2:XUser {userId:'usr02'})
MERGE (ua2:XAccount {accountId:'acct02'})
MERGE (u2)-[:can_access]->(ca2:AccessRule)-[:to_service]->(s)
MERGE (ca2)-[:with_account]->(ua2)
And a query:
MATCH (u:XUser {userId: 'usr01'})
OPTIONAL MATCH ps = (u)-[:can_access]->(ca1:AccessRule)-[:to_service]->(s:XService)
OPTIONAL MATCH pa = (ca1)-[:with_account]->(a:XAccount)
RETURN u, ps, pa

Correct parameter binding for SELECT WHERE .. LIKE using fmdb?

First time user of fmdb here, trying to start off doing things correctly. I have a simple single table that I wish to perform a SELECT WHERE .. LIKE query on and after trying several of the documented approaches, I can't get any to yield the correct results.
e.g.
// 'filter' is an NSString * containing a fragment of
// text that we want in the 'track' column
NSDictionary *params =
[NSDictionary dictionaryWithObjectsAndKeys:filter, #"filter", nil];
FMResultSet *results =
[db executeQuery:#"SELECT * FROM items WHERE track LIKE '%:filter%' ORDER BY linkNum;"
withParameterDictionary:params];
Or
results = [db executeQuery:#"SELECT * FROM items WHERE track LIKE '%?%' ORDER BY linkNum;", filter];
Or
results = [db executeQuery:#"SELECT * FROM items WHERE track LIKE '%?%' ORDER BY linkNum;" withArgumentsInArray:#[filter]];
I've stepped through and all methods converge in the fmdb method:
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args
Depending on the approach, and therefore which params are nil, it then either calls sqlite3_bind_parameter_count(pStmt), which always returns zero, or, for the dictionary case, calls sqlite3_bind_parameter_index(..), which also returns zero, so the parameter doesn't get slotted into the LIKE and then the resultSet from the query is wrong.
I know that this is absolutely the wrong way to do it (SQL injection), but it's the only way I've managed to have my LIKE honoured:
NSString *queryString = [NSString stringWithFormat:#"SELECT * FROM items WHERE track LIKE '%%%#%%' ORDER BY linkNum;", filter];
results = [db executeQuery:queryString];
(I've also tried all permutations but with escaped double-quotes in place of the single quotes shown here)
Update:
I've also tried fmdb's own …WithFormat variant, which should provide convenience and protection from injection:
[db executeQueryWithFormat:#"SELECT * FROM items WHERE track LIKE '%%%#%%' ORDER BY linkNum;", filter];
Again, stepping into the debugger I can see that the LIKE gets transformed from this:
… LIKE '%%%#%%' ORDER BY linkNum;
To this:
… LIKE '%%?%' ORDER BY linkNum;
… which also goes on to return zero from sqlite3_bind_parameter_count(), where I would expect a positive value equal to "the index of the largest (rightmost) parameter." (from the sqlite docs)
The error was to include any quotes at all:
[db executeQuery:#"SELECT * FROM items WHERE track LIKE ? ORDER BY linkNum;", filter];
… and the % is now in the filter variable, rather than in the query.

CouchDB View with 2 Keys

I am looking for a general solution to a problem with couchdb views.
For example, have a view result like this:
{"total_rows":4,"offset":0,"rows":[
{"id":"1","key":["imported","1"],"value":null},
{"id":"2","key":["imported","2"],"value":null},
{"id":"3","key":["imported","3"],"value":null},
{"id":"4","key":["mapped","4"],"value":null},
{"id":"5,"key":["mapped","5"],"value":null}
]
1) If I want to select only "imported" documents I would use this:
view?startkey=["imported"]&endkey=["imported",{}]
2) If I want to select all imported documents with an higher id then 2:
view?startkey=["imported",2]&endkey=["imported",{}]
3) If I want to select all imported documents with an id between 2 and 4:
view?startkey=["imported",2]&endkey=["imported",4]
My Questtion is: How can I select all Rows with an id between 2 and 4?
You can try to extend the solution above, but prepend keys with a kind of "emit index" flag like this:
map: function (doc) {
emit ([0, doc.number, doc.category]); // direct order
emit ([1, doc.category, doc.number]); // reverse order
}
so you will be able to request them with
view?startkey=[0, 2]&endkey=[0, 4, {}]
or
view?startkey=[1, 'imported', 2]&endkey=[1, 'imported', 4]
But 2 different views will be better anyway.
I ran into the same problem a little while ago so I'll explain my solution. Inside of any map function you can have multiple emit() calls. A map function in your case might look like:
function(doc) {
emit([doc.number, doc.category], null);
emit([doc.category, doc.number], null);
}
You can also use ?include_docs=true to get the documents back from any of your queries. Then your query to get back rows 2 to 4 would be
view?startkey=[2]&endkey=[4,{}]
You can view the rules for sorting at CouchDB View Collation

Resources