Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. const Schmervice = require('@hapipal/schmervice')
  2. const cosineSimilarity = require('compute-cosine-similarity')
  3. const haversine = require('haversine')
  4. const { profiles } = require('../../db/mock')
  5. const rand = max => {
  6. return Math.floor(Math.random() * max) < 1
  7. ? 1
  8. : Math.floor(Math.random() * max)
  9. }
  10. const runMatch = (allYins, allYangs) => {
  11. balanceSides(allYins, allYangs)
  12. // You only need to engage from one side
  13. engageEveryone(allYins)
  14. allYins.forEach(yin => yin.clearPlaceholderMatches())
  15. allYangs.forEach(yang => yang.clearPlaceholderMatches())
  16. console.log('---')
  17. }
  18. const engageEveryone = allYinsOrYangs => {
  19. let done
  20. do {
  21. done = true
  22. allYinsOrYangs.forEach(yinOrYang => {
  23. if (!yinOrYang.otp) {
  24. done = false
  25. const yangOrYin = yinOrYang.getNextCandidate()
  26. if (!yangOrYin.otp || yangOrYin.prefers(yinOrYang)) {
  27. yinOrYang.engageTo(yangOrYin)
  28. }
  29. }
  30. })
  31. } while (!done)
  32. }
  33. const balanceSides = (yins, yangs) => {
  34. let diff = Math.abs(yangs.length - yins.length)
  35. let smallerList = yangs.length < yins.length ? yangs : yins
  36. const totalProfiles = yangs.length + yins.length + 1
  37. for (let i = 0; i < diff; i++) {
  38. smallerList.push(new ProfileFacade(totalProfiles + i))
  39. }
  40. }
  41. class ProfileFacade {
  42. constructor(id, matchQueue) {
  43. this.realId = id ? id : undefined
  44. this.matchCandidateIndex = 0
  45. this.otp = null
  46. this.matchQueue = matchQueue?.length ? matchQueue : []
  47. this.fOrder = []
  48. }
  49. clearPlaceholderMatches(){
  50. this.matchQueue = this.matchQueue.filter(
  51. match => match.realId == false,
  52. )
  53. this.fOrder = this.fOrder.filter(
  54. potentialFiance => potentialFiance.realId == false,
  55. )
  56. if (this.otp && this.otp.realId) {
  57. this.otp = null
  58. }
  59. }
  60. rank(p) {
  61. for (let i = 0; i < this.matchQueue.length; i++) {
  62. if (this.matchQueue[i] === p) {
  63. return i
  64. }
  65. }
  66. return this.matchQueue.length + 1
  67. }
  68. prefers(p) {
  69. return this.rank(p) < this.rank(this.otp)
  70. }
  71. getNextCandidate() {
  72. if (this.matchCandidateIndex >= this.matchQueue.length) return null
  73. return this.matchQueue[this.matchCandidateIndex++]
  74. }
  75. engageTo(p) {
  76. if (p.otp) { p.otp.otp = null }
  77. p.otp = this
  78. p.fOrder.unshift(this)
  79. if (this.otp) { this.otp.otp = null }
  80. this.otp = p
  81. this.fOrder.unshift(p)
  82. }
  83. }
  84. const magic = 1000
  85. const scoreResponses = (seeker, potentialMatch) => {
  86. if (seeker.responses.length != potentialMatch.responses.length)
  87. return {
  88. error: `complete responses for profile: ${seeker.profile_id} unqeual to profile: ${potentialMatch.profile_id} | ${seeker.responses.length}:${potentialMatch.responses.length}`,
  89. }
  90. const checkValCb = res => {
  91. const val = parseInt(res.val)
  92. return isNaN(val) ? 0 : val
  93. }
  94. return Math.floor(
  95. cosineSimilarity(
  96. seeker.responses.map(checkValCb),
  97. potentialMatch.responses.map(checkValCb),
  98. ) * magic,
  99. )
  100. }
  101. const filterByDistance = (profileList, max) => {
  102. return profileList.filter(profile => {
  103. const profileDistance = Math.floor(parseFloat(profile.distance) * 100)
  104. const adjustedMaxDistance = Math.floor(parseFloat(max) * 100)
  105. return profileDistance <= adjustedMaxDistance
  106. })
  107. }
  108. const scoreAll = (profileList, userProfile) => {
  109. return profileList.map(profile => {
  110. return {
  111. // Uncomment to return the whole profile
  112. // ...profile,
  113. profile_id: profile.profile_id,
  114. score: scoreResponses(userProfile, profile),
  115. distance: profile.distance
  116. }
  117. })
  118. }
  119. /**
  120. * Grab the zip code string
  121. */
  122. const getZipCodeFromProfile = (profile) => {
  123. // There should only be one zip code entry per profile
  124. let zip = profile.responses.filter(response => response.response_key_id == 16)[0]
  125. const responseIndexForZip = profile.responses.indexOf(zip)
  126. if(responseIndexForZip >= 0) {
  127. profile.responses.splice(responseIndexForZip, 1)
  128. }
  129. return zip.val
  130. }
  131. /**
  132. * Class to hold our retrieved profile information
  133. * in a convenient wrapper
  134. * !: This needs to match the responseSchema in profiles.js
  135. */
  136. class CompleteProfile {
  137. constructor(profile, type) {
  138. this.user_id = profile.user_id // int user_id
  139. this.profile_id = profile.profile_id // int profile_id
  140. this.responses = profile.responses // [] of all responses
  141. this.user_type = type
  142. }
  143. }
  144. module.exports = class ProfileService extends Schmervice.Service {
  145. constructor(...args) {
  146. super(...args)
  147. }
  148. /**
  149. * Internal method to get list of profile_ids for this user
  150. * @param {number} userId
  151. * @returns {Array} List of all profile_ids for user
  152. */
  153. async _getProfileIdsForUserId(userId) {
  154. const { Profile } = this.server.models()
  155. /** Grab every Profile associated with this id */
  156. const allProfiles = await Profile.query().where('user_id', userId)
  157. /** Copy a list of the just the Profiles */
  158. const profileIdsToGrab = allProfiles.map(profile => profile.profile_id)
  159. /** Uncomment to dedupe the list just in case */
  160. return [...new Set(profileIdsToGrab)]
  161. }
  162. async getCompleteProfilesFor(userId, type) {
  163. const { Profile } = this.server.models()
  164. const dedupedProfileIds = await this._getProfileIdsForUserId(userId)
  165. const profilesEntries = await Profile.query()
  166. .whereIn('profile_id', dedupedProfileIds)
  167. .withGraphFetched('responses')
  168. //** Get responses asociated with each profile_id */
  169. return profilesEntries.map(profile => {
  170. return new CompleteProfile(profile, type)
  171. })
  172. }
  173. /**
  174. * Save responses in a profile
  175. * @param {number} userId
  176. * @param {Array} responses
  177. * @returns {object}
  178. */
  179. async saveResponsesCreateProfileFor(userId, responses, txn) {
  180. const { Profile, Response } = this.server.models()
  181. const profile = await Profile.query(txn).insert({
  182. user_id: userId,
  183. })
  184. for (const responseToSave of responses) {
  185. const responseInfo = {
  186. profile_id: profile.id,
  187. response_key_id: responseToSave.response_key_id,
  188. val: responseToSave.val,
  189. }
  190. await Response.query(txn).insert(responseInfo)
  191. }
  192. //** Work around for HAPI returning profile_id as id */
  193. return { user_id: profile.user_id, profile_id: profile.id }
  194. }
  195. /** Update responses in place
  196. * @param {number} profileId
  197. * @param {Array} responses
  198. * @returns {Array} updated responses
  199. */
  200. async updateResponsesInProfile(profileId, responses, txn) {
  201. const { Response } = this.server.models()
  202. for (const responseToSave of responses) {
  203. await Response.query(txn)
  204. .update({
  205. response_id: responseToSave.response_id,
  206. profile_id: responseToSave.profile_id,
  207. response_key_id: responseToSave.response_key_id,
  208. val: responseToSave.val,
  209. })
  210. .where({
  211. profile_id: profileId,
  212. })
  213. .where({
  214. response_id: responseToSave.response_id,
  215. })
  216. }
  217. return await Response.query(txn).where({
  218. profile_id: profileId,
  219. })
  220. }
  221. /** Add response
  222. * @param {Object} response to save
  223. * @returns {null} updated responses
  224. * @returns {Array} updated responses
  225. */
  226. async saveResponseForProfile(profileId, responseToSave) {
  227. const { Response } = this.server.models()
  228. let allResponses = await Response.query().where({
  229. profile_id: profileId,
  230. })
  231. const matchingResponses = allResponses.filter(response => response.response_key_id == responseToSave.response_key_id)
  232. // ?:Maybe bad idea
  233. if(matchingResponses.length > 0) { return null }
  234. await await Response.query().insert(responseToSave)
  235. return allResponses
  236. }
  237. /**
  238. * Delete a profile
  239. * @param {number} userId
  240. * @param {number} profileId
  241. * @returns
  242. */
  243. async deleteProfile(userId, profileId) {
  244. const { Profile } = this.server.models()
  245. const dedupedGroupings = await this._getProfileIdsForUserId(userId)
  246. /** Do NOTHING if NOT in Grouping */
  247. if (!dedupedGroupings.includes(profileId)) return
  248. return await Profile.query().delete().where('profile_id', profileId)
  249. }
  250. /**
  251. * Score a profile
  252. * @param {number} profileId
  253. * @returns {Array} Ordered and scored Profiles
  254. */
  255. async scoreProfilesFor(profileId, maxDistance, distanceUnit) {
  256. const { Profile } = this.server.models()
  257. // Our User Profile to score for
  258. const userProfile = await Profile.query()
  259. .findOne('profile_id', profileId)
  260. .withGraphFetched('responses')
  261. .withGraphFetched('user')
  262. // Move unneeded responses
  263. const userZip = getZipCodeFromProfile(userProfile)
  264. // Find all Profiles that are NOT of our userProfile.type
  265. // ie. If userProfile.type == seeker, then find: poster
  266. let profileIdsOfOppositeType = await Profile.query()
  267. .withGraphFetched('responses')
  268. .withGraphFetched('user')
  269. // TODO: Let Objection optimize this
  270. const isPosterOpposite = userProfile.user.is_poster == 1 ? 0 : 1
  271. profileIdsOfOppositeType = profileIdsOfOppositeType.filter(profile => profile.user.is_poster == isPosterOpposite)
  272. const profilePlusDistance = await Promise.all(profileIdsOfOppositeType.map(async profile => {
  273. const targetZip = getZipCodeFromProfile(profile)
  274. const distance = await this._compareDistance(userZip, targetZip, distanceUnit)
  275. return {
  276. ...profile,
  277. distance: [distance.toFixed(2), distanceUnit]
  278. }
  279. }))
  280. const distanceFilteredProfiles = filterByDistance(profilePlusDistance, maxDistance)
  281. const scoredProfilesWithDistance = scoreAll(distanceFilteredProfiles, userProfile)
  282. // Order by score
  283. return scoredProfilesWithDistance.sort((a, b) => a.score - b.score)
  284. }
  285. async calcMatches() {
  286. const { Profile } = this.server.models()
  287. // Grab all profiles with matchQueues
  288. let allProfiles = await Profile.query()
  289. .withGraphFetched('user')
  290. // !:FAKE Score everyone
  291. allProfiles = allProfiles.map(profile => {
  292. // Copy the profile and add a queue
  293. const profilePlusFakeScore = {
  294. ...profile,
  295. queue: []
  296. }
  297. // Generate a random order of profile_ids 1:7
  298. while (profilePlusFakeScore.queue.length < 4) {
  299. profilePlusFakeScore.queue.push(rand(4))
  300. profilePlusFakeScore.queue = [...new Set(profilePlusFakeScore.queue)]
  301. }
  302. //Instantiate a profile facade for each id for matching
  303. profilePlusFakeScore.queue = profilePlusFakeScore.queue.map(randId => new ProfileFacade(randId))
  304. return profilePlusFakeScore
  305. })
  306. // ! FAKE -- END
  307. const seekers = allProfiles.filter(profile => profile.user.is_poster == 0)
  308. const posters = allProfiles.filter(profile => profile.user.is_poster == 1)
  309. const yins = seekers.map(profile => new ProfileFacade(profile.profile_id, profile.queue))
  310. const yangs = posters.map(profile => new ProfileFacade(profile.profile_id, profile.queue))
  311. runMatch(yins, yangs)
  312. return yins
  313. }
  314. /**
  315. * Use the db for zipcode info
  316. * @param {string} zipCode
  317. * @param {object}
  318. */
  319. async _latLonForZip(zipCode) {
  320. const { ZipCode } = this.server.models()
  321. const zipInfo = await ZipCode.query().findOne('zip_code_id', parseInt(zipCode))
  322. const latitude = parseFloat(zipInfo.latitude)
  323. const longitude = parseFloat(zipInfo.longitude)
  324. return { latitude, longitude }
  325. }
  326. /**
  327. * Get the distance between two zipcodes
  328. * using the haversine formula
  329. * @param {string} start_zip
  330. * @param {string} end_zip
  331. * @param {number} distance in miles
  332. */
  333. async _compareDistance(start_zip, end_zip, distanceUnit) {
  334. if(!start_zip || !end_zip) return
  335. const start = await this._latLonForZip(start_zip)
  336. const end = await this._latLonForZip(end_zip)
  337. return haversine(start, end, { unit: distanceUnit })
  338. }
  339. }