Pārlūkot izejas kodu

: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 gadus atpakaļ
vecāks
revīzija
87c10a738d

+ 34
- 0
backend/db/mock.js Parādīt failu

@@ -1,6 +1,40 @@
1 1
 module.exports = {
2 2
     users: [],
3 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 38
     response_keys: [
5 39
         {
6 40
             response_key_id: 1,

+ 11
- 0
backend/db/seeds/12-tags.js Parādīt failu

@@ -0,0 +1,11 @@
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 Parādīt failu

@@ -0,0 +1,11 @@
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 Parādīt failu

@@ -0,0 +1,18 @@
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 Parādīt failu

@@ -0,0 +1,15 @@
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 Parādīt failu

@@ -1,5 +1,6 @@
1 1
 const Schwifty = require('@hapipal/schwifty')
2 2
 const Joi = require('joi')
3
+const TagAssociation = require('./tag-association')
3 4
 const Response = require('./response')
4 5
 const User = require('./user')
5 6
 
@@ -9,6 +10,14 @@ module.exports = class Profile extends Schwifty.Model {
9 10
     }
10 11
     static get relationMappings() {
11 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 21
             responses: {
13 22
                 relation: Schwifty.Model.HasManyRelation,
14 23
                 modelClass: Response,

+ 30
- 0
backend/lib/models/tag-association.js Parādīt failu

@@ -0,0 +1,30 @@
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 Parādīt failu

@@ -0,0 +1,16 @@
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 Parādīt failu

@@ -2,6 +2,10 @@ const Objection = require('objection')
2 2
 const Schmervice = require('@hapipal/schmervice')
3 3
 
4 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 9
 const ResponseModel = require('../models/response')
6 10
 const ZipCodeModel = require('../models/zip-code')
7 11
 const MatchQueueModel = require('../models/matchqueue')
@@ -22,6 +26,10 @@ module.exports = {
22 26
     version: '1.0.0',
23 27
     register: async (server, options) => {
24 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 33
         await server.registerModel(ResponseModel)
26 34
         await server.registerModel(ZipCodeModel)
27 35
         await server.registerModel(MatchQueueModel)

+ 1
- 0
backend/lib/routes/profile/queue.js Parādīt failu

@@ -21,6 +21,7 @@ const responseSchemas = {
21 21
                 user_id: Joi.number(),
22 22
                 user_name: Joi.string(),
23 23
                 responses: Joi.array().items(),
24
+                tags: Joi.array().items(),
24 25
                 user_media: Joi.string(),
25 26
                 user_type: Joi.any(),
26 27
                 user: Joi.object()

+ 1
- 0
backend/lib/routes/user/list-profiles.js Parādīt failu

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

+ 83
- 24
backend/lib/services/profile.js Parādīt failu

@@ -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
     /**

Notiek ielāde…
Atcelt
Saglabāt