plugin JID Search: Update cutoff ratio + dedup:

- `RATIO_CUTOFF` has been adapted to avoid returning approximate matches.
- In case of duplicate entries (e.g., rooms returned by the local search are also returned
  by the external one), we keep only the item with the most metadata.
- Exclude `None` values in `jid_search` bridge method.
This commit is contained in:
Goffi 2026-08-24 11:50:22 +02:00
commit de6c050db2

View file

@ -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,