diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb index 53f0e01e7..d6e8a9728 100644 --- a/app/helpers/tags_helper.rb +++ b/app/helpers/tags_helper.rb @@ -3,12 +3,4 @@ module TagsHelper return nil if tag.blank? "tag-type-#{tag.category}" end - - def tag_alias_for_pattern(tag, pattern) - return nil if pattern.blank? - - tag.consequent_aliases.find do |tag_alias| - !tag.name.ilike?(pattern) && tag_alias.antecedent_name.ilike?(pattern) - end - end end diff --git a/app/logical/autocomplete_service.rb b/app/logical/autocomplete_service.rb index 4896bf225..d05cb6696 100644 --- a/app/logical/autocomplete_service.rb +++ b/app/logical/autocomplete_service.rb @@ -63,10 +63,45 @@ class AutocompleteService end def autocomplete_tag(string) - tags = Tag.names_matches_with_aliases(string, limit) + if string.starts_with?("/") + string = string + "*" unless string.include?("*") + results = tag_matches(string) + results += tag_abbreviation_matches(string) + results = results.uniq.sort_by { |r| [r[:antecedent].length, -r[:post_count]] }.take(limit) + elsif string.include?("*") + results = tag_matches(string) + else + results = tag_matches(string + "*") + results = tag_autocorrect_matches(string) if results.blank? + end + + results + end + + def tag_matches(string) + name_matches = Tag.nonempty.name_matches(string).order(post_count: :desc).limit(limit) + alias_matches = Tag.nonempty.alias_matches(string).order(post_count: :desc).limit(limit) + union = "((#{name_matches.to_sql}) UNION (#{alias_matches.to_sql})) AS tags" + tags = Tag.from(union).order(post_count: :desc).limit(limit).includes(:consequent_aliases) tags.map do |tag| - { type: "tag", label: tag.name.tr("_", " "), value: tag.name, antecedent: tag.antecedent_name, category: tag.category, post_count: tag.post_count, source: nil, weight: nil } + { type: "tag", label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count, antecedent: tag.tag_alias_for_pattern(string)&.antecedent_name } + end + end + + def tag_abbreviation_matches(string) + tags = Tag.nonempty.abbreviation_matches(string).order(post_count: :desc).limit(limit) + + tags.map do |tag| + { type: "tag", label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count, antecedent: "/" + tag.abbreviation } + end + end + + def tag_autocorrect_matches(string) + tags = Tag.nonempty.fuzzy_name_matches(string).order_similarity(string).limit(limit) + + tags.map do |tag| + { type: "tag", label: tag.pretty_name, value: tag.name, category: tag.category, post_count: tag.post_count } end end diff --git a/app/models/tag.rb b/app/models/tag.rb index c9ca52da9..899718add 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -1,4 +1,6 @@ class Tag < ApplicationRecord + ABBREVIATION_REGEXP = /([a-z0-9])[a-z0-9']*($|[^a-z0-9']+)/ + has_one :wiki_page, :foreign_key => "title", :primary_key => "name" has_one :artist, :foreign_key => "name", :primary_key => "name" has_one :antecedent_alias, -> {active}, :class_name => "TagAlias", :foreign_key => "antecedent_name", :primary_key => "name" @@ -249,7 +251,7 @@ class Tag < ApplicationRecord end def alias_matches(name) - where(name: TagAlias.active.where_ilike(:antecedent_name, normalize_name(name)).select(:consequent_name)) + where(name: TagAlias.active.where_like(:antecedent_name, normalize_name(name)).select(:consequent_name)) end def name_or_alias_matches(name) @@ -260,6 +262,11 @@ class Tag < ApplicationRecord nonempty.name_matches(tag).order(post_count: :desc, name: :asc).limit(limit).pluck(:name) end + def abbreviation_matches(abbrev) + abbrev = abbrev.delete_prefix("/") + where("regexp_replace(tags.name, ?, '\\1', 'g') LIKE ?", ABBREVIATION_REGEXP.source, abbrev.to_escaped_for_sql_like) + end + def search(params) q = super @@ -352,6 +359,18 @@ class Tag < ApplicationRecord Post.system_tag_match(name) end + def abbreviation + name.gsub(ABBREVIATION_REGEXP, "\\1") + end + + def tag_alias_for_pattern(pattern) + return nil if pattern.blank? + + consequent_aliases.find do |tag_alias| + !name.ilike?(pattern) && tag_alias.antecedent_name.ilike?(pattern) + end + end + def self.model_restriction(table) super.where(table[:post_count].gt(0)) end diff --git a/app/views/tags/index.html.erb b/app/views/tags/index.html.erb index 8648b1466..d884a56cb 100644 --- a/app/views/tags/index.html.erb +++ b/app/views/tags/index.html.erb @@ -10,7 +10,7 @@ <%= link_to_wiki "?", tag.name, class: tag_class(tag) %> <%= link_to tag.name, posts_path(tags: tag.name), class: tag_class(tag) %> - <% tag_alias = tag_alias_for_pattern(tag, params[:search][:name_or_alias_matches]) %> + <% tag_alias = tag.tag_alias_for_pattern(params[:search][:name_or_alias_matches]) %> <% if tag_alias.present? %> ← <%= link_to tag_alias.antecedent_name, tag_alias, class: "fineprint" %> <% end %> diff --git a/test/unit/autocomplete_service_test.rb b/test/unit/autocomplete_service_test.rb index fdcf7aeb7..8379bb295 100644 --- a/test/unit/autocomplete_service_test.rb +++ b/test/unit/autocomplete_service_test.rb @@ -80,6 +80,37 @@ class AutocompleteServiceTest < ActiveSupport::TestCase assert_autocomplete_includes("touhou", "~tou", :tag_query) end + should "autocomplete tag abbreviations" do + create(:tag, name: "mole", post_count: 150) + create(:tag, name: "mole_under_eye", post_count: 100) + create(:tag, name: "mole_under_mouth", post_count: 50) + + assert_autocomplete_equals(%w[mole mole_under_eye mole_under_mouth], "/m", :tag_query) + assert_autocomplete_equals(%w[mole_under_eye mole_under_mouth], "/mu", :tag_query) + assert_autocomplete_equals(%w[mole_under_mouth], "/mum", :tag_query) + assert_autocomplete_equals(%w[mole_under_eye], "/mue", :tag_query) + assert_autocomplete_equals(%w[mole_under_eye], "/*ue", :tag_query) + + assert_autocomplete_includes("mole_under_eye", "-/mue", :tag_query) + assert_autocomplete_includes("mole_under_eye", "~/mue", :tag_query) + end + + should "autocomplete wildcard searches" do + create(:tag, name: "mole", post_count: 150) + create(:tag, name: "mole_under_eye", post_count: 100) + create(:tag, name: "mole_under_mouth", post_count: 50) + + assert_autocomplete_equals(%w[mole mole_under_eye mole_under_mouth], "mole*", :tag_query) + assert_autocomplete_equals(%w[mole_under_eye mole_under_mouth], "*under*", :tag_query) + assert_autocomplete_equals(%w[mole_under_eye], "*eye", :tag_query) + end + + should "autocorrect misspelled tags" do + create(:tag, name: "touhou") + + assert_autocomplete_equals(%w[touhou], "touhuo", :tag_query) + end + should "autocomplete static metatags" do assert_autocomplete_equals(["status:active"], "status:act", :tag_query) assert_autocomplete_equals(["parent:active"], "parent:act", :tag_query)