discourse/app/models/search_log.rb
Sam Saffron 30990006a9 DEV: enable frozen string literal on all files
This reduces chances of errors where consumers of strings mutate inputs
and reduces memory usage of the app.

Test suite passes now, but there may be some stuff left, so we will run
a few sites on a branch prior to merging
2019-05-13 09:31:32 +08:00

194 lines
4.5 KiB
Ruby

# frozen_string_literal: true
require_dependency 'enum'
class SearchLog < ActiveRecord::Base
validates_presence_of :term
belongs_to :user
attr_reader :ctr
def ctr
return 0 if click_through == 0 || searches == 0
((click_through.to_f / searches.to_f) * 100).ceil(1)
end
def self.search_types
@search_types ||= Enum.new(
header: 1,
full_page: 2
)
end
def self.search_result_types
@search_result_types ||= Enum.new(
topic: 1,
user: 2,
category: 3,
tag: 4
)
end
def self.redis_key(ip_address:, user_id: nil)
if user_id
"__SEARCH__LOG_#{user_id}"
else
"__SEARCH__LOG_#{ip_address}"
end
end
# for testing
def self.clear_debounce_cache!
$redis.keys("__SEARCH__LOG_*").each do |k|
$redis.del(k)
end
end
def self.log(term:, search_type:, ip_address:, user_id: nil)
return [:error] if term.blank?
search_type = search_types[search_type]
return [:error] unless search_type.present? && ip_address.present?
ip_address = nil if user_id
key = redis_key(user_id: user_id, ip_address: ip_address)
result = nil
if existing = $redis.get(key)
id, old_term = existing.split(",", 2)
if term.start_with?(old_term)
where(id: id.to_i).update_all(
created_at: Time.zone.now,
term: term
)
result = [:updated, id.to_i]
end
end
if !result
log = self.create!(
term: term,
search_type: search_type,
ip_address: ip_address,
user_id: user_id
)
result = [:created, log.id]
end
$redis.setex(key, 5, "#{result[1]},#{term}")
result
end
def self.term_details(term, period = :weekly, search_type = :all)
details = []
result = SearchLog.select("COUNT(*) AS count, created_at::date AS date")
.where(
'lower(term) = ? AND created_at > ?',
term.downcase, start_of(period)
)
result = result.where('search_type = ?', search_types[search_type]) if search_type == :header || search_type == :full_page
result = result.where('search_result_id IS NOT NULL') if search_type == :click_through_only
result
.order("date")
.group("date")
.each do |record|
details << { x: Date.parse(record['date'].to_s), y: record['count'] }
end
return {
type: "search_log_term",
title: I18n.t("search_logs.graph_title"),
start_date: start_of(period),
end_date: Time.zone.now,
data: details,
period: period.to_s
}
end
def self.trending(period = :all, search_type = :all)
SearchLog.trending_from(start_of(period), search_type: search_type)
end
def self.trending_from(start_date, options = {})
end_date = options[:end_date]
search_type = options[:search_type] || :all
limit = options[:limit] || 100
select_sql = <<~SQL
lower(term) term,
COUNT(*) AS searches,
SUM(CASE
WHEN search_result_id IS NOT NULL THEN 1
ELSE 0
END) AS click_through
SQL
result = SearchLog.select(select_sql)
.where('created_at > ?', start_date)
if end_date
result = result.where('created_at < ?', end_date)
end
unless search_type == :all
result = result.where('search_type = ?', search_types[search_type])
end
result.group('lower(term)')
.order('searches DESC, click_through DESC, term ASC')
.limit(limit)
end
def self.clean_up
search_id = SearchLog.order(:id).offset(SiteSetting.search_query_log_max_size).limit(1).pluck(:id)
if search_id.present?
SearchLog.where('id < ?', search_id[0]).delete_all
end
SearchLog.where('created_at < TIMESTAMP ?', SiteSetting.search_query_log_max_retention_days.days.ago).delete_all
end
def self.start_of(period)
period =
case period
when :yearly then 1.year.ago
when :monthly then 1.month.ago
when :quarterly then 3.months.ago
when :weekly then 1.week.ago
when :daily then Time.zone.now
else 1000.years.ago
end
period&.to_date
end
private_class_method :start_of
end
# == Schema Information
#
# Table name: search_logs
#
# id :integer not null, primary key
# term :string not null
# user_id :integer
# ip_address :inet
# search_result_id :integer
# search_type :integer not null
# created_at :datetime not null
# search_result_type :integer
#
# Indexes
#
# index_search_logs_on_created_at (created_at)
#