FEATURE: prioritize group search order based on prefix match (#16093)

Our @mention user search prioritized users based on prefix matches.

So if searching for `sa` we will display `sam`, `asam` in that order

Previously, we did not prioritize group matches based on prefix. This change ensures better parity.

Implementation notes:

1. User search only prioritizes based on username prefix, not name prefix. TBD if we want to change that.
2. @mention on client side will show 0 group matches if we fill up all the spots with user matches. TBD if we want to unconditionally show the first / second group match.

Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
This commit is contained in:
Sam
2022-03-03 16:57:52 +11:00
committed by GitHub
parent 2d79275481
commit 3bf5692c72
4 changed files with 63 additions and 28 deletions

View File

@ -1171,8 +1171,7 @@ class UsersController < ApplicationController
end end
end end
groups = Group.search_groups(term, groups: groups) groups = Group.search_groups(term, groups: groups, sort: :auto)
groups = groups.order('groups.name asc')
to_render[:groups] = groups.map do |m| to_render[:groups] = groups.map do |m|
{ name: m.name, full_name: m.full_name } { name: m.name, full_name: m.full_name }
@ -1328,7 +1327,7 @@ class UsersController < ApplicationController
end end
render json: success_json render json: success_json
rescue Discourse::InvalidAccess => e rescue Discourse::InvalidAccess
render_json_error(I18n.t("notification_level.#{@error_message}")) render_json_error(I18n.t("notification_level.#{@error_message}"))
end end

View File

@ -560,12 +560,24 @@ class Group < ActiveRecord::Base
lookup_group(name) || refresh_automatic_group!(name) lookup_group(name) || refresh_automatic_group!(name)
end end
def self.search_groups(name, groups: nil, custom_scope: {}) def self.search_groups(name, groups: nil, custom_scope: {}, sort: :none)
groups ||= Group groups ||= Group
groups.where( relation = groups.where(
"name ILIKE :term_like OR full_name ILIKE :term_like", term_like: "%#{name}%" "name ILIKE :term_like OR full_name ILIKE :term_like", term_like: "%#{name}%"
) )
if sort == :auto
prefix = "#{name.gsub("_", "\\_")}%"
relation = relation.reorder(
DB.sql_fragment(
"CASE WHEN name ILIKE :like OR full_name ILIKE :like THEN 0 ELSE 1 END ASC, name ASC",
like: prefix
)
)
end
relation
end end
def self.lookup_group(name) def self.lookup_group(name)

View File

@ -923,21 +923,36 @@ describe Group do
end end
describe '.search_groups' do describe '.search_groups' do
fab!(:group) { Fabricate(:group, name: 'tEsT_more_things', full_name: 'Abc something awesome') }
let(:messageable_group) { Fabricate(:group, name: "MessageableGroup", messageable_level: Group::ALIAS_LEVELS[:everyone]) } def search_group_names(name)
Group.search_groups(name, sort: :auto).map(&:name)
end
it 'should return the right groups' do it 'should return the right groups' do
group group_name = Fabricate(:group, name: 'tEsT_more_things', full_name: 'Abc something awesome').name
expect(Group.search_groups('te')).to eq([group]) expect(search_group_names('te')).to eq([group_name])
expect(Group.search_groups('TE')).to eq([group]) expect(search_group_names('TE')).to eq([group_name])
expect(Group.search_groups('es')).to eq([group]) expect(search_group_names('es')).to eq([group_name])
expect(Group.search_groups('ES')).to eq([group]) expect(search_group_names('ES')).to eq([group_name])
expect(Group.search_groups('ngs')).to eq([group]) expect(search_group_names('ngs')).to eq([group_name])
expect(Group.search_groups('sOmEthi')).to eq([group]) expect(search_group_names('sOmEthi')).to eq([group_name])
expect(Group.search_groups('abc')).to eq([group]) expect(search_group_names('abc')).to eq([group_name])
expect(Group.search_groups('sOmEthi')).to eq([group]) expect(search_group_names('sOmEthi')).to eq([group_name])
expect(Group.search_groups('test2')).to eq([]) expect(search_group_names('test2')).to eq([])
end
it "should prioritize prefix matches on group's name or fullname" do
Fabricate(:group, name: 'pears_11', full_name: 'fred apple')
Fabricate(:group, name: 'apples', full_name: 'jane orange')
Fabricate(:group, name: 'oranges2', full_name: 'nothing')
Fabricate(:group, name: 'oranges1', full_name: 'ms fred')
expect(search_group_names('ap')).to eq(['apples', 'pears_11'])
expect(search_group_names('fr')).to eq(['pears_11', 'oranges1'])
expect(search_group_names('oran')).to eq(['oranges1', 'oranges2', 'apples'])
expect(search_group_names('pearsX11')).to eq([])
end end
end end

View File

@ -1788,7 +1788,7 @@ describe UsersController do
inviter = Fabricate(:user, trust_level: 2) inviter = Fabricate(:user, trust_level: 2)
sign_in(inviter) sign_in(inviter)
invite = Fabricate(:invite, invited_by: inviter) invite = Fabricate(:invite, invited_by: inviter)
invited_user = Fabricate(:invited_user, invite: invite, user: invitee) _invited_user = Fabricate(:invited_user, invite: invite, user: invitee)
get "/u/#{inviter.username}/invited.json" get "/u/#{inviter.username}/invited.json"
expect(response.status).to eq(200) expect(response.status).to eq(200)
@ -1848,7 +1848,7 @@ describe UsersController do
it 'allows admin to see invites' do it 'allows admin to see invites' do
inviter = Fabricate(:user, trust_level: 2) inviter = Fabricate(:user, trust_level: 2)
admin = sign_in(Fabricate(:admin)) _admin = sign_in(Fabricate(:admin))
invite = Fabricate(:invite, invited_by: inviter, email: nil, max_redemptions_allowed: 5, expires_at: 1.month.from_now, emailed_status: Invite.emailed_status_types[:not_required]) invite = Fabricate(:invite, invited_by: inviter, email: nil, max_redemptions_allowed: 5, expires_at: 1.month.from_now, emailed_status: Invite.emailed_status_types[:not_required])
get "/u/#{inviter.username}/invited/pending.json" get "/u/#{inviter.username}/invited/pending.json"
@ -1863,7 +1863,7 @@ describe UsersController do
context 'without permission to see invite links' do context 'without permission to see invite links' do
it 'does not return invites' do it 'does not return invites' do
user = Fabricate(:user, trust_level: 2) _user = Fabricate(:user, trust_level: 2)
inviter = admin inviter = admin
Fabricate(:invite, invited_by: inviter, email: nil, max_redemptions_allowed: 5, expires_at: 1.month.from_now, emailed_status: Invite.emailed_status_types[:not_required]) Fabricate(:invite, invited_by: inviter, email: nil, max_redemptions_allowed: 5, expires_at: 1.month.from_now, emailed_status: Invite.emailed_status_types[:not_required])
@ -3984,7 +3984,7 @@ describe UsersController do
mentionable_level: Group::ALIAS_LEVELS[:everyone], mentionable_level: Group::ALIAS_LEVELS[:everyone],
messageable_level: Group::ALIAS_LEVELS[:nobody], messageable_level: Group::ALIAS_LEVELS[:nobody],
visibility_level: Group.visibility_levels[:public], visibility_level: Group.visibility_levels[:public],
name: 'aaa1' name: 'aaa1bbb'
) )
end end
@ -3993,7 +3993,7 @@ describe UsersController do
mentionable_level: Group::ALIAS_LEVELS[:everyone], mentionable_level: Group::ALIAS_LEVELS[:everyone],
messageable_level: Group::ALIAS_LEVELS[:nobody], messageable_level: Group::ALIAS_LEVELS[:nobody],
visibility_level: Group.visibility_levels[:logged_on_users], visibility_level: Group.visibility_levels[:logged_on_users],
name: 'aaa2' name: 'bbb1aaa'
) )
end end
@ -4002,7 +4002,7 @@ describe UsersController do
mentionable_level: Group::ALIAS_LEVELS[:nobody], mentionable_level: Group::ALIAS_LEVELS[:nobody],
messageable_level: Group::ALIAS_LEVELS[:everyone], messageable_level: Group::ALIAS_LEVELS[:everyone],
visibility_level: Group.visibility_levels[:logged_on_users], visibility_level: Group.visibility_levels[:logged_on_users],
name: 'aaa3' name: 'ccc1aaa'
) )
end end
@ -4011,7 +4011,7 @@ describe UsersController do
mentionable_level: Group::ALIAS_LEVELS[:members_mods_and_admins], mentionable_level: Group::ALIAS_LEVELS[:members_mods_and_admins],
messageable_level: Group::ALIAS_LEVELS[:members_mods_and_admins], messageable_level: Group::ALIAS_LEVELS[:members_mods_and_admins],
visibility_level: Group.visibility_levels[:members], visibility_level: Group.visibility_levels[:members],
name: 'aaa4' name: 'ddd1aaa'
) )
end end
@ -4020,6 +4020,15 @@ describe UsersController do
sign_in(user) sign_in(user)
end end
it "correctly sorts on prefix" do
get "/u/search/users.json", params: { include_groups: "true", term: 'bbb' }
expect(response.status).to eq(200)
groups = response.parsed_body["groups"]
expect(groups.map { |g| g["name"] }).to eq(["bbb1aaa", "aaa1bbb"])
end
it "does not search for groups if there is no term" do it "does not search for groups if there is no term" do
get "/u/search/users.json", params: { include_groups: "true" } get "/u/search/users.json", params: { include_groups: "true" }
@ -4612,7 +4621,7 @@ describe UsersController do
it "creates a security key for the user" do it "creates a security key for the user" do
simulate_localhost_webauthn_challenge simulate_localhost_webauthn_challenge
create_second_factor_security_key create_second_factor_security_key
response_parsed = response.parsed_body _response_parsed = response.parsed_body
post "/u/register_second_factor_security_key.json", params: valid_security_key_create_post_data post "/u/register_second_factor_security_key.json", params: valid_security_key_create_post_data
@ -4626,7 +4635,7 @@ describe UsersController do
it "shows a security key error and does not create a key" do it "shows a security key error and does not create a key" do
stub_as_dev_localhost stub_as_dev_localhost
create_second_factor_security_key create_second_factor_security_key
response_parsed = response.parsed_body _response_parsed = response.parsed_body
post "/u/register_second_factor_security_key.json", params: { post "/u/register_second_factor_security_key.json", params: {
id: "bad id", id: "bad id",
@ -4651,8 +4660,8 @@ describe UsersController do
end end
context 'when user has a registered totp and security key' do context 'when user has a registered totp and security key' do
before do before do
totp_second_factor = Fabricate(:user_second_factor_totp, user: user1) _totp_second_factor = Fabricate(:user_second_factor_totp, user: user1)
security_key_second_factor = Fabricate(:user_security_key, user: user1, factor_type: UserSecurityKey.factor_types[:second_factor]) _security_key_second_factor = Fabricate(:user_security_key, user: user1, factor_type: UserSecurityKey.factor_types[:second_factor])
end end
it 'should disable all totp and security keys' do it 'should disable all totp and security keys' do