|
|
@@ -1,25 +1,27 @@
|
|
1
|
1
|
const Schmervice = require('@hapipal/schmervice')
|
|
2
|
|
-const cosineSimilarity = require('compute-cosine-similarity')
|
|
3
|
2
|
const haversine = require('haversine')
|
|
4
|
|
-const zipcodeKey = 7
|
|
5
|
|
-const magic = 1000
|
|
6
|
|
-const scoreResponses = (seeker, potentialMatch) => {
|
|
|
3
|
+
|
|
|
4
|
+const _ZIPCODEKEY = 7
|
|
|
5
|
+const scoreResponses = (seeker, potentialMatch, prescoreLookup) => {
|
|
7
|
6
|
if (seeker.responses.length != potentialMatch.responses.length)
|
|
8
|
7
|
return {
|
|
9
|
8
|
error: `complete responses for profile: ${seeker.profile_id} unqeual to profile: ${potentialMatch.profile_id} | ${seeker.responses.length}:${potentialMatch.responses.length}`,
|
|
10
|
9
|
}
|
|
11
|
|
-
|
|
12
|
|
- const checkValCb = res => {
|
|
13
|
|
- const val = parseInt(res.val)
|
|
14
|
|
- return isNaN(val) ? 0 : val
|
|
|
10
|
+
|
|
|
11
|
+ const aRes = [...seeker.responses]
|
|
|
12
|
+ const bRes = [...potentialMatch.responses]
|
|
|
13
|
+
|
|
|
14
|
+ const composite = []
|
|
|
15
|
+ while(aRes.length + bRes.length > 0) {
|
|
|
16
|
+ const mKey = resList => {
|
|
|
17
|
+ let el = resList.shift()
|
|
|
18
|
+ let pair = el.val
|
|
|
19
|
+ el = resList.shift()
|
|
|
20
|
+ return `${pair}:${el.val}`
|
|
|
21
|
+ }
|
|
|
22
|
+ composite.push(prescoreLookup[mKey(aRes)][mKey(bRes)])
|
|
15
|
23
|
}
|
|
16
|
|
-
|
|
17
|
|
- return Math.floor(
|
|
18
|
|
- cosineSimilarity(
|
|
19
|
|
- seeker.responses.map(checkValCb),
|
|
20
|
|
- potentialMatch.responses.map(checkValCb),
|
|
21
|
|
- ) * magic,
|
|
22
|
|
- )
|
|
|
24
|
+ return { total: Math.round(composite.reduce((a, b) => a + b) / composite.length), aspects: composite }
|
|
23
|
25
|
}
|
|
24
|
26
|
const filterByDistance = (profileList, max) => {
|
|
25
|
27
|
return profileList.filter(profile => {
|
|
|
@@ -28,13 +30,13 @@ const filterByDistance = (profileList, max) => {
|
|
28
|
30
|
return profileDistance <= adjustedMaxDistance
|
|
29
|
31
|
})
|
|
30
|
32
|
}
|
|
31
|
|
-const scoreAll = (profileList, userProfile) => {
|
|
|
33
|
+const scoreAll = (profileList, userProfile, prescoreLookup) => {
|
|
32
|
34
|
return profileList.map(profile => {
|
|
33
|
35
|
return {
|
|
34
|
36
|
// Uncomment to return the whole profile
|
|
35
|
37
|
// ...profile,
|
|
36
|
38
|
profile_id: profile.profile_id,
|
|
37
|
|
- score: scoreResponses(userProfile, profile),
|
|
|
39
|
+ score: scoreResponses(userProfile, profile, prescoreLookup),
|
|
38
|
40
|
distance: profile.distance,
|
|
39
|
41
|
}
|
|
40
|
42
|
})
|
|
|
@@ -46,7 +48,7 @@ const getZipCodeFromProfile = profile => {
|
|
46
|
48
|
// There should only be one zip code entry per profile
|
|
47
|
49
|
let zipRes = profile.responses.filter(
|
|
48
|
50
|
// Whatever the zipcode questions is
|
|
49
|
|
- response => response.response_key_id == zipcodeKey,
|
|
|
51
|
+ response => response.response_key_id == _ZIPCODEKEY,
|
|
50
|
52
|
)[0]
|
|
51
|
53
|
|
|
52
|
54
|
const responseIndexForZip = profile.responses.indexOf(zipRes)
|
|
|
@@ -56,6 +58,25 @@ const getZipCodeFromProfile = profile => {
|
|
56
|
58
|
return zipRes.val
|
|
57
|
59
|
}
|
|
58
|
60
|
|
|
|
61
|
+const makeScoreLookup = (aspects, labels) => {
|
|
|
62
|
+ const labelLookup = {}
|
|
|
63
|
+ labels.forEach(label => labelLookup[label.aspect_id] = label)
|
|
|
64
|
+
|
|
|
65
|
+ const scoreLookup = {}
|
|
|
66
|
+ aspects.forEach(aspect => {
|
|
|
67
|
+ const key = labelLookup[aspect.aspect_id]
|
|
|
68
|
+ scoreLookup[`${key.a}:${key.b}`] = {}
|
|
|
69
|
+ Object.keys(aspect).forEach(aspect_id => {
|
|
|
70
|
+ if(!labelLookup[aspect_id]) return
|
|
|
71
|
+ const comp = labelLookup[aspect_id]
|
|
|
72
|
+ const score = aspect[aspect_id]
|
|
|
73
|
+ scoreLookup[`${key.a}:${key.b}`][`${comp.a}:${comp.b}`] = score
|
|
|
74
|
+ })
|
|
|
75
|
+
|
|
|
76
|
+ })
|
|
|
77
|
+ return scoreLookup
|
|
|
78
|
+}
|
|
|
79
|
+
|
|
59
|
80
|
/**
|
|
60
|
81
|
* Class to hold our retrieved profile information
|
|
61
|
82
|
* in a convenient wrapper
|
|
|
@@ -68,6 +89,7 @@ class CompleteProfile {
|
|
68
|
89
|
this.user_name = profile.user.user_name // string user_name
|
|
69
|
90
|
this.user_media = profile.user_media // string user_media
|
|
70
|
91
|
this.responses = profile.responses // [] of all responses
|
|
|
92
|
+ this.tags = profile.tags // [] of all tags
|
|
71
|
93
|
this.user_type = type
|
|
72
|
94
|
}
|
|
73
|
95
|
}
|
|
|
@@ -75,8 +97,27 @@ class CompleteProfile {
|
|
75
|
97
|
module.exports = class ProfileService extends Schmervice.Service {
|
|
76
|
98
|
constructor(...args) {
|
|
77
|
99
|
super(...args)
|
|
|
100
|
+ this.scoreLookup = {}
|
|
|
101
|
+ this.tagLookup = {}
|
|
|
102
|
+ }
|
|
|
103
|
+ async _setScoreLookup() {
|
|
|
104
|
+ if(!Object.keys(this.scoreLookup).length) {
|
|
|
105
|
+ const { Aspect, AspectLabel } = this.server.models()
|
|
|
106
|
+ const aspects = await Aspect.query()
|
|
|
107
|
+ const labels = await AspectLabel.query()
|
|
|
108
|
+ this.scoreLookup = makeScoreLookup(aspects, labels)
|
|
|
109
|
+ }
|
|
|
110
|
+ }
|
|
|
111
|
+ async _setTagLookup() {
|
|
|
112
|
+ if(!Object.keys(this.tagLookup).length) {
|
|
|
113
|
+ const { Tag } = this.server.models()
|
|
|
114
|
+ const allTagDescriptions = await Tag.query()
|
|
|
115
|
+ allTagDescriptions.forEach(desc => this.tagLookup[desc.tag_id] = {
|
|
|
116
|
+ description: desc.tag_description,
|
|
|
117
|
+ category: desc.tag_category,
|
|
|
118
|
+ })
|
|
|
119
|
+ }
|
|
78
|
120
|
}
|
|
79
|
|
-
|
|
80
|
121
|
/**
|
|
81
|
122
|
* Internal method to get list of profile_ids for this user
|
|
82
|
123
|
* @param {number} userId
|
|
|
@@ -97,16 +138,22 @@ module.exports = class ProfileService extends Schmervice.Service {
|
|
97
|
138
|
|
|
98
|
139
|
async getCompleteProfilesFor(userId, type) {
|
|
99
|
140
|
const { Profile } = this.server.models()
|
|
|
141
|
+ await this._setTagLookup()
|
|
100
|
142
|
|
|
101
|
143
|
const dedupedProfileIds = await this._getProfileIdsForUserId(userId)
|
|
102
|
|
-
|
|
|
144
|
+
|
|
103
|
145
|
const profilesEntries = await Profile.query()
|
|
104
|
146
|
.whereIn('profile_id', dedupedProfileIds)
|
|
|
147
|
+ .withGraphFetched('tags')
|
|
105
|
148
|
.withGraphFetched('responses')
|
|
106
|
149
|
// CHECKTHIS: Added this because we added user.user_name to CompleteProfile
|
|
107
|
150
|
// so without this, we get undefined user_name
|
|
108
|
151
|
.withGraphFetched('user')
|
|
109
|
152
|
|
|
|
153
|
+ profilesEntries.forEach(profile => {
|
|
|
154
|
+ profile.tags = profile.tags.map(tag => this.tagLookup[tag.tag_id])
|
|
|
155
|
+ })
|
|
|
156
|
+
|
|
110
|
157
|
//** Get responses asociated with each profile_id */
|
|
111
|
158
|
return profilesEntries.map(profile => {
|
|
112
|
159
|
return new CompleteProfile(profile, type)
|
|
|
@@ -115,12 +162,18 @@ module.exports = class ProfileService extends Schmervice.Service {
|
|
115
|
162
|
|
|
116
|
163
|
async getProfilesFor(profileIdArray, type, includeResponses = true) {
|
|
117
|
164
|
const { Profile } = this.server.models()
|
|
118
|
|
- // profilesEntries is profiles in database row order
|
|
|
165
|
+ await this._setScoreLookup()
|
|
|
166
|
+ await this._setTagLookup()
|
|
|
167
|
+
|
|
|
168
|
+
|
|
|
169
|
+ // profilesEntries is profiles in dataaspect_labelsbase row order
|
|
119
|
170
|
const profilesEntries = includeResponses ? await Profile.query()
|
|
120
|
171
|
.whereIn('profile_id', profileIdArray)
|
|
|
172
|
+ .withGraphFetched('tags')
|
|
121
|
173
|
.withGraphFetched('responses')
|
|
122
|
174
|
.withGraphFetched('user') : await Profile.query()
|
|
123
|
175
|
.whereIn('profile_id', profileIdArray)
|
|
|
176
|
+ .withGraphFetched('tags')
|
|
124
|
177
|
.withGraphFetched('user')
|
|
125
|
178
|
|
|
126
|
179
|
// taking the info from profilesEntries
|
|
|
@@ -134,10 +187,13 @@ module.exports = class ProfileService extends Schmervice.Service {
|
|
134
|
187
|
if(!includeResponses) {
|
|
135
|
188
|
delete complete['responses']
|
|
136
|
189
|
}
|
|
|
190
|
+ if(entry?.tags?.length){
|
|
|
191
|
+ complete.tags = entry.tags.map(tag => this.tagLookup[tag.tag_id])
|
|
|
192
|
+ }
|
|
137
|
193
|
completeProfiles.push(complete)
|
|
138
|
194
|
}
|
|
139
|
195
|
})
|
|
140
|
|
- })
|
|
|
196
|
+ })
|
|
141
|
197
|
return completeProfiles
|
|
142
|
198
|
}
|
|
143
|
199
|
|
|
|
@@ -245,6 +301,8 @@ module.exports = class ProfileService extends Schmervice.Service {
|
|
245
|
301
|
*/
|
|
246
|
302
|
async scoreProfilesFor(profileId, maxDistance, distanceUnit) {
|
|
247
|
303
|
const { Profile } = this.server.models()
|
|
|
304
|
+
|
|
|
305
|
+ await this._setScoreLookup()
|
|
248
|
306
|
|
|
249
|
307
|
// Our User Profile to score for
|
|
250
|
308
|
const userProfile = await Profile.query()
|
|
|
@@ -269,7 +327,7 @@ module.exports = class ProfileService extends Schmervice.Service {
|
|
269
|
327
|
|
|
270
|
328
|
// Only include profiles that included zipcode response
|
|
271
|
329
|
profileIdsOfOppositeType = profileIdsOfOppositeType.filter(profile => {
|
|
272
|
|
- const zipcodeResponses = profile.responses.filter(response => response.response_key_id == zipcodeKey)
|
|
|
330
|
+ const zipcodeResponses = profile.responses.filter(res => res.response_key_id == _ZIPCODEKEY)
|
|
273
|
331
|
return zipcodeResponses.length > 0
|
|
274
|
332
|
})
|
|
275
|
333
|
|
|
|
@@ -299,10 +357,11 @@ module.exports = class ProfileService extends Schmervice.Service {
|
|
299
|
357
|
const scoredProfilesWithDistance = scoreAll(
|
|
300
|
358
|
distanceFilteredProfiles,
|
|
301
|
359
|
userProfile,
|
|
|
360
|
+ this.scoreLookup
|
|
302
|
361
|
)
|
|
303
|
362
|
|
|
304
|
363
|
// Order by score
|
|
305
|
|
- return scoredProfilesWithDistance.sort((a, b) => a.score - b.score)
|
|
|
364
|
+ return scoredProfilesWithDistance.sort((a, b) => b.score.total - a.score.total)
|
|
306
|
365
|
}
|
|
307
|
366
|
|
|
308
|
367
|
/**
|