Hello,
please pardon my limited experience as this is my first project with Parse Server and in programming world overall (a mechanical engineer here…).
I am trying to build mobile app that would connect people mainly for language tandem, fellow travellers, compatriots in foreign country and also allow to date (as we all know Tinder). Visual side is pretty defined and you might have a look on the prototype here: Felse
I decided to go with Parse to not lock the project in Google Firebase and as I am already at the phase where I need to define back-end and freeze the data structure I would like to kindly ask you for your opinions and experience. If such topic should be opened elsewhere I apologise, feel free to point me on other forums.
As you might noticed there are multiple types of search and each has a little different criteria. Apart from the “travel” one I got it working and it is also fetching data fast enough when I have only a first 10 fake profiles in my prototype database. I constructed following query function in swift (Xcode):
func fetchProfiles(completion: (([Profile]) -> Void)? = nil) {
guard let userController = self.userController else { return }
let currentProfile = userController.currentProfile
let currentUser = userController.currentPrivateUser
let currentGender = Gender(rawValue: currentProfile.gender)
guard currentGender != .notDefined else {
if let topVC = UIApplication.topViewController() {
UIAlerts.setGenderAlert(viewController: topVC)
}
return
}
//create basic query with values that are common for all specific queries
var basicConstraints = [QueryConstraint]()
//restrict age to prefered range
basicConstraints.append(contentsOf: ["age" > currentUser.minAge, "age" < currentUser.maxAge])
if currentProfile.sameSexualityOnly {
basicConstraints.append("orie" == currentProfile.orientation)
}
//if user prefers only same sexuality partners, set criteria for the inversed values so that it finds also profiles with not specified partners orientation (nil)
for sexuality in SexOrientation.allCases {
if sexuality.rawValue != currentProfile.orientation {
basicConstraints.append("pOri" != sexuality.rawValue)
}
}
//TODO: add a constraint that prevents fetching the same profile over an over again
//perhaps "objectId" notContainedBy, but then the profile would need to store a huge array of user connections/interactions/"swipes"
let basicQuery = PrsProfile.query(basicConstraints)
//create specific queries for each search type (this should extend to 6+ later)
var specificQueries: [Query<PrsProfile>] = []
// ----- date partners -----
//TODO: add check --> only if location is defined
if currentProfile.date {
var constraints = [QueryConstraint]()
//look for people that are looking for other gender or notDefined. If field "date" is nil, then the users do not look for date
constraints.append(currentGender == .woman ? "date" < Gender.man.rawValue : "date" > Gender.woman.rawValue)
//if device user specify certain gender, look only for users of that gender
if currentProfile.dateData != Gender.notDefined.rawValue {
constraints.append("gndr" == currentProfile.dateData)
}
//restrict search only within given geoBox. Query type "near" cannot be used in combine query (must be top query)
//TODO: investigate performance difference between "near" and "withinGeoBox"
constraints.append(withinGeoBox(key: "loc", fromSouthWest: ParseGeoPoint(latitude: 45, longitude: 45), toNortheast: ParseGeoPoint(latitude: 55, longitude: 55)))
//if user is interested in one-night-stand only, filter that field also
if currentProfile.ons {
constraints.append("ons" == true)
}
//append query to array for combined query call
let dateQuery = PrsProfile.query(constraints)
specificQueries.append(dateQuery)
}
// ----- language tandem partners -----
if currentProfile.tandem, currentProfile.learnLanguages.count > 0 {
var constraints = [QueryConstraint]()
//look for people that are looking for other gender or notDefined. If field "tndm" is nil, then the users do not look for language tandem partners
constraints.append(currentGender == .woman ? "tndm" < Gender.man.rawValue : "tndm" > Gender.woman.rawValue)
//if device user specify certain gender, look only for users of that gender
if currentProfile.tandemData != Gender.notDefined.rawValue {
constraints.append("gndr" == currentProfile.tandemData)
}
//check for people speaking or learning any of the language that device user is learning. It has to ne "or" to fetch user that speak at least one of nativeLanguages
var languageQueries = [Query<PrsProfile>]()
for language in currentProfile.learnLanguages {
languageQueries.append(PrsProfile.query(containsAll(key: "nlg", array: [language])))
languageQueries.append(PrsProfile.query(containsAll(key: "slg", array: [language])))
}
constraints.append(or(queries: languageQueries))
//append query to array for combined query call
let tandemQuery = PrsProfile.query(constraints)
specificQueries.append(tandemQuery)
}
// ----- travel buddy query -----
if currentProfile.travel, currentProfile.tripsGeos.count > 0 {
var constraints = [QueryConstraint]()
//look for people that are looking for other gender or notDefined. If field "trvl" is nil, then the users do not look for travel tandem buddy
constraints.append(currentGender == .woman ? "trvl" < Gender.man.rawValue : "trvl" > Gender.woman.rawValue)
//if device user specify certain gender, look only for users of that gender
if currentProfile.travelData != Gender.notDefined.rawValue {
constraints.append("gndr" == currentProfile.travelData)
}
//go through current user travel destinations and search for users that hase same Geohash in profile and:
// 1) end date of their trip is larger than current user start date
// 2) start date of their trip is smaller than current user end date
//that guarantee to fetch only people that intersect with current user trip
var travelDestinationsQueries = [Query<PrsProfile>]()
for geoHash in currentProfile.tripsGeos {
let startDateString = geoHash.geohash + String(describing: geoHash.startDate)
let endDateString = geoHash.geohash + String(describing: geoHash.endDate)
//TODO: How to fetch?
//profile can have two arrays "startGeos" & "endGeos"
//but then I would need to combine somehow "hasPrefix" with "largerThan"/"smallerThan"
//Saving tripGeos as a separate object type is an option, but assuming that each user has 5 tripGeos in average, this creates another huge table eventually
}
//append query to array for combined query call
let travelQuery = PrsProfile.query(constraints)
specificQueries.append(travelQuery)
}
// ----- compatriots query -----
// TODO: guard that user is not living in a country where his native language is official language --> could be misused for more match count
if currentProfile.compatriots, currentProfile.nativeLanguages.count > 0 {
var constraints = [QueryConstraint]()
//look for people that are looking for other gender or notDefined. If field "comp" is nil, then the users do not look for compatriots
constraints.append(currentGender == .woman ? "comp" < Gender.man.rawValue : "comp" > Gender.woman.rawValue)
//if device user specify certain gender, look only for users of that gender
if currentProfile.compatriotsData != Gender.notDefined.rawValue {
constraints.append("gndr" == currentProfile.compatriotsData)
}
//check for people speaking same native language. It has to ne "or" to fetch user that speak at least one of nativeLanguages
var nativeLanguageQueries = [Query<PrsProfile>]()
for language in currentProfile.nativeLanguages {
nativeLanguageQueries.append(PrsProfile.query(containsAll(key: "nlg", array: [language])))
}
constraints.append(or(queries: nativeLanguageQueries))
//restrict search only within given geoBox. Query type "near" cannot be used in combine query (must be top query)
//TODO: investigate performance difference between "near" and "withinGeoBox"
constraints.append(withinGeoBox(key: "loc", fromSouthWest: ParseGeoPoint(latitude: 45, longitude: 45), toNortheast: ParseGeoPoint(latitude: 55, longitude: 55)))
//append query to array for combined query call
let compatriotsQuery = PrsProfile.query(constraints)
specificQueries.append(compatriotsQuery)
}
guard specificQueries.count > 0 else {
print("cannot run combined query on empty Queries array")
return
}
//generates combined query where it returns profiles that match any of the 4 query types
let combinedSpecificQuery = PrsProfile.query(or(queries: specificQueries))
//creates final query
let finalQuery = PrsProfile.query(and(queries: [basicQuery, combinedSpecificQuery]))
print("---> \n \(combinedSpecificQuery) \n <----")
finalQuery.find { result in
switch result {
case .success(let profiles):
print("parse profiles found: \(profiles)")
case .failure(let errror):
fatalError(errror.localizedDescription)
}
}
}
printing the query shows the length of the request that I do not find a huge:
Query(method: “GET”, limit: 100, skip: 0, keys: nil, include: nil, order: nil, isCount: nil, explain: nil, hint: nil, where: ParseSwift.QueryWhere(constraints: [“$or”: [ParseSwift.QueryConstraint(key: “$or”, value: [ParseSwift.OrAndQuery<Felse.PrsProfile>(query: ParseSwift.Query<Felse.PrsProfile>(method: “GET”, limit: 100, skip: 0, keys: nil, include: nil, order: nil, isCount: nil, explain: nil, hint: nil, where: ParseSwift.QueryWhere(constraints: [“ons”: [ParseSwift.QueryConstraint(key: “ons”, value: true, comparator: nil)], “date”: [ParseSwift.QueryConstraint(key: “date”, value: 0, comparator: Optional(Comparator(stringValue: “$gt”, intValue: nil)))], “gndr”: [ParseSwift.QueryConstraint(key: “gndr”, value: 0, comparator: nil)], “loc”: [ParseSwift.QueryConstraint(key: “loc”, value: [“$box”: [GeoPoint ({“__type”:“GeoPoint”,“longitude”:45,“latitude”:45}), GeoPoint ({“__type”:“GeoPoint”,“longitude”:55,“latitude”:55})]], comparator: Optional(Comparator(stringValue: “$within”, intValue: nil)))]]), excludeKeys: nil, readPreference: nil, includeReadPreference: nil, subqueryReadPreference: nil, distinct: nil, fields: nil)), ParseSwift.OrAndQuery<Felse.PrsProfile>(query: ParseSwift.Query<Felse.PrsProfile>(method: “GET”, limit: 100, skip: 0, keys: nil, include: nil, order: nil, isCount: nil, explain: nil, hint: nil, where: ParseSwift.QueryWhere(constraints: [“trvl”: [ParseSwift.QueryConstraint(key: “trvl”, value: 0, comparator: Optional(Comparator(stringValue: “$gt”, intValue: nil)))]]), excludeKeys: nil, readPreference: nil, includeReadPreference: nil, subqueryReadPreference: nil, distinct: nil, fields: nil))], comparator: nil)]]), excludeKeys: nil, readPreference: nil, includeReadPreference: nil, subqueryReadPreference: nil, distinct: nil, fields: nil)
I understand that this is very broad topic and there might be many ways how to achieve desired behaviour. The reason why I ask is to prevent any “technology debt” and my project falling apart after it reaches certain number of users (I invested 2 years of my free time in it so far). To make long story short here comes my questions:
-
Any hints on how to search through the “tripGeos” and return only profiles that has intersection with my tripGeo (geohash, startDate, endDate)?
-
how to prevent refetching the same profiles again and again? I would need to pass an Array to
basicConstraints.append(notContainedIn(key: <String>, array: <[Encodable]>))
but this Array might get 1000+ objectId after some time -
Is it realistic to use such query on database that would grow larger? it works somehow now when there are 10 entries in the database table. Has anyone experience with 10.000 or 100.000 or more entries?
-
As I am a beginner I can’t clearly decide if I should implement ElasticSearch to handle this functionality - as this adds a lot of complexity (I am currently using back4app) and I have no experience with ElasticSearch
Any opinion or comment is welcome!
Thank you kindly!