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:
@@ -125,9 +125,24 @@ module Searchable
|
||||
where("? ~<< ANY(#{qualified_column_for(attr)})", "(?#{flags})#{regex}")
|
||||
end
|
||||
|
||||
# Perform a Postgres full-text search on an array of strings. Assumes the query is already escaped.
|
||||
# The column should have a `array_to_tsvector(column) using gin` index for best performance.
|
||||
#
|
||||
# @see https://www.postgresql.org/docs/current/datatype-textsearch.html#DATATYPE-TSQUERY
|
||||
def where_array_to_tsvector_matches(attr, query)
|
||||
where("array_to_tsvector(#{qualified_column_for(attr)}) @@ ?::tsquery", query)
|
||||
end
|
||||
|
||||
def where_any_in_array_starts_with(attr, value)
|
||||
where("array_to_tsvector(#{qualified_column_for(attr)}) @@ ?", value.to_escaped_for_tsquery + ":*")
|
||||
where_array_to_tsvector_matches(attr, value.to_escaped_for_tsquery + ":*")
|
||||
end
|
||||
|
||||
def where_all_in_array_like(attr, patterns)
|
||||
where_array_to_tsvector_matches(attr, escape_patterns_for_tsquery(patterns).join(" & "))
|
||||
end
|
||||
|
||||
def where_any_in_array_like(attr, patterns)
|
||||
where_array_to_tsvector_matches(attr, escape_patterns_for_tsquery(patterns).join(" | "))
|
||||
end
|
||||
|
||||
def where_text_includes_lower(attr, values)
|
||||
@@ -614,9 +629,21 @@ module Searchable
|
||||
private
|
||||
|
||||
def qualified_column_for(attr)
|
||||
return attr if attr.to_s.include?(".")
|
||||
"#{table_name}.#{column_for_attribute(attr).name}"
|
||||
end
|
||||
|
||||
# @param patterns [Array<String>] An array of wildcard patterns to escape for a tsquery search.
|
||||
def escape_patterns_for_tsquery(patterns)
|
||||
patterns.map do |pattern|
|
||||
if pattern.ends_with?("*")
|
||||
pattern.delete_suffix("*").to_escaped_for_tsquery + ":*"
|
||||
else
|
||||
pattern.to_escaped_for_tsquery
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Convert a column name or a raw SQL fragment to an Arel node.
|
||||
#
|
||||
# @param field [String, Arel::Nodes::Node] an Arel node, the name of a table
|
||||
|
||||
Reference in New Issue
Block a user