|
1 | 1 | import {type APIHandler} from 'api/helpers/endpoint' |
2 | 2 | import {convertRow} from 'shared/profiles/supabase' |
3 | | -import {createSupabaseDirectClient} from 'shared/supabase/init' |
| 3 | +import {createSupabaseDirectClient, pgp} from 'shared/supabase/init' |
4 | 4 | import {from, join, leftJoin, limit, orderBy, renderSql, select, where,} from 'shared/supabase/sql-builder' |
5 | | -import {getCompatibleProfiles} from 'api/compatible-profiles' |
6 | | -import {intersection} from 'lodash' |
7 | | -import {MAX_INT, MIN_BIO_LENGTH, MIN_INT} from "common/constants"; |
| 5 | +import {MIN_BIO_LENGTH} from "common/constants"; |
| 6 | +import {compact} from "lodash"; |
8 | 7 |
|
9 | 8 | export type profileQueryType = { |
10 | 9 | limit?: number | undefined, |
@@ -40,7 +39,7 @@ export type profileQueryType = { |
40 | 39 | lastModificationWithin?: string | undefined, |
41 | 40 | } |
42 | 41 |
|
43 | | -const userActivityColumns = ['last_online_time'] |
| 42 | +// const userActivityColumns = ['last_online_time'] |
44 | 43 |
|
45 | 44 |
|
46 | 45 | export const loadProfiles = async (props: profileQueryType) => { |
@@ -84,86 +83,35 @@ export const loadProfiles = async (props: profileQueryType) => { |
84 | 83 | const keywords = name ? name.split(",").map(q => q.trim()).filter(Boolean) : [] |
85 | 84 | // console.debug('keywords:', keywords) |
86 | 85 |
|
87 | | - // TODO: do this in SQL for better performance. Precompute compatibility scores: |
88 | | - // - Have a table compatibility_scores(user_id_1, user_id_2, score) that updates whenever answers from either user change. |
89 | | - // - Query this table directly with "ORDER BY score DESC LIMIT {limit}". |
90 | | - if (orderByParam === 'compatibility_score') { |
91 | | - if (!compatibleWithUserId) { |
92 | | - console.error('Incompatible with user ID') |
93 | | - throw Error('Incompatible with user ID') |
94 | | - } |
95 | | - |
96 | | - const {compatibleProfiles} = await getCompatibleProfiles(compatibleWithUserId) |
97 | | - let profiles = compatibleProfiles.filter( |
98 | | - (l) => |
99 | | - (!name || l.user.name.toLowerCase().includes(name.toLowerCase())) && |
100 | | - (!genders || genders.includes(l.gender ?? '')) && |
101 | | - (!education_levels || education_levels.includes(l.education_level ?? '')) && |
102 | | - (!mbti || mbti.includes(l.mbti ?? '')) && |
103 | | - (!pref_gender || intersection(pref_gender, l.pref_gender).length) && |
104 | | - (!pref_age_min || (l.age ?? MAX_INT) >= pref_age_min) && |
105 | | - (!pref_age_max || (l.age ?? MIN_INT) <= pref_age_max) && |
106 | | - (!drinks_min || (l.drinks_per_month ?? MAX_INT) >= drinks_min) && |
107 | | - (!drinks_max || (l.drinks_per_month ?? MIN_INT) <= drinks_max) && |
108 | | - (!pref_relation_styles || |
109 | | - intersection(pref_relation_styles, l.pref_relation_styles).length) && |
110 | | - (!pref_romantic_styles || |
111 | | - intersection(pref_romantic_styles, l.pref_romantic_styles).length) && |
112 | | - (!diet || |
113 | | - intersection(diet, l.diet).length) && |
114 | | - (!political_beliefs || |
115 | | - intersection(political_beliefs, l.political_beliefs).length) && |
116 | | - (!relationship_status || |
117 | | - intersection(relationship_status, l.relationship_status).length) && |
118 | | - (!languages || |
119 | | - intersection(languages, l.languages).length) && |
120 | | - (!religion || |
121 | | - intersection(religion, l.religion).length) && |
122 | | - (!wants_kids_strength || |
123 | | - wants_kids_strength == -1 || |
124 | | - !l.wants_kids_strength || |
125 | | - l.wants_kids_strength == -1 || |
126 | | - (wants_kids_strength >= 2 |
127 | | - ? l.wants_kids_strength >= wants_kids_strength |
128 | | - : l.wants_kids_strength <= wants_kids_strength)) && |
129 | | - (has_kids == undefined || |
130 | | - has_kids == -1 || |
131 | | - (has_kids == 0 && !l.has_kids) || |
132 | | - (l.has_kids && l.has_kids > 0)) && |
133 | | - (is_smoker === undefined || l.is_smoker === is_smoker) && |
134 | | - (!l.disabled) && |
135 | | - (l.id.toString() != skipId) && |
136 | | - (!geodbCityIds || |
137 | | - (l.geodb_city_id && geodbCityIds.includes(l.geodb_city_id))) && |
138 | | - (!filterLocation || ( |
139 | | - l.city_latitude && l.city_longitude && |
140 | | - Math.abs(l.city_latitude - lat) < radius / 69.0 && |
141 | | - Math.abs(l.city_longitude - lon) < radius / (69.0 * Math.cos(lat * Math.PI / 180)) && |
142 | | - Math.pow(l.city_latitude - lat, 2) + Math.pow((l.city_longitude - lon) * Math.cos(lat * Math.PI / 180), 2) < Math.pow(radius / 69.0, 2) |
143 | | - )) && |
144 | | - (shortBio || (l.bio_length ?? 0) >= MIN_BIO_LENGTH) |
145 | | - ) |
146 | | - |
147 | | - const count = profiles.length |
148 | | - |
149 | | - const cursor = after |
150 | | - ? profiles.findIndex((l) => l.id.toString() === after) + 1 |
151 | | - : 0 |
152 | | - console.debug(cursor) |
153 | | - |
154 | | - if (limitParam) profiles = profiles.slice(cursor, cursor + limitParam) |
155 | | - |
156 | | - return {profiles, count} |
| 86 | + if (orderByParam === 'compatibility_score' && !compatibleWithUserId) { |
| 87 | + console.error('Incompatible with user ID') |
| 88 | + throw Error('Incompatible with user ID') |
157 | 89 | } |
158 | 90 |
|
159 | | - const tablePrefix = userActivityColumns.includes(orderByParam) ? 'user_activity' : 'profiles' |
| 91 | + const tablePrefix = orderByParam === 'compatibility_score' |
| 92 | + ? 'compatibility_scores' |
| 93 | + : orderByParam === 'last_online_time' |
| 94 | + ? 'user_activity' |
| 95 | + : 'profiles' |
| 96 | + |
160 | 97 | const userActivityJoin = 'user_activity on user_activity.user_id = profiles.user_id' |
| 98 | + const compatibilityScoreJoin = pgp.as.format(`compatibility_scores cs on (cs.user_id_1 = LEAST(profiles.user_id, $(compatibleWithUserId)) and cs.user_id_2 = GREATEST(profiles.user_id, $(compatibleWithUserId)))`, {compatibleWithUserId}) |
| 99 | + |
| 100 | + const _orderBy = orderByParam === 'compatibility_score' ? 'cs.score' : `${tablePrefix}.${orderByParam}` |
| 101 | + const afterFilter = renderSql( |
| 102 | + select(_orderBy), |
| 103 | + from('profiles'), |
| 104 | + orderByParam === 'last_online_time' && leftJoin(userActivityJoin), |
| 105 | + orderByParam === 'compatibility_score' && compatibleWithUserId && join(compatibilityScoreJoin), |
| 106 | + where('profiles.id = $(after)', {after}), |
| 107 | + ) |
161 | 108 |
|
162 | | - const tableSelection = [ |
| 109 | + const tableSelection = compact([ |
163 | 110 | from('profiles'), |
164 | 111 | join('users on users.id = profiles.user_id'), |
165 | | - leftJoin(userActivityJoin), |
166 | | - ] |
| 112 | + orderByParam === 'last_online_time' && leftJoin(userActivityJoin), |
| 113 | + orderByParam === 'compatibility_score' && compatibleWithUserId && join(compatibilityScoreJoin), |
| 114 | + ]) |
167 | 115 |
|
168 | 116 | const filters = [ |
169 | 117 | where('looking_for_matches = true'), |
@@ -281,29 +229,27 @@ export const loadProfiles = async (props: profileQueryType) => { |
281 | 229 | lastModificationWithin && where(`last_modification_time >= NOW() - INTERVAL $(lastModificationWithin)`, {lastModificationWithin}), |
282 | 230 | ] |
283 | 231 |
|
| 232 | + let selectCols = 'profiles.*, name, username, users.data as user' |
| 233 | + if (orderByParam === 'compatibility_score') { |
| 234 | + selectCols += ', cs.score as compatibility_score' |
| 235 | + } else if (orderByParam === 'last_online_time') { |
| 236 | + selectCols += ', user_activity.last_online_time' |
| 237 | + } |
| 238 | + |
284 | 239 | const query = renderSql( |
285 | | - select('profiles.*, name, username, users.data as user, user_activity.last_online_time'), |
| 240 | + select(selectCols), |
286 | 241 | ...tableSelection, |
287 | 242 | ...filters, |
288 | | - |
289 | | - orderBy(`${tablePrefix}.${orderByParam} DESC`), |
290 | | - |
291 | | - after && where( |
292 | | - `${tablePrefix}.${orderByParam} < ( |
293 | | - SELECT ${tablePrefix}.${orderByParam} |
294 | | - FROM profiles |
295 | | - LEFT JOIN ${userActivityJoin} |
296 | | - WHERE profiles.id = $(after) |
297 | | - )`, {after}), |
298 | | - |
| 243 | + orderBy(`${_orderBy} DESC`), |
| 244 | + after && where(`${_orderBy} < (${afterFilter})`), |
299 | 245 | limitParam && limit(limitParam), |
300 | 246 | ) |
301 | 247 |
|
302 | | - // console.debug('query:', query) |
| 248 | + console.debug('query:', query) |
303 | 249 |
|
304 | 250 | const profiles = await pg.map(query, [], convertRow) |
305 | 251 |
|
306 | | - console.debug('profiles:', profiles) |
| 252 | + // console.debug('profiles:', profiles) |
307 | 253 |
|
308 | 254 | const countQuery = renderSql( |
309 | 255 | select(`count(*) as count`), |
|
0 commit comments