4691b0068c
* Remove the search button from UI header when logged out (#25631) * Change account search to match by text when opted-in (#25599) Co-authored-by: Eugen Rochko <eugen@zeonfederated.com> * Fix ResolveURLService not resolving local URLs for remote content (#25637) * Remove `pkg-config` gem dependency (#25615) * Update Crowdin configuration file * Fix onboarding prompt being displayed because of disconnection gaps (#25617) * Use an Immutable Record as the root state (#25584) * Add index to backups on `user_id` column (#25647) * Fix rails `rewhere` deprecation warning in directories api controller (#25625) * Remove unused routes (#25578) * Fixing an issue with a missing argument (#2261) undefined * Update uri to version 0.12.2 (CVE fix) (#25657) * Change local and federated timelines to be in a single firehose column (#25641) * Fix HTTP 500 in `/api/v1/emails/check_confirmation` (#25595) * Rails 7 update (#24241) * Change dropdown icon above compose form from ellipsis to bars in web UI (#25661) * Prevent duplicate concurrent calls of `/api/*/instance` in web UI (#25663) * Revert "Rails 7 update" (#25667) * [Glitch] Remove the search button from UI header when logged out Port285a691936
to glitch-soc Signed-off-by: Claire <claire.github-309c@sitedethib.com> * [Glitch] Fix onboarding prompt being displayed because of disconnection gaps Port9934949fc4
to glitch-soc Signed-off-by: Claire <claire.github-309c@sitedethib.com> * [Glitch] Use an Immutable Record as the root state Port78ba12f0bf
to glitch-soc Signed-off-by: Claire <claire.github-309c@sitedethib.com> * [Glitch] Change local and federated timelines to be in a single firehose column Portcea9db5a0b
to glitch-soc Signed-off-by: Claire <claire.github-309c@sitedethib.com> * [Glitch] Change dropdown icon above compose form from ellipsis to bars in web UI Port0512537eb6
to glitch-soc Signed-off-by: Claire <claire.github-309c@sitedethib.com> * [Glitch] Prevent duplicate concurrent calls of `/api/*/instance` in web UI Port5b46345459
to glitch-soc Signed-off-by: Claire <claire.github-309c@sitedethib.com> * Show local-only posts in “All” by default, and add back option to toggle it * Fix showing local only toots in "All" (#2265) * Fix warnings about missing dependency in hooks Signed-off-by: Plastikmensch <plastikmensch@users.noreply.github.com> * Add `allowLocalOnly` to timelineId Without this local-only toots will never be loaded. feedType is checked to be public to not show local-only toots in the "Remote" tab. Signed-off-by: Plastikmensch <plastikmensch@users.noreply.github.com> --------- Signed-off-by: Plastikmensch <plastikmensch@users.noreply.github.com> * Add regex filter back to firehose (#2266) * Add regex filter back to firehose The regex filter will apply to all tabs and not be automatically applied when pinned. Signed-off-by: Plastikmensch <plastikmensch@users.noreply.github.com> * Keep regex when pinned Signed-off-by: Plastikmensch <plastikmensch@users.noreply.github.com> --------- Signed-off-by: Plastikmensch <plastikmensch@users.noreply.github.com> --------- Signed-off-by: Claire <claire.github-309c@sitedethib.com> Signed-off-by: Plastikmensch <plastikmensch@users.noreply.github.com> Co-authored-by: Claire <claire.github-309c@sitedethib.com> Co-authored-by: jsgoldstein <jakegoldstein95@gmail.com> Co-authored-by: Eugen Rochko <eugen@zeonfederated.com> Co-authored-by: Renaud Chaput <renchap@gmail.com> Co-authored-by: Matt Jankowski <matt@jankowski.online> Co-authored-by: Vivianne <puttabutta@gmail.com> Co-authored-by: Daniel M Brasil <danielmbrasil@protonmail.com> Co-authored-by: mogaminsk <mgmnjp@icloud.com> Co-authored-by: Plastikmensch <Plastikmensch@users.noreply.github.com>
152 lines
5 KiB
Ruby
152 lines
5 KiB
Ruby
# frozen_string_literal: true
|
||
|
||
module AccountSearch
|
||
extend ActiveSupport::Concern
|
||
|
||
DISALLOWED_TSQUERY_CHARACTERS = /['?\\:‘’]/
|
||
|
||
TEXT_SEARCH_RANKS = <<~SQL.squish
|
||
(
|
||
setweight(to_tsvector('simple', accounts.display_name), 'A') ||
|
||
setweight(to_tsvector('simple', accounts.username), 'B') ||
|
||
setweight(to_tsvector('simple', coalesce(accounts.domain, '')), 'C')
|
||
)
|
||
SQL
|
||
|
||
REPUTATION_SCORE_FUNCTION = <<~SQL.squish
|
||
(
|
||
greatest(0, coalesce(s.followers_count, 0)) / (
|
||
greatest(0, coalesce(s.following_count, 0)) + 1.0
|
||
)
|
||
)
|
||
SQL
|
||
|
||
FOLLOWERS_SCORE_FUNCTION = <<~SQL.squish
|
||
log(
|
||
greatest(0, coalesce(s.followers_count, 0)) + 2
|
||
)
|
||
SQL
|
||
|
||
TIME_DISTANCE_FUNCTION = <<~SQL.squish
|
||
(
|
||
case
|
||
when s.last_status_at is null then 0
|
||
else exp(
|
||
-1.0 * (
|
||
(
|
||
greatest(0, abs(extract(DAY FROM age(s.last_status_at))) - 30.0)^2) /#{' '}
|
||
(2.0 * ((-1.0 * 30^2) / (2.0 * ln(0.3)))
|
||
)
|
||
)
|
||
)
|
||
end
|
||
)
|
||
SQL
|
||
|
||
BOOST = <<~SQL.squish
|
||
(
|
||
(#{REPUTATION_SCORE_FUNCTION} + #{FOLLOWERS_SCORE_FUNCTION} + #{TIME_DISTANCE_FUNCTION}) / 3.0
|
||
)
|
||
SQL
|
||
|
||
BASIC_SEARCH_SQL = <<~SQL.squish
|
||
SELECT
|
||
accounts.*,
|
||
#{BOOST} * ts_rank_cd(#{TEXT_SEARCH_RANKS}, to_tsquery('simple', :tsquery), 32) AS rank
|
||
FROM accounts
|
||
LEFT JOIN users ON accounts.id = users.account_id
|
||
LEFT JOIN account_stats AS s ON accounts.id = s.account_id
|
||
WHERE to_tsquery('simple', :tsquery) @@ #{TEXT_SEARCH_RANKS}
|
||
AND accounts.suspended_at IS NULL
|
||
AND accounts.moved_to_account_id IS NULL
|
||
AND (accounts.domain IS NOT NULL OR (users.approved = TRUE AND users.confirmed_at IS NOT NULL))
|
||
ORDER BY rank DESC
|
||
LIMIT :limit OFFSET :offset
|
||
SQL
|
||
|
||
ADVANCED_SEARCH_WITH_FOLLOWING = <<~SQL.squish
|
||
WITH first_degree AS (
|
||
SELECT target_account_id
|
||
FROM follows
|
||
WHERE account_id = :id
|
||
UNION ALL
|
||
SELECT :id
|
||
)
|
||
SELECT
|
||
accounts.*,
|
||
(count(f.id) + 1) * #{BOOST} * ts_rank_cd(#{TEXT_SEARCH_RANKS}, to_tsquery('simple', :tsquery), 32) AS rank
|
||
FROM accounts
|
||
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = :id)
|
||
LEFT JOIN account_stats AS s ON accounts.id = s.account_id
|
||
WHERE accounts.id IN (SELECT * FROM first_degree)
|
||
AND to_tsquery('simple', :tsquery) @@ #{TEXT_SEARCH_RANKS}
|
||
AND accounts.suspended_at IS NULL
|
||
AND accounts.moved_to_account_id IS NULL
|
||
GROUP BY accounts.id, s.id
|
||
ORDER BY rank DESC
|
||
LIMIT :limit OFFSET :offset
|
||
SQL
|
||
|
||
ADVANCED_SEARCH_WITHOUT_FOLLOWING = <<~SQL.squish
|
||
SELECT
|
||
accounts.*,
|
||
#{BOOST} * ts_rank_cd(#{TEXT_SEARCH_RANKS}, to_tsquery('simple', :tsquery), 32) AS rank,
|
||
count(f.id) AS followed
|
||
FROM accounts
|
||
LEFT OUTER JOIN follows AS f ON
|
||
(accounts.id = f.account_id AND f.target_account_id = :id) OR (accounts.id = f.target_account_id AND f.account_id = :id)
|
||
LEFT JOIN users ON accounts.id = users.account_id
|
||
LEFT JOIN account_stats AS s ON accounts.id = s.account_id
|
||
WHERE to_tsquery('simple', :tsquery) @@ #{TEXT_SEARCH_RANKS}
|
||
AND accounts.suspended_at IS NULL
|
||
AND accounts.moved_to_account_id IS NULL
|
||
AND (accounts.domain IS NOT NULL OR (users.approved = TRUE AND users.confirmed_at IS NOT NULL))
|
||
GROUP BY accounts.id, s.id
|
||
ORDER BY followed DESC, rank DESC
|
||
LIMIT :limit OFFSET :offset
|
||
SQL
|
||
|
||
def searchable_text
|
||
PlainTextFormatter.new(note, local?).to_s if discoverable?
|
||
end
|
||
|
||
def searchable_properties
|
||
[].tap do |properties|
|
||
properties << 'bot' if bot?
|
||
properties << 'verified' if fields.any?(&:verified?)
|
||
end
|
||
end
|
||
|
||
class_methods do
|
||
def search_for(terms, limit: 10, offset: 0)
|
||
tsquery = generate_query_for_search(terms)
|
||
|
||
find_by_sql([BASIC_SEARCH_SQL, { limit: limit, offset: offset, tsquery: tsquery }]).tap do |records|
|
||
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
|
||
end
|
||
end
|
||
|
||
def advanced_search_for(terms, account, limit: 10, following: false, offset: 0)
|
||
tsquery = generate_query_for_search(terms)
|
||
sql_template = following ? ADVANCED_SEARCH_WITH_FOLLOWING : ADVANCED_SEARCH_WITHOUT_FOLLOWING
|
||
|
||
find_by_sql([sql_template, { id: account.id, limit: limit, offset: offset, tsquery: tsquery }]).tap do |records|
|
||
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
|
||
end
|
||
end
|
||
|
||
private
|
||
|
||
def generate_query_for_search(unsanitized_terms)
|
||
terms = unsanitized_terms.gsub(DISALLOWED_TSQUERY_CHARACTERS, ' ')
|
||
|
||
# The final ":*" is for prefix search.
|
||
# The trailing space does not seem to fit any purpose, but `to_tsquery`
|
||
# behaves differently with and without a leading space if the terms start
|
||
# with `./`, `../`, or `.. `. I don't understand why, so, in doubt, keep
|
||
# the same query.
|
||
"' #{terms} ':*"
|
||
end
|
||
end
|
||
end
|