From c4ffc357a3d3658526f6fd879364145333eea6b0 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 18 Jan 2026 11:30:41 -0500 Subject: [PATCH] Backport pull request #15983 from jellyfin/release-10.11.z Prioritize better matches on search Original-merge: a518160a6ff471541b7daae6d54c8b896bb1f2e6 Merged-by: crobibero Backported-by: Bond_009 --- .../Item/BaseItemRepository.cs | 29 ++++++++++++------- .../Item/OrderMapper.cs | 27 +++++++++++++++++ 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 646a9c4836..a2f0e78c03 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1615,29 +1615,36 @@ public sealed class BaseItemRepository IOrderedQueryable? orderedQuery = null; + // When searching, prioritize by match quality: exact match > prefix match > contains + if (hasSearch) + { + orderedQuery = query.OrderBy(OrderMapper.MapSearchRelevanceOrder(filter.SearchTerm!)); + } + var firstOrdering = orderBy.FirstOrDefault(); if (firstOrdering != default) { var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context); - if (firstOrdering.SortOrder == SortOrder.Ascending) + if (orderedQuery is null) { - orderedQuery = query.OrderBy(expression); + // No search relevance ordering, start fresh + orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending + ? query.OrderBy(expression) + : query.OrderByDescending(expression); } else { - orderedQuery = query.OrderByDescending(expression); + // Search relevance ordering already applied, chain with ThenBy + orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending + ? orderedQuery.ThenBy(expression) + : orderedQuery.ThenByDescending(expression); } if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName) { - if (firstOrdering.SortOrder is SortOrder.Ascending) - { - orderedQuery = orderedQuery.ThenBy(e => e.Name); - } - else - { - orderedQuery = orderedQuery.ThenByDescending(e => e.Name); - } + orderedQuery = firstOrdering.SortOrder is SortOrder.Ascending + ? orderedQuery.ThenBy(e => e.Name) + : orderedQuery.ThenByDescending(e => e.Name); } } diff --git a/Jellyfin.Server.Implementations/Item/OrderMapper.cs b/Jellyfin.Server.Implementations/Item/OrderMapper.cs index 192ee74996..1ae7cc6c4a 100644 --- a/Jellyfin.Server.Implementations/Item/OrderMapper.cs +++ b/Jellyfin.Server.Implementations/Item/OrderMapper.cs @@ -6,6 +6,7 @@ using System.Linq.Expressions; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using Microsoft.EntityFrameworkCore; @@ -68,4 +69,30 @@ public static class OrderMapper _ => e => e.SortName }; } + + /// + /// Creates an expression to order search results by match quality. + /// Prioritizes: exact match (0) > prefix match with word boundary (1) > prefix match (2) > contains (3). + /// + /// The search term to match against. + /// An expression that returns an integer representing match quality (lower is better). + public static Expression> MapSearchRelevanceOrder(string searchTerm) + { + var cleanSearchTerm = GetCleanValue(searchTerm); + var searchPrefix = cleanSearchTerm + " "; + return e => + e.CleanName == cleanSearchTerm ? 0 : + e.CleanName!.StartsWith(searchPrefix) ? 1 : + e.CleanName!.StartsWith(cleanSearchTerm) ? 2 : 3; + } + + private static string GetCleanValue(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return value; + } + + return value.RemoveDiacritics().ToLowerInvariant(); + } }