diff --git a/app/models/tag.rb b/app/models/tag.rb index a0afdfa0c..15754cc1c 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -18,22 +18,25 @@ class Tag < ApplicationRecord validate :synonym_unique validates :name, uniqueness: { scope: [:tag_set_id], case_sensitive: false } + # Fuzzy-searches tags by name, excerpt, and synonym name + # @param term [String] search term + # @return [ActiveRecord::Relation] def self.search(term) - stripped = term.strip + value = "%#{sanitize_sql_like(term.strip)}%" + # Query to search on tags, the name is used for sorting. - q1 = where('tags.name LIKE ?', "%#{sanitize_sql_like(stripped)}%") - .or(where('tags.excerpt LIKE ?', "%#{sanitize_sql_like(stripped)}%")) - .select(Arel.sql('name AS sortname, tags.*')) + q1 = where('tags.name LIKE ?', value) + .or(where('tags.excerpt LIKE ?', value)) + .select('tags.*') # Query to search on synonyms, the synonym name is used for sorting. - # The order clause here actually applies to the union of q1 and q2 (so not just q2). q2 = joins(:tag_synonyms) - .where('tag_synonyms.name LIKE ?', "%#{sanitize_sql_like(stripped)}%") - .select(Arel.sql('tag_synonyms.name AS sortname, tags.*')) - .order(Arel.sql(sanitize_sql_array(['sortname LIKE ? DESC, sortname', "#{sanitize_sql_like(stripped)}%"]))) + .where('tag_synonyms.name LIKE ?', value) + .select('tags.*') # Select from the union of the above queries, select only the tag columns such that we can distinct them from(Arel.sql("((#{q1.to_sql}) UNION (#{q2.to_sql})) tags")) + .order(Arel.sql(sanitize_sql_array(['name LIKE ? DESC, name', value]))) .select(Tag.column_names.map { |c| "tags.#{c}" }) .distinct end diff --git a/test/models/tag_test.rb b/test/models/tag_test.rb index 195cb735b..8cd5cc401 100644 --- a/test/models/tag_test.rb +++ b/test/models/tag_test.rb @@ -6,4 +6,18 @@ class TagTest < ActiveSupport::TestCase test 'is community related' do assert_community_related(Tag) end + + test 'search should correctly order tags' do + term = 'us' + + tags = Tag.search(term).to_a + + name_match_sorted = tags.select { |t| t.name.include?(term) }.sort { |a, b| a.name <=> b.name } + excerpt_match_sorted = tags.select { |t| t.excerpt&.include?(term) }.sort { |a, b| a.name <=> b.name } + sorted_tags = name_match_sorted + excerpt_match_sorted + + sorted_tags.each_with_index do |tag, idx| + assert_equal tag.name, tags[idx].name + end + end end