# frozen_string_literal: true class TagSearchService < BaseService def call(query, options = {}) @query = query.strip.gsub(/\A#/, '') @offset = options.delete(:offset).to_i @limit = options.delete(:limit).to_i @options = options results = from_elasticsearch if Chewy.enabled? results ||= from_database results end private def from_elasticsearch query = { function_score: { query: { multi_match: { query: @query, fields: %w(name.edge_ngram name), type: 'most_fields', operator: 'and', }, }, functions: [ { field_value_factor: { field: 'usage', modifier: 'log2p', missing: 0, }, }, { gauss: { last_status_at: { scale: '7d', offset: '14d', decay: 0.5, }, }, }, ], boost_mode: 'multiply', }, } filter = { bool: { should: [ { term: { reviewed: { value: true, }, }, }, { match: { name: { query: @query, }, }, }, ], }, } definition = TagsIndex.query(query) definition = definition.filter(filter) if @options[:exclude_unreviewed] ensure_exact_match(definition.limit(@limit).offset(@offset).objects.compact) rescue Faraday::ConnectionFailed, Parslet::ParseFailed nil end # Since the ElasticSearch Query doesn't guarantee the exact match will be the # first result or that it will even be returned, patch the results accordingly def ensure_exact_match(results) return results unless @offset.nil? || @offset.zero? normalized_query = Tag.normalize(@query) exact_match = results.find { |tag| tag.name.downcase == normalized_query } exact_match ||= Tag.find_normalized(normalized_query) unless exact_match.nil? results.delete(exact_match) results = [exact_match] + results end results end def from_database Tag.search_for(@query, @limit, @offset, @options) end end