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

:sparkles: doing distance filtering on score endpoint

tags/0.0.1
J преди 4 години
родител
ревизия
34459d3eb5
променени са 7 файла, в които са добавени 130 реда и са изтрити 21 реда
  1. 13
    0
      backend/db/mock.js
  2. 18
    0
      backend/lib/models/zip-code.js
  3. 2
    0
      backend/lib/plugins/profile.js
  4. 7
    3
      backend/lib/routes/profile/score.js
  5. 84
    18
      backend/lib/services/profile.js
  6. 5
    0
      backend/package-lock.json
  7. 1
    0
      backend/package.json

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

@@ -153,6 +153,12 @@ module.exports = {
153 153
             response_key_prompt: 'what kind of schedule are you looking for',
154 154
             response_key_description: null,
155 155
         },
156
+        {
157
+            response_key_id: 16,
158
+            response_key_category: 'locationPref',
159
+            response_key_prompt: 'what is your zip code',
160
+            response_key_description: null,
161
+        },
156 162
     ],
157 163
     responses: [
158 164
         {
@@ -659,6 +665,13 @@ module.exports = {
659 665
             response_key_id: 11,
660 666
             val: '180',
661 667
         },
668
+        { response_id: 85, profile_id: 1, response_key_id: 16, val: '90065' },
669
+        { response_id: 86, profile_id: 2, response_key_id: 16, val: '90012' },
670
+        { response_id: 87, profile_id: 3, response_key_id: 16, val: '90023' },
671
+        { response_id: 88, profile_id: 4, response_key_id: 16, val: '90001' },
672
+        { response_id: 89, profile_id: 5, response_key_id: 16, val: '90015' },
673
+        { response_id: 90, profile_id: 6, response_key_id: 16, val: '91203' },
674
+        { response_id: 91, profile_id: 7, response_key_id: 16, val: '90210' },
662 675
     ],
663 676
     memberships: [
664 677
         {

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

@@ -0,0 +1,18 @@
1
+const Schwifty = require('@hapipal/schwifty')
2
+const Joi = require('joi')
3
+
4
+module.exports = class ZipCode extends Schwifty.Model {
5
+    static get tableName() {
6
+        return 'zip_codes'
7
+    }
8
+    static get joiSchema() {
9
+        return Joi.object({
10
+            zip_code_id: Joi.number(),
11
+            latitude: Joi.string().required(),
12
+            longitude: Joi.string().required(),
13
+            city: Joi.string().required(),
14
+            state: Joi.string().required(),
15
+            county: Joi.string().required(),
16
+        })
17
+    }
18
+}

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

@@ -3,6 +3,7 @@ const Schmervice = require('@hapipal/schmervice')
3 3
 
4 4
 const ProfileModel = require('../models/profile')
5 5
 const ResponseModel = require('../models/response')
6
+const ZipCodeModel = require('../models/zip-code')
6 7
 
7 8
 const ProfileService = require('../services/profile')
8 9
 
@@ -16,6 +17,7 @@ module.exports = {
16 17
     register: async (server, options) => {
17 18
         await server.registerModel(ProfileModel)
18 19
         await server.registerModel(ResponseModel)
20
+        await server.registerModel(ZipCodeModel)
19 21
 
20 22
         // Bind to global context
21 23
         // So we can use Objection transactions

+ 7
- 3
backend/lib/routes/profile/score.js Целия файл

@@ -20,7 +20,10 @@ const validators = {
20 20
     }),
21 21
 
22 22
     /** Validate the route query (/active/{thing}?limit=10&offset=10) */
23
-    // query: true,
23
+    query:Joi.object({
24
+        max_distance: Joi.number(),
25
+        unit: Joi.string(),
26
+    }),
24 27
     /** Validate the incoming payload (POST method) */
25 28
     // payload: true,
26 29
 }
@@ -44,8 +47,9 @@ module.exports = {
44 47
         handler: async function (request, h) {
45 48
             const { profileService } = request.services()
46 49
             const profileId = request.params.profile_id
47
-            const profiles = await profileService.scoreProfilesFor(profileId)
48
-
50
+            const maxDistanceMiles = request.query.max_distance
51
+            const distanceUnit = request.query.unit ? request.query.unit : 'mile'
52
+            const profiles = await profileService.scoreProfilesFor(profileId, maxDistanceMiles, distanceUnit)
49 53
             try {
50 54
                 if(!profiles){
51 55
                     throw new RangeError('Unable to score profiles')

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

@@ -1,5 +1,7 @@
1 1
 const Schmervice = require('@hapipal/schmervice')
2 2
 const cosineSimilarity = require('compute-cosine-similarity')
3
+const haversine = require('haversine')
4
+const profile = require('../plugins/profile')
3 5
 
4 6
 const magic = 1000
5 7
 const scoreResponses = (seeker, potentialMatch) => {
@@ -159,38 +161,102 @@ module.exports = class ProfileService extends Schmervice.Service {
159 161
 
160 162
         return await Profile.query().delete().where('profile_id', profileId)
161 163
     }
162
-
164
+    /** 
165
+     * Grab the zip code string
166
+    */
167
+    _getZipCodeFromProfile(profile) {
168
+        // There should only be one zip code entry per profile
169
+        let zip = profile.responses.filter(response => response.response_key_id == 16)[0]
170
+        const responseIndexForZip = profile.responses.indexOf(zip)
171
+        if(responseIndexForZip >= 0) {
172
+            profile.responses.splice(responseIndexForZip, 1)
173
+        }
174
+        return zip.val
175
+    }
163 176
     /**
164 177
      * Score a profile
165 178
      * @param {number} profileId
166 179
      * @returns {Array} Ordered and scored Profiles
167 180
      */
168
-    async scoreProfilesFor(profileId) {
181
+    async scoreProfilesFor(profileId, maxDistanceMiles, distanceUnit) {
169 182
         const { Profile } = this.server.models()
170
-
183
+        
171 184
         // Our User Profile to score for
172 185
         const userProfile = await Profile.query()
173
-            .findOne('profile_id', profileId)
174
-            .withGraphFetched('responses')
175
-            .withGraphFetched('user')
176
-
177
-        const isPosterOpposite = userProfile.user.is_poster == 1 ? 0 : 1
186
+        .findOne('profile_id', profileId)
187
+        .withGraphFetched('responses')
188
+        .withGraphFetched('user')
189
+        
190
+        // Move unneeded responses
191
+        const userZip = this._getZipCodeFromProfile(userProfile)
178 192
 
179 193
         // Find all Profiles that are NOT of our userProfile.type
180 194
         // ie. If userProfile.type == seeker, then find: poster
181 195
         let profileIdsOfOppositeType = await Profile.query()
182
-            .withGraphFetched('responses')
183
-            .withGraphFetched('user')
184
-
196
+        .withGraphFetched('responses')
197
+        .withGraphFetched('user')
198
+        
185 199
         // TODO: Let Objection optimize this
186
-        profileIdsOfOppositeType = profileIdsOfOppositeType.filter(
187
-            profile => profile.user.is_poster == isPosterOpposite,
188
-        )
189
-
190
-        const scored = profileIdsOfOppositeType.map(profile => ({
191
-            profile_id: profile.profile_id,
192
-            score: scoreResponses(userProfile, profile),
200
+        const isPosterOpposite = userProfile.user.is_poster == 1 ? 0 : 1
201
+        profileIdsOfOppositeType = profileIdsOfOppositeType.filter(profile => profile.user.is_poster == isPosterOpposite)
202
+        
203
+        const profilePlusDistance = await Promise.all(profileIdsOfOppositeType.map(async profile => {
204
+            const targetZip = this._getZipCodeFromProfile(profile)
205
+            const distance = await this._compareDistance(userZip, targetZip, distanceUnit)
206
+            return {
207
+                ...profile,
208
+                distance: [distance.toFixed(2), distanceUnit]
209
+            }
193 210
         }))
211
+
212
+        // Filter by distance
213
+        // TODO: probably do this with a query
214
+        const distanceFiltered = profilePlusDistance.filter(profile => {
215
+            const profileDistance = Math.floor(parseFloat(profile.distance) * 100)
216
+            const adjustedMaxDistance = Math.floor(parseFloat(maxDistanceMiles) * 100)
217
+            return profileDistance <= adjustedMaxDistance
218
+        })
219
+
220
+        const scored = distanceFiltered.map(profile => {
221
+            return {
222
+                // Uncomment to return the whole profile
223
+                // ...profile,
224
+                profile_id: profile.profile_id,
225
+                score: scoreResponses(userProfile, profile),
226
+                distance: profile.distance
227
+            }
228
+        })
229
+        // Order by score
194 230
         return scored.sort((a, b) => a.score - b.score)
195 231
     }
232
+    
233
+    /**
234
+     * Use the db for zipcode info
235
+     * @param {string} zipCode
236
+     * @param {object}
237
+     */
238
+    async _latLonForZip(zipCode) {
239
+        const { ZipCode } = this.server.models()
240
+
241
+        const zipInfo = await ZipCode.query().findOne('zip_code_id', parseInt(zipCode))
242
+        const latitude = parseFloat(zipInfo.latitude)
243
+        const longitude =  parseFloat(zipInfo.longitude)
244
+        
245
+        return { latitude, longitude }
246
+    }
247
+    /**
248
+     * Get the distance between two zipcodes
249
+     * using the haversine formula
250
+     * @param {string} start_zip
251
+     * @param {string} end_zip
252
+     * @param {number} distance in miles
253
+     */
254
+    async _compareDistance(start_zip, end_zip, distanceUnit) {
255
+        if(!start_zip || !end_zip) return
256
+        
257
+        const start = await this._latLonForZip(start_zip)
258
+        const end = await this._latLonForZip(end_zip)
259
+        
260
+        return haversine(start, end, { unit: distanceUnit })
261
+    }
196 262
 }

+ 5
- 0
backend/package-lock.json Целия файл

@@ -3112,6 +3112,11 @@
3112 3112
         "type-fest": "^0.8.0"
3113 3113
       }
3114 3114
     },
3115
+    "haversine": {
3116
+      "version": "1.1.1",
3117
+      "resolved": "https://registry.npmjs.org/haversine/-/haversine-1.1.1.tgz",
3118
+      "integrity": "sha512-KW4MS8+krLIeiw8bF5z532CptG0ZyGGFj0UbKMxx25lKnnJ1hMUbuzQl+PXQjNiDLnl1bOyz23U6hSK10r4guw=="
3119
+    },
3115 3120
     "homedir-polyfill": {
3116 3121
       "version": "1.0.3",
3117 3122
       "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",

+ 1
- 0
backend/package.json Целия файл

@@ -25,6 +25,7 @@
25 25
     "dotenv": "^10.0.0",
26 26
     "exiting": "^6.0.1",
27 27
     "hapi-swagger": "^14.1.3",
28
+    "haversine": "^1.1.1",
28 29
     "joi": "^17.4.0",
29 30
     "knex": "^0.21.19",
30 31
     "mysql": "^2.18.1",

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