Move to distance filtering to improve accuracy and speed

This commit is contained in:
MartinBraquet
2025-10-17 16:43:27 +02:00
parent 7dc1a8790d
commit 58fdaa26ca
8 changed files with 95 additions and 20 deletions

View File

@@ -22,6 +22,9 @@ export type profileQueryType = {
is_smoker?: boolean | undefined,
shortBio?: boolean | undefined,
geodbCityIds?: String[] | undefined,
lat?: number | undefined,
lon?: number | undefined,
radius?: number | undefined,
compatibleWithUserId?: string | undefined,
skipId?: string | undefined,
orderBy?: string | undefined,
@@ -49,12 +52,17 @@ export const loadProfiles = async (props: profileQueryType) => {
is_smoker,
shortBio,
geodbCityIds,
lat,
lon,
radius,
compatibleWithUserId,
orderBy: orderByParam = 'created_time',
lastModificationWithin,
skipId,
} = props
const filterLocation = lat && lon && radius
const keywords = name ? name.split(",").map(q => q.trim()).filter(Boolean) : []
// console.debug('keywords:', keywords)
@@ -90,6 +98,12 @@ export const loadProfiles = async (props: profileQueryType) => {
(l.id.toString() != skipId) &&
(!geodbCityIds ||
(l.geodb_city_id && geodbCityIds.includes(l.geodb_city_id))) &&
(!filterLocation ||(
l.city_latitude && l.city_longitude &&
Math.abs(l.city_latitude - lat) < radius / 69.0 &&
Math.abs(l.city_longitude - lon) < radius / (69.0 * Math.cos(lat * Math.PI / 180)) &&
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)
)) &&
((l.bio_length ?? 0) >= MIN_BIO_LENGTH)
)
@@ -137,13 +151,13 @@ export const loadProfiles = async (props: profileQueryType) => {
pref_relation_styles?.length &&
where(
`pref_relation_styles IS NULL OR pref_relation_styles = '{}' OR pref_relation_styles && $(pref_relation_styles)`,
{ pref_relation_styles }
{pref_relation_styles}
),
pref_romantic_styles?.length &&
where(
`pref_romantic_styles IS NULL OR pref_romantic_styles = '{}' OR pref_romantic_styles && $(pref_romantic_styles)`,
{ pref_romantic_styles }
{pref_romantic_styles}
),
!!wants_kids_strength &&
@@ -163,6 +177,18 @@ export const loadProfiles = async (props: profileQueryType) => {
geodbCityIds?.length &&
where(`geodb_city_id = ANY($(geodbCityIds))`, {geodbCityIds}),
// miles par degree of lat: earth's radius (3950 miles) * pi / 180 = 69.0
filterLocation && where(`
city_latitude BETWEEN $(target_lat) - ($(radius) / 69.0)
AND $(target_lat) + ($(radius) / 69.0)
AND city_longitude BETWEEN $(target_lon) - ($(radius) / (69.0 * COS(RADIANS($(target_lat)))))
AND $(target_lon) + ($(radius) / (69.0 * COS(RADIANS($(target_lat)))))
AND SQRT(
POWER(city_latitude - $(target_lat), 2)
+ POWER((city_longitude - $(target_lon)) * COS(RADIANS($(target_lat))), 2)
) <= $(radius) / 69.0
`, {target_lat: lat, target_lon: lon, radius}),
skipId && where(`profiles.user_id != $(skipId)`, {skipId}),
orderBy(`${tablePrefix}.${orderByParam} DESC`),
@@ -174,7 +200,7 @@ export const loadProfiles = async (props: profileQueryType) => {
LEFT JOIN ${userActivityJoin}
WHERE profiles.id = $(after)
)`,
{ after }
{after}
),
!shortBio && where(`bio_length >= ${MIN_BIO_LENGTH}`, {MIN_BIO_LENGTH}),

View File

@@ -81,6 +81,15 @@ CREATE INDEX IF NOT EXISTS idx_profiles_last_mod_24h
CREATE INDEX IF NOT EXISTS idx_profiles_bio_length
ON profiles (bio_length);
-- Fastest general-purpose index
DROP INDEX IF EXISTS profiles_lat_lon_idx;
CREATE INDEX profiles_lat_lon_idx ON profiles (city_latitude, city_longitude);
-- Optional additional index for large tables / clustered inserts
DROP INDEX IF EXISTS profiles_lat_lon_brin_idx;
CREATE INDEX profiles_lat_lon_brin_idx ON profiles USING BRIN (city_latitude, city_longitude) WITH (pages_per_range = 32);
-- Functions and Triggers
CREATE

View File

@@ -355,6 +355,9 @@ export const API = (_apiTypeCheck = {
is_smoker: z.coerce.boolean().optional(),
shortBio: z.coerce.boolean().optional(),
geodbCityIds: arraybeSchema.optional(),
lat: z.coerce.number().optional(),
lon: z.coerce.number().optional(),
radius: z.coerce.number().optional(),
compatibleWithUserId: z.string().optional(),
orderBy: z
.enum(['last_online_time', 'created_time', 'compatibility_score'])

View File

@@ -2,9 +2,18 @@ import {Profile, ProfileRow} from "common/love/profile";
import {cloneDeep} from "lodash";
import {filterDefined} from "common/util/array";
// export type TargetArea = {
// lat: number
// lon: number
// radius: number
// }
export type FilterFields = {
orderBy: 'last_online_time' | 'created_time' | 'compatibility_score'
geodbCityIds: string[] | null
lat: number | null
lon: number | null
radius: number | null
genders: string[]
name: string | undefined
shortBio: boolean | undefined
@@ -18,6 +27,7 @@ export type FilterFields = {
| 'pref_age_min'
| 'pref_age_max'
>
export const orderProfiles = (
profiles: Profile[],
starredUserIds: string[] | undefined
@@ -39,6 +49,9 @@ export const orderProfiles = (
}
export const initialFilters: Partial<FilterFields> = {
geodbCityIds: undefined,
lat: undefined,
lon: undefined,
radius: undefined,
name: undefined,
genders: undefined,
pref_age_max: undefined,
@@ -51,4 +64,8 @@ export const initialFilters: Partial<FilterFields> = {
shortBio: undefined,
orderBy: 'created_time',
}
export type OriginLocation = { id: string; name: string }
export const FilterKeys = Object.keys(initialFilters) as (keyof FilterFields)[]
export type OriginLocation = { id: string; name: string, lat: number, lon: number }

View File

@@ -25,6 +25,18 @@ export type locationType = {
radius: number
}
const skippedKeys = [
'pref_age_min',
'pref_age_max',
'geodbCityIds',
'orderBy',
'shortBio',
'targetArea',
'lat',
'lon',
'radius',
]
export function formatFilters(filters: Partial<FilterFields>, location: locationType | null): String[] | null {
const entries: String[] = []
@@ -53,7 +65,7 @@ export function formatFilters(filters: Partial<FilterFields>, location: location
const typedKey = key as keyof FilterFields
if (value === undefined || value === null) return
if (typedKey == 'pref_age_min' || typedKey == 'pref_age_max' || typedKey == 'geodbCityIds' || typedKey == 'orderBy' || typedKey == 'shortBio') return
if (skippedKeys.includes(typedKey)) return
if (Array.isArray(value) && value.length === 0) return
if (initialFilters[typedKey] === value) return

View File

@@ -75,7 +75,7 @@ export function LocationFilter(props: {
if (!city) {
setLocation(undefined)
} else {
setLocation({ id: city.geodb_city_id, name: city.city })
setLocation({ id: city.geodb_city_id, name: city.city, lat: city.latitude, lon: city.longitude })
setLastCity(city)
}
}
@@ -123,7 +123,7 @@ function DistanceSlider(props: {
}) {
const { radius, setRadius } = props
const snapValues = [10, 50, 100, 200, 300]
const snapValues = [10, 50, 100, 200, 300, 500]
const snapToValue = (value: number) => {
const closest = snapValues.reduce((prev, curr) =>
@@ -158,7 +158,7 @@ function LocationResults(props: {
}) {
const { showAny, cities, onCitySelected, loading, className } = props
// delay loading animation by 150ms
// delay loading animation by 150 ms
const [debouncedLoading, setDebouncedLoading] = useState(loading)
useEffect(() => {
if (loading) {

View File

@@ -1,10 +1,8 @@
import {Profile} from "common/love/profile";
import {useIsLooking} from "web/hooks/use-is-looking";
import {usePersistentLocalState} from "web/hooks/use-persistent-local-state";
import {useCallback} from "react";
import {useCallback, useEffect} from "react";
import {debounce, isEqual} from "lodash";
import {useNearbyCities} from "web/hooks/use-nearby-locations";
import {useEffectCheckEquality} from "web/hooks/use-effect-check-equality";
import {wantsKidsDatabase, wantsKidsDatabaseToWantsKidsFilter, wantsKidsToHasKidsFilter} from "common/wants-kids";
import {FilterFields, initialFilters, OriginLocation} from "common/filters";
import {MAX_INT, MIN_INT} from "common/constants";
@@ -13,9 +11,11 @@ export const useFilters = (you: Profile | undefined) => {
const isLooking = useIsLooking()
const [filters, setFilters] = usePersistentLocalState<Partial<FilterFields>>(
isLooking ? initialFilters : {...initialFilters, orderBy: 'created_time'},
'profile-filters-2'
'profile-filters-4'
)
// console.log('filters', filters)
const updateFilter = (newState: Partial<FilterFields>) => {
const updatedState = {...newState}
@@ -31,6 +31,8 @@ export const useFilters = (you: Profile | undefined) => {
}
}
// console.log('updating filters', updatedState)
setFilters((prevState) => ({...prevState, ...updatedState}))
}
@@ -54,11 +56,17 @@ export const useFilters = (you: Profile | undefined) => {
OriginLocation | undefined | null
>(undefined, 'nearby-origin-location')
const nearbyCities = useNearbyCities(location?.id, radius)
// const nearbyCities = useNearbyCities(location?.id, radius)
//
// useEffectCheckEquality(() => {
// updateFilter({geodbCityIds: nearbyCities})
// }, [nearbyCities])
useEffectCheckEquality(() => {
updateFilter({geodbCityIds: nearbyCities})
}, [nearbyCities])
useEffect(() => {
if (location?.lat && location?.lon) {
updateFilter({lat: location.lat, lon: location.lon, radius: radius})
}
}, [location?.id, radius]);
const locationFilterProps = {
location,
@@ -99,8 +107,8 @@ export const useFilters = (you: Profile | undefined) => {
updateFilter(yourFilters)
setRadius(100)
debouncedSetRadius(100) // clear any pending debounced sets
if (you?.geodb_city_id && you.city) {
setLocation({id: you?.geodb_city_id, name: you?.city})
if (you?.geodb_city_id && you.city && you.city_latitude && you.city_longitude) {
setLocation({id: you?.geodb_city_id, name: you?.city, lat: you?.city_latitude, lon: you?.city_longitude})
}
} else {
clearFilters()

View File

@@ -6,7 +6,7 @@ import {useCompatibleProfiles} from 'web/hooks/use-profiles'
import {getStars} from 'web/lib/supabase/stars'
import {useCallback, useEffect, useRef, useState} from 'react'
import {ProfileGrid} from 'web/components/profile-grid'
import {CompassLoadingIndicator, LoadingIndicator} from 'web/components/widgets/loading-indicator'
import {CompassLoadingIndicator} from 'web/components/widgets/loading-indicator'
import {Title} from 'web/components/widgets/title'
import {useGetter} from 'web/hooks/use-getter'
import {usePersistentInMemoryState} from 'web/hooks/use-persistent-in-memory-state'
@@ -28,7 +28,7 @@ export function ProfilesHome() {
locationFilterProps,
} = useFilters(you ?? undefined);
const [profiles, setProfiles] = usePersistentInMemoryState<Profile[] | undefined>(undefined, 'profile-profiles');
const [profiles, setProfiles] = usePersistentInMemoryState<Profile[] | undefined>(undefined, 'profiles');
const {bookmarkedSearches, refreshBookmarkedSearches} = useBookmarkedSearches(user?.id)
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [isReloading, setIsReloading] = useState(false);