2019-05-02 18:17:27 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2017-07-13 13:34:31 -04:00
|
|
|
class SearchLog < ActiveRecord::Base
|
2024-07-08 01:58:20 -04:00
|
|
|
MAXIMUM_USER_AGENT_LENGTH = 2000
|
|
|
|
|
2018-05-21 15:22:11 -04:00
|
|
|
validates_presence_of :term
|
2024-07-08 01:58:20 -04:00
|
|
|
validates :user_agent, length: { maximum: MAXIMUM_USER_AGENT_LENGTH }
|
2017-07-13 13:34:31 -04:00
|
|
|
|
2019-03-28 23:50:25 -04:00
|
|
|
belongs_to :user
|
|
|
|
|
2018-12-18 08:43:46 -05:00
|
|
|
def ctr
|
|
|
|
return 0 if click_through == 0 || searches == 0
|
|
|
|
|
|
|
|
((click_through.to_f / searches.to_f) * 100).ceil(1)
|
|
|
|
end
|
|
|
|
|
2017-07-13 13:34:31 -04:00
|
|
|
def self.search_types
|
|
|
|
@search_types ||= Enum.new(header: 1, full_page: 2)
|
|
|
|
end
|
|
|
|
|
2017-11-28 12:54:27 -05:00
|
|
|
def self.search_result_types
|
|
|
|
@search_result_types ||= Enum.new(topic: 1, user: 2, category: 3, tag: 4)
|
|
|
|
end
|
|
|
|
|
2018-01-14 22:48:28 -05:00
|
|
|
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!
|
2019-12-03 04:05:53 -05:00
|
|
|
Discourse.redis.keys("__SEARCH__LOG_*").each { |k| Discourse.redis.del(k) }
|
2018-01-14 22:48:28 -05:00
|
|
|
end
|
|
|
|
|
2024-07-05 15:05:00 -04:00
|
|
|
def self.log(term:, search_type:, ip_address:, user_agent: nil, user_id: nil)
|
2018-01-16 19:02:31 -05:00
|
|
|
return [:error] if term.blank?
|
|
|
|
|
2024-08-08 13:41:10 -04:00
|
|
|
can_log_search =
|
|
|
|
DiscoursePluginRegistry.apply_modifier(:search_log_can_log, term: term, user_id: user_id)
|
|
|
|
return if !can_log_search
|
|
|
|
|
2017-07-17 11:57:13 -04:00
|
|
|
search_type = search_types[search_type]
|
2024-05-27 06:27:13 -04:00
|
|
|
return [:error] if search_type.blank? || ip_address.blank?
|
2017-07-17 11:57:13 -04:00
|
|
|
|
2018-05-21 14:48:06 -04:00
|
|
|
ip_address = nil if user_id
|
2018-01-14 22:48:28 -05:00
|
|
|
key = redis_key(user_id: user_id, ip_address: ip_address)
|
2017-07-13 13:34:31 -04:00
|
|
|
|
2024-07-08 01:58:20 -04:00
|
|
|
if user_agent && user_agent.length > MAXIMUM_USER_AGENT_LENGTH
|
|
|
|
user_agent = user_agent.truncate(MAXIMUM_USER_AGENT_LENGTH, omission: "")
|
|
|
|
end
|
2024-07-05 15:05:00 -04:00
|
|
|
|
2018-01-14 22:48:28 -05:00
|
|
|
result = nil
|
|
|
|
|
2019-12-03 04:05:53 -05:00
|
|
|
if existing = Discourse.redis.get(key)
|
2018-01-14 22:48:28 -05:00
|
|
|
id, old_term = existing.split(",", 2)
|
2018-04-22 22:00:37 -04:00
|
|
|
|
2018-01-14 22:48:28 -05:00
|
|
|
if term.start_with?(old_term)
|
|
|
|
where(id: id.to_i).update_all(created_at: Time.zone.now, term: term)
|
2018-04-22 22:00:37 -04:00
|
|
|
|
2018-01-14 22:48:28 -05:00
|
|
|
result = [:updated, id.to_i]
|
|
|
|
end
|
|
|
|
end
|
2017-07-13 13:34:31 -04:00
|
|
|
|
2018-01-14 22:48:28 -05:00
|
|
|
if !result
|
2018-04-22 22:00:37 -04:00
|
|
|
log =
|
2024-07-05 15:05:00 -04:00
|
|
|
self.create!(
|
|
|
|
term: term,
|
|
|
|
search_type: search_type,
|
|
|
|
ip_address: ip_address,
|
|
|
|
user_agent: user_agent,
|
|
|
|
user_id: user_id,
|
|
|
|
)
|
2018-04-22 22:00:37 -04:00
|
|
|
|
2018-01-14 22:48:28 -05:00
|
|
|
result = [:created, log.id]
|
2017-07-13 13:34:31 -04:00
|
|
|
end
|
2018-01-14 22:48:28 -05:00
|
|
|
|
2019-12-03 04:05:53 -05:00
|
|
|
Discourse.redis.setex(key, 5, "#{result[1]},#{term}")
|
2018-01-14 22:48:28 -05:00
|
|
|
|
|
|
|
result
|
2017-07-13 13:34:31 -04:00
|
|
|
end
|
2017-07-14 14:29:31 -04:00
|
|
|
|
2017-12-19 21:41:31 -05:00
|
|
|
def self.term_details(term, period = :weekly, search_type = :all)
|
|
|
|
details = []
|
|
|
|
|
|
|
|
result =
|
|
|
|
SearchLog.select("COUNT(*) AS count, created_at::date AS date").where(
|
2019-03-28 23:30:15 -04:00
|
|
|
"lower(term) = ? AND created_at > ?",
|
|
|
|
term.downcase,
|
|
|
|
start_of(period),
|
|
|
|
)
|
2017-12-19 21:41:31 -05:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2019-03-28 23:30:15 -04:00
|
|
|
result
|
2017-12-19 21:41:31 -05:00
|
|
|
.order("date")
|
|
|
|
.group("date")
|
|
|
|
.each { |record| details << { x: Date.parse(record["date"].to_s), y: record["count"] } }
|
|
|
|
|
2019-11-14 15:10:51 -05:00
|
|
|
{
|
2017-12-19 21:41:31 -05:00
|
|
|
type: "search_log_term",
|
|
|
|
title: I18n.t("search_logs.graph_title"),
|
2018-01-12 21:43:49 -05:00
|
|
|
start_date: start_of(period),
|
|
|
|
end_date: Time.zone.now,
|
|
|
|
data: details,
|
|
|
|
period: period.to_s,
|
2017-12-19 21:41:31 -05:00
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2017-11-28 12:54:27 -05:00
|
|
|
def self.trending(period = :all, search_type = :all)
|
2018-09-07 10:54:38 -04:00
|
|
|
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
|
2017-11-28 12:54:27 -05:00
|
|
|
WHEN search_result_id IS NOT NULL THEN 1
|
|
|
|
ELSE 0
|
2018-12-18 16:15:48 -05:00
|
|
|
END) AS click_through
|
2018-09-07 10:54:38 -04:00
|
|
|
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
|
2017-11-28 12:54:27 -05:00
|
|
|
|
2018-12-18 16:15:48 -05:00
|
|
|
result.group("lower(term)").order("searches DESC, click_through DESC, term ASC").limit(limit)
|
2017-11-14 19:13:50 -05:00
|
|
|
end
|
|
|
|
|
2017-07-14 14:29:31 -04:00
|
|
|
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?
|
2018-05-21 14:56:51 -04:00
|
|
|
SearchLog.where(
|
|
|
|
"created_at < TIMESTAMP ?",
|
|
|
|
SiteSetting.search_query_log_max_retention_days.days.ago,
|
|
|
|
).delete_all
|
2017-07-14 14:29:31 -04:00
|
|
|
end
|
2019-03-28 23:50:25 -04:00
|
|
|
|
|
|
|
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
|
2017-07-13 13:34:31 -04:00
|
|
|
end
|
2017-04-26 14:47:36 -04:00
|
|
|
|
|
|
|
# == Schema Information
|
|
|
|
#
|
|
|
|
# Table name: search_logs
|
|
|
|
#
|
2017-12-05 10:29:14 -05:00
|
|
|
# id :integer not null, primary key
|
|
|
|
# term :string not null
|
|
|
|
# user_id :integer
|
2018-05-21 14:48:06 -04:00
|
|
|
# ip_address :inet
|
2017-12-05 10:29:14 -05:00
|
|
|
# search_result_id :integer
|
|
|
|
# search_type :integer not null
|
|
|
|
# created_at :datetime not null
|
|
|
|
# search_result_type :integer
|
2024-07-05 15:05:00 -04:00
|
|
|
# user_agent :string(2000)
|
2017-04-26 14:47:36 -04:00
|
|
|
#
|
2019-04-02 01:17:55 -04:00
|
|
|
# Indexes
|
|
|
|
#
|
2021-11-25 15:44:15 -05:00
|
|
|
# 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)
|
2019-04-02 01:17:55 -04:00
|
|
|
#
|