Files
danbooru/app/models/saved_search.rb
evazion 2bbdc5d143 jobs: migrate saved searches to ActiveJob.
* Fix tests to run the searches for real instead of mocking everything out.

* Fix SavedSearch.populate to only use the read only database in
  production because in breaks things in tests. Specifically:
  the posts get created in one db connection but searched for in
  another, but the second transaction doesn't see the uncommitted posts
  in the first transaction, so the search doesn't work.
2019-08-16 20:49:35 -05:00

164 lines
4.1 KiB
Ruby

class SavedSearch < ApplicationRecord
REDIS_EXPIRY = 3600
QUERY_LIMIT = 1000
def self.enabled?
Danbooru.config.redis_url.present?
end
concerning :Redis do
class_methods do
extend Memoist
def redis
::Redis.new(url: Danbooru.config.redis_url)
end
memoize :redis
def post_ids_for(user_id, label: nil)
label = normalize_label(label) if label
queries = queries_for(user_id, label: label)
post_ids = Set.new
update_count = 0
queries.each do |query|
redis_key = "search:#{query}"
if redis.exists(redis_key)
sub_ids = redis.smembers(redis_key).map(&:to_i)
post_ids.merge(sub_ids)
redis.expire(redis_key, REDIS_EXPIRY)
elsif CurrentUser.is_gold? && update_count < 5
SavedSearch.populate(query)
sub_ids = redis.smembers(redis_key).map(&:to_i)
post_ids.merge(sub_ids)
update_count += 1
else
PopulateSavedSearchJob.perform_later(query)
end
end
post_ids.to_a.sort.last(QUERY_LIMIT)
end
end
end
concerning :Labels do
class_methods do
def normalize_label(label)
label.
to_s.
strip.
downcase.
gsub(/[[:space:]]/, "_")
end
def search_labels(user_id, params)
labels = labels_for(user_id)
if params[:label].present?
query = Regexp.escape(params[:label]).gsub("\\*", ".*")
query = ".*#{query}.*" unless query.include?("*")
query = /\A#{query}\z/
labels = labels.grep(query)
end
labels
end
def labels_for(user_id)
SavedSearch.
where(user_id: user_id).
order("label").
pluck(Arel.sql("distinct unnest(labels) as label"))
end
end
def normalize_labels
self.labels = labels.
map {|x| SavedSearch.normalize_label(x)}.
reject {|x| x.blank?}
end
def label_string
labels.join(" ")
end
def label_string=(val)
self.labels = val.to_s.split(/[[:space:]]+/)
end
def labels=(labels)
labels = labels.map { |label| SavedSearch.normalize_label(label) }
super(labels)
end
end
concerning :Search do
class_methods do
def populate(query)
CurrentUser.as_system do
redis_key = "search:#{query}"
return if redis.exists(redis_key)
post_ids = Post.tag_match(query, read_only: Rails.env.production?).limit(QUERY_LIMIT).pluck(:id)
redis.sadd(redis_key, post_ids)
redis.expire(redis_key, REDIS_EXPIRY)
end
rescue Exception
# swallow
end
end
end
concerning :Queries do
class_methods do
def queries_for(user_id, label: nil, options: {})
SavedSearch.
where(user_id: user_id).
labeled(label).
pluck(:query).
map {|x| Tag.normalize_query(x, sort: true)}.
sort.
uniq
end
end
def normalized_query
Tag.normalize_query(query, sort: true)
end
def normalize_query
self.query = Tag.normalize_query(query, sort: false)
end
end
attr_accessor :disable_labels
belongs_to :user
validates :query, presence: true
validate :validate_count
before_create :update_user_on_create
after_destroy :update_user_on_destroy
before_validation :normalize_query
before_validation :normalize_labels
scope :labeled, ->(label) { label.present? ? where("labels @> string_to_array(?, '~~~~')", label) : where("true") }
def validate_count
if user.saved_searches.count + 1 > user.max_saved_searches
self.errors[:user] << "can only have up to #{user.max_saved_searches} " + "saved search".pluralize(user.max_saved_searches)
end
end
def update_user_on_create
if !user.has_saved_searches?
user.update(has_saved_searches: true)
end
end
def update_user_on_destroy
if user.saved_searches.count == 0
user.update(has_saved_searches: false)
end
end
def disable_labels=(value)
CurrentUser.update(disable_categorized_saved_searches: true) if value.to_s.truthy?
end
end