Firebase for complex query. A no go? - firebase

I've moved from parse-server to firebase for my new project, but reached a point in the project where I beginning to think it was a bad idea.
Basically, I'm making an app where people can post information about concerts going on in their town.
My first challenge was to filter the events, so a user only get events in his/her own town. I did this by structure the data after cities:
{
concerts: {
"New york": {
...,
...
},
"Chicago": {
...,
...
}
}
}
Then I figure I need another filter for the type of concert, e.g rock, pop, etc. So I though I did another restructure. However, there probably need to be 5-10 more filters, and it will become very hard to structure the database in a good way.
I though about multiple query, but this wasn't allowed:
firebase.database().ref("concerts")
.orderByChild("type").equalTo("rock")
.orderByChild("length").equalTo("2")
.orderByChild("artist").equalTo("beatles")
I thought about fetching everything from the server, and then filter the result in the client. I see however two problems with this:
There might be a ton of unnecessarily data being downloaded.
Some concerts will be locked only to certain users (e.g users who have gone to at least 10 other concerts), and there might be a security aspect of pulling home these concerts to user not being allowed to see them.
I thought about combining filters to create query keys, like this this, but with over 10 filters, it will become to complex.
Is there a solution to this or should I forget about firebase for this use case?
Thanks in advance

Incredibly complex queries can be crafted in Firebase. The data needs to be stored in a structure that lends itself to being queried and most importantly, don't be afraid of duplicate data.
For example, lets assume we have an app that enables a user to select a concert for a particular year and month, a specific city, and in a particular genre.
There are 3 parameters
year_month
city
genre
The UI first queries the user to select a City
Austin
then the UI asks to select a year and month
201704
then a genre
Rock
Your Firebase structure looks like this
concerts
concert_00
city: Memphis
year_month: 201706
genre: Country
city_date_genre: Memphis_201606_Country
concert_01
city: Austin
year_month: 201704
genre: Rock
city_date_genre: Austin_201704_Rock
concert_02
city: Seattle
year_month: 201705
genre: Disco
city_date_genre: Seattle_201705_Disco
Your UI has already polled the user for the query info and with that, build a query string
Austin_201704_Rock
and then query the 'city_date_genre' node for that string and you have your data.
What if the user wanted to know all of the concerts in Austin for April 2017
queryStartingAt("Austin_201704").queryEndingAt("Austin_201704")
You could easily expand on this by adding another query node and changing the order of the data
concerts
concert_00
city: Memphis
year_month: 201706
genre: Country
city_date_genre: Memphis_201606_Country
city_genre_date: Memphis_Country_201606
And depending on which order the user selects their data, you could query the associated node.
Adding additional nodes is a tiny amount of data and allows for very open ended queries for the data you need.

I see this is an old post, but I'd like to take this opportunity to point others running into a similar Firebase issues to AceBase, which is is a free and open source alternative to the Firebase realtime database. The lack of proper querying and indexing options in Firebase was one of the reasons AceBase was built. Using AceBase would enable you to query your data like so:
const snapshots = await db.ref('concerts')
.query()
.filter('city', '==', 'New York')
.filter('date', 'between', [today, nextWeek]) // today & nextWeek being Dates
.filter('genre', 'in', ['rock', 'blues', 'country'])
.get();
Because AceBase supports indexing, adding 1 or more indexes to the the queried fields will make those queries run incredibly fast, even with millions of records. It supports simple indexes, but also FullText and Geo indexes, so you could also query your data with a location and keywords:
.filter('location', 'geo:nearby', { lat: 40.730610, long: -73.935242, radius: 10000 }) // New York center with 10km radius
.filter('title', 'fulltext:contains', '"John Mayer" OR "Kenny Wayne Shepherd"')
If you want to limit results to allow paging, simply add skip and limit: .skip(80).limit(20)
Additionally, if you'd want to make the query deliver realtime results so any newly added concert will immediately notify your app - simply adding event listeners will upgrade it to a realtime query:
const results = await db.ref('concerts')
.filter('location', 'geo:nearby', { lat: 40.730610, long: -73.935242, radius: 10000 })
.on('add', addConcert)
.on('remove', removeConcert)
.get();
function addConcert(match) {
results.push(match.snapshot);
updateUI();
}
function removeConcert(match) {
const index = results.findIndex(r => r.ref.path === match.ref.path);
results.splice(index, 1);
updateUI();
}
If you want to know more about AceBase, check it out at npm: https://www.npmjs.com/package/acebase. AceBase is free and its entire source code is available on GitHub. Have fun!

Related

DynamoDB data structure / architecture to support set of particular queries

I currently have a lambda function pushing property data into a DynamoDB with streams enabled.
When a new item is added, the stream triggers another Lambda function which should query against a second DynamoDB table to see if there is a 'user query' in the table matching the new object in the stream.
The items in the first table which are pushed into the stream look like this...
{
Item: {
partitionKey: 'myTableId',
bedrooms: 3,
latitude: 52.4,
longitude: -2.6,
price: 200000,
toRent: false,
},
}
The second table contains active user queries. For example one user is looking for a house within a 30 mile radius of his location, between £150000 and £300000.
An example of this query object in the second table looks like this...
{
Item: {
partitionKey: 'myTableId',
bedrooms: 3,
minPrice: 150000,
maxPrice: 300000,
minLatitude: 52.3,
maxLatitude: 52.5
minLongitude: -2.5,
maxLongitude: -2.7,
toRent: false,
userId: 'userId',
},
}
When a new property enters the stream, I want to trigger a lambda which queries against the second table. I want to write something along the lines of...
get me all user queries where bedrooms == streamItem.bedrooms AND minPrice < streamItem.price AND maxPrice > streamItem.price AND minLatitude < streamItem.latitude AND maxLatitude > streamItem.latitude.
Ideally I want to achieve this via queries and filters, without scanning.
I'm happy to completely restructure the tables to suit the above requirements.
Been reading and reading and haven't found a suitable answer, so hoping an expert can point me in the right direction!
Thank you in advance
There's no silver bullet with DynamoDB here. Your only tools are the PK/SK lookup by value and range, filters to brute force things after that, and GSIs to give an alternate point of view. You're going to have to get creative. The details depend on your circumstances.
Like if you know you're getting all those specific values every time, you can construct a PK that is bed##rent# and an SK of price. Then for those three attributes you can do exact index-based resolution and filter for the geo attributes.
If you wanted, you could quantize the price range values (store pre-determined price ranges as singular values) and put that into the PK as well. Like divide prices into 50k chunks, each of which gets a name of the leading value. If someone wanted 150,000 to 250,000 then you'd lookup using two PKs, the "150" and "200" blocks.
You get PK/SK + GSI + filter. That's it. So it's up to you to invent a solution using them, aiming for efficiency.

Dropdown from values of a database field

I have an issue related to the data filtering. I have a Google Drive table to store data, and I want to show one field of this data source in a dropdown to make a filter by this field (Country).
The problem is that this dropdown filter it's only showing the countries that appears on the current page of the list. For example, if in the first page appears one country (Thailand) on the dropdown I'll only see Thailand.
If we move to the second page of the list we have another two countries (Spain and Portugal) and then the dropdown will only show Spain and Portugal.
What I really want is a dropdown which shows all the countries, no matter if they aren't on the current page, but I don't know how to fix it. ​
​This the the configuration of the Country Selector:
In the help, it's said we should use #datasource.model.fields.COUNTRY.possibleValues,
but if I use this paramater as Options, nothing is displayed in the selector.
I have spend a lot of hours trying to fix this issue and I don't find the solution, and I would like to check with you if it's an issue or I'm doing something wrong...
Could you help me?
You are using the same datasource for your dropdown and table and by #distinct()#sort() you are filtering items that are already loaded to browser (opposed to the whole dataset stored in database).
You need to have a separate datasource for your dropdown. There are at least three techniques to do this:
Possible values
You can predefine allowed values for your Country field and use them to populate drop down options both in create form and table filtering #datasource.model.fields.Country.possibleValues as you mentioned in question:
Create model for countries
By introducing dedicated related model for countries you can get the following benefits:
normalized data (you will not store the same country multiple times)
you'll be able to keep your countries list clean (with current approach there is possibility to have the same country with different spellings like 'US', 'USA', 'United State', etc)
app users when they create new records will be able to choose the country they need from dropdown (opposed to error prone typing it every time for all new records).
your dropdown bindings will be as simple as these:
// for names
#datasources.Countries.items..Names
// for options
#datasources.Countries.items.._key
// for value
#datasource.query.filters.Country._key._equals
Create Calculated Model
With Calculated Model you'll be able to squeeze unique country values from your table. You server query script can look similar to this:
function getUniqueCountries_() {
var consumptions = app.models.Consumption.newQuery().run();
var countries = [];
consumptions.reduce(function (allCountries, consumption) {
if (!allCountries[consumption.Country]) {
var country = app.models.CountryCalc.newRecord();
country.Name = consumption.Country;
countries.push(country);
allCountries[consumption.Country] = true;
}
}, {});
return countries;
}
However with growth of your Consumption table it can give you significant performance overhead. In this case I would rather look into direction of Cloud SQL and Calculated SQL model.
Note:
I gave a pretty broad answer that also covers similar situations when number of field options can be unlimited (opposed to limited countries number).

How to implement "NOT IN" in firebase?

I have a dataset of objects, e.g. cars. I want to make a system where users are presented an object, and can decide whether or not they like the car. But, I want to show them each car only once.
- allCars
- car1
- car2
...
- car348237
- carsLiked
- user1
- carsLiked
- car123
- car234
- carsNotLiked
- car321
- user2
- carsLiked
- carsNotLiked
Given some user, e.g. user1, how can I make a selection from allCars, WHITOUT cars that the user has already seen? In SQL I would do something like " WHERE carId NOT IN (car123, car234, car321) "
Any idea how I can do this in firebase (without filtering on clientside, I know how to do that)...? Any structure possible, using some kind of index.? I struggled for sometime but didn't find a solution.
Denormalization is a key.
I would replicate a set of all cars in each user's object and than I would delete the car object already displayed to user.
cars:{
CAR_AUDO_ID: {
//car object
}
users:{
user1:{
car_selection:{
CAR_AUTO_ID: true //reference to above
...
},
cars_liked:{
},
cars_disliked:{
}
}
coming from SQL it might sound like a lot of replication, but that the way to go with firebase.
in case you have something like 10K+ cars, of course the above would be overkill. If users are presented a random car than I would focus on random number generator where I would store only already picked numbers. In that case the best would be to use priority ordered list and your key would be something like increment counter.

Selecting data in Firebase

Saw the Firebase / Angular video you did and immediately got very excited about the project I have just started. One thing I am struggling to get my head around is how to select data at sub-levels. What I mean is: say I had something like this:
How can I select all records with the agent 'agent_1' and/or records with the box_id greater than 600 (plus other fields) without creating lots of indexes for each search term? I don't really want to download all the data to the client and then loop through the records as there will eventually be a lot of data.
Eventually, the app should be able to filter data on different fields simultaneously. For example, I would have a select box for agents, which may return all the agent_1 records. Then I would add the filter 'all boxes with id > 600' and then perhaps 'box weight > 24kg' etc.
It seems from what I have read, this is only possible by having an id field for each record, and then an index dataset for each field one would like to search against. This is simple enough for one field. However, I guess the only way to filter the data with further fields would be to get the ids on the next index dataset and do the filtering on the client.
Am I right in this approach? It seems quite long-winded.
What would be awesome would be to be able to do this:
https://xxxxx.firebaseio.com/boxes/?agent=agent_1/?box_id>600
Just a thought! :-)
Thanks!
first Import angular like following
import { AngularFireDatabase } from "angularfire2/database";
also import in app.module.ts
secondly inject database into your constructor an like
constructor(private database:AngularFireDatabase ){
const rootRef = database.database.ref();
var data= rootRef.child("boxes").orderByChild("agent").equalTo("agent_3")
}

Paginating chronologically prioritized Firebase children

tl;dr Performing basic pagination in Firebase via startAt, endAt and limit is terribly complicated, there must be an easier way.
I'm constructing an administration interface for a large number of user submissions. My initial (and current) idea is to simply fetch everything and perform pagination on the client. There is however a noticable delay when fetching 2000+ records (containing 5-6 small number/string fields each) which I attribute to a data payload of over 1.5mb.
Currently all entries are added via push but I'm a bit lost as to how paginate through the huge list.
To fetch the first page of data I'm using endAt with a limit of 5:
ref.endAt().limit(10).on('child_added', function(snapshot) {
console.log(snapshot.name(), snapshot.val().name)
})
which results in the following:
-IlNo79stfiYZ61fFkx3 #46 John
-IlNo7AmMk0iXp98oKh5 #47 Robert
-IlNo7BeDXbEe7rB6IQ3 #48 Andrew
-IlNo7CX-WzM0caCS0Xp #49 Frank
-IlNo7DNA0SzEe8Sua16 #50 Jimmmy
Firstly to figure out how many pages there are I am keeping a separate counter that's updated whenever someone adds or removes a record.
Secondly since I'm using push I have no way of navigating to a specific page since I don't know the name of the last record for a specific page meaning an interface like this is not currently possible:
To make it simpler I decided on simply having next/previous buttons, this however also presents a new problem; if I use the name of the first record in the previous result-set I can paginate to the next page using the following:
ref.endAt(null, '-IlNo79stfiYZ61fFkx3').limit(5).on('child_added', function(snapshot) {
console.log(snapshot.name(), snapshot.val().name)
})
The result of this operation is as follows:
-IlNo76KDsN53rB1xb-K #42 William
-IlNo77CtgQvjuonF2nH #43 Christian
-IlNo7857XWfMipCa8bv #44 Jim
-IlNo78z11Bkj-XJjbg_ #45 Richard
-IlNo79stfiYZ61fFkx3 #46 John
Now I have the next page except it's shifted one position meaning I have to adjust my limit and ignore the last record.
To move back one page I'll have to keep a separate list on the client of every record I've received so far and figure out what name to pass to startAt.
Is there an easier way of doing this or should I just go back to fetching everything?
We're working on adding an "offset()" query to allow for easy pagination. We'll also be adding a special endpoint to allow you to read the number of children at a location without actually loading them from the server.
Both of these are going to take a bit though. In the meantime the method you describe (or doing it all on the client) are probably your best bet.
If you have a data structure that is append-only, you could potentially also do pagination when you write the data. For example: put the first 50 in /page1, put the second 50 in /page2, etc.
Another way to accomplish this is with two trees:
Tree of ids: {
id1: id1,
id2: id2,
id3: id3
}
Tree of data: {
id1: ...,
id2: ...,
id3: ...
}
You can then load the entire tree of ids (or a big chunk of it) to the client, and do fancy pagination with that tree of ids.
Here's a hack for paginate in each direction.
// get 1-5
ref.startAt().limit(5)
// get 6-10 from 5
ref.startAt(null, '5th-firebase-id' + 1).limit(5)
// get 11-15 from 10
ref.startAt(null, '10th-firebase-id' + 1).limit(5)
Basically it's a hack for startAtExclusive(). You can anything to the end of the id.
Also figured out endAtExclusive() for going backwards.
// get 6-10 from 11
ref.endAt(null, '11th-firebase-id'.slice(0, -1)).limit(5)...
// get 1-5 from 6
ref.endAt(null, '6th-firebase-id'.slice(0, -1)).limit(5)...
Will play with this some more but seems to work with push ids. Replace limit with limitToFirst or limitToLast if using firebase queries.

Resources