diff --git a/db/migrations/20171003103621_create_recommendations.sql b/db/migrations/20171003103621_create_recommendations.sql new file mode 100644 index 0000000..1013ecb --- /dev/null +++ b/db/migrations/20171003103621_create_recommendations.sql @@ -0,0 +1,11 @@ +-- +micrate Up +CREATE TABLE recommendations ( + id BIGSERIAL PRIMARY KEY, + announcement_id INTEGER NOT NULL, + recommended_id INTEGER NOT NULL, + created_at TIMESTAMP, + updated_at TIMESTAMP +); + +-- +micrate Down +DROP TABLE IF EXISTS recommendations; diff --git a/public/stylesheets/main.css b/public/stylesheets/main.css index ffc75fa..cf4d386 100644 --- a/public/stylesheets/main.css +++ b/public/stylesheets/main.css @@ -844,6 +844,7 @@ a:focus { position: relative; text-transform: uppercase; z-index: 2; + padding: 5% 5% 1%; } .post-navigation .post-title { @@ -855,6 +856,10 @@ a:focus { z-index: 2; } +.nav-links .post-title:not(:last-child) { + border-bottom: 1px solid rgba(51, 51, 51, 0.1); +} + .post-navigation .nav-next, .post-navigation .nav-previous { background-position: center; @@ -4109,7 +4114,7 @@ div.connect-twitter { } .post-navigation a { - padding: 5% 10%; + padding: 4% 10%; } .pagination { diff --git a/shard.lock b/shard.lock index 35ed610..96507bb 100644 --- a/shard.lock +++ b/shard.lock @@ -80,6 +80,10 @@ shards: github: luislavena/radix version: 0.3.8 + recommender: + github: hugoabonizio/recommender.cr + version: 0.1.0 + redis: github: stefanwille/crystal-redis version: 1.8.0 @@ -90,7 +94,7 @@ shards: shell-table: github: jwaldrip/shell-table.cr - version: 0.9.1 + version: 0.9.2 sidekiq: github: mperham/sidekiq.cr @@ -112,6 +116,10 @@ shards: github: crystal-lang/crystal-sqlite3 version: 0.8.2 + stemmer: + github: hugoabonizio/stemmer.cr + commit: 1ca4d8c2023c32da006c9ae138f90b327e939016 + string_inflection: github: mosop/string_inflection version: 0.2.1 diff --git a/shard.yml b/shard.yml index e02a346..e2701f2 100644 --- a/shard.yml +++ b/shard.yml @@ -43,6 +43,10 @@ dependencies: github: crystal-community/autolink.cr version: 0.1.3 + recommender: + github: hugoabonizio/recommender.cr + version: 0.1.0 + development_dependencies: spec2: github: waterlink/spec2.cr diff --git a/src/controllers/announcement_controller.cr b/src/controllers/announcement_controller.cr index 8fdde9d..b2f25d4 100644 --- a/src/controllers/announcement_controller.cr +++ b/src/controllers/announcement_controller.cr @@ -14,7 +14,6 @@ class AnnouncementController < ApplicationController def show if announcement = Announcement.find params["id"] - newer, older = announcement.next, announcement.prev render("show.slang") else redirect_to "/" diff --git a/src/models/announcement.cr b/src/models/announcement.cr index d81e105..e4f8b80 100644 --- a/src/models/announcement.cr +++ b/src/models/announcement.cr @@ -22,6 +22,8 @@ class Announcement < Granite::ORM::Base field description : String timestamps + has_many :recommendations + validate :title, "is too short", ->(this : Announcement) { this.title.to_s.size >= 5 } @@ -102,6 +104,10 @@ class Announcement < Granite::ORM::Base Autolink.auto_link(Markdown.to_html(description.not_nil!)) end + def load_recommendations + Announcement.all("WHERE id IN(#{recommendations.map(&.recommended_id).join(',')})") + end + def self.random Announcement.all("ORDER BY RANDOM() LIMIT 1").first? end diff --git a/src/models/recommendation.cr b/src/models/recommendation.cr new file mode 100644 index 0000000..ea1be91 --- /dev/null +++ b/src/models/recommendation.cr @@ -0,0 +1,11 @@ +require "granite_orm/adapter/pg" + +class Recommendation < Granite::ORM::Base + adapter pg + + belongs_to :announcement + + field announcement_id : Int32 + field recommended_id : Int32 + timestamps +end diff --git a/src/views/announcement/show.slang b/src/views/announcement/show.slang index 1ffdc16..8fde3db 100644 --- a/src/views/announcement/show.slang +++ b/src/views/announcement/show.slang @@ -4,14 +4,7 @@ nav.navigation.post-navigation role="navigation" div.nav-links - - if newer - div.nav-next - a href="/announcements/#{newer.id}" - span.meta-nav Previous - span.post-title = newer.title - - - if older - div.nav-previous - a href="/announcements/#{older.id}" - span.meta-nav Next - span.post-title = older.title + span.meta-nav See more + - announcement.load_recommendations.each do |recommended| + a.post-title href="/announcements/#{recommended.id}" + = recommended.title diff --git a/src/workers/recommender.cr b/src/workers/recommender.cr new file mode 100644 index 0000000..1275d01 --- /dev/null +++ b/src/workers/recommender.cr @@ -0,0 +1,40 @@ +require "recommender" +require "sidekiq" +require "../models/announcement" +require "../models/recommendation" + +module Workers + class Recommender + include Sidekiq::Worker + + def perform + clear_recommendations + announcements = Announcement.all + preprocessed_announcements = announcements.map { |a| "#{a.title} #{a.description}" } + recommender = ::Recommender::ContentBased.new(preprocessed_announcements) + + signal = Channel(Nil).new + + preprocessed_announcements.each_with_index do |_, i| + ids = recommender.similar_to(i).first(3) + ids.each do |j| + data = { + :announcement_id => announcements[i].id, + :recommended_id => announcements[j].id, + } + recommendation = Recommendation.new(data) + spawn do + recommendation.save + signal.send nil + end + end + end + + preprocessed_announcements.size.times { signal.receive } + end + + private def clear_recommendations + Recommendation.clear + end + end +end