< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.Library.Search.SqlSearchProvider
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/Library/Search/SqlSearchProvider.cs
Line coverage
9%
Covered lines: 8
Uncovered lines: 73
Coverable lines: 81
Total lines: 230
Line coverage: 9.8%
Branch coverage
0%
Covered branches: 0
Total branches: 30
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 6/8/2026 - 12:16:15 AM Line coverage: 9.8% (8/81) Branch coverage: 0% (0/30) Total lines: 230 6/8/2026 - 12:16:15 AM Line coverage: 9.8% (8/81) Branch coverage: 0% (0/30) Total lines: 230

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
.ctor(...)100%11100%
get_Name()100%11100%
get_Type()100%210%
get_Priority()100%11100%
CanSearch(...)100%210%
SearchAsync()0%2040%
ApplyTypeFilter(...)0%7280%
ApplyMediaTypeFilter(...)0%620%
ApplyParentFilter(...)0%2040%
ApplyUserAccessFilter(...)0%4260%
MapKindsToTypeNames(...)0%4260%

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/Library/Search/SqlSearchProvider.cs

#LineLine coverage
 1#pragma warning disable RS0030 // Do not use banned APIs
 2#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string compari
 3
 4using System;
 5using System.Collections.Generic;
 6using System.Linq;
 7using System.Threading;
 8using System.Threading.Tasks;
 9using Jellyfin.Data.Enums;
 10using Jellyfin.Database.Implementations;
 11using Jellyfin.Database.Implementations.Entities;
 12using Jellyfin.Extensions;
 13using MediaBrowser.Controller.Entities;
 14using MediaBrowser.Controller.Library;
 15using MediaBrowser.Controller.Persistence;
 16using MediaBrowser.Model.Configuration;
 17using Microsoft.EntityFrameworkCore;
 18
 19namespace Emby.Server.Implementations.Library.Search;
 20
 21/// <summary>
 22/// Built-in SQL-based search provider that queries the library database directly.
 23/// </summary>
 24public class SqlSearchProvider : IInternalSearchProvider
 25{
 26    private const int DefaultSearchLimit = 100;
 27    private const float ExactMatchScore = 100f;
 28    private const float PrefixMatchScore = 80f;
 29    private const float WordPrefixMatchScore = 75f;
 30    private const float ContainsMatchScore = 50f;
 31
 032    private static readonly Guid _placeholderId = Guid.Parse("00000000-0000-0000-0000-000000000001");
 33
 34    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 35    private readonly IItemTypeLookup _itemTypeLookup;
 36    private readonly ILibraryManager _libraryManager;
 37    private readonly IUserManager _userManager;
 38    private readonly IItemQueryHelpers _queryHelpers;
 39
 40    /// <summary>
 41    /// Initializes a new instance of the <see cref="SqlSearchProvider"/> class.
 42    /// </summary>
 43    /// <param name="dbProvider">The database context factory.</param>
 44    /// <param name="itemTypeLookup">The item type lookup.</param>
 45    /// <param name="libraryManager">The library manager.</param>
 46    /// <param name="userManager">The user manager.</param>
 47    /// <param name="queryHelpers">The shared item query helpers.</param>
 48    public SqlSearchProvider(
 49        IDbContextFactory<JellyfinDbContext> dbProvider,
 50        IItemTypeLookup itemTypeLookup,
 51        ILibraryManager libraryManager,
 52        IUserManager userManager,
 53        IItemQueryHelpers queryHelpers)
 54    {
 2155        _dbProvider = dbProvider;
 2156        _itemTypeLookup = itemTypeLookup;
 2157        _libraryManager = libraryManager;
 2158        _userManager = userManager;
 2159        _queryHelpers = queryHelpers;
 2160    }
 61
 62    /// <inheritdoc/>
 2163    public string Name => "Database";
 64
 65    /// <inheritdoc/>
 066    public MetadataPluginType Type => MetadataPluginType.SearchProvider;
 67
 68    /// <inheritdoc/>
 2169    public int Priority => 100; // Low priority - runs as fallback
 70
 71    /// <inheritdoc/>
 72    public bool CanSearch(SearchProviderQuery query)
 73    {
 74        // SQL search can always handle any query
 075        return true;
 76    }
 77
 78    /// <inheritdoc/>
 79    public async Task<IReadOnlyList<SearchResult>> SearchAsync(SearchProviderQuery query, CancellationToken cancellation
 80    {
 081        ArgumentNullException.ThrowIfNull(query);
 082        ArgumentException.ThrowIfNullOrWhiteSpace(query.SearchTerm);
 83
 084        var rawSearchTerm = query.SearchTerm.Trim().RemoveDiacritics();
 085        if (string.IsNullOrEmpty(rawSearchTerm))
 86        {
 087            return [];
 88        }
 89
 090        var cleanSearchTerm = rawSearchTerm.GetCleanValue();
 091        if (string.IsNullOrEmpty(cleanSearchTerm))
 92        {
 093            return [];
 94        }
 95
 096        var cleanPrefix = cleanSearchTerm + " ";
 97        // OriginalTitle is stored mixed-case and isn't pre-normalized like CleanName,
 98        // so match it via a case-insensitive LIKE rather than a per-row case conversion
 99        // that may not translate to SQL on every provider.
 0100        var likeOriginal = $"%{rawSearchTerm}%";
 0101        var limit = query.Limit ?? DefaultSearchLimit;
 102
 0103        var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 0104        await using (dbContext.ConfigureAwait(false))
 105        {
 106            // Lightweight projection: select only what's needed to score and identify items.
 0107            var dbQuery = dbContext.BaseItems
 0108                .AsNoTracking()
 0109                .Where(e => e.Id != _placeholderId)
 0110                .Where(e => !e.IsVirtualItem)
 0111                .Where(e => e.CleanName!.Contains(cleanSearchTerm)
 0112                    || (e.OriginalTitle != null && EF.Functions.Like(e.OriginalTitle, likeOriginal)));
 113
 0114            dbQuery = ApplyTypeFilter(dbQuery, query.IncludeItemTypes, query.ExcludeItemTypes);
 0115            dbQuery = ApplyMediaTypeFilter(dbQuery, query.MediaTypes);
 0116            dbQuery = ApplyParentFilter(dbQuery, query.ParentId);
 0117            dbQuery = ApplyUserAccessFilter(dbContext, dbQuery, query.UserId);
 118
 119            // Compute the score in SQL: the ternary translates to a CASE WHEN. CleanName is
 120            // the pre-normalized (lowercase, diacritic-stripped) form, so we score against it
 121            // directly without any per-row case conversion. Items that match only via
 122            // OriginalTitle fall through to the Contains tier.
 123            // Tie-break by Id for deterministic ordering so the explicit OrderBy + Take
 124            // satisfies EF Core's row-limiting-with-OrderBy requirement.
 0125            var scored = dbQuery.Select(e => new
 0126            {
 0127                e.Id,
 0128                Score =
 0129                    (e.CleanName == cleanSearchTerm) ? ExactMatchScore
 0130                    : e.CleanName!.StartsWith(cleanSearchTerm) ? PrefixMatchScore
 0131                    : e.CleanName!.Contains(cleanPrefix) ? WordPrefixMatchScore
 0132                    : ContainsMatchScore
 0133            });
 134
 0135            return await scored
 0136                .OrderByDescending(x => x.Score)
 0137                .ThenBy(x => x.Id)
 0138                .Take(limit)
 0139                .Select(x => new SearchResult(x.Id, x.Score))
 0140                .ToArrayAsync(cancellationToken)
 0141                .ConfigureAwait(false);
 142        }
 0143    }
 144
 145    private IQueryable<BaseItemEntity> ApplyTypeFilter(
 146        IQueryable<BaseItemEntity> query,
 147        BaseItemKind[] includeItemTypes,
 148        BaseItemKind[] excludeItemTypes)
 149    {
 0150        if (includeItemTypes.Length > 0)
 151        {
 0152            var includeTypeNames = MapKindsToTypeNames(includeItemTypes);
 0153            if (includeTypeNames.Count > 0)
 154            {
 0155                query = query.Where(e => includeTypeNames.Contains(e.Type));
 156            }
 157        }
 0158        else if (excludeItemTypes.Length > 0)
 159        {
 0160            var excludeTypeNames = MapKindsToTypeNames(excludeItemTypes);
 0161            if (excludeTypeNames.Count > 0)
 162            {
 0163                query = query.Where(e => !excludeTypeNames.Contains(e.Type));
 164            }
 165        }
 166
 0167        return query;
 168    }
 169
 170    private static IQueryable<BaseItemEntity> ApplyMediaTypeFilter(
 171        IQueryable<BaseItemEntity> query,
 172        MediaType[] mediaTypes)
 173    {
 0174        if (mediaTypes.Length == 0)
 175        {
 0176            return query;
 177        }
 178
 0179        var mediaTypeNames = mediaTypes.Select(m => m.ToString()).ToArray();
 0180        return query.Where(e => e.MediaType != null && mediaTypeNames.Contains(e.MediaType));
 181    }
 182
 183    private static IQueryable<BaseItemEntity> ApplyParentFilter(
 184        IQueryable<BaseItemEntity> query,
 185        Guid? parentId)
 186    {
 0187        if (!parentId.HasValue || parentId.Value.IsEmpty())
 188        {
 0189            return query;
 190        }
 191
 0192        var pid = parentId.Value;
 0193        return query.Where(e => e.ParentId == pid || e.Parents!.Any(p => p.ParentItemId == pid));
 194    }
 195
 196    private IQueryable<BaseItemEntity> ApplyUserAccessFilter(
 197        JellyfinDbContext dbContext,
 198        IQueryable<BaseItemEntity> query,
 199        Guid? userId)
 200    {
 0201        if (!userId.HasValue || userId.Value.IsEmpty())
 202        {
 0203            return query;
 204        }
 205
 0206        var user = _userManager.GetUserById(userId.Value);
 0207        if (user is null)
 208        {
 0209            return query;
 210        }
 211
 0212        var accessFilter = new InternalItemsQuery(user);
 0213        _libraryManager.ConfigureUserAccess(accessFilter, user);
 0214        return _queryHelpers.ApplyAccessFiltering(dbContext, query, accessFilter);
 215    }
 216
 217    private List<string> MapKindsToTypeNames(BaseItemKind[] kinds)
 218    {
 0219        var list = new List<string>(kinds.Length);
 0220        foreach (var kind in kinds)
 221        {
 0222            if (_itemTypeLookup.BaseItemKindNames.TryGetValue(kind, out var name) && name is not null)
 223            {
 0224                list.Add(name);
 225            }
 226        }
 227
 0228        return list;
 229    }
 230}