autocomplete: rework tag autocomplete behavior.
Reworks tag autocomplete to work the same way for all users. Previously autocomplete for Builders worked differently than autocomplete for regular users. This is how it works now: * If the search starts with a slash (/), then do a tag abbreviation match. For example, `/evth` matches eyebrows_visible_through_hair. * Otherwise if the search contains a wildcard (*), then just do a simple wildcard search. * Otherwise do a tag prefix match against tags and aliases. For example, `black` matches all tags or aliases beginning with `black`. * If the tag prefix match returns no results, then do a autocorrect match. The differences for regular users: * You can abbreviate tags with a slash (/). The differences for Builders: * Now tag abbreviations have to start with a slash (/). * Autocorrect isn't performed unless a regular search returns no results. * Results are always sorted by tag count. Before different types of results (regular tag matches, alias matches, abbreviation matches, and autocorrect matches) were all mixed together based on a tag weighting scheme.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user