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

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