diff options
| author | Bobby <[email protected]> | 2023-06-04 05:57:02 -0400 |
|---|---|---|
| committer | Bobby <[email protected]> | 2023-06-04 05:57:02 -0400 |
| commit | d4c53e387874f049be45f02e760282af41034965 (patch) | |
| tree | cefabac5157935ea086b8ecaf4140a615c9d80c5 | |
| parent | bf43bb721fe46642feb67f63fbfe243dd8bfd40d (diff) | |
| download | thatcomputerscientist-d4c53e387874f049be45f02e760282af41034965.tar.xz thatcomputerscientist-d4c53e387874f049be45f02e760282af41034965.zip | |
Article Recommendation Engine
| -rw-r--r-- | blog/recommender.py | 44 | ||||
| -rw-r--r-- | blog/views.py | 5 | ||||
| -rw-r--r-- | requirements.txt | 2 | ||||
| -rw-r--r-- | static/css/styles.css | 2 | ||||
| -rw-r--r-- | templates/blog/post.html | 36 |
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 |
