Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

profile.js 9.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. const Schmervice = require('@hapipal/schmervice')
  2. const cosineSimilarity = require('compute-cosine-similarity')
  3. const haversine = require('haversine')
  4. const profile = require('../plugins/profile')
  5. const magic = 1000
  6. const scoreResponses = (seeker, potentialMatch) => {
  7. if (seeker.responses.length != potentialMatch.responses.length)
  8. return {
  9. error: `complete responses for profile: ${seeker.profile_id} unqeual to profile: ${potentialMatch.profile_id} | ${seeker.responses.length}:${potentialMatch.responses.length}`,
  10. }
  11. const checkValCb = res => {
  12. const val = parseInt(res.val)
  13. return isNaN(val) ? 0 : val
  14. }
  15. return Math.floor(
  16. cosineSimilarity(
  17. seeker.responses.map(checkValCb),
  18. potentialMatch.responses.map(checkValCb),
  19. ) * magic,
  20. )
  21. }
  22. const filterByDistance = (profileList, max) => {
  23. return profileList.filter(profile => {
  24. const profileDistance = Math.floor(parseFloat(profile.distance) * 100)
  25. const adjustedMaxDistance = Math.floor(parseFloat(max) * 100)
  26. return profileDistance <= adjustedMaxDistance
  27. })
  28. }
  29. const scoreAll = (profileList, userProfile) => {
  30. return profileList.map(profile => {
  31. return {
  32. // Uncomment to return the whole profile
  33. // ...profile,
  34. profile_id: profile.profile_id,
  35. score: scoreResponses(userProfile, profile),
  36. distance: profile.distance
  37. }
  38. })
  39. }
  40. /**
  41. * Grab the zip code string
  42. */
  43. const getZipCodeFromProfile = (profile) => {
  44. // There should only be one zip code entry per profile
  45. let zip = profile.responses.filter(response => response.response_key_id == 16)[0]
  46. const responseIndexForZip = profile.responses.indexOf(zip)
  47. if(responseIndexForZip >= 0) {
  48. profile.responses.splice(responseIndexForZip, 1)
  49. }
  50. return zip.val
  51. }
  52. /**
  53. * Class to hold our retrieved profile information
  54. * in a convenient wrapper
  55. * !: This needs to match the responseSchema in profiles.js
  56. */
  57. class CompleteProfile {
  58. constructor(profile, type) {
  59. this.user_id = profile.user_id // int user_id
  60. this.profile_id = profile.profile_id // int profile_id
  61. this.responses = profile.responses // [] of all responses
  62. this.user_type = type
  63. }
  64. }
  65. module.exports = class ProfileService extends Schmervice.Service {
  66. constructor(...args) {
  67. super(...args)
  68. }
  69. /**
  70. * Internal method to get list of profile_ids for this user
  71. * @param {number} userId
  72. * @returns {Array} List of all profile_ids for user
  73. */
  74. async _getProfileIdsForUserId(userId) {
  75. const { Profile } = this.server.models()
  76. /** Grab every Profile associated with this id */
  77. const allProfiles = await Profile.query().where('user_id', userId)
  78. /** Copy a list of the just the Profiles */
  79. const profileIdsToGrab = allProfiles.map(profile => profile.profile_id)
  80. /** Uncomment to dedupe the list just in case */
  81. return [...new Set(profileIdsToGrab)]
  82. }
  83. async getCompleteProfilesFor(userId, type) {
  84. const { Profile } = this.server.models()
  85. const dedupedProfileIds = await this._getProfileIdsForUserId(userId)
  86. const profilesEntries = await Profile.query()
  87. .whereIn('profile_id', dedupedProfileIds)
  88. .withGraphFetched('responses')
  89. //** Get responses asociated with each profile_id */
  90. return profilesEntries.map(profile => {
  91. return new CompleteProfile(profile, type)
  92. })
  93. }
  94. /**
  95. * Save responses in a profile
  96. * @param {number} userId
  97. * @param {Array} responses
  98. * @returns {object}
  99. */
  100. async saveResponsesCreateProfileFor(userId, responses, txn) {
  101. const { Profile, Response } = this.server.models()
  102. const profile = await Profile.query(txn).insert({
  103. user_id: userId,
  104. })
  105. for (const responseToSave of responses) {
  106. const responseInfo = {
  107. profile_id: profile.id,
  108. response_key_id: responseToSave.response_key_id,
  109. val: responseToSave.val,
  110. }
  111. await Response.query(txn).insert(responseInfo)
  112. }
  113. //** Work around for HAPI returning profile_id as id */
  114. return { user_id: profile.user_id, profile_id: profile.id }
  115. }
  116. /** Update responses in place
  117. * @param {number} profileId
  118. * @param {Array} responses
  119. * @returns {Array} updated responses
  120. */
  121. async updateResponsesInProfile(profileId, responses, txn) {
  122. const { Response } = this.server.models()
  123. for (const responseToSave of responses) {
  124. await Response.query(txn)
  125. .update({
  126. response_id: responseToSave.response_id,
  127. profile_id: responseToSave.profile_id,
  128. response_key_id: responseToSave.response_key_id,
  129. val: responseToSave.val,
  130. })
  131. .where({
  132. profile_id: profileId,
  133. })
  134. .where({
  135. response_id: responseToSave.response_id,
  136. })
  137. }
  138. return await Response.query(txn).where({
  139. profile_id: profileId,
  140. })
  141. }
  142. /** Add response
  143. * @param {Object} response to save
  144. * @returns {null} updated responses
  145. * @returns {Array} updated responses
  146. */
  147. async saveResponseForProfile(profileId, responseToSave) {
  148. const { Response } = this.server.models()
  149. let allResponses = await Response.query().where({
  150. profile_id: profileId,
  151. })
  152. const matchingResponses = allResponses.filter(response => response.response_key_id == responseToSave.response_key_id)
  153. // ?:Maybe bad idea
  154. if(matchingResponses.length > 0) { return null }
  155. await await Response.query().insert(responseToSave)
  156. return allResponses
  157. }
  158. /**
  159. * Delete a profile
  160. * @param {number} userId
  161. * @param {number} profileId
  162. * @returns
  163. */
  164. async deleteProfile(userId, profileId) {
  165. const { Profile } = this.server.models()
  166. const dedupedGroupings = await this._getProfileIdsForUserId(userId)
  167. /** Do NOTHING if NOT in Grouping */
  168. if (!dedupedGroupings.includes(profileId)) return
  169. return await Profile.query().delete().where('profile_id', profileId)
  170. }
  171. /**
  172. * Score a profile
  173. * @param {number} profileId
  174. * @returns {Array} Ordered and scored Profiles
  175. */
  176. async scoreProfilesFor(profileId, maxDistance, distanceUnit) {
  177. const { Profile } = this.server.models()
  178. // Our User Profile to score for
  179. const userProfile = await Profile.query()
  180. .findOne('profile_id', profileId)
  181. .withGraphFetched('responses')
  182. .withGraphFetched('user')
  183. // Move unneeded responses
  184. const userZip = getZipCodeFromProfile(userProfile)
  185. // Find all Profiles that are NOT of our userProfile.type
  186. // ie. If userProfile.type == seeker, then find: poster
  187. let profileIdsOfOppositeType = await Profile.query()
  188. .withGraphFetched('responses')
  189. .withGraphFetched('user')
  190. // TODO: Let Objection optimize this
  191. const isPosterOpposite = userProfile.user.is_poster == 1 ? 0 : 1
  192. profileIdsOfOppositeType = profileIdsOfOppositeType.filter(profile => profile.user.is_poster == isPosterOpposite)
  193. const profilePlusDistance = await Promise.all(profileIdsOfOppositeType.map(async profile => {
  194. const targetZip = getZipCodeFromProfile(profile)
  195. const distance = await this._compareDistance(userZip, targetZip, distanceUnit)
  196. return {
  197. ...profile,
  198. distance: [distance.toFixed(2), distanceUnit]
  199. }
  200. }))
  201. const distanceFilteredProfiles = filterByDistance(profilePlusDistance, maxDistance)
  202. const scoredProfilesWithDistance = scoreAll(distanceFilteredProfiles, userProfile)
  203. // Order by score
  204. return scoredProfilesWithDistance.sort((a, b) => a.score - b.score)
  205. }
  206. /**
  207. * Use the db for zipcode info
  208. * @param {string} zipCode
  209. * @param {object}
  210. */
  211. async _latLonForZip(zipCode) {
  212. const { ZipCode } = this.server.models()
  213. const zipInfo = await ZipCode.query().findOne('zip_code_id', parseInt(zipCode))
  214. const latitude = parseFloat(zipInfo.latitude)
  215. const longitude = parseFloat(zipInfo.longitude)
  216. return { latitude, longitude }
  217. }
  218. /**
  219. * Get the distance between two zipcodes
  220. * using the haversine formula
  221. * @param {string} start_zip
  222. * @param {string} end_zip
  223. * @param {number} distance in miles
  224. */
  225. async _compareDistance(start_zip, end_zip, distanceUnit) {
  226. if(!start_zip || !end_zip) return
  227. const start = await this._latLonForZip(start_zip)
  228. const end = await this._latLonForZip(end_zip)
  229. return haversine(start, end, { unit: distanceUnit })
  230. }
  231. }