Skip to content

Commit ac8cfb5

Browse files
committed
Added recommendations 'Algorithm'
1 parent 2a17c7b commit ac8cfb5

File tree

4 files changed

+134
-1
lines changed

4 files changed

+134
-1
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""
2+
PyMatcha - A Python Dating Website
3+
Copyright (C) 2018-2019 jlasne/gmorer
4+
<jlasne@student.42.fr> - <gmorer@student.42.fr>
5+
6+
This program is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU General Public License as published by
8+
the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
This program is distributed in the hope that it will be useful,
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU General Public License for more details.
15+
16+
You should have received a copy of the GNU General Public License
17+
along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
"""
19+
from flask import Blueprint
20+
from flask_jwt_extended import current_user
21+
from flask_jwt_extended import jwt_required
22+
from PyMatcha import redis
23+
from PyMatcha.utils.errors import NotFoundError
24+
from PyMatcha.utils.success import SuccessOutput
25+
26+
recommendations_bp = Blueprint("recommendations", __name__)
27+
28+
29+
@recommendations_bp.route("/recommendations", methods=["GET"])
30+
@jwt_required
31+
def get_recommendations():
32+
recommendations = redis.get(f"user_recommendations:{str(current_user.id)}")
33+
if not recommendations:
34+
raise NotFoundError("Recommendations not calculated yet", "Please come back later")
35+
return SuccessOutput("recommendations", recommendations)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from typing import List
2+
3+
from fuzzywuzzy import fuzz
4+
from Geohash import decode
5+
from geopy.distance import distance
6+
7+
8+
def _get_distance(geohash_1: str, geohash_2: str) -> float:
9+
coords_1 = decode(geohash_1)
10+
coords_2 = decode(geohash_2)
11+
return distance(coords_1, coords_2).kilometers
12+
13+
14+
def _get_common_tags(tag_list_1: list, tag_list_2: list) -> List[str]:
15+
"""Will return the list of common tags
16+
"""
17+
common_tags = []
18+
for tag_1 in tag_list_1:
19+
for tag_2 in tag_list_2:
20+
if fuzz.partial_ratio(tag_1, tag_2) >= 70:
21+
common_tags.append(tag_1 if len(tag_1) > len(tag_2) else tag_2)
22+
return list(set(common_tags))
23+
24+
25+
def _get_age_diff(age_1: int, age_2: int) -> int:
26+
return -1 * (age_1 - age_2)
27+
28+
29+
def _get_inverted_gender(gender, orientation):
30+
# TODO: Handle other gender and bisexual and other orientations
31+
if orientation == "heterosexual":
32+
return "male" if gender == "female" else "female"
33+
elif orientation == "homosexual":
34+
return "male" if gender == "male" else "female"
35+
else:
36+
return "other"

backend/PyMatcha/utils/tasks.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
def setup_periodic_tasks(sender, **kwargs):
1818
sender.add_periodic_task(60, update_offline_users.s(), name="Update online users every minute")
1919
sender.add_periodic_task(3600, update_heat_scores.s(), name="Update heat scores every hour")
20+
sender.add_periodic_task(60, update_user_recommendations.s(), name="Update user recommendations every minute")
2021

2122

2223
@celery.task
@@ -92,3 +93,60 @@ def update_offline_users():
9293
return "Updated online status for {} users. {} passed offline and {} passed or stayed online.".format(
9394
count, offline_count, online_count
9495
)
96+
97+
98+
def default_date_converter(o):
99+
if isinstance(o, datetime.datetime):
100+
return o.__str__()
101+
102+
103+
@celery.task
104+
def update_user_recommendations():
105+
today = datetime.datetime.utcnow()
106+
count = 0
107+
for user_to_update in User.select_all():
108+
count += 1
109+
user_to_update_recommendations = []
110+
111+
user_to_update_age = (
112+
today.year
113+
- user_to_update.birthdate.year
114+
- ((today.month, today.day) < (user_to_update.birthdate.month, user_to_update.birthdate.day))
115+
)
116+
user_to_update_tags = [t.name for t in user_to_update.get_tags()]
117+
118+
inverted_gender = _get_inverted_gender(user_to_update.gender, user_to_update.orientation)
119+
120+
for user in User.get_multis(orientation=user_to_update.orientation, gender=inverted_gender):
121+
if user.id == user_to_update.id:
122+
continue
123+
score = 0
124+
125+
distance = _get_distance(user_to_update.geohash, user.geohash)
126+
score -= distance
127+
128+
user_age = (
129+
today.year
130+
- user.birthdate.year
131+
- ((today.month, today.day) < (user.birthdate.month, user.birthdate.day))
132+
)
133+
age_diff = _get_age_diff(user_to_update_age, user_age)
134+
score -= age_diff
135+
136+
user_tags = [t.name for t in user.get_tags()]
137+
common_tags = _get_common_tags(user_to_update_tags, user_tags)
138+
score += len(common_tags) * 2
139+
140+
score += user.heat_score
141+
142+
d = {"score": score, "common_tags": common_tags, "distance": distance}
143+
d.update(user.to_dict())
144+
user_to_update_recommendations.append(d)
145+
user_to_update_recommendations_sorted = sorted(
146+
user_to_update_recommendations, key=lambda x: x["score"], reverse=True
147+
)
148+
redis.set(
149+
f"user_recommendations:{str(user_to_update.id)}",
150+
json.dumps(user_to_update_recommendations_sorted, default=default_date_converter),
151+
)
152+
return f"Successfully updated recommendations for {count} users."

backend/requirements.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,8 @@ lorem==0.1.1
2929
future==0.18.2
3030
ip2geotools==0.1.5
3131

32-
argon2-cffi==20.1.0
32+
argon2-cffi==20.1.0
33+
34+
geopy==2.0.0
35+
fuzzywuzzy==0.18.0
36+
python-Levenshtein==0.12.0

0 commit comments

Comments
 (0)