aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/Library/Search
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations/Library/Search')
-rw-r--r--Emby.Server.Implementations/Library/Search/SearchManager.cs24
-rw-r--r--Emby.Server.Implementations/Library/Search/SqlSearchProvider.cs38
2 files changed, 49 insertions, 13 deletions
diff --git a/Emby.Server.Implementations/Library/Search/SearchManager.cs b/Emby.Server.Implementations/Library/Search/SearchManager.cs
index 0e180753a6..a8ee416b31 100644
--- a/Emby.Server.Implementations/Library/Search/SearchManager.cs
+++ b/Emby.Server.Implementations/Library/Search/SearchManager.cs
@@ -112,13 +112,12 @@ public class SearchManager : ISearchManager
return externalResults;
}
- var internalResults = await internalTask.ConfigureAwait(false);
if (_internalProviders.Length > 0)
{
_logger.LogDebug("No results from external providers, using internal provider results");
}
- return internalResults;
+ return await internalTask.ConfigureAwait(false);
}
private async Task<IReadOnlyList<SearchResult>> FilterByUserAccessAsync(
@@ -144,17 +143,24 @@ public class SearchManager : ISearchManager
baseQuery = _queryHelpers.ApplyAccessFiltering(dbContext, baseQuery, accessFilter);
- var allowedCount = await baseQuery.CountAsync(cancellationToken).ConfigureAwait(false);
- if (allowedCount == candidates.Count)
+ var allowed = await baseQuery
+ .Select(e => new { e.Id, e.PrimaryVersionId })
+ .ToListAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ var allowedIds = allowed.Select(e => e.Id).ToHashSet();
+
+ // A provider can return both an alternate version and the primary it belongs to, and the
+ // two are one item to the user.
+ allowedIds.ExceptWith(allowed
+ .Where(e => e.PrimaryVersionId.HasValue && allowedIds.Contains(e.PrimaryVersionId.Value))
+ .Select(e => e.Id));
+
+ if (allowedIds.Count == candidates.Count)
{
return candidates;
}
- var allowedIds = await baseQuery
- .Select(e => e.Id)
- .ToHashSetAsync(cancellationToken)
- .ConfigureAwait(false);
-
var filtered = candidates.Where(c => allowedIds.Contains(c.ItemId)).ToList();
if (filtered.Count < candidates.Count)
{
diff --git a/Emby.Server.Implementations/Library/Search/SqlSearchProvider.cs b/Emby.Server.Implementations/Library/Search/SqlSearchProvider.cs
index c4d3b249d5..a5ea02df51 100644
--- a/Emby.Server.Implementations/Library/Search/SqlSearchProvider.cs
+++ b/Emby.Server.Implementations/Library/Search/SqlSearchProvider.cs
@@ -10,6 +10,7 @@ using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
+using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
@@ -36,6 +37,7 @@ public class SqlSearchProvider : IInternalSearchProvider
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
private readonly IItemQueryHelpers _queryHelpers;
+ private readonly IServerConfigurationManager _configurationManager;
/// <summary>
/// Initializes a new instance of the <see cref="SqlSearchProvider"/> class.
@@ -45,18 +47,21 @@ public class SqlSearchProvider : IInternalSearchProvider
/// <param name="libraryManager">The library manager.</param>
/// <param name="userManager">The user manager.</param>
/// <param name="queryHelpers">The shared item query helpers.</param>
+ /// <param name="configurationManager">The configuration manager.</param>
public SqlSearchProvider(
IDbContextFactory<JellyfinDbContext> dbProvider,
IItemTypeLookup itemTypeLookup,
ILibraryManager libraryManager,
IUserManager userManager,
- IItemQueryHelpers queryHelpers)
+ IItemQueryHelpers queryHelpers,
+ IServerConfigurationManager configurationManager)
{
_dbProvider = dbProvider;
_itemTypeLookup = itemTypeLookup;
_libraryManager = libraryManager;
_userManager = userManager;
_queryHelpers = queryHelpers;
+ _configurationManager = configurationManager;
}
/// <inheritdoc/>
@@ -98,6 +103,12 @@ public class SqlSearchProvider : IInternalSearchProvider
// so match it via a case-insensitive LIKE rather than a per-row case conversion
// that may not translate to SQL on every provider.
var likeOriginal = $"%{rawSearchTerm}%";
+
+ // Great benefit for users with metadata in non-latin script
+ // since SortName is among other things stored transliterated.
+ var sortNameShape = BaseItem.GetSortName(rawSearchTerm, true, _configurationManager.Configuration);
+ var likeSortName = string.IsNullOrWhiteSpace(sortNameShape) ? null : $"%{sortNameShape}%";
+
var limit = query.Limit ?? DefaultSearchLimit;
var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
@@ -107,14 +118,27 @@ public class SqlSearchProvider : IInternalSearchProvider
var dbQuery = dbContext.BaseItems
.AsNoTracking()
.Where(e => e.Id != _placeholderId)
- .Where(e => !e.IsVirtualItem)
- .Where(e => e.CleanName!.Contains(cleanSearchTerm)
- || (e.OriginalTitle != null && EF.Functions.Like(e.OriginalTitle, likeOriginal)));
+ .Where(e => !e.IsVirtualItem);
+
+ if (likeSortName is null)
+ {
+ dbQuery = dbQuery
+ .Where(e => e.CleanName!.Contains(cleanSearchTerm)
+ || (e.OriginalTitle != null && EF.Functions.Like(e.OriginalTitle, likeOriginal)));
+ }
+ else
+ {
+ dbQuery = dbQuery
+ .Where(e => e.CleanName!.Contains(cleanSearchTerm)
+ || (e.OriginalTitle != null && EF.Functions.Like(e.OriginalTitle, likeOriginal))
+ || (e.SortName != null && EF.Functions.Like(e.SortName, likeSortName)));
+ }
dbQuery = ApplyTypeFilter(dbQuery, query.IncludeItemTypes, query.ExcludeItemTypes);
dbQuery = ApplyMediaTypeFilter(dbQuery, query.MediaTypes);
dbQuery = ApplyParentFilter(dbQuery, query.ParentId);
dbQuery = ApplyUserAccessFilter(dbContext, dbQuery, query);
+ dbQuery = ExcludeVersionsOfMatchedPrimaries(dbQuery);
// Compute the score in SQL: the ternary translates to a CASE WHEN. CleanName is
// the pre-normalized (lowercase, diacritic-stripped) form, so we score against it
@@ -193,6 +217,12 @@ public class SqlSearchProvider : IInternalSearchProvider
return query.Where(e => e.ParentId == pid || e.Parents!.Any(p => p.ParentItemId == pid));
}
+ private static IQueryable<BaseItemEntity> ExcludeVersionsOfMatchedPrimaries(IQueryable<BaseItemEntity> query)
+ {
+ var matched = query;
+ return query.Where(e => e.PrimaryVersionId == null || !matched.Any(p => p.Id == e.PrimaryVersionId));
+ }
+
private IQueryable<BaseItemEntity> ApplyUserAccessFilter(
JellyfinDbContext dbContext,
IQueryable<BaseItemEntity> query,