Files
danbooru/app/models/saved_search.rb
evazion f0299a8945 aliases: refactor tag moving code.
* Factor out the code for moving tags from tag aliases to a separate
  TagMover class.

* When aliasing two tags that have conflicting wikis, merge the old wiki
  into the new one instead of failing with an error. Merge the other names
  fields, replace the old wiki body with a message linking to the new
  wiki, and mark the old wiki as deleted.

* When aliasing two tags that have conflicting artist entries, merge the
  old artist into the new one instead of silently ignore the conflict.
  Merge the group name, other names, and urls fields, and mark the old
  artist as deleted.

* When two tags have conflicting wikis or artist entries, but the old
  wiki or artist entry is deleted, then just ignore the old wiki or
  artist and don't try to merge it.

* Fix it so that when saved searches are rewritten, we rewrite negated
  searches too.
2020-08-26 17:05:41 -05:00

187 lines
4.7 KiB
Ruby

class SavedSearch < ApplicationRecord
REDIS_EXPIRY = 1.hour
QUERY_LIMIT = 1000
concerning :Redis do
extend Memoist
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)
queries = queries_for(user_id, label: label)
post_ids = Set.new
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)
else
PopulateSavedSearchJob.perform_later(query)
end
end
post_ids.to_a.sort.last(QUERY_LIMIT)
end
end
def refreshed_at
ttl = SavedSearch.redis.ttl("search:#{normalized_query}")
return nil if ttl < 0
(REDIS_EXPIRY.to_i - ttl).seconds.ago
end
memoize :refreshed_at
def cached_size
SavedSearch.redis.scard("search:#{normalized_query}")
end
memoize :cached_size
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(&: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 search(params)
q = super
q = q.search_attributes(params, :query)
if params[:label]
q = q.labeled(params[:label])
end
case params[:order]
when "query"
q = q.order(:query).order(id: :desc)
when "label"
q = q.order(:labels).order(id: :desc)
else
q = q.apply_default_order(params)
end
q
end
def populate(query, timeout: 10_000)
redis_key = "search:#{query}"
return if redis.exists?(redis_key)
post_ids = Post.with_timeout(timeout, [], query: query) do
Post.system_tag_match(query).limit(QUERY_LIMIT).pluck(:id)
end
if post_ids.present?
redis.sadd(redis_key, post_ids)
redis.expire(redis_key, REDIS_EXPIRY.to_i)
end
end
end
end
concerning :Queries do
class_methods do
def queries_for(user_id, label: nil, options: {})
searches = SavedSearch.where(user_id: user_id)
searches = searches.labeled(label) if label.present?
queries = searches.map(&:normalized_query)
queries.sort.uniq
end
def rewrite_queries!(old_name, new_name)
has_tag(old_name).find_each do |ss|
ss.lock!
ss.rewrite_query(old_name, new_name)
ss.save!
end
end
end
def normalized_query
PostQueryBuilder.new(query).normalized_query.to_s
end
def normalize_query
self.query = PostQueryBuilder.new(query).normalized_query(sort: false).to_s
end
def rewrite_query(old_name, new_name)
self.query.gsub!(/(?:\A| )([-~])?#{Regexp.escape(old_name)}(?: |\z)/i) { " #{$1}#{new_name} " }
self.query.strip!
end
end
attr_reader :disable_labels
belongs_to :user
validates :query, presence: true
validate :validate_count
before_validation :normalize_query
before_validation :normalize_labels
scope :labeled, ->(label) { where_array_includes_any_lower(:labels, [normalize_label(label)]) }
scope :has_tag, ->(name) { where_regex(:query, "(^| )[~-]?#{Regexp.escape(name)}( |$)", flags: "i") }
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 disable_labels=(value)
user.update(disable_categorized_saved_searches: true) if value.to_s.truthy?
end
def self.available_includes
[:user]
end
end