diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 51c948955984b75fe31907742bc026959e0e6332..f07450eb077c633a381a270970b2cdf4f5fb99c2 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -115,7 +115,7 @@ class Api::V1::AccountsController < ApiController end def search - @accounts = SearchService.new.call(params[:q], limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:resolve] == 'true', current_account) + @accounts = AccountSearchService.new.call(params[:q], limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:resolve] == 'true', current_account) set_account_counters_maps(@accounts) unless @accounts.nil? diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..6b12924581dbcc0eec2fa8a966f55414faf768cd --- /dev/null +++ b/app/controllers/api/v1/search_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Api::V1::SearchController < ApiController + respond_to :json + + def index + @search = OpenStruct.new(SearchService.new.call(params[:q], 5, params[:resolve] == 'true', current_account)) + end +end diff --git a/app/models/account.rb b/app/models/account.rb index aa0af563cd44b619e04e1287ec7e3f3e0f1acea2..c35620812b523d9fa09b5c4d501aa9471f5564db 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -222,8 +222,8 @@ SQL end def search_for(terms, limit = 10) - textsearch = '(setweight(to_tsvector(\'simple\', accounts.display_name), \'A\') || setweight(to_tsvector(\'simple\', accounts.username), \'B\') || setweight(to_tsvector(\'simple\', coalesce(accounts.domain, \'\')), \'C\'))' - query = 'to_tsquery(\'simple\', \'\'\' \' || ? || \' \'\'\' || \':*\')' + textsearch = '(setweight(to_tsvector(\'simple\', accounts.display_name), \'A\') || setweight(to_tsvector(\'simple\', accounts.username), \'B\') || setweight(to_tsvector(\'simple\', coalesce(accounts.domain, \'\')), \'C\'))' + query = 'to_tsquery(\'simple\', \'\'\' \' || ? || \' \'\'\' || \':*\')' sql = <<SQL SELECT diff --git a/app/models/tag.rb b/app/models/tag.rb index 0d2fe43b8ece04c1220ff36ac0d987fdc1341ec1..e2ad8e4db588e2a2d846f70bd7bea300b57cd1a7 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -10,4 +10,23 @@ class Tag < ApplicationRecord def to_param name end + + class << self + def search_for(terms, limit = 5) + textsearch = 'to_tsvector(\'simple\', tags.name)' + query = 'to_tsquery(\'simple\', \'\'\' \' || ? || \' \'\'\' || \':*\')' + + sql = <<SQL + SELECT + tags.*, + ts_rank_cd(#{textsearch}, #{query}) AS rank + FROM tags + WHERE #{query} @@ #{textsearch} + ORDER BY rank DESC + LIMIT ? +SQL + + Tag.find_by_sql([sql, terms, terms, limit]) + end + end end diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..f55439dcba78507252d7f21a08ebdf5e00596ac3 --- /dev/null +++ b/app/services/account_search_service.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class AccountSearchService < BaseService + def call(query, limit, resolve = false, account = nil) + return [] if query.blank? || query.start_with?('#') + + username, domain = query.gsub(/\A@/, '').split('@') + domain = nil if TagManager.instance.local_domain?(domain) + + if domain.nil? + exact_match = Account.find_local(username) + results = account.nil? ? Account.search_for(username, limit) : Account.advanced_search_for(username, account, limit) + else + exact_match = Account.find_remote(username, domain) + results = account.nil? ? Account.search_for("#{username} #{domain}", limit) : Account.advanced_search_for("#{username} #{domain}", account, limit) + end + + results = [exact_match] + results.reject { |a| a.id == exact_match.id } if exact_match + + if resolve && !exact_match && !domain.nil? + results = [FollowRemoteAccountService.new.call("#{username}@#{domain}")] + end + + results + end +end diff --git a/app/services/fetch_remote_resource_service.rb b/app/services/fetch_remote_resource_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..80aa74365a3b4b83c99159c2e15d02e075014ff5 --- /dev/null +++ b/app/services/fetch_remote_resource_service.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class FetchRemoteResourceService < BaseService + def call(url) + atom_url, body = FetchAtomService.new.call(url) + + return nil if atom_url.nil? + + xml = Nokogiri::XML(body) + xml.encoding = 'utf-8' + + if xml.root.name == 'feed' + FetchRemoteAccountService.new.call(atom_url) + elsif xml.root.name == 'entry' + FetchRemoteStatusService.new.call(atom_url) + end + end +end diff --git a/app/services/search_service.rb b/app/services/search_service.rb index 19fc1697357db6b11627ad4c1da8a53b77c5fb5d..159c0371376e5575b1449c3bb04cdd9e88674104 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -2,23 +2,18 @@ class SearchService < BaseService def call(query, limit, resolve = false, account = nil) - return if query.blank? || query.start_with?('#') + return if query.blank? - username, domain = query.gsub(/\A@/, '').split('@') - domain = nil if TagManager.instance.local_domain?(domain) + results = { accounts: [], hashtags: [], statuses: [] } - if domain.nil? - exact_match = Account.find_local(username) - results = account.nil? ? Account.search_for(username, limit) : Account.advanced_search_for(username, account, limit) - else - exact_match = Account.find_remote(username, domain) - results = account.nil? ? Account.search_for("#{username} #{domain}", limit) : Account.advanced_search_for("#{username} #{domain}", account, limit) - end + if query =~ /\Ahttps?:\/\// + resource = FetchRemoteResourceService.new.call(query) - results = [exact_match] + results.reject { |a| a.id == exact_match.id } if exact_match - - if resolve && !exact_match && !domain.nil? - results = [FollowRemoteAccountService.new.call("#{username}@#{domain}")] + results[:accounts] << resource if resource.is_a?(Account) + results[:statuses] << resource if resource.is_a?(Status) + else + results[:accounts] = AccountSearchService.new.call(query, limit, resolve, account) + results[:hashtags] = Tag.search_for(query.gsub(/\A#/, ''), limit) unless query.start_with?('@') end results diff --git a/app/views/api/v1/search/index.rabl b/app/views/api/v1/search/index.rabl new file mode 100644 index 0000000000000000000000000000000000000000..d10ac9e0f8f92695fa3823e2fd788f3e5bdb3cc8 --- /dev/null +++ b/app/views/api/v1/search/index.rabl @@ -0,0 +1,13 @@ +object @search + +child accounts: :accounts do + extends 'api/v1/accounts/show' +end + +node(:hashtags) do |search| + search.hashtags.map(&:name) +end + +child statuses: :statuses do + extends 'api/v1/statuses/show' +end diff --git a/config/routes.rb b/config/routes.rb index ea766e1b32f2855aca22ff2abdd2afec12016560..b3f623c04369a4a9315c36a5ed9e943668fb5b7d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -129,6 +129,8 @@ Rails.application.routes.draw do get '/timelines/public', to: 'timelines#public', as: :public_timeline get '/timelines/tag/:id', to: 'timelines#tag', as: :hashtag_timeline + get '/search', to: 'search#index', as: :search + resources :follows, only: [:create] resources :media, only: [:create] resources :apps, only: [:create]