Преглед на файлове

:sparkles: added mock for tags and associations | including tags on profiles | using prescore table | including aspect scores and composite scores

tags/0.0.1
toj преди 4 години
родител
ревизия
87c10a738d

+ 34
- 0
backend/db/mock.js Целия файл

1
 module.exports = {
1
 module.exports = {
2
     users: [],
2
     users: [],
3
     profiles: [],
3
     profiles: [],
4
+    tags: [
5
+        {
6
+            tag_id: 1,
7
+            tag_category: 'verification',
8
+            tag_description: 'verified',
9
+            is_active: true,
10
+        },
11
+    ],
12
+    tag_associations: [
13
+        {
14
+            tag_association_id: 1,
15
+            profile_id: 1,
16
+            tag_id: 1,
17
+            is_deleted: false,
18
+        },
19
+        {
20
+            tag_association_id: 2,
21
+            profile_id: 2,
22
+            tag_id: 1,
23
+            is_deleted: false,
24
+        },
25
+        {
26
+            tag_association_id: 3,
27
+            profile_id: 3,
28
+            tag_id: 1,
29
+            is_deleted: false,
30
+        },
31
+        {
32
+            tag_association_id: 4,
33
+            profile_id: 5,
34
+            tag_id: 1,
35
+            is_deleted: false,
36
+        },
37
+    ],
4
     response_keys: [
38
     response_keys: [
5
         {
39
         {
6
             response_key_id: 1,
40
             response_key_id: 1,

+ 11
- 0
backend/db/seeds/12-tags.js Целия файл

1
+const mock = require('../mock')
2
+
3
+exports.seed = function (knex) {
4
+    // Deletes ALL existing entries
5
+    return knex('tags')
6
+        .truncate()
7
+        .then(function () {
8
+            // Inserts seed entries
9
+            return knex('tags').insert(mock.tags)
10
+        })
11
+}

+ 11
- 0
backend/db/seeds/13-tag_associations.js Целия файл

1
+const mock = require('../mock')
2
+
3
+exports.seed = function (knex) {
4
+    // Deletes ALL existing entries
5
+    return knex('tag_associations')
6
+        .truncate()
7
+        .then(function () {
8
+            // Inserts seed entries
9
+            return knex('tag_associations').insert(mock.tag_associations)
10
+        })
11
+}

+ 18
- 0
backend/lib/models/aspect.js Целия файл

1
+const Schwifty = require('@hapipal/schwifty')
2
+const Joi = require('joi')
3
+const config = require('../../db/data-generator/config.json')
4
+
5
+const aspects = { aspect_id: Joi.number() }
6
+const possible_combinations = Math.pow(config.scoreVals.length, 2)
7
+for(let i = 1; i <= possible_combinations; i++) {
8
+    aspects[i] = Joi.number()
9
+}
10
+
11
+module.exports = class Aspect extends Schwifty.Model {
12
+    static get tableName() {
13
+        return 'prescored_aspects'
14
+    }
15
+    static get joiSchema() {
16
+        return Joi.object(aspects)
17
+    }
18
+}

+ 15
- 0
backend/lib/models/aspect_label.js Целия файл

1
+const Schwifty = require('@hapipal/schwifty')
2
+const Joi = require('joi')
3
+
4
+module.exports = class AspectLabel extends Schwifty.Model {
5
+    static get tableName() {
6
+        return 'aspect_labels'
7
+    }
8
+    static get joiSchema() {
9
+        return Joi.object({ 
10
+            aspect_id: Joi.number(),
11
+            a: Joi.string(),
12
+            b: Joi.string()
13
+        })
14
+    }
15
+}

+ 9
- 0
backend/lib/models/profile.js Целия файл

1
 const Schwifty = require('@hapipal/schwifty')
1
 const Schwifty = require('@hapipal/schwifty')
2
 const Joi = require('joi')
2
 const Joi = require('joi')
3
+const TagAssociation = require('./tag-association')
3
 const Response = require('./response')
4
 const Response = require('./response')
4
 const User = require('./user')
5
 const User = require('./user')
5
 
6
 
9
     }
10
     }
10
     static get relationMappings() {
11
     static get relationMappings() {
11
         return {
12
         return {
13
+            tags: {
14
+                relation: Schwifty.Model.HasManyRelation,
15
+                modelClass: TagAssociation,
16
+                join: {
17
+                    from: 'tag_associations.profile_id',
18
+                    to: 'profiles.profile_id',
19
+                },
20
+            },
12
             responses: {
21
             responses: {
13
                 relation: Schwifty.Model.HasManyRelation,
22
                 relation: Schwifty.Model.HasManyRelation,
14
                 modelClass: Response,
23
                 modelClass: Response,

+ 30
- 0
backend/lib/models/tag-association.js Целия файл

1
+const Schwifty = require('@hapipal/schwifty')
2
+const Joi = require('joi')
3
+
4
+const Tag = require('./tag')
5
+
6
+module.exports = class TagAssociation extends Schwifty.Model {
7
+    static get tableName() {
8
+        return 'tag_associations'
9
+    }
10
+    static get relationMappings() {
11
+        return {
12
+            description: {
13
+                relation: Schwifty.Model.BelongsToOneRelation,
14
+                modelClass: Tag,
15
+                join: {
16
+                    from: 'tag_associations.tag_id',
17
+                    to: 'tags.tag_id',
18
+                },
19
+            },
20
+        }
21
+    }
22
+    static get joiSchema() {
23
+        return Joi.object({
24
+            tag_association_id: Joi.number(),
25
+            profile_id: Joi.number(),
26
+            tag_id: Joi.number(),
27
+            is_deleted: Joi.bool(),
28
+        })
29
+    }
30
+}

+ 16
- 0
backend/lib/models/tag.js Целия файл

1
+const Schwifty = require('@hapipal/schwifty')
2
+const Joi = require('joi')
3
+
4
+module.exports = class Tag extends Schwifty.Model {
5
+    static get tableName() {
6
+        return 'tags'
7
+    }
8
+    static get joiSchema() {
9
+        return Joi.object({
10
+            tag_id: Joi.number(),
11
+            tag_category: Joi.string(),
12
+            tag_description: Joi.string(),
13
+            is_active: Joi.bool(),
14
+        })
15
+    }
16
+}

+ 8
- 0
backend/lib/plugins/profile.js Целия файл

2
 const Schmervice = require('@hapipal/schmervice')
2
 const Schmervice = require('@hapipal/schmervice')
3
 
3
 
4
 const ProfileModel = require('../models/profile')
4
 const ProfileModel = require('../models/profile')
5
+const TagModel = require('../models/tag')
6
+const TagAssociationModel = require('../models/tag-association')
7
+const AspectModel = require('../models/aspect')
8
+const AspectLabelModel = require('../models/aspect_label')
5
 const ResponseModel = require('../models/response')
9
 const ResponseModel = require('../models/response')
6
 const ZipCodeModel = require('../models/zip-code')
10
 const ZipCodeModel = require('../models/zip-code')
7
 const MatchQueueModel = require('../models/matchqueue')
11
 const MatchQueueModel = require('../models/matchqueue')
22
     version: '1.0.0',
26
     version: '1.0.0',
23
     register: async (server, options) => {
27
     register: async (server, options) => {
24
         await server.registerModel(ProfileModel)
28
         await server.registerModel(ProfileModel)
29
+        await server.registerModel(TagModel)
30
+        await server.registerModel(TagAssociationModel)
31
+        await server.registerModel(AspectModel)
32
+        await server.registerModel(AspectLabelModel)
25
         await server.registerModel(ResponseModel)
33
         await server.registerModel(ResponseModel)
26
         await server.registerModel(ZipCodeModel)
34
         await server.registerModel(ZipCodeModel)
27
         await server.registerModel(MatchQueueModel)
35
         await server.registerModel(MatchQueueModel)

+ 1
- 0
backend/lib/routes/profile/queue.js Целия файл

21
                 user_id: Joi.number(),
21
                 user_id: Joi.number(),
22
                 user_name: Joi.string(),
22
                 user_name: Joi.string(),
23
                 responses: Joi.array().items(),
23
                 responses: Joi.array().items(),
24
+                tags: Joi.array().items(),
24
                 user_media: Joi.string(),
25
                 user_media: Joi.string(),
25
                 user_type: Joi.any(),
26
                 user_type: Joi.any(),
26
                 user: Joi.object()
27
                 user: Joi.object()

+ 1
- 0
backend/lib/routes/user/list-profiles.js Целия файл

37
         // and this route utilizes getCompleteProfiles
37
         // and this route utilizes getCompleteProfiles
38
         user_name: Joi.string(),
38
         user_name: Joi.string(),
39
         user_media: Joi.string(),
39
         user_media: Joi.string(),
40
+        tags: Joi.array().items(),
40
         responses: Joi.array().items(
41
         responses: Joi.array().items(
41
             Joi.object({
42
             Joi.object({
42
                 response_key_id: Joi.number().required(),
43
                 response_key_id: Joi.number().required(),

+ 83
- 24
backend/lib/services/profile.js Целия файл

1
 const Schmervice = require('@hapipal/schmervice')
1
 const Schmervice = require('@hapipal/schmervice')
2
-const cosineSimilarity = require('compute-cosine-similarity')
3
 const haversine = require('haversine')
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
     if (seeker.responses.length != potentialMatch.responses.length)
6
     if (seeker.responses.length != potentialMatch.responses.length)
8
         return {
7
         return {
9
             error: `complete responses for profile: ${seeker.profile_id} unqeual to profile: ${potentialMatch.profile_id} | ${seeker.responses.length}:${potentialMatch.responses.length}`,
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
 const filterByDistance = (profileList, max) => {
26
 const filterByDistance = (profileList, max) => {
25
     return profileList.filter(profile => {
27
     return profileList.filter(profile => {
28
         return profileDistance <= adjustedMaxDistance
30
         return profileDistance <= adjustedMaxDistance
29
     })
31
     })
30
 }
32
 }
31
-const scoreAll = (profileList, userProfile) => {
33
+const scoreAll = (profileList, userProfile, prescoreLookup) => {
32
     return profileList.map(profile => {
34
     return profileList.map(profile => {
33
         return {
35
         return {
34
             // Uncomment to return the whole profile
36
             // Uncomment to return the whole profile
35
             // ...profile,
37
             // ...profile,
36
             profile_id: profile.profile_id,
38
             profile_id: profile.profile_id,
37
-            score: scoreResponses(userProfile, profile),
39
+            score: scoreResponses(userProfile, profile, prescoreLookup),
38
             distance: profile.distance,
40
             distance: profile.distance,
39
         }
41
         }
40
     })
42
     })
46
     // There should only be one zip code entry per profile
48
     // There should only be one zip code entry per profile
47
     let zipRes = profile.responses.filter(
49
     let zipRes = profile.responses.filter(
48
         // Whatever the zipcode questions is
50
         // Whatever the zipcode questions is
49
-        response => response.response_key_id == zipcodeKey,
51
+        response => response.response_key_id == _ZIPCODEKEY,
50
     )[0]
52
     )[0]
51
 
53
 
52
     const responseIndexForZip = profile.responses.indexOf(zipRes)
54
     const responseIndexForZip = profile.responses.indexOf(zipRes)
56
     return zipRes.val
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
  * Class to hold our retrieved profile information
81
  * Class to hold our retrieved profile information
61
  * in a convenient wrapper
82
  * in a convenient wrapper
68
         this.user_name = profile.user.user_name // string user_name
89
         this.user_name = profile.user.user_name // string user_name
69
         this.user_media = profile.user_media // string user_media
90
         this.user_media = profile.user_media // string user_media
70
         this.responses = profile.responses // [] of all responses
91
         this.responses = profile.responses // [] of all responses
92
+        this.tags = profile.tags // [] of all tags
71
         this.user_type = type
93
         this.user_type = type
72
     }
94
     }
73
 }
95
 }
75
 module.exports = class ProfileService extends Schmervice.Service {
97
 module.exports = class ProfileService extends Schmervice.Service {
76
     constructor(...args) {
98
     constructor(...args) {
77
         super(...args)
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
      * Internal method to get list of profile_ids for this user
122
      * Internal method to get list of profile_ids for this user
82
      * @param {number} userId
123
      * @param {number} userId
97
 
138
 
98
     async getCompleteProfilesFor(userId, type) {
139
     async getCompleteProfilesFor(userId, type) {
99
         const { Profile } = this.server.models()
140
         const { Profile } = this.server.models()
141
+        await this._setTagLookup()
100
 
142
 
101
         const dedupedProfileIds = await this._getProfileIdsForUserId(userId)
143
         const dedupedProfileIds = await this._getProfileIdsForUserId(userId)
102
-
144
+        
103
         const profilesEntries = await Profile.query()
145
         const profilesEntries = await Profile.query()
104
             .whereIn('profile_id', dedupedProfileIds)
146
             .whereIn('profile_id', dedupedProfileIds)
147
+            .withGraphFetched('tags')
105
             .withGraphFetched('responses')
148
             .withGraphFetched('responses')
106
             // CHECKTHIS: Added this because we added user.user_name to CompleteProfile
149
             // CHECKTHIS: Added this because we added user.user_name to CompleteProfile
107
             // so without this, we get undefined user_name
150
             // so without this, we get undefined user_name
108
             .withGraphFetched('user')
151
             .withGraphFetched('user')
109
 
152
 
153
+        profilesEntries.forEach(profile => {
154
+            profile.tags = profile.tags.map(tag => this.tagLookup[tag.tag_id])
155
+        })
156
+        
110
         //** Get responses asociated with each profile_id */
157
         //** Get responses asociated with each profile_id */
111
         return profilesEntries.map(profile => {
158
         return profilesEntries.map(profile => {
112
             return new CompleteProfile(profile, type)
159
             return new CompleteProfile(profile, type)
115
 
162
 
116
     async getProfilesFor(profileIdArray, type, includeResponses = true) {
163
     async getProfilesFor(profileIdArray, type, includeResponses = true) {
117
         const { Profile } = this.server.models()
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
         const profilesEntries = includeResponses ? await Profile.query()
170
         const profilesEntries = includeResponses ? await Profile.query()
120
             .whereIn('profile_id', profileIdArray)
171
             .whereIn('profile_id', profileIdArray)
172
+            .withGraphFetched('tags')
121
             .withGraphFetched('responses')
173
             .withGraphFetched('responses')
122
             .withGraphFetched('user') : await Profile.query()
174
             .withGraphFetched('user') : await Profile.query()
123
             .whereIn('profile_id', profileIdArray)
175
             .whereIn('profile_id', profileIdArray)
176
+            .withGraphFetched('tags')
124
             .withGraphFetched('user') 
177
             .withGraphFetched('user') 
125
 
178
 
126
         // taking the info from profilesEntries
179
         // taking the info from profilesEntries
134
                     if(!includeResponses) {
187
                     if(!includeResponses) {
135
                         delete complete['responses']
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
                     completeProfiles.push(complete)
193
                     completeProfiles.push(complete)
138
                 }
194
                 }
139
             })
195
             })
140
-        })  
196
+        })
141
         return completeProfiles
197
         return completeProfiles
142
     }
198
     }
143
 
199
 
245
      */
301
      */
246
     async scoreProfilesFor(profileId, maxDistance, distanceUnit) {
302
     async scoreProfilesFor(profileId, maxDistance, distanceUnit) {
247
         const { Profile } = this.server.models()
303
         const { Profile } = this.server.models()
304
+        
305
+        await this._setScoreLookup()
248
 
306
 
249
         // Our User Profile to score for
307
         // Our User Profile to score for
250
         const userProfile = await Profile.query()
308
         const userProfile = await Profile.query()
269
 
327
 
270
         // Only include profiles that included zipcode response
328
         // Only include profiles that included zipcode response
271
         profileIdsOfOppositeType = profileIdsOfOppositeType.filter(profile => {
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
             return zipcodeResponses.length > 0
331
             return zipcodeResponses.length > 0
274
         })
332
         })
275
 
333
 
299
         const scoredProfilesWithDistance = scoreAll(
357
         const scoredProfilesWithDistance = scoreAll(
300
             distanceFilteredProfiles,
358
             distanceFilteredProfiles,
301
             userProfile,
359
             userProfile,
360
+            this.scoreLookup
302
         )
361
         )
303
 
362
 
304
         // Order by score
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
     /**

Loading…
Отказ
Запис