diff options
author | Eugen Rochko <eugen@zeonfederated.com> | 2018-05-27 21:45:30 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-05-27 21:45:30 +0200 |
commit | 9bd23dc4e51ba47283a8e3a66cd94b4e624a5235 (patch) | |
tree | 119802887a7b894ea3aac5e28a8a7a15524c1c35 /app/models | |
parent | 63c7b9157274f57c496399a1a5c728b32415034c (diff) |
Track trending tags (#7638)
* Track trending tags - Half-life of 1 day - Historical usage in daily buckets (last 7 days stored) - GET /api/v1/trends Fix #271 * Add trends to web UI * Don't render compose form on search route, adjust search results header * Disqualify tag from trends if it's in disallowed hashtags setting * Count distinct accounts using tag, ignore silenced accounts
Diffstat (limited to 'app/models')
-rw-r--r-- | app/models/tag.rb | 16 | ||||
-rw-r--r-- | app/models/trending_tags.rb | 61 |
2 files changed, 77 insertions, 0 deletions
diff --git a/app/models/tag.rb b/app/models/tag.rb index 8b1b02412..4f31f796e 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -21,6 +21,22 @@ class Tag < ApplicationRecord name end + def history + days = [] + + 7.times do |i| + day = i.days.ago.beginning_of_day.to_i + + days << { + day: day.to_s, + uses: Redis.current.get("activity:tags:#{id}:#{day}") || '0', + accounts: Redis.current.pfcount("activity:tags:#{id}:#{day}:accounts").to_s, + } + end + + days + end + class << self def search_for(term, limit = 5) pattern = sanitize_sql_like(term.strip) + '%' diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb new file mode 100644 index 000000000..eedd92644 --- /dev/null +++ b/app/models/trending_tags.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +class TrendingTags + KEY = 'trending_tags' + HALF_LIFE = 1.day.to_i + MAX_ITEMS = 500 + EXPIRE_HISTORY_AFTER = 7.days.seconds + + class << self + def record_use!(tag, account, at_time = Time.now.utc) + return if disallowed_hashtags.include?(tag.name) || account.silenced? + + increment_vote!(tag.id, at_time) + increment_historical_use!(tag.id, at_time) + increment_unique_use!(tag.id, account.id, at_time) + end + + def get(limit) + tag_ids = redis.zrevrange(KEY, 0, limit).map(&:to_i) + tags = Tag.where(id: tag_ids).to_a.map { |tag| [tag.id, tag] }.to_h + tag_ids.map { |tag_id| tags[tag_id] }.compact + end + + private + + def increment_vote!(tag_id, at_time) + redis.zincrby(KEY, (2**((at_time.to_i - epoch) / HALF_LIFE)).to_f, tag_id.to_s) + redis.zremrangebyrank(KEY, 0, -MAX_ITEMS) if rand < (2.to_f / MAX_ITEMS) + end + + def increment_historical_use!(tag_id, at_time) + key = "activity:tags:#{tag_id}:#{at_time.beginning_of_day.to_i}" + redis.incrby(key, 1) + redis.expire(key, EXPIRE_HISTORY_AFTER) + end + + def increment_unique_use!(tag_id, account_id, at_time) + key = "activity:tags:#{tag_id}:#{at_time.beginning_of_day.to_i}:accounts" + redis.pfadd(key, account_id) + redis.expire(key, EXPIRE_HISTORY_AFTER) + end + + # The epoch needs to be 2.5 years in the future if the half-life is one day + # While dynamic, it will always be the same within one year + def epoch + @epoch ||= Date.new(Date.current.year + 2.5, 10, 1).to_datetime.to_i + end + + def disallowed_hashtags + return @disallowed_hashtags if defined?(@disallowed_hashtags) + + @disallowed_hashtags = Setting.disallowed_hashtags.nil? ? [] : Setting.disallowed_hashtags + @disallowed_hashtags = @disallowed_hashtags.split(' ') if @disallowed_hashtags.is_a? String + @disallowed_hashtags = @disallowed_hashtags.map(&:downcase) + end + + def redis + Redis.current + end + end +end |