| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430 |
- const Schmervice = require('@hapipal/schmervice')
- const cosineSimilarity = require('compute-cosine-similarity')
- const haversine = require('haversine')
-
- const runMatch = (allYins, allYangs) => {
- balanceSides(allYins, allYangs)
- console.log(allYins.length, ':', allYangs.length)
- // You only need to engage from one side
- engageEveryone(allYins)
-
- console.log('---')
- }
- const engageEveryone = allYins => {
- let done
- do {
- console.log('rerunning...')
- done = true
- allYins.forEach(yin => {
- // Keep matching if no true pairing is found
- console.log(yin.realId, yin.otp?.realId)
- if (!yin.otp) {
- done = false
- const yang = yin.getNextCandidate()
- if (!yang.otp || yang.prefers(yin)) {
- yin.engageTo(yang)
- }
- } else {
- console.log(yin.otp.realId)
- }
- })
- } while (!done)
- }
- const balanceSides = (yins, yangs) => {
- let diff = Math.abs(yangs.length - yins.length)
- let smallerList = yangs.length < yins.length ? yangs : yins
- const totalProfiles = yangs.length + yins.length + 1
- for (let i = 0; i < diff; i++) {
- smallerList.push(new ProfileFacade(totalProfiles + i))
- }
- }
- class ProfileFacade {
- constructor(id, matchQueue) {
- this.realId = id ? id : undefined
-
- this.matchCandidateIndex = 0
- this.otp = null
- this.matchQueue = matchQueue?.length ? matchQueue : []
- this.fOrder = []
- }
- clearPlaceholderMatches() {
- this.matchQueue = this.matchQueue.filter(match => match.realId == false)
- this.fOrder = this.fOrder.filter(
- potentialFiance => potentialFiance.realId == false,
- )
- if (this.otp && this.otp.realId) {
- this.otp = null
- }
- }
- rank(id) {
- const idQueue = this.matchQueue.map(
- profileFacade => profileFacade.realId,
- )
- return idQueue.includes(id)
- ? idQueue.indexOf(id)
- : this.matchQueue.length + 1
- }
- prefers(p) {
- return this.rank(p.realId) < this.rank(this.otp.realId)
- }
- getNextCandidate() {
- if (this.matchCandidateIndex >= this.matchQueue.length) return null
- this.matchCandidateIndex = this.matchCandidateIndex + 1
- return this.matchQueue[this.matchCandidateIndex - 1]
- }
- engageTo(p) {
- if (p.otp) {
- p.otp.otp = null
- }
- if (this.otp) {
- this.otp.otp = null
- }
-
- p.otp = this
- p.fOrder.unshift(this)
-
- this.otp = p
- this.fOrder.unshift(p)
- console.log(
- 'partners pref: ',
- this.otp.matchQueue.map(f => f.realId).indexOf(this.realId) + 1,
- 'choice',
- )
- }
- }
-
- const magic = 1000
- const scoreResponses = (seeker, potentialMatch) => {
- if (seeker.responses.length != potentialMatch.responses.length)
- return {
- error: `complete responses for profile: ${seeker.profile_id} unqeual to profile: ${potentialMatch.profile_id} | ${seeker.responses.length}:${potentialMatch.responses.length}`,
- }
-
- const checkValCb = res => {
- const val = parseInt(res.val)
- return isNaN(val) ? 0 : val
- }
-
- return Math.floor(
- cosineSimilarity(
- seeker.responses.map(checkValCb),
- potentialMatch.responses.map(checkValCb),
- ) * magic,
- )
- }
- const filterByDistance = (profileList, max) => {
- return profileList.filter(profile => {
- const profileDistance = Math.floor(parseFloat(profile.distance) * 100)
- const adjustedMaxDistance = Math.floor(parseFloat(max) * 100)
- return profileDistance <= adjustedMaxDistance
- })
- }
- const scoreAll = (profileList, userProfile) => {
- return profileList.map(profile => {
- return {
- // Uncomment to return the whole profile
- // ...profile,
- profile_id: profile.profile_id,
- score: scoreResponses(userProfile, profile),
- distance: profile.distance,
- }
- })
- }
- /**
- * Grab the zip code string
- */
- const getZipCodeFromProfile = profile => {
- // There should only be one zip code entry per profile
- let zip = profile.responses.filter(
- response => response.response_key_id == 16,
- )[0]
- const responseIndexForZip = profile.responses.indexOf(zip)
- if (responseIndexForZip >= 0) {
- profile.responses.splice(responseIndexForZip, 1)
- }
- return zip.val
- }
-
- /**
- * Class to hold our retrieved profile information
- * in a convenient wrapper
- * !: This needs to match the responseSchema in profiles.js
- */
- class CompleteProfile {
- constructor(profile, type) {
- this.user_id = profile.user_id // int user_id
- this.profile_id = profile.profile_id // int profile_id
- this.responses = profile.responses // [] of all responses
- this.user_type = type
- }
- }
-
- module.exports = class ProfileService extends Schmervice.Service {
- constructor(...args) {
- super(...args)
- }
-
- /**
- * Internal method to get list of profile_ids for this user
- * @param {number} userId
- * @returns {Array} List of all profile_ids for user
- */
- async _getProfileIdsForUserId(userId) {
- const { Profile } = this.server.models()
-
- /** Grab every Profile associated with this id */
- const allProfiles = await Profile.query().where('user_id', userId)
-
- /** Copy a list of the just the Profiles */
- const profileIdsToGrab = allProfiles.map(profile => profile.profile_id)
-
- /** Uncomment to dedupe the list just in case */
- return [...new Set(profileIdsToGrab)]
- }
-
- async getCompleteProfilesFor(userId, type) {
- const { Profile } = this.server.models()
-
- const dedupedProfileIds = await this._getProfileIdsForUserId(userId)
-
- const profilesEntries = await Profile.query()
- .whereIn('profile_id', dedupedProfileIds)
- .withGraphFetched('responses')
-
- //** Get responses asociated with each profile_id */
- return profilesEntries.map(profile => {
- return new CompleteProfile(profile, type)
- })
- }
-
- /**
- * Save responses in a profile
- * @param {number} userId
- * @param {Array} responses
- * @returns {object}
- */
- async saveResponsesCreateProfileFor(userId, responses, txn) {
- const { Profile, Response } = this.server.models()
-
- const profile = await Profile.query(txn).insert({
- user_id: userId,
- })
- for (const responseToSave of responses) {
- const responseInfo = {
- profile_id: profile.id,
- response_key_id: responseToSave.response_key_id,
- val: responseToSave.val,
- }
- await Response.query(txn).insert(responseInfo)
- }
- //** Work around for HAPI returning profile_id as id */
- return { user_id: profile.user_id, profile_id: profile.id }
- }
-
- /** Update responses in place
- * @param {number} profileId
- * @param {Array} responses
- * @returns {Array} updated responses
- */
- async updateResponsesInProfile(profileId, responses, txn) {
- const { Response } = this.server.models()
- for (const responseToSave of responses) {
- await Response.query(txn)
- .update({
- response_id: responseToSave.response_id,
- profile_id: responseToSave.profile_id,
- response_key_id: responseToSave.response_key_id,
- val: responseToSave.val,
- })
- .where({
- profile_id: profileId,
- })
- .where({
- response_id: responseToSave.response_id,
- })
- }
- return await Response.query(txn).where({
- profile_id: profileId,
- })
- }
-
- /** Add response
- * @param {Object} response to save
- * @returns {null} updated responses
- * @returns {Array} updated responses
- */
- async saveResponseForProfile(profileId, responseToSave) {
- const { Response } = this.server.models()
- let allResponses = await Response.query().where({
- profile_id: profileId,
- })
- const matchingResponses = allResponses.filter(
- response =>
- response.response_key_id == responseToSave.response_key_id,
- )
-
- // ?:Maybe bad idea
- if (matchingResponses.length > 0) {
- return null
- }
-
- await await Response.query().insert(responseToSave)
- return allResponses
- }
-
- /**
- * Delete a profile
- * @param {number} userId
- * @param {number} profileId
- * @returns
- */
- async deleteProfile(userId, profileId) {
- const { Profile } = this.server.models()
-
- const dedupedGroupings = await this._getProfileIdsForUserId(userId)
-
- /** Do NOTHING if NOT in Grouping */
- if (!dedupedGroupings.includes(profileId)) return
-
- return await Profile.query().delete().where('profile_id', profileId)
- }
-
- /**
- * Score a profile
- * @param {number} profileId
- * @returns {Array} Ordered and scored Profiles
- */
- async scoreProfilesFor(profileId, maxDistance, distanceUnit) {
- const { Profile } = this.server.models()
-
- // Our User Profile to score for
- const userProfile = await Profile.query()
- .findOne('profile_id', profileId)
- .withGraphFetched('responses')
- .withGraphFetched('user')
-
- // Move unneeded responses
- const userZip = getZipCodeFromProfile(userProfile)
-
- // Find all Profiles that are NOT of our userProfile.type
- // ie. If userProfile.type == seeker, then find: poster
- let profileIdsOfOppositeType = await Profile.query()
- .withGraphFetched('responses')
- .withGraphFetched('user')
-
- // TODO: Let Objection optimize this
- const isPosterOpposite = userProfile.user.is_poster == 1 ? 0 : 1
- profileIdsOfOppositeType = profileIdsOfOppositeType.filter(
- profile => profile.user.is_poster == isPosterOpposite,
- )
-
- const profilePlusDistance = await Promise.all(
- profileIdsOfOppositeType.map(async profile => {
- const targetZip = getZipCodeFromProfile(profile)
- const distance = await this._compareDistance(
- userZip,
- targetZip,
- distanceUnit,
- )
- return {
- ...profile,
- distance: [distance.toFixed(2), distanceUnit],
- }
- }),
- )
-
- const distanceFilteredProfiles = filterByDistance(
- profilePlusDistance,
- maxDistance,
- )
-
- const scoredProfilesWithDistance = scoreAll(
- distanceFilteredProfiles,
- userProfile,
- )
-
- // Order by score
- return scoredProfilesWithDistance.sort((a, b) => a.score - b.score)
- }
- async calcMatches() {
- const { Profile } = this.server.models()
-
- // Grab all profiles with matchQueues
- let allProfiles = await Profile.query().withGraphFetched('user')
-
- // !:FAKE Score everyone
- // const profileIds = allProfiles.map(profile => profile.profile_id)
- const scoredProfileQueuesById = {}
- for (let profile of allProfiles) {
- const profileQueue = await this.scoreProfilesFor(
- profile.profile_id,
- 10000,
- 'mile',
- )
- scoredProfileQueuesById[profile.profile_id] = profileQueue.map(
- profile => profile.profile_id,
- )
- }
- const allProfileFacadesWithQueue = allProfiles.map(profile => {
- const profileFacadeQueue = scoredProfileQueuesById[
- profile.profile_id
- ].map(id => {
- const subQueue = scoredProfileQueuesById[id].map(
- id => new ProfileFacade(id),
- )
- return new ProfileFacade(id, subQueue)
- })
- return new ProfileFacade(profile.profile_id, profileFacadeQueue)
- })
- // // ! FAKE -- END
-
- const seekerIds = allProfiles
- .filter(profile => profile.user.is_poster == 0)
- .map(profile => profile.profile_id)
- const posterIds = allProfiles
- .filter(profile => profile.user.is_poster == 1)
- .map(profile => profile.profile_id)
-
- const yins = seekerIds.map(id => allProfileFacadesWithQueue[id])
- const yangs = posterIds.map(id => allProfileFacadesWithQueue[id])
-
- runMatch(yins, yangs)
- return yins
- }
- /**
- * Use the db for zipcode info
- * @param {string} zipCode
- * @param {object}
- */
- async _latLonForZip(zipCode) {
- const { ZipCode } = this.server.models()
-
- const zipInfo = await ZipCode.query().findOne(
- 'zip_code_id',
- parseInt(zipCode),
- )
- if (!zipInfo) {
- console.log(zipCode)
- }
-
- return {
- latitude: parseFloat(zipInfo.latitude),
- longitude: parseFloat(zipInfo.longitude),
- }
- }
- /**
- * Get the distance between two zipcodes
- * using the haversine formula
- * @param {string} start_zip
- * @param {string} end_zip
- * @param {number} distance in miles
- */
- async _compareDistance(start_zip, end_zip, distanceUnit) {
- if (!start_zip || !end_zip) return
-
- const start = await this._latLonForZip(start_zip)
- const end = await this._latLonForZip(end_zip)
-
- return haversine(start, end, { unit: distanceUnit })
- }
- }
|