tags: allow tag abbreviations in searches and during tagging.
Expand the tag abbreviation system introduced in b0be8ae45 so that it
works in searches and when tagging posts, not just in autocomplete.
For example, you can tag a post with /evth and it will add the tag
eyebrows_visible_through_hair. You can search for /evth and it will
search for the tag eyebrows_visible_through_hair.
Some more examples:
* /ops is short for one-piece_swimsuit
* /hooe is short for hair_over_one_eye
* /saol is short for standing_on_one_leg
* /tlozbotw is short for the_legend_of_zelda:_breath_of_the_wild
If two tags have the same abbreviation, then the larger tag takes
precedence. For example, /be is short for blue_eyes, not brown_eyes,
because blue_eyes is the bigger tag.
If there is an existing shortcut alias that conflicts with the
abbreviation, then the alias take precedence. For example, /sh is short
for suzumiya_haruhi, not short_hair, because there's an old alias for
/sh -> suzumiya_haruhi.
This commit is contained in:
@@ -67,9 +67,13 @@ class AutocompleteService
|
||||
def autocomplete_tag(string)
|
||||
if string.starts_with?("/")
|
||||
string = string + "*" unless string.include?("*")
|
||||
|
||||
results = tag_matches(string)
|
||||
results += tag_abbreviation_matches(string)
|
||||
results = results.sort_by { |r| [r[:antecedent].to_s.size, -r[:post_count]] }
|
||||
results = results.sort_by do |r|
|
||||
[r[:type] == "tag-alias" ? 0 : 1, r[:antecedent].to_s.size, -r[:post_count]]
|
||||
end
|
||||
|
||||
results = results.uniq { |r| r[:value] }.take(limit)
|
||||
elsif string.include?("*")
|
||||
results = tag_matches(string)
|
||||
|
||||
@@ -53,7 +53,7 @@ module Searchable
|
||||
end
|
||||
|
||||
def where_iequals(attr, value)
|
||||
where_ilike(attr, value.gsub(/\\/, '\\\\').gsub(/\*/, '\*'))
|
||||
where_ilike(attr, value.escape_wildcards)
|
||||
end
|
||||
|
||||
# https://www.postgresql.org/docs/current/static/functions-matching.html#FUNCTIONS-POSIX-REGEXP
|
||||
|
||||
@@ -270,6 +270,11 @@ class Tag < ApplicationRecord
|
||||
where("regexp_replace(tags.name, ?, '\\1', 'g') LIKE ?", ABBREVIATION_REGEXP.source, abbrev.to_escaped_for_sql_like)
|
||||
end
|
||||
|
||||
def find_by_abbreviation(abbrev)
|
||||
abbrev = abbrev.delete_prefix("/")
|
||||
abbreviation_matches(abbrev.escape_wildcards).order(post_count: :desc).first
|
||||
end
|
||||
|
||||
def search(params)
|
||||
q = search_attributes(params, :id, :created_at, :updated_at, :is_locked, :category, :post_count, :name, :wiki_page, :artist, :antecedent_alias, :consequent_aliases, :antecedent_implications, :consequent_implications, :dtext_links)
|
||||
|
||||
|
||||
@@ -7,7 +7,15 @@ class TagAlias < TagRelationship
|
||||
def self.to_aliased(names)
|
||||
names = Array(names).map(&:to_s)
|
||||
return [] if names.empty?
|
||||
|
||||
aliases = active.where(antecedent_name: names).map { |ta| [ta.antecedent_name, ta.consequent_name] }.to_h
|
||||
|
||||
abbreviations = names.select { |name| name.starts_with?("/") && !aliases.has_key?(name) }
|
||||
abbreviations.each do |abbrev|
|
||||
tag = Tag.nonempty.find_by_abbreviation(abbrev)
|
||||
aliases[abbrev] = tag.name if tag.present?
|
||||
end
|
||||
|
||||
names.map { |name| aliases[name] || name }
|
||||
end
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@ module Danbooru
|
||||
string
|
||||
end
|
||||
|
||||
# escape \ and * characters so that they're treated literally in LIKE searches.
|
||||
def escape_wildcards
|
||||
gsub(/\\/, '\\\\').gsub(/\*/, '\*')
|
||||
end
|
||||
|
||||
def to_escaped_for_tsquery_split
|
||||
scan(/\S+/).map {|x| x.to_escaped_for_tsquery}.join(" & ")
|
||||
end
|
||||
|
||||
@@ -97,6 +97,14 @@ class AutocompleteServiceTest < ActiveSupport::TestCase
|
||||
assert_autocomplete_includes("mole_under_eye", "-/mue", :tag_query)
|
||||
assert_autocomplete_includes("mole_under_eye", "~/mue", :tag_query)
|
||||
end
|
||||
|
||||
should "list aliases before abbreviations" do
|
||||
create(:tag, name: "hair_ribbon", post_count: 300_000)
|
||||
create(:tag, name: "hakurei_reimu", post_count: 50_000)
|
||||
create(:tag_alias, antecedent_name: "/hr", consequent_name: "hakurei_reimu")
|
||||
|
||||
assert_autocomplete_equals(%w[hakurei_reimu hair_ribbon], "/hr", :tag_query)
|
||||
end
|
||||
end
|
||||
|
||||
should "autocomplete tags from wiki and artist other names" do
|
||||
|
||||
@@ -1018,6 +1018,17 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
|
||||
assert_tag_match([post2], "-kitten")
|
||||
end
|
||||
|
||||
should "resolve abbreviations to the actual tag" do
|
||||
tag1 = create(:tag, name: "hair_ribbon", post_count: 300_000)
|
||||
tag2 = create(:tag, name: "hakurei_reimu", post_count: 50_000)
|
||||
ta1 = create(:tag_alias, antecedent_name: "/hr", consequent_name: "hakurei_reimu")
|
||||
post1 = create(:post, tag_string: "hair_ribbon")
|
||||
post2 = create(:post, tag_string: "hakurei_reimu")
|
||||
|
||||
assert_tag_match([post2], "/hr")
|
||||
assert_tag_match([post1], "-/hr")
|
||||
end
|
||||
|
||||
should "fail for more than 6 tags" do
|
||||
post1 = create(:post, rating: "s")
|
||||
|
||||
|
||||
@@ -598,6 +598,10 @@ class PostTest < ActiveSupport::TestCase
|
||||
context "tagged with a valid tag" do
|
||||
subject { @post }
|
||||
|
||||
setup do
|
||||
create(:tag, name: "hakurei_reimu")
|
||||
end
|
||||
|
||||
should allow_value("touhou 100%").for(:tag_string)
|
||||
should allow_value("touhou FOO").for(:tag_string)
|
||||
should allow_value("touhou -foo").for(:tag_string)
|
||||
@@ -618,6 +622,8 @@ class PostTest < ActiveSupport::TestCase
|
||||
# \u3000 = ideographic space, \u00A0 = no-break space
|
||||
should allow_value("touhou\u3000foo").for(:tag_string)
|
||||
should allow_value("touhou\u00A0foo").for(:tag_string)
|
||||
|
||||
should allow_value("/hr").for(:tag_string)
|
||||
end
|
||||
|
||||
context "tagged with an invalid tag" do
|
||||
@@ -661,6 +667,16 @@ class PostTest < ActiveSupport::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
context "tagged with an abbreviation" do
|
||||
should "expand the abbreviation" do
|
||||
create(:tag, name: "hair_ribbon", post_count: 300_000)
|
||||
create(:tag, name: "hakurei_reimu", post_count: 50_000)
|
||||
|
||||
@post.update!(tag_string: "aaa /hr")
|
||||
assert_equal("aaa hair_ribbon", @post.reload.tag_string)
|
||||
end
|
||||
end
|
||||
|
||||
context "tagged with a metatag" do
|
||||
context "for typing a tag" do
|
||||
setup do
|
||||
@@ -1190,6 +1206,17 @@ class PostTest < ActiveSupport::TestCase
|
||||
|
||||
assert_equal("aaa", @post.tag_string)
|
||||
end
|
||||
|
||||
should "resolve abbreviations" do
|
||||
create(:tag, name: "hair_ribbon", post_count: 300_000)
|
||||
create(:tag, name: "hakurei_reimu", post_count: 50_000)
|
||||
|
||||
@post.update!(tag_string: "aaa hair_ribbon hakurei_reimu")
|
||||
assert_equal("aaa hair_ribbon hakurei_reimu", @post.reload.tag_string)
|
||||
|
||||
@post.update!(tag_string: "aaa hair_ribbon hakurei_reimu -/hr")
|
||||
assert_equal("aaa hakurei_reimu", @post.reload.tag_string)
|
||||
end
|
||||
end
|
||||
|
||||
context "tagged with animated_gif or animated_png" do
|
||||
|
||||
@@ -79,6 +79,18 @@ class TagAliasTest < ActiveSupport::TestCase
|
||||
assert_equal(["bbb", "bbb"], TagAlias.to_aliased(["aaa", "aaa"]))
|
||||
end
|
||||
|
||||
should "handle abbreviations in TagAlias.to_aliased" do
|
||||
create(:tag, name: "hair_ribbon", post_count: 300_000)
|
||||
create(:tag, name: "hakurei_reimu", post_count: 50_000)
|
||||
create(:tag, name: "kirisama_marisa", post_count: 50_000)
|
||||
create(:tag, name: "kaname_madoka", post_count: 20_000)
|
||||
create(:tag_alias, antecedent_name: "/hr", consequent_name: "hakurei_reimu")
|
||||
|
||||
assert_equal(["hakurei_reimu"], TagAlias.to_aliased(["/hr"]))
|
||||
assert_equal(["kirisama_marisa"], TagAlias.to_aliased(["/km"]))
|
||||
assert_equal(["hakurei_reimu", "kirisama_marisa"], TagAlias.to_aliased(["/hr", "/km"]))
|
||||
end
|
||||
|
||||
context "saved searches" do
|
||||
should "move saved searches" do
|
||||
@ss1 = create(:saved_search, query: "123 ... 456", user: CurrentUser.user)
|
||||
|
||||
Reference in New Issue
Block a user