Просмотр исходного кода

:sparkles: starting to create scoring service

tags/0.0.1
j 4 лет назад
Родитель
Сommit
4ddf923f08

+ 1
- 0
backend/db/survey-generator.js Просмотреть файл

@@ -245,6 +245,7 @@ if (generatedSeekers.length == generatedProviders.length) {
245 245
     console.log(generatedProviders[0].matchPref.map(m => m.id))
246 246
 
247 247
     console.log('---')
248
+
248 249
     console.log('user:', generatedSeekers[0].id, '| seeker')
249 250
     console.log(
250 251
         'otp',

+ 13
- 0
backend/lib/models/profile.js Просмотреть файл

@@ -1,10 +1,23 @@
1 1
 const Schwifty = require('@hapipal/schwifty')
2 2
 const Joi = require('joi')
3
+const Response = require('./response')
3 4
 
4 5
 module.exports = class Profile extends Schwifty.Model {
5 6
     static get tableName() {
6 7
         return 'profiles'
7 8
     }
9
+    static get relationMappings() {
10
+        return {
11
+            responses: {
12
+                relation: Schwifty.Model.HasManyRelation,
13
+                modelClass: Response,
14
+                join: {
15
+                    from: 'responses.profile_id',
16
+                    to: 'profiles.profile_id',
17
+                },
18
+            },
19
+        }
20
+    }
8 21
     static get joiSchema() {
9 22
         return Joi.object({
10 23
             profile_id: Joi.number(),

+ 2
- 0
backend/lib/plugins/profile.js Просмотреть файл

@@ -7,6 +7,7 @@ const ResponseModel = require('../models/response')
7 7
 const ProfileService = require('../services/profile')
8 8
 
9 9
 const ProfileProfilesRoute = require('../routes/profile/profiles')
10
+const ProfileScoreRoute = require('../routes/profile/score')
10 11
 const ProfileCreateRoute = require('../routes/profile/create')
11 12
 const ProfileUpdateRoute = require('../routes/profile/update')
12 13
 
@@ -27,6 +28,7 @@ module.exports = {
27 28
         server.registerService(ProfileService)
28 29
 
29 30
         await server.route(ProfileProfilesRoute)
31
+        await server.route(ProfileScoreRoute)
30 32
         await server.route(ProfileCreateRoute)
31 33
         await server.route(ProfileUpdateRoute)
32 34
     },

+ 15
- 8
backend/lib/routes/profile/profiles.js Просмотреть файл

@@ -15,7 +15,10 @@ const validators = {
15 15
     // headers: true,
16 16
 
17 17
     /** Validate the route params (/active/{thing}) */
18
-    params: Joi.object({ user_id: Joi.number() }),
18
+    params: Joi.object({
19
+        user_type: Joi.string(),
20
+        user_id: Joi.number(),
21
+    }),
19 22
 
20 23
     /** Validate the route query (/active/{thing}?limit=10&offset=10) */
21 24
     // query: true,
@@ -25,23 +28,23 @@ const validators = {
25 28
 
26 29
 const responseSchemas = {
27 30
     profilesList: Joi.object({
28
-        profile_id: Joi.number().required(),
29
-        user_id: Joi.number().required(),
31
+        profile_id: Joi.number().integer().greater(0).required(),
32
+        user_id: Joi.number().integer().greater(0).required(),
30 33
         responses: Joi.array().items(
31 34
             Joi.object({
32
-                response_id: Joi.number().required(),
33
-                profile_id: Joi.number().required(),
34 35
                 response_key_id: Joi.number().required(),
36
+                profile_id: Joi.number().required(),
37
+                response_id: Joi.number().required(),
35 38
                 val: Joi.string().required(),
36 39
             }),
37 40
         ),
38
-        response_keys: Joi.array(),
41
+        user_type: Joi.string().required(),
39 42
     }),
40 43
 }
41 44
 
42 45
 module.exports = {
43 46
     method: 'GET',
44
-    path: '/{user_id}',
47
+    path: '/{user_type}/{user_id}',
45 48
     options: {
46 49
         ...pluginConfig.docs,
47 50
         tags: ['api'],
@@ -51,7 +54,11 @@ module.exports = {
51 54
         handler: async function (request, h) {
52 55
             const { profileService } = request.services()
53 56
             const userId = request.params.user_id
54
-            const profiles = await profileService.getCompleteProfilesFor(userId)
57
+            const type = request.params.user_type
58
+            const profiles = await profileService.getCompleteProfilesFor(
59
+                userId,
60
+                type,
61
+            )
55 62
             try {
56 63
                 return {
57 64
                     ok: true,

+ 73
- 0
backend/lib/routes/profile/score.js Просмотреть файл

@@ -0,0 +1,73 @@
1
+'use strict'
2
+
3
+const Joi = require('joi')
4
+
5
+const pluginConfig = {
6
+    handlerType: 'score',
7
+    docs: {
8
+        description: 'scores',
9
+        notes: 'A list of profile scores',
10
+    },
11
+}
12
+
13
+const validators = {
14
+    /** Validate the header (cookie check) */
15
+    // headers: true,
16
+
17
+    /** Validate the route params (/active/{thing}) */
18
+    params: Joi.object({
19
+        user_id: Joi.number(),
20
+    }),
21
+
22
+    /** Validate the route query (/active/{thing}?limit=10&offset=10) */
23
+    // query: true,
24
+    /** Validate the incoming payload (POST method) */
25
+    // payload: true,
26
+}
27
+
28
+const responseSchemas = {}
29
+
30
+module.exports = {
31
+    method: 'GET',
32
+    path: '/score/{user_id}',
33
+    options: {
34
+        ...pluginConfig.docs,
35
+        tags: ['api'],
36
+        /** Protect this route with authentication? */
37
+        auth: false,
38
+
39
+        handler: async function (request, h) {
40
+            const { profileService } = request.services()
41
+            const userId = request.params.user_id
42
+            const profiles = await profileService.scoreProfilesFor(userId)
43
+            try {
44
+                return {
45
+                    ok: true,
46
+                    handler: pluginConfig.handlerType,
47
+                    data: profiles,
48
+                }
49
+            } catch (err) {
50
+                return {
51
+                    ok: false,
52
+                    handler: pluginConfig.handlerType,
53
+                    data: { error: `${err}` },
54
+                }
55
+            }
56
+        },
57
+
58
+        /** Validate based on validators object */
59
+        validate: {
60
+            ...validators,
61
+            failAction: 'log',
62
+        },
63
+
64
+        /** Validate the server response */
65
+        response: {
66
+            schema: Joi.object({
67
+                ok: Joi.bool(),
68
+                handler: Joi.string(),
69
+                data: Joi.array().items(),
70
+            }),
71
+        },
72
+    },
73
+}

+ 3
- 2
backend/lib/routes/survey/questions.js Просмотреть файл

@@ -24,8 +24,9 @@ const validators = {
24 24
 const responseSchemas = {
25 25
     responseKeysList: Joi.object({
26 26
         response_key_id: Joi.number().required(),
27
-        profile_id: Joi.number().required(),
28
-        val: Joi.string().required(),
27
+        response_key_category: Joi.string().required(),
28
+        response_key_prompt: Joi.string().required(),
29
+        response_key_description: Joi.any(),
29 30
     }),
30 31
 }
31 32
 

+ 0
- 1
backend/lib/routes/survey/responses.js Просмотреть файл

@@ -24,7 +24,6 @@ const validators = {
24 24
 const responseSchemas = {
25 25
     responseKeysList: Joi.object({
26 26
         response_key_id: Joi.number().required(),
27
-        profile_id: Joi.number().required(),
28 27
     }),
29 28
 }
30 29
 

+ 18
- 9
backend/lib/services/match-maker.js Просмотреть файл

@@ -1,8 +1,20 @@
1
-class ScoreKeeper {
2
-    constructor() {}
3
-    score(proposer) {
4
-        return { ...proposer }
5
-    }
1
+const similarity = require('compute-cosine-similarity')
2
+const magic = 1000
3
+
4
+const ScoreKeeper = {
5
+    score: (seeker, potentialMatch) => {
6
+        const seekerResponseValues = seeker.profileResponses.map(res =>
7
+            parseInt(Object.values(res)),
8
+        )
9
+        const potentialMatchResponseValues =
10
+            potentialMatch.profileResponses.map(res =>
11
+                parseInt(Object.values(res)),
12
+            )
13
+        return Math.floor(
14
+            similarity(seekerResponseValues, potentialMatchResponseValues) *
15
+                magic,
16
+        )
17
+    },
6 18
 }
7 19
 
8 20
 module.exports = class MatchMaker {
@@ -10,10 +22,7 @@ module.exports = class MatchMaker {
10 22
         this.proposer = settings.proposer
11 23
 
12 24
         // Score main profile
13
-        this.keeper = new ScoreKeeper()
14
-        this.proposer.score = this.keeper.score(this.proposer)
15
-
16
-        this.profiles = this.getProfilesFor(settings)
25
+        this.keeper = ScoreKeeper
17 26
     }
18 27
     runPrematch(settings) {
19 28
         // grab all profiles form the db

+ 78
- 21
backend/lib/services/profile.js Просмотреть файл

@@ -1,4 +1,42 @@
1 1
 const Schmervice = require('@hapipal/schmervice')
2
+const cosineSimilarity = require('compute-cosine-similarity')
3
+
4
+const magic = 1000
5
+const scoreResponses = (seeker, potentialMatch) => {
6
+    if (seeker.responses.length != potentialMatch.responses.length)
7
+        return {
8
+            error: `complete responses for profile: ${seeker.profile_id} unqeual to profile: ${potentialMatch.profile_id}`,
9
+        }
10
+
11
+    const checkValCb = res => {
12
+        const val = parseInt(Object.values(res))
13
+        return isNaN(val) ? 0 : val
14
+    }
15
+    return Math.floor(
16
+        cosineSimilarity(
17
+            seeker.responses.map(checkValCb),
18
+            potentialMatch.responses.map(checkValCb),
19
+        ) * magic,
20
+    )
21
+}
22
+const userTypes = {
23
+    seeker: 'seeker',
24
+    poster: 'poster',
25
+}
26
+
27
+/**
28
+ * Class to hold our retrieved profile information
29
+ * in a convenient wrapper
30
+ * !: This needs to match the responseSchema in profiles.js
31
+ */
32
+class CompleteProfile {
33
+    constructor(profile, type) {
34
+        this.user_id = profile.user_id // int user_id
35
+        this.profile_id = profile.profile_id // int profile_id
36
+        this.responses = profile.responses // [] of all responses
37
+        this.user_type = type
38
+    }
39
+}
2 40
 
3 41
 module.exports = class ProfileService extends Schmervice.Service {
4 42
     constructor(...args) {
@@ -23,30 +61,18 @@ module.exports = class ProfileService extends Schmervice.Service {
23 61
         return [...new Set(profileIdsToGrab)]
24 62
     }
25 63
 
26
-    async getCompleteProfilesFor(userId) {
27
-        const { Profile, Response } = this.server.models()
64
+    async getCompleteProfilesFor(userId, type) {
65
+        const { Profile } = this.server.models()
28 66
 
29
-        const dedupedProfiles = await this._getProfileIdsForUserId(userId)
67
+        const dedupedProfileIds = await this._getProfileIdsForUserId(userId)
30 68
 
31
-        const responses = await Response.query().whereIn(
32
-            'profile_id',
33
-            dedupedProfiles,
34
-        )
35
-        const profiles = await Profile.query().whereIn(
36
-            'profile_id',
37
-            dedupedProfiles,
38
-        )
69
+        const profilesEntries = await Profile.query()
70
+            .whereIn('profile_id', dedupedProfileIds)
71
+            .withGraphFetched('responses')
39 72
 
40 73
         //** Get responses asociated with each profile_id */
41
-        return profiles.map(profile => {
42
-            if (!profile.responses) profile.responses = []
43
-            profile.response_keys = []
44
-            responses.forEach(response => {
45
-                if (response.profile_id !== profile.profile_id) return
46
-                profile.response_keys.push(response.response_key_id)
47
-                profile.responses.push(response)
48
-            })
49
-            return profile
74
+        return profilesEntries.map(profile => {
75
+            return new CompleteProfile(profile, type)
50 76
         })
51 77
     }
52 78
 
@@ -57,6 +83,8 @@ module.exports = class ProfileService extends Schmervice.Service {
57 83
      * @returns {object}
58 84
      */
59 85
     async saveResponsesCreateProfileFor(userId, responses, txn) {
86
+        const { Profile, Response } = this.server.models()
87
+
60 88
         const profile = await Profile.query(txn).insert({
61 89
             user_id: userId,
62 90
         })
@@ -78,7 +106,7 @@ module.exports = class ProfileService extends Schmervice.Service {
78 106
      * @returns {Array} updated responses
79 107
      */
80 108
     async updateResponsesInProfile(profileId, responses, txn) {
81
-        const { Profile, Response } = this.server.models()
109
+        const { Response } = this.server.models()
82 110
         for (const responseToSave of responses) {
83 111
             await Response.query(txn)
84 112
                 .update({
@@ -115,4 +143,33 @@ module.exports = class ProfileService extends Schmervice.Service {
115 143
 
116 144
         return await Profile.query().delete().where('profile_id', profileId)
117 145
     }
146
+
147
+    /**
148
+     * Score a profile
149
+     * @param {number} userId
150
+     * @returns {Array} Ordered and scored Profiles
151
+     */
152
+    async scoreProfilesFor(userId) {
153
+        const { User } = this.server.models(['user'])
154
+        const { Profile } = this.server.models()
155
+
156
+        const user = await User.query().findOne('user_id', userId)
157
+        const userProfile = await Profile.query()
158
+            .findOne('user_id', userId)
159
+            .withGraphFetched('responses')
160
+        const isPosterOpposite = user.is_poster == 1 ? 0 : 1
161
+        const userType = Object.keys(userTypes)[isPosterOpposite]
162
+
163
+        const profileIdsOfOppositeType = await Profile.query().withGraphFetched(
164
+            'responses',
165
+        )
166
+
167
+        return profileIdsOfOppositeType.map(profile => ({
168
+            profile_id: profile.profile_id,
169
+            score: scoreResponses(
170
+                userProfile,
171
+                new CompleteProfile(profile, userType),
172
+            ),
173
+        }))
174
+    }
118 175
 }

Загрузка…
Отмена
Сохранить