require_dependency 'enum' class SearchLog < ActiveRecord::Base validates_presence_of :term 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('term LIKE ?', term) .where('created_at > ?', 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.group(:term) .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, COUNT(DISTINCT ip_address) AS unique_searches 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 = result.group('lower(term)') .order('unique_searches DESC, click_through ASC, term ASC') .limit(limit).to_a end def self.start_of(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 1.day.ago else 1000.years.ago end 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 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 #