discourse/app/models/search_log.rb
2023-01-09 14:14:59 +00:00

176 lines
4.5 KiB
Ruby

# frozen_string_literal: true
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!
Discourse.redis.keys("__SEARCH__LOG_*").each { |k| Discourse.redis.del(k) }
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 = Discourse.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
Discourse.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 { |record| details << { x: Date.parse(record["date"].to_s), y: record["count"] } }
{
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)
result = result.where("created_at < ?", end_date) if end_date
result = result.where("search_type = ?", search_types[search_type]) unless search_type == :all
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)
SearchLog.where("id < ?", search_id[0]).delete_all if search_id.present?
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
1.year.ago
when :monthly
1.month.ago
when :quarterly
3.months.ago
when :weekly
1.week.ago
when :daily
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)
# index_search_logs_on_user_id_and_created_at (user_id,created_at) WHERE (user_id IS NOT NULL)
#