autocomplete: switch to word-based tag matching.

Switch autocomplete to match individual words in the tag, instead of
only matching the start of the tag.

For example, "hair" matches any tag containing the word "hair", not just tags
starting with "hair". "long_hair" matches all tags containing the words "long"
and "hair", which includes "very_long_hair" and "absurdly_long_hair".

Words can be in any order and words can be left out. So "closed_eye" matches
"one_eye_closed". "asuka_langley_souryuu" matches "souryuu_asuka_langley".

This has several advantages:

* You can search characters by first name. For example, "miku" matches "hatsune_miku".
  "zelda" matches both "princess_zelda" and "the_legend_of_zelda".
* You can find the right tag even if you get the word order wrong, or forget a word.
  For example, "eyes_closed" matches "closed_eyes". "hair_over_eye" matches "hair_over_one_eye".
* You can find more related tags. For example, searching "skirt" shows all tags
  containing the word "skirt", not just tags starting with "skirt".

The downside is this may break muscle memory by changing the autocomplete order of
some tags. This is an acceptable trade-off.

You can get the old behavior by writing a "*" at the end of the tag. For
example, searching "skirt*" gives the same results as before.
This commit is contained in:
evazion
2022-09-01 17:41:54 -05:00
parent ec382357b8
commit f8e4e5724f
3 changed files with 122 additions and 15 deletions

View File

@@ -279,6 +279,24 @@ class Tag < ApplicationRecord
def parsable_into_words?(name)
name.match?(/[a-zA-Z0-9]{2}/)
end
# True if the `string` contains all the words in the `query`.
#
# Tag.includes_all_words?("holding_hands", ["hand*", "hold*"]) => true
def includes_all_words?(string, query)
words = parse_words(string)
query.all? { |pattern| words.any? { |word| word.ilike?(pattern) }}
end
# Parse a string into a query for performing a word-based search.
#
# Tag.parse_query("holding_hand") => ["holding", "hand*"]
# Tag.parse_query("looking_at_") => ["looking", "at"]
def parse_query(string)
query = parse_words(string)
query[-1] += "*" unless string.match?(/[#{WORD_DELIMITERS}]\z/)
query
end
end
end
@@ -452,6 +470,19 @@ class Tag < ApplicationRecord
end
end
# If this tag has aliases, find the shortest alias matching the given pattern.
def tag_alias_for_word_pattern(query)
query = Tag.parse_query(query)
aliases = consequent_aliases.sort_by { |ca| [ca.antecedent_name.size, ca.antecedent_name] }
aliases.find do |tag_alias|
name_matches = Tag.includes_all_words?(name, query)
antecedent_matches = Tag.includes_all_words?(tag_alias.antecedent_name, query)
antecedent_matches && !name_matches
end
end
def is_aliased?
aliased_tag.present?
end