diff --git a/libervia/backend/plugins/plugin_misc_jid_search.py b/libervia/backend/plugins/plugin_misc_jid_search.py index 5cb83b0c..0286b168 100644 --- a/libervia/backend/plugins/plugin_misc_jid_search.py +++ b/libervia/backend/plugins/plugin_misc_jid_search.py @@ -45,7 +45,8 @@ PLUGIN_INFO = { C.PI_HANDLER: "no", C.PI_DESCRIPTION: _("""Search for XMPP entities"""), } -RATIO_CUTOFF = 0.6 +# Minimal ratio for fuzzy matching +RATIO_CUTOFF = 0.75 # Used when a search term matches a substring of a relevant data. PARTIAL_MATCH_RATIO = 0.8 MAX_CACHE_SIZE = 10 @@ -99,6 +100,24 @@ class SearchItems(RootModel): self.root.sort(key=key, reverse=reverse) # type: ignore +def _data_rank(item: SearchItem) -> tuple[bool, bool, bool, bool]: + """Rank a search item by its metadata completeness + + We can have duplicate items when allowing external search (local MUC can be referenced + globally). To choose the item to keep, we check the one with most metadata, and chose + the local one in case of equivalent data. + + @param item: The item to rank. + @return: A tuple to use for comparaison. + """ + return ( + getattr(item, "nusers", None) is not None, + bool(getattr(item, "description", None)), + bool(getattr(item, "language", None)), + getattr(item, "local", False), + ) + + class Options(BaseModel): entities: bool = Field( default=False, description="Search for entities for direct chat" @@ -137,7 +156,9 @@ class JidSearch: client = self.host.get_client(profile) options = Options.model_validate_json(options_s) d = defer.ensureDeferred(self.search(client, search_term, options)) - d.addCallback(lambda search_items: search_items.model_dump_json()) + d.addCallback( + lambda search_items: search_items.model_dump_json(exclude_none=True) + ) return d async def search( @@ -217,6 +238,20 @@ class JidSearch: ) ) + # Deduplicate by entity, keeping the most informative item (the global directory + # often provides more data) + by_entity = {} + for item in matches: + existing = by_entity.get(item.entity) + if existing is None: + by_entity[item.entity] = item + elif _data_rank(item) > _data_rank(existing): + # the entity keeps the best match score of its duplicates + item.exact_match = item.exact_match or existing.exact_match + item.relevance = max(item.relevance or 0, existing.relevance or 0) or None + by_entity[item.entity] = item + matches = SearchItems(list(by_entity.values())) + matches.sort( key=lambda item: ( item.exact_match,