diff --git a/app/logical/autocomplete_service.rb b/app/logical/autocomplete_service.rb index 0c5d39f93..f75101098 100644 --- a/app/logical/autocomplete_service.rb +++ b/app/logical/autocomplete_service.rb @@ -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) diff --git a/app/logical/concerns/searchable.rb b/app/logical/concerns/searchable.rb index cac920cc8..a83a9c83a 100644 --- a/app/logical/concerns/searchable.rb +++ b/app/logical/concerns/searchable.rb @@ -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 diff --git a/app/models/tag.rb b/app/models/tag.rb index 27255bed9..527f8a797 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -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) diff --git a/app/models/tag_alias.rb b/app/models/tag_alias.rb index 9c460e27a..b3cb9c4f2 100644 --- a/app/models/tag_alias.rb +++ b/app/models/tag_alias.rb @@ -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 diff --git a/config/initializers/core_extensions.rb b/config/initializers/core_extensions.rb index 61f85f9d0..ef7dba48d 100644 --- a/config/initializers/core_extensions.rb +++ b/config/initializers/core_extensions.rb @@ -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 diff --git a/test/unit/autocomplete_service_test.rb b/test/unit/autocomplete_service_test.rb index 1abe96f80..e0463f36e 100644 --- a/test/unit/autocomplete_service_test.rb +++ b/test/unit/autocomplete_service_test.rb @@ -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 diff --git a/test/unit/post_query_builder_test.rb b/test/unit/post_query_builder_test.rb index 193cbbad5..f4882c287 100644 --- a/test/unit/post_query_builder_test.rb +++ b/test/unit/post_query_builder_test.rb @@ -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") diff --git a/test/unit/post_test.rb b/test/unit/post_test.rb index d08737624..ee470de05 100644 --- a/test/unit/post_test.rb +++ b/test/unit/post_test.rb @@ -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 diff --git a/test/unit/tag_alias_test.rb b/test/unit/tag_alias_test.rb index 1aeb005b9..4b5a44bc8 100644 --- a/test/unit/tag_alias_test.rb +++ b/test/unit/tag_alias_test.rb @@ -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)