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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  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. /**
  173. * Save responses in a profile
  174. * @param {number} userId
  175. * @param {Array} responses
  176. * @returns {object}
  177. */
  178. async saveResponsesCreateProfileFor(userId, responses, txn) {
  179. const { Profile, Response } = this.server.models()
  180. const profile = await Profile.query(txn).insert({
  181. user_id: userId,
  182. })
  183. for (const responseToSave of responses) {
  184. const responseInfo = {
  185. profile_id: profile.id,
  186. response_key_id: responseToSave.response_key_id,
  187. val: responseToSave.val,
  188. }
  189. await Response.query(txn).insert(responseInfo)
  190. }
  191. //** Work around for HAPI returning profile_id as id */
  192. return { user_id: profile.user_id, profile_id: profile.id }
  193. }
  194. /** Update responses in place
  195. * @param {number} profileId
  196. * @param {Array} responses
  197. * @returns {Array} updated responses
  198. */
  199. async updateResponsesInProfile(profileId, responses, txn) {
  200. const { Response } = this.server.models()
  201. for (const responseToSave of responses) {
  202. await Response.query(txn)
  203. .update({
  204. response_id: responseToSave.response_id,
  205. profile_id: responseToSave.profile_id,
  206. response_key_id: responseToSave.response_key_id,
  207. val: responseToSave.val,
  208. })
  209. .where({
  210. profile_id: profileId,
  211. })
  212. .where({
  213. response_id: responseToSave.response_id,
  214. })
  215. }
  216. return await Response.query(txn).where({
  217. profile_id: profileId,
  218. })
  219. }
  220. /** Add response
  221. * @param {Object} response to save
  222. * @returns {null} updated responses
  223. * @returns {Array} updated responses
  224. */
  225. async saveResponseForProfile(profileId, responseToSave) {
  226. const { Response } = this.server.models()
  227. let allResponses = await Response.query().where({
  228. profile_id: profileId,
  229. })
  230. const matchingResponses = allResponses.filter(
  231. response =>
  232. response.response_key_id == responseToSave.response_key_id,
  233. )
  234. // ?:Maybe bad idea
  235. if (matchingResponses.length > 0) {
  236. return null
  237. }
  238. await await Response.query().insert(responseToSave)
  239. return allResponses
  240. }
  241. /**
  242. * Delete a profile
  243. * @param {number} userId
  244. * @param {number} profileId
  245. * @returns
  246. */
  247. async deleteProfile(userId, profileId) {
  248. const { Profile } = this.server.models()
  249. const dedupedGroupings = await this._getProfileIdsForUserId(userId)
  250. /** Do NOTHING if NOT in Grouping */
  251. if (!dedupedGroupings.includes(profileId)) return
  252. return await Profile.query().delete().where('profile_id', profileId)
  253. }
  254. /**
  255. * Score a profile
  256. * @param {number} profileId
  257. * @returns {Array} Ordered and scored Profiles
  258. */
  259. async scoreProfilesFor(profileId, maxDistance, distanceUnit) {
  260. const { Profile } = this.server.models()
  261. // Our User Profile to score for
  262. const userProfile = await Profile.query()
  263. .findOne('profile_id', profileId)
  264. .withGraphFetched('responses')
  265. .withGraphFetched('user')
  266. // Move unneeded responses
  267. const userZip = getZipCodeFromProfile(userProfile)
  268. // Find all Profiles that are NOT of our userProfile.type
  269. // ie. If userProfile.type == seeker, then find: poster
  270. let profileIdsOfOppositeType = await Profile.query()
  271. .withGraphFetched('responses')
  272. .withGraphFetched('user')
  273. // TODO: Let Objection optimize this
  274. const isPosterOpposite = userProfile.user.is_poster == 1 ? 0 : 1
  275. profileIdsOfOppositeType = profileIdsOfOppositeType.filter(
  276. profile => profile.user.is_poster == isPosterOpposite,
  277. )
  278. const profilePlusDistance = await Promise.all(
  279. profileIdsOfOppositeType.map(async profile => {
  280. const targetZip = getZipCodeFromProfile(profile)
  281. const distance = await this._compareDistance(
  282. userZip,
  283. targetZip,
  284. distanceUnit,
  285. )
  286. return {
  287. ...profile,
  288. distance: [distance.toFixed(2), distanceUnit],
  289. }
  290. }),
  291. )
  292. const distanceFilteredProfiles = filterByDistance(
  293. profilePlusDistance,
  294. maxDistance,
  295. )
  296. const scoredProfilesWithDistance = scoreAll(
  297. distanceFilteredProfiles,
  298. userProfile,
  299. )
  300. // Order by score
  301. return scoredProfilesWithDistance.sort((a, b) => a.score - b.score)
  302. }
  303. async calcMatches() {
  304. const { Profile } = this.server.models()
  305. // Grab all profiles with matchQueues
  306. let allProfiles = await Profile.query().withGraphFetched('user')
  307. const seekerIds = allProfiles
  308. .filter(profile => profile.user.is_poster == 0)
  309. .map(profile => profile.profile_id)
  310. const posterIds = allProfiles
  311. .filter(profile => profile.user.is_poster == 1)
  312. .map(profile => profile.profile_id)
  313. let diff = Math.abs(posterIds.length - seekerIds.length)
  314. let smallerList =
  315. posterIds.length < seekerIds.length ? posterIds : seekerIds
  316. // ADD DUMMY IDS TO THE SMALLER LIST
  317. for (let d = 0; d < diff; d++) {
  318. smallerList.push(allProfiles.length + d)
  319. }
  320. // !:FAKE Score everyone
  321. const scoredProfileQueuesById = {}
  322. for (let profile of allProfiles) {
  323. const profileQueue = await this.scoreProfilesFor(
  324. profile.profile_id,
  325. 10000,
  326. 'mile',
  327. )
  328. scoredProfileQueuesById[profile.profile_id] = profileQueue.map(
  329. profile => profile.profile_id,
  330. )
  331. }
  332. const allProfileFacadesWithQueue = allProfiles.map(profile => {
  333. const profileFacadeQueue = scoredProfileQueuesById[
  334. profile.profile_id
  335. ].map(id => {
  336. const subQueue = scoredProfileQueuesById[id].map(
  337. id => new ProfileFacade(id),
  338. )
  339. return new ProfileFacade(id, subQueue)
  340. })
  341. return new ProfileFacade(profile.profile_id, profileFacadeQueue)
  342. })
  343. // // ! FAKE -- END
  344. const yins = seekerIds.map(id => allProfileFacadesWithQueue[id])
  345. const yangs = posterIds.map(id => allProfileFacadesWithQueue[id])
  346. runMatch(yins, yangs)
  347. return yins
  348. }
  349. /**
  350. * Use the db for zipcode info
  351. * @param {string} zipCode
  352. * @param {object}
  353. */
  354. async _latLonForZip(zipCode) {
  355. const { ZipCode } = this.server.models()
  356. const zipInfo = await ZipCode.query().findOne(
  357. 'zip_code_id',
  358. parseInt(zipCode),
  359. )
  360. if (!zipInfo) {
  361. console.log(zipCode)
  362. }
  363. return {
  364. latitude: parseFloat(zipInfo.latitude),
  365. longitude: parseFloat(zipInfo.longitude),
  366. }
  367. }
  368. /**
  369. * Get the distance between two zipcodes
  370. * using the haversine formula
  371. * @param {string} start_zip
  372. * @param {string} end_zip
  373. * @param {number} distance in miles
  374. */
  375. async _compareDistance(start_zip, end_zip, distanceUnit) {
  376. if (!start_zip || !end_zip) return
  377. const start = await this._latLonForZip(start_zip)
  378. const end = await this._latLonForZip(end_zip)
  379. return haversine(start, end, { unit: distanceUnit })
  380. }
  381. }