aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2023-06-04 05:57:02 -0400
committerBobby <[email protected]>2023-06-04 05:57:02 -0400
commitd4c53e387874f049be45f02e760282af41034965 (patch)
treecefabac5157935ea086b8ecaf4140a615c9d80c5
parentbf43bb721fe46642feb67f63fbfe243dd8bfd40d (diff)
downloadthatcomputerscientist-d4c53e387874f049be45f02e760282af41034965.tar.xz
thatcomputerscientist-d4c53e387874f049be45f02e760282af41034965.zip
Article Recommendation Engine
-rw-r--r--blog/recommender.py44
-rw-r--r--blog/views.py5
-rw-r--r--requirements.txt2
-rw-r--r--static/css/styles.css2
-rw-r--r--templates/blog/post.html36
5 files changed, 87 insertions, 2 deletions
diff --git a/blog/recommender.py b/blog/recommender.py
new file mode 100644
index 00000000..24d95518
--- /dev/null
+++ b/blog/recommender.py
@@ -0,0 +1,44 @@
+# This is a very simple recommender system that recommends posts based on the
+# current post user is reading.
+
+from .models import Post
+import numpy as np
+from sklearn.feature_extraction.text import TfidfVectorizer
+from sklearn.metrics.pairwise import cosine_similarity
+from bs4 import BeautifulSoup
+from .context_processors import add_excerpt, add_num_comments
+
+def next_read(post):
+ current_post = Post.objects.get(id=post.id)
+ posts = Post.objects.filter(is_public=True).exclude(id=current_post.id)
+
+ # Our method is very simple. First we compare the bodies of the posts to
+ # find the similarity between them. Then we sort the posts based on their
+ # similarity and return the post with the highest similarity.
+ #
+ # If no post has similarity > 0.5, we return the post with the highest
+ # number of views, preferably in the same category. If there is no post in
+ # the same category, we return the post with the highest number of views
+ # regardless of the category.
+
+ vectorizer = TfidfVectorizer(stop_words='english')
+ vectors = vectorizer.fit_transform([BeautifulSoup(post.body, 'html.parser').text for post in posts])
+ current_vector = vectorizer.transform([current_post.body])
+ similarity = cosine_similarity(current_vector, vectors).flatten()
+ similarity = np.nan_to_num(similarity)
+ max_similarity = np.argmax(similarity)
+
+ if similarity[max_similarity] > 0.5:
+ post = posts[max_similarity]
+ post.excerpt = add_excerpt(post)
+ post.num_comments = add_num_comments(post)
+ return post
+ else:
+ posts = posts.order_by('-views')
+ if posts:
+ post = posts[0]
+ post.excerpt = add_excerpt(post)
+ post.num_comments = add_num_comments(post)
+ return post
+ else:
+ return None
diff --git a/blog/views.py b/blog/views.py
index c5d9db5e..f293a4ad 100644
--- a/blog/views.py
+++ b/blog/views.py
@@ -27,6 +27,7 @@ from .context_processors import (add_excerpt, add_num_comments, avatar_list,
comment_processor, highlight_code_blocks,
recent_posts)
from .models import AnonymousCommentUser, Category, Comment, Post
+from .recommender import next_read
load_dotenv()
@@ -173,7 +174,9 @@ def post(request, slug):
request.meta['description'] = BeautifulSoup(first_paragraph, 'html.parser').get_text()
request.meta['image'] = 'https://thatcomputerscientist.com/ignis/post_image/720/' + str(post.id) + '.gif'
- return render(request, 'blog/post.html', {'title': post.title, 'post': post, 'tags': tags, 'comments': comments, 'view_count': view_count})
+ read_next = next_read(post)
+
+ return render(request, 'blog/post.html', {'title': post.title, 'post': post, 'tags': tags, 'comments': comments, 'view_count': view_count, 'read_next': read_next})
else:
if request.user.is_authenticated and request.user.is_superuser or request.user.is_staff:
return render(request, 'blog/post.html', {'title': post.title, 'post': post, 'tags': tags, 'comments': comments, 'view_count': view_count})
diff --git a/requirements.txt b/requirements.txt
index 40365543..d3b692e7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -21,3 +21,5 @@ channels
channels_redis
daphne
user_agents
+numpy
+scikit-learn
diff --git a/static/css/styles.css b/static/css/styles.css
index 28f45ddf..2b01ba16 100644
--- a/static/css/styles.css
+++ b/static/css/styles.css
@@ -252,7 +252,7 @@ blockquote {
.highlight {
background: #311b4f26 !important;
- padding: 20px;
+ padding: 5px 20px;
box-sizing: border-box;
border-radius: 8px;
text-align: left !important;
diff --git a/templates/blog/post.html b/templates/blog/post.html
index 15e7a27a..c8fcd4e8 100644
--- a/templates/blog/post.html
+++ b/templates/blog/post.html
@@ -47,6 +47,42 @@
</div>
</div>
+{% if read_next %}
+<h2 class="mtsbitem">Read Next</h2>
+<div id="read-next">
+ <div class="post">
+ <div class="post-header">
+ <h1><a style="color: #f4ebff;" href="{% url 'blog:post' read_next.slug %}">{{ read_next.title }}</a></h1>
+ <div class="author-info">
+ {% with read_next.author.userprofile_set.first as userprofile %}
+ <span style="background-image: url('{% static 'images/avatars/' %}{{ userprofile.avatar_url }}.gif');" class="post-profile-image"></span>
+ {% endwith %}
+ <span>
+ <a href="{% url 'blog:user_activity' read_next.author %}" style="font-weight: bold;">
+ {{ read_next.author.first_name }} {{ read_next.author.last_name }}
+ </a>
+ </span>
+ <span>posted in</span>
+ <span>
+ <a href="{% url 'blog:categories' %}/{{ read_next.category.slug }}" style="">
+ {{ read_next.category }}
+ </a>
+ </span>
+ <span style="margin-left: 4px;"><b>|</b></span>
+ <span style="margin-left: 4px;">{% localtime on %}{{ read_next.date | date:"M d, Y" }}{% endlocaltime %}</span>
+ </div>
+ </div>
+ <div class="post-body">
+ <img class="post-image post-image-l" src="{% url 'ignis:post_image' '350' read_next.id %}.gif">
+ {{ read_next.excerpt | safe }}
+ </div>
+ <div class="post-actions" style="clear: both;">
+ <a href="{% url 'blog:post' read_next.slug %}">Continue Reading</a> | <a href="{% url 'blog:post' read_next.slug %}#comments">{{ read_next.num_comments }}
+ Opinion{% if not read_next.comments.count == 1 %}s{% endif %}</a>
+ </div>
+ </div>
+</div>
+{% endif %}
<h2 class="mtsbitem">Comments
<a href="#header" class="pa-btn" style="float: right; margin-top: 0px; text-transform: capitalize; font-weight: normal;">
Back to Top