Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 13e9029

Browse files
Add a config option to prioritise local users in user directory search results (#9383)
This PR adds a homeserver config option, `user_directory.prefer_local_users`, that when enabled will show local users higher in user directory search results than remote users. This option is off by default. Note that turning this on doesn't necessarily mean that remote users will always be put below local users, but they should be assuming all other ranking factors (search query match, profile information present etc) are identical. This is useful for, say, University networks that are openly federating, but want to prioritise local students and staff in the user directory over other random users.
1 parent 9bc7474 commit 13e9029

File tree

5 files changed

+159
-9
lines changed

5 files changed

+159
-9
lines changed

changelog.d/9383.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a configuration option, `user_directory.prefer_local_users`, which when enabled will make it more likely for users on the same server as you to appear above other users.

docs/sample_config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2554,9 +2554,14 @@ spam_checker:
25542554
# rebuild the user_directory search indexes, see
25552555
# https://github.com/matrix-org/synapse/blob/master/docs/user_directory.md
25562556
#
2557+
# 'prefer_local_users' defines whether to prioritise local users in
2558+
# search query results. If True, local users are more likely to appear above
2559+
# remote users when searching the user directory. Defaults to false.
2560+
#
25572561
#user_directory:
25582562
# enabled: true
25592563
# search_all_users: false
2564+
# prefer_local_users: false
25602565

25612566

25622567
# User Consent configuration

synapse/config/user_directory.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class UserDirectoryConfig(Config):
2626
def read_config(self, config, **kwargs):
2727
self.user_directory_search_enabled = True
2828
self.user_directory_search_all_users = False
29+
self.user_directory_search_prefer_local_users = False
2930
user_directory_config = config.get("user_directory", None)
3031
if user_directory_config:
3132
self.user_directory_search_enabled = user_directory_config.get(
@@ -34,6 +35,9 @@ def read_config(self, config, **kwargs):
3435
self.user_directory_search_all_users = user_directory_config.get(
3536
"search_all_users", False
3637
)
38+
self.user_directory_search_prefer_local_users = user_directory_config.get(
39+
"prefer_local_users", False
40+
)
3741

3842
def generate_config_section(self, config_dir_path, server_name, **kwargs):
3943
return """
@@ -49,7 +53,12 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs):
4953
# rebuild the user_directory search indexes, see
5054
# https://github.com/matrix-org/synapse/blob/master/docs/user_directory.md
5155
#
56+
# 'prefer_local_users' defines whether to prioritise local users in
57+
# search query results. If True, local users are more likely to appear above
58+
# remote users when searching the user directory. Defaults to false.
59+
#
5260
#user_directory:
5361
# enabled: true
5462
# search_all_users: false
63+
# prefer_local_users: false
5564
"""

synapse/storage/databases/main/user_directory.py

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,11 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore):
556556
def __init__(self, database: DatabasePool, db_conn, hs):
557557
super().__init__(database, db_conn, hs)
558558

559+
self._prefer_local_users_in_search = (
560+
hs.config.user_directory_search_prefer_local_users
561+
)
562+
self._server_name = hs.config.server_name
563+
559564
async def remove_from_user_dir(self, user_id: str) -> None:
560565
def _remove_from_user_dir_txn(txn):
561566
self.db_pool.simple_delete_txn(
@@ -754,9 +759,24 @@ async def search_user_dir(self, user_id, search_term, limit):
754759
)
755760
"""
756761

762+
# We allow manipulating the ranking algorithm by injecting statements
763+
# based on config options.
764+
additional_ordering_statements = []
765+
ordering_arguments = ()
766+
757767
if isinstance(self.database_engine, PostgresEngine):
758768
full_query, exact_query, prefix_query = _parse_query_postgres(search_term)
759769

770+
# If enabled, this config option will rank local users higher than those on
771+
# remote instances.
772+
if self._prefer_local_users_in_search:
773+
# This statement checks whether a given user's user ID contains a server name
774+
# that matches the local server
775+
statement = "* (CASE WHEN user_id LIKE ? THEN 2.0 ELSE 1.0 END)"
776+
additional_ordering_statements.append(statement)
777+
778+
ordering_arguments += ("%:" + self._server_name,)
779+
760780
# We order by rank and then if they have profile info
761781
# The ranking algorithm is hand tweaked for "best" results. Broadly
762782
# the idea is we give a higher weight to exact matches.
@@ -767,7 +787,7 @@ async def search_user_dir(self, user_id, search_term, limit):
767787
FROM user_directory_search as t
768788
INNER JOIN user_directory AS d USING (user_id)
769789
WHERE
770-
%s
790+
%(where_clause)s
771791
AND vector @@ to_tsquery('simple', ?)
772792
ORDER BY
773793
(CASE WHEN d.user_id IS NOT NULL THEN 4.0 ELSE 1.0 END)
@@ -787,33 +807,54 @@ async def search_user_dir(self, user_id, search_term, limit):
787807
8
788808
)
789809
)
810+
%(order_case_statements)s
790811
DESC,
791812
display_name IS NULL,
792813
avatar_url IS NULL
793814
LIMIT ?
794-
""" % (
795-
where_clause,
815+
""" % {
816+
"where_clause": where_clause,
817+
"order_case_statements": " ".join(additional_ordering_statements),
818+
}
819+
args = (
820+
join_args
821+
+ (full_query, exact_query, prefix_query)
822+
+ ordering_arguments
823+
+ (limit + 1,)
796824
)
797-
args = join_args + (full_query, exact_query, prefix_query, limit + 1)
798825
elif isinstance(self.database_engine, Sqlite3Engine):
799826
search_query = _parse_query_sqlite(search_term)
800827

828+
# If enabled, this config option will rank local users higher than those on
829+
# remote instances.
830+
if self._prefer_local_users_in_search:
831+
# This statement checks whether a given user's user ID contains a server name
832+
# that matches the local server
833+
#
834+
# Note that we need to include a comma at the end for valid SQL
835+
statement = "user_id LIKE ? DESC,"
836+
additional_ordering_statements.append(statement)
837+
838+
ordering_arguments += ("%:" + self._server_name,)
839+
801840
sql = """
802841
SELECT d.user_id AS user_id, display_name, avatar_url
803842
FROM user_directory_search as t
804843
INNER JOIN user_directory AS d USING (user_id)
805844
WHERE
806-
%s
845+
%(where_clause)s
807846
AND value MATCH ?
808847
ORDER BY
809848
rank(matchinfo(user_directory_search)) DESC,
849+
%(order_statements)s
810850
display_name IS NULL,
811851
avatar_url IS NULL
812852
LIMIT ?
813-
""" % (
814-
where_clause,
815-
)
816-
args = join_args + (search_query, limit + 1)
853+
""" % {
854+
"where_clause": where_clause,
855+
"order_statements": " ".join(additional_ordering_statements),
856+
}
857+
args = join_args + (search_query,) + ordering_arguments + (limit + 1,)
817858
else:
818859
# This should be unreachable.
819860
raise Exception("Unrecognized database engine")

tests/handlers/test_user_directory.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import synapse.rest.admin
2020
from synapse.api.constants import EventTypes, RoomEncryptionAlgorithms, UserTypes
21+
from synapse.api.room_versions import RoomVersion, RoomVersions
2122
from synapse.rest.client.v1 import login, room
2223
from synapse.rest.client.v2_alpha import user_directory
2324
from synapse.storage.roommember import ProfileInfo
@@ -46,6 +47,8 @@ def make_homeserver(self, reactor, clock):
4647
def prepare(self, reactor, clock, hs):
4748
self.store = hs.get_datastore()
4849
self.handler = hs.get_user_directory_handler()
50+
self.event_builder_factory = self.hs.get_event_builder_factory()
51+
self.event_creation_handler = self.hs.get_event_creation_handler()
4952

5053
def test_handle_local_profile_change_with_support_user(self):
5154
support_user_id = "@support:test"
@@ -547,6 +550,97 @@ def test_initial_share_all_users(self):
547550
s = self.get_success(self.handler.search_users(u1, u4, 10))
548551
self.assertEqual(len(s["results"]), 1)
549552

553+
@override_config(
554+
{
555+
"user_directory": {
556+
"enabled": True,
557+
"search_all_users": True,
558+
"prefer_local_users": True,
559+
}
560+
}
561+
)
562+
def test_prefer_local_users(self):
563+
"""Tests that local users are shown higher in search results when
564+
user_directory.prefer_local_users is True.
565+
"""
566+
# Create a room and few users to test the directory with
567+
searching_user = self.register_user("searcher", "password")
568+
searching_user_tok = self.login("searcher", "password")
569+
570+
room_id = self.helper.create_room_as(
571+
searching_user,
572+
room_version=RoomVersions.V1.identifier,
573+
tok=searching_user_tok,
574+
)
575+
576+
# Create a few local users and join them to the room
577+
local_user_1 = self.register_user("user_xxxxx", "password")
578+
local_user_2 = self.register_user("user_bbbbb", "password")
579+
local_user_3 = self.register_user("user_zzzzz", "password")
580+
581+
self._add_user_to_room(room_id, RoomVersions.V1, local_user_1)
582+
self._add_user_to_room(room_id, RoomVersions.V1, local_user_2)
583+
self._add_user_to_room(room_id, RoomVersions.V1, local_user_3)
584+
585+
# Create a few "remote" users and join them to the room
586+
remote_user_1 = "@user_aaaaa:remote_server"
587+
remote_user_2 = "@user_yyyyy:remote_server"
588+
remote_user_3 = "@user_ccccc:remote_server"
589+
self._add_user_to_room(room_id, RoomVersions.V1, remote_user_1)
590+
self._add_user_to_room(room_id, RoomVersions.V1, remote_user_2)
591+
self._add_user_to_room(room_id, RoomVersions.V1, remote_user_3)
592+
593+
local_users = [local_user_1, local_user_2, local_user_3]
594+
remote_users = [remote_user_1, remote_user_2, remote_user_3]
595+
596+
# Populate the user directory via background update
597+
self._add_background_updates()
598+
while not self.get_success(
599+
self.store.db_pool.updates.has_completed_background_updates()
600+
):
601+
self.get_success(
602+
self.store.db_pool.updates.do_next_background_update(100), by=0.1
603+
)
604+
605+
# The local searching user searches for the term "user", which other users have
606+
# in their user id
607+
results = self.get_success(
608+
self.handler.search_users(searching_user, "user", 20)
609+
)["results"]
610+
received_user_id_ordering = [result["user_id"] for result in results]
611+
612+
# Typically we'd expect Synapse to return users in lexicographical order,
613+
# assuming they have similar User IDs/display names, and profile information.
614+
615+
# Check that the order of returned results using our module is as we expect,
616+
# i.e our local users show up first, despite all users having lexographically mixed
617+
# user IDs.
618+
[self.assertIn(user, local_users) for user in received_user_id_ordering[:3]]
619+
[self.assertIn(user, remote_users) for user in received_user_id_ordering[3:]]
620+
621+
def _add_user_to_room(
622+
self, room_id: str, room_version: RoomVersion, user_id: str,
623+
):
624+
# Add a user to the room.
625+
builder = self.event_builder_factory.for_room_version(
626+
room_version,
627+
{
628+
"type": "m.room.member",
629+
"sender": user_id,
630+
"state_key": user_id,
631+
"room_id": room_id,
632+
"content": {"membership": "join"},
633+
},
634+
)
635+
636+
event, context = self.get_success(
637+
self.event_creation_handler.create_new_client_event(builder)
638+
)
639+
640+
self.get_success(
641+
self.hs.get_storage().persistence.persist_event(event, context)
642+
)
643+
550644

551645
class TestUserDirSearchDisabled(unittest.HomeserverTestCase):
552646
user_id = "@test:test"

0 commit comments

Comments
 (0)