* 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.
164 lines
4.1 KiB
Ruby
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
|