Browse Source

optimize searching for shows, seasons and movies (#2768)

* load search logging level on startup

* optimize searching for shows, seasons and movies

* use season metadata directly
pull/2771/head
Jason Dove 4 months ago committed by GitHub
parent
commit
974020a98f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 21
      ErsatzTV.Application/Search/Queries/SearchMoviesHandler.cs
  3. 41
      ErsatzTV.Application/Search/Queries/SearchTelevisionSeasonsHandler.cs
  4. 22
      ErsatzTV.Application/Search/Queries/SearchTelevisionShowsHandler.cs
  5. 25
      ErsatzTV.Application/Search/Queries/SearchUsingSearchIndexHandler.cs
  6. 9
      ErsatzTV.Core/Interfaces/Search/ISearchIndex.cs
  7. 2
      ErsatzTV.Infrastructure.Tests/Search/SearchQueryParserTests.cs
  8. 34
      ErsatzTV.Infrastructure/Search/ElasticSearchIndex.cs
  9. 46
      ErsatzTV.Infrastructure/Search/LuceneSearchIndex.cs
  10. 3
      ErsatzTV.Infrastructure/Search/Models/ElasticSearchItem.cs
  11. 9
      ErsatzTV.Infrastructure/Search/SearchQueryParser.cs
  12. 8
      ErsatzTV/Services/RunOnce/LoadLoggingLevelService.cs

2
CHANGELOG.md

@ -15,6 +15,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -15,6 +15,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Use code signing on all Windows executables (`ErsatzTV-Windows.exe`, `ErsatzTV.exe`, `ErsatzTV.Scanner.exe`)
- Respect `z_index` (draw order) on all graphics element types
- Restore default UI font that was erroneously removed in v26.1.1
- Use configured searching log level on startup, instead of the default log level of `Information`
- MySql: fix searching for shows and seasons in schedule items editor
## [26.1.1] - 2026-01-08
### Fixed

21
ErsatzTV.Application/Search/Queries/SearchMoviesHandler.cs

@ -1,23 +1,32 @@ @@ -1,23 +1,32 @@
using System.Collections.Immutable;
using System.Globalization;
using Bugsnag;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Search;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Search;
public class SearchMoviesHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<SearchMovies, List<NamedMediaItemViewModel>>
public class SearchMoviesHandler(
IClient client,
ISearchIndex searchIndex,
IDbContextFactory<TvContext> dbContextFactory)
: SearchUsingSearchIndexHandler(client, searchIndex), IRequestHandler<SearchMovies, List<NamedMediaItemViewModel>>
{
public async Task<List<NamedMediaItemViewModel>> Handle(SearchMovies request, CancellationToken cancellationToken)
{
ImmutableHashSet<int> ids = await Search(LuceneSearchIndex.MovieType, request.Query, cancellationToken);
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.MovieMetadata
.TagWithCallSite()
.AsNoTracking()
.Where(m => EF.Functions.Like(m.Title + " " + m.Year, $"%{request.Query}%"))
.OrderBy(m => m.Title)
.ThenBy(m => m.Year)
.Take(10)
.Where(mm => ids.Contains(mm.MovieId))
.OrderBy(mm => mm.Title)
.ThenBy(mm => mm.Year)
.ToListAsync(cancellationToken)
.Map(list => list.Map(ToNamedMediaItem).ToList());
}

41
ErsatzTV.Application/Search/Queries/SearchTelevisionSeasonsHandler.cs

@ -1,30 +1,45 @@ @@ -1,30 +1,45 @@
using System.Collections.Immutable;
using System.Globalization;
using Bugsnag;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Search;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Search;
public class SearchTelevisionSeasonsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<SearchTelevisionSeasons, List<NamedMediaItemViewModel>>
public class SearchTelevisionSeasonsHandler(
IClient client,
ISearchIndex searchIndex,
IDbContextFactory<TvContext> dbContextFactory)
: SearchUsingSearchIndexHandler(client, searchIndex),
IRequestHandler<SearchTelevisionSeasons, List<NamedMediaItemViewModel>>
{
public async Task<List<NamedMediaItemViewModel>> Handle(
SearchTelevisionSeasons request,
CancellationToken cancellationToken)
{
ImmutableHashSet<int> ids = await Search(LuceneSearchIndex.SeasonType, request.Query, cancellationToken);
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await (from season in dbContext.Set<Season>()
join seasonMetadata in dbContext.Set<SeasonMetadata>()
on season.Id equals seasonMetadata.SeasonId
join showMetadata in dbContext.Set<ShowMetadata>()
on season.ShowId equals showMetadata.ShowId
where EF.Functions.Like(showMetadata.Title + " " + seasonMetadata.Title, $"%{request.Query}%")
orderby showMetadata.Title, season.SeasonNumber
select new TelevisionSeason(season.Id, showMetadata.Title, showMetadata.Year, season.SeasonNumber))
.Take(20)
return await dbContext.SeasonMetadata
.TagWithCallSite()
.AsNoTracking()
.Include(s => s.Season)
.ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
.Where(sm => ids.Contains(sm.SeasonId))
.ToListAsync(cancellationToken)
.Map(list => list.Map(ToNamedMediaItem).ToList());
.Map(list => list.Map(sm => new TelevisionSeason(
sm.SeasonId,
sm.Season.Show.ShowMetadata.HeadOrNone().Match(s => s.Title, string.Empty),
sm.Year,
sm.Season.SeasonNumber))
.OrderBy(s => s.Title)
.ThenBy(s => s.SeasonNumber)
.Map(ToNamedMediaItem)
.ToList());
}
private static NamedMediaItemViewModel ToNamedMediaItem(TelevisionSeason season) =>

22
ErsatzTV.Application/Search/Queries/SearchTelevisionShowsHandler.cs

@ -1,25 +1,35 @@ @@ -1,25 +1,35 @@
using System.Collections.Immutable;
using System.Globalization;
using Bugsnag;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Search;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Search;
public class SearchTelevisionShowsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<SearchTelevisionShows, List<NamedMediaItemViewModel>>
public class SearchTelevisionShowsHandler(
IClient client,
ISearchIndex searchIndex,
IDbContextFactory<TvContext> dbContextFactory)
: SearchUsingSearchIndexHandler(client, searchIndex),
IRequestHandler<SearchTelevisionShows, List<NamedMediaItemViewModel>>
{
public async Task<List<NamedMediaItemViewModel>> Handle(
SearchTelevisionShows request,
CancellationToken cancellationToken)
{
ImmutableHashSet<int> ids = await Search(LuceneSearchIndex.ShowType, request.Query, cancellationToken);
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.ShowMetadata
.TagWithCallSite()
.AsNoTracking()
.Where(s => EF.Functions.Like(s.Title + " " + s.Year, $"%{request.Query}%"))
.OrderBy(s => s.Title)
.ThenBy(s => s.Year)
.Take(10)
.Where(sm => ids.Contains(sm.ShowId))
.OrderBy(sm => sm.Title)
.ThenBy(sm => sm.Year)
.ToListAsync(cancellationToken)
.Map(list => list.Map(ToNamedMediaItem).ToList());
}

25
ErsatzTV.Application/Search/Queries/SearchUsingSearchIndexHandler.cs

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
using System.Collections.Immutable;
using Bugsnag;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Infrastructure.Search;
namespace ErsatzTV.Application.Search;
public abstract class SearchUsingSearchIndexHandler(IClient client, ISearchIndex searchIndex)
{
private const int PageSize = 10;
protected async Task<ImmutableHashSet<int>> Search(string type, string query, CancellationToken cancellationToken)
{
var searchResult = await searchIndex.Search(
client,
$"type:{type} AND *{query.Replace(" ", @"\ ")}*",
string.Empty,
0,
PageSize,
[LuceneSearchIndex.TitleAndYearSearchField],
cancellationToken);
return searchResult.Items.Select(i => i.Id).ToImmutableHashSet();
}
}

9
ErsatzTV.Core/Interfaces/Search/ISearchIndex.cs

@ -45,5 +45,14 @@ public interface ISearchIndex : IDisposable @@ -45,5 +45,14 @@ public interface ISearchIndex : IDisposable
int limit,
CancellationToken cancellationToken);
Task<SearchResult> Search(
IClient client,
string query,
string smartCollectionName,
int skip,
int limit,
List<string> defaultFields,
CancellationToken cancellationToken);
void Commit();
}

2
ErsatzTV.Infrastructure.Tests/Search/SearchQueryParserTests.cs

@ -22,7 +22,7 @@ public class SearchQueryParserTests @@ -22,7 +22,7 @@ public class SearchQueryParserTests
ISmartCollectionCache smartCollectionCache = Substitute.For<ISmartCollectionCache>();
var parser = new SearchQueryParser(smartCollectionCache, Substitute.For<ILogger<SearchQueryParser>>());
Query result = await parser.ParseQuery(input, null, CancellationToken.None);
Query result = await parser.ParseQuery(input, null, [LuceneSearchIndex.TitleField], CancellationToken.None);
result.ToString().ShouldBe(expected);
}
}

34
ErsatzTV.Infrastructure/Search/ElasticSearchIndex.cs

@ -48,7 +48,7 @@ public class ElasticSearchIndex : ISearchIndex @@ -48,7 +48,7 @@ public class ElasticSearchIndex : ISearchIndex
return exists.IsValidResponse;
}
public int Version => 48;
public int Version => 49;
public async Task<bool> Initialize(
ILocalFileSystem localFileSystem,
@ -167,18 +167,38 @@ public class ElasticSearchIndex : ISearchIndex @@ -167,18 +167,38 @@ public class ElasticSearchIndex : ISearchIndex
return deleteResponse.IsValidResponse;
}
public Task<SearchResult> Search(
IClient client,
string query,
string smartCollectionName,
int skip,
int limit,
CancellationToken cancellationToken) => Search(
client,
query,
smartCollectionName,
skip,
limit,
[LuceneSearchIndex.TitleField],
cancellationToken);
public async Task<SearchResult> Search(
IClient client,
string query,
string smartCollectionName,
int skip,
int limit,
List<string> defaultFields,
CancellationToken cancellationToken)
{
var items = new List<MinimalElasticSearchItem>();
var totalCount = 0;
Query parsedQuery = await _searchQueryParser.ParseQuery(query, smartCollectionName, cancellationToken);
Query parsedQuery = await _searchQueryParser.ParseQuery(
query,
smartCollectionName,
defaultFields,
cancellationToken);
ES.SearchResponse<MinimalElasticSearchItem> response = await _client.SearchAsync<MinimalElasticSearchItem>(
s => s.Indices(IndexName)
@ -225,6 +245,7 @@ public class ElasticSearchIndex : ISearchIndex @@ -225,6 +245,7 @@ public class ElasticSearchIndex : ISearchIndex
.Text(t => t.LibraryName, t => t.Store(false))
.Keyword(t => t.LibraryId, t => t.Store(false))
.Keyword(t => t.TitleAndYear, t => t.Store(false))
.Text(t => t.TitleAndYearSearch, t => t.Store(false))
.Keyword(t => t.JumpLetter, t => t.Store())
.Keyword(t => t.State, t => t.Store(false))
.Text(t => t.MetadataKind, t => t.Store(false))
@ -315,6 +336,7 @@ public class ElasticSearchIndex : ISearchIndex @@ -315,6 +336,7 @@ public class ElasticSearchIndex : ISearchIndex
LibraryName = movie.LibraryPath.Library.Name,
LibraryId = movie.LibraryPath.Library.Id,
TitleAndYear = LuceneSearchIndex.GetTitleAndYear(metadata),
TitleAndYearSearch = LuceneSearchIndex.GetTitleAndYearSearch(metadata),
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata),
State = movie.State.ToString(),
MetadataKind = metadata.MetadataKind.ToString(),
@ -374,6 +396,7 @@ public class ElasticSearchIndex : ISearchIndex @@ -374,6 +396,7 @@ public class ElasticSearchIndex : ISearchIndex
LibraryName = show.LibraryPath.Library.Name,
LibraryId = show.LibraryPath.Library.Id,
TitleAndYear = LuceneSearchIndex.GetTitleAndYear(metadata),
TitleAndYearSearch = LuceneSearchIndex.GetTitleAndYearSearch(metadata),
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata),
State = show.State.ToString(),
MetadataKind = metadata.MetadataKind.ToString(),
@ -440,6 +463,7 @@ public class ElasticSearchIndex : ISearchIndex @@ -440,6 +463,7 @@ public class ElasticSearchIndex : ISearchIndex
LibraryName = season.LibraryPath.Library.Name,
LibraryId = season.LibraryPath.Library.Id,
TitleAndYear = titleAndYear,
TitleAndYearSearch = LuceneSearchIndex.GetTitleAndYearSearch(metadata),
JumpLetter = LuceneSearchIndex.GetJumpLetter(showMetadata),
State = season.State.ToString(),
SeasonNumber = season.SeasonNumber,
@ -498,6 +522,7 @@ public class ElasticSearchIndex : ISearchIndex @@ -498,6 +522,7 @@ public class ElasticSearchIndex : ISearchIndex
LibraryName = artist.LibraryPath.Library.Name,
LibraryId = artist.LibraryPath.Library.Id,
TitleAndYear = LuceneSearchIndex.GetTitleAndYear(metadata),
TitleAndYearSearch = LuceneSearchIndex.GetTitleAndYearSearch(metadata),
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata),
State = artist.State.ToString(),
MetadataKind = metadata.MetadataKind.ToString(),
@ -545,6 +570,7 @@ public class ElasticSearchIndex : ISearchIndex @@ -545,6 +570,7 @@ public class ElasticSearchIndex : ISearchIndex
LibraryName = musicVideo.LibraryPath.Library.Name,
LibraryId = musicVideo.LibraryPath.Library.Id,
TitleAndYear = LuceneSearchIndex.GetTitleAndYear(metadata),
TitleAndYearSearch = LuceneSearchIndex.GetTitleAndYearSearch(metadata),
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata),
State = musicVideo.State.ToString(),
MetadataKind = metadata.MetadataKind.ToString(),
@ -626,6 +652,7 @@ public class ElasticSearchIndex : ISearchIndex @@ -626,6 +652,7 @@ public class ElasticSearchIndex : ISearchIndex
LibraryName = episode.LibraryPath.Library.Name,
LibraryId = episode.LibraryPath.Library.Id,
TitleAndYear = LuceneSearchIndex.GetTitleAndYear(metadata),
TitleAndYearSearch = LuceneSearchIndex.GetTitleAndYearSearch(metadata),
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata),
State = episode.State.ToString(),
MetadataKind = metadata.MetadataKind.ToString(),
@ -695,6 +722,7 @@ public class ElasticSearchIndex : ISearchIndex @@ -695,6 +722,7 @@ public class ElasticSearchIndex : ISearchIndex
LibraryName = otherVideo.LibraryPath.Library.Name,
LibraryId = otherVideo.LibraryPath.Library.Id,
TitleAndYear = LuceneSearchIndex.GetTitleAndYear(metadata),
TitleAndYearSearch = LuceneSearchIndex.GetTitleAndYearSearch(metadata),
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata),
State = otherVideo.State.ToString(),
MetadataKind = metadata.MetadataKind.ToString(),
@ -751,6 +779,7 @@ public class ElasticSearchIndex : ISearchIndex @@ -751,6 +779,7 @@ public class ElasticSearchIndex : ISearchIndex
LibraryName = song.LibraryPath.Library.Name,
LibraryId = song.LibraryPath.Library.Id,
TitleAndYear = LuceneSearchIndex.GetTitleAndYear(metadata),
TitleAndYearSearch = LuceneSearchIndex.GetTitleAndYearSearch(metadata),
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata),
State = song.State.ToString(),
MetadataKind = metadata.MetadataKind.ToString(),
@ -800,6 +829,7 @@ public class ElasticSearchIndex : ISearchIndex @@ -800,6 +829,7 @@ public class ElasticSearchIndex : ISearchIndex
LibraryName = image.LibraryPath.Library.Name,
LibraryId = image.LibraryPath.Library.Id,
TitleAndYear = LuceneSearchIndex.GetTitleAndYear(metadata),
TitleAndYearSearch = LuceneSearchIndex.GetTitleAndYearSearch(metadata),
JumpLetter = LuceneSearchIndex.GetJumpLetter(metadata),
State = image.State.ToString(),
MetadataKind = metadata.MetadataKind.ToString(),

46
ErsatzTV.Infrastructure/Search/LuceneSearchIndex.cs

@ -90,6 +90,7 @@ public sealed class LuceneSearchIndex : ISearchIndex @@ -90,6 +90,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
public const string SongType = "song";
public const string ImageType = "image";
public const string RemoteStreamType = "remote_stream";
public const string TitleAndYearSearchField = "title_and_year_search";
private readonly string _cleanShutdownPath;
private readonly List<CultureInfo> _cultureInfos;
@ -118,7 +119,7 @@ public sealed class LuceneSearchIndex : ISearchIndex @@ -118,7 +119,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
return Task.FromResult(directoryExists && fileExists);
}
public int Version => 49;
public int Version => 50;
public async Task<bool> Initialize(
ILocalFileSystem localFileSystem,
@ -209,12 +210,29 @@ public sealed class LuceneSearchIndex : ISearchIndex @@ -209,12 +210,29 @@ public sealed class LuceneSearchIndex : ISearchIndex
return Task.FromResult(true);
}
// default to title field only
public Task<SearchResult> Search(
IClient client,
string query,
string smartCollectionName,
int skip,
int limit,
CancellationToken cancellationToken) => Search(
client,
query,
smartCollectionName,
skip,
limit,
[TitleField],
cancellationToken);
public async Task<SearchResult> Search(
IClient client,
string query,
string smartCollectionName,
int skip,
int limit,
List<string> defaultFields,
CancellationToken cancellationToken)
{
var metadata = new Dictionary<string, string>
@ -237,7 +255,11 @@ public sealed class LuceneSearchIndex : ISearchIndex @@ -237,7 +255,11 @@ public sealed class LuceneSearchIndex : ISearchIndex
using DirectoryReader reader = _writer.GetReader(true);
var searcher = new IndexSearcher(reader);
int hitsLimit = limit == 0 ? searcher.IndexReader.MaxDoc : skip + limit;
Query parsedQuery = await _searchQueryParser.ParseQuery(query, smartCollectionName, cancellationToken);
Query parsedQuery = await _searchQueryParser.ParseQuery(
query,
smartCollectionName,
defaultFields,
cancellationToken);
// TODO: figure out if this is actually needed
// var filter = new DuplicateFilter(TitleAndYearField);
var sort = new Sort(new SortField(SortTitleField, SortFieldType.STRING));
@ -445,6 +467,7 @@ public sealed class LuceneSearchIndex : ISearchIndex @@ -445,6 +467,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
movie.LibraryPath.Library.Id.ToString(CultureInfo.InvariantCulture),
Field.Store.NO),
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
new TextField(TitleAndYearSearchField, GetTitleAndYearSearch(metadata), Field.Store.NO),
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES),
new StringField(StateField, movie.State.ToString(), Field.Store.NO),
new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO)
@ -642,6 +665,7 @@ public sealed class LuceneSearchIndex : ISearchIndex @@ -642,6 +665,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
show.LibraryPath.Library.Id.ToString(CultureInfo.InvariantCulture),
Field.Store.NO),
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
new TextField(TitleAndYearSearchField, GetTitleAndYearSearch(metadata), Field.Store.NO),
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES),
new StringField(StateField, show.State.ToString(), Field.Store.NO),
new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO)
@ -765,6 +789,7 @@ public sealed class LuceneSearchIndex : ISearchIndex @@ -765,6 +789,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
season.LibraryPath.Library.Id.ToString(CultureInfo.InvariantCulture),
Field.Store.NO),
new StringField(TitleAndYearField, titleAndYear, Field.Store.NO),
new TextField(TitleAndYearSearchField, GetTitleAndYearSearch(metadata), Field.Store.NO),
new StringField(JumpLetterField, GetJumpLetter(showMetadata), Field.Store.YES),
new StringField(StateField, season.State.ToString(), Field.Store.NO),
new Int32Field(SeasonNumberField, season.SeasonNumber, Field.Store.NO),
@ -879,6 +904,7 @@ public sealed class LuceneSearchIndex : ISearchIndex @@ -879,6 +904,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
artist.LibraryPath.Library.Id.ToString(CultureInfo.InvariantCulture),
Field.Store.NO),
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
new TextField(TitleAndYearSearchField, GetTitleAndYearSearch(metadata), Field.Store.NO),
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES),
new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO)
};
@ -948,6 +974,7 @@ public sealed class LuceneSearchIndex : ISearchIndex @@ -948,6 +974,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
musicVideo.LibraryPath.Library.Id.ToString(CultureInfo.InvariantCulture),
Field.Store.NO),
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
new TextField(TitleAndYearSearchField, GetTitleAndYearSearch(metadata), Field.Store.NO),
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES),
new StringField(StateField, musicVideo.State.ToString(), Field.Store.NO),
new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO)
@ -1061,6 +1088,7 @@ public sealed class LuceneSearchIndex : ISearchIndex @@ -1061,6 +1088,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
episode.LibraryPath.Library.Id.ToString(CultureInfo.InvariantCulture),
Field.Store.NO),
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
new TextField(TitleAndYearSearchField, GetTitleAndYearSearch(metadata), Field.Store.NO),
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES),
new StringField(StateField, episode.State.ToString(), Field.Store.NO),
new Int32Field(SeasonNumberField, episode.Season?.SeasonNumber ?? 0, Field.Store.NO),
@ -1213,6 +1241,7 @@ public sealed class LuceneSearchIndex : ISearchIndex @@ -1213,6 +1241,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
otherVideo.LibraryPath.Library.Id.ToString(CultureInfo.InvariantCulture),
Field.Store.NO),
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
new TextField(TitleAndYearSearchField, GetTitleAndYearSearch(metadata), Field.Store.NO),
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES),
new StringField(StateField, otherVideo.State.ToString(), Field.Store.NO),
new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO)
@ -1317,6 +1346,7 @@ public sealed class LuceneSearchIndex : ISearchIndex @@ -1317,6 +1346,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
song.LibraryPath.Library.Id.ToString(CultureInfo.InvariantCulture),
Field.Store.NO),
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
new TextField(TitleAndYearSearchField, GetTitleAndYearSearch(metadata), Field.Store.NO),
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES),
new StringField(StateField, song.State.ToString(), Field.Store.NO),
new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO)
@ -1392,6 +1422,7 @@ public sealed class LuceneSearchIndex : ISearchIndex @@ -1392,6 +1422,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
image.LibraryPath.Library.Id.ToString(CultureInfo.InvariantCulture),
Field.Store.NO),
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
new TextField(TitleAndYearSearchField, GetTitleAndYearSearch(metadata), Field.Store.NO),
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES),
new StringField(StateField, image.State.ToString(), Field.Store.NO),
new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO)
@ -1465,6 +1496,7 @@ public sealed class LuceneSearchIndex : ISearchIndex @@ -1465,6 +1496,7 @@ public sealed class LuceneSearchIndex : ISearchIndex
remoteStream.LibraryPath.Library.Id.ToString(CultureInfo.InvariantCulture),
Field.Store.NO),
new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO),
new TextField(TitleAndYearSearchField, GetTitleAndYearSearch(metadata), Field.Store.NO),
new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES),
new StringField(StateField, remoteStream.State.ToString(), Field.Store.NO),
new TextField(MetadataKindField, metadata.MetadataKind.ToString(), Field.Store.NO)
@ -1608,6 +1640,16 @@ public sealed class LuceneSearchIndex : ISearchIndex @@ -1608,6 +1640,16 @@ public sealed class LuceneSearchIndex : ISearchIndex
_ => $"{Title(metadata)}_{metadata.Year}".ToLowerInvariant()
};
internal static string GetTitleAndYearSearch(Core.Domain.Metadata metadata) =>
metadata switch
{
MovieMetadata mm => $"{mm.Title ?? string.Empty} {mm.Year}".ToLowerInvariant(),
SeasonMetadata sm => $"{sm.Season.Show.ShowMetadata.Head().Title ?? string.Empty} {sm.Title}"
.ToLowerInvariant(),
ShowMetadata sm => $"{sm.Title ?? string.Empty} {sm.Year}".ToLowerInvariant(),
_ => $"{metadata.Title ?? string.Empty} {metadata.Year}".ToLowerInvariant()
};
private static string Title(Core.Domain.Metadata metadata) =>
(metadata.Title ?? string.Empty).Replace(' ', '_');

3
ErsatzTV.Infrastructure/Search/Models/ElasticSearchItem.cs

@ -19,6 +19,9 @@ public class ElasticSearchItem : MinimalElasticSearchItem @@ -19,6 +19,9 @@ public class ElasticSearchItem : MinimalElasticSearchItem
[JsonPropertyName(LuceneSearchIndex.TitleAndYearField)]
public string TitleAndYear { get; set; }
[JsonPropertyName(LuceneSearchIndex.TitleAndYearSearchField)]
public string TitleAndYearSearch { get; set; }
[JsonPropertyName(LuceneSearchIndex.StateField)]
public string State { get; set; }

9
ErsatzTV.Infrastructure/Search/SearchQueryParser.cs

@ -41,6 +41,7 @@ public partial class SearchQueryParser(ISmartCollectionCache smartCollectionCach @@ -41,6 +41,7 @@ public partial class SearchQueryParser(ISmartCollectionCache smartCollectionCach
{ LuceneSearchIndex.TagFullField, keywordAnalyzer },
{ LuceneSearchIndex.CollectionField, lowercaseKeywordAnalyzer },
{ LuceneSearchIndex.TitleAndYearSearchField, lowercaseKeywordAnalyzer },
{ LuceneSearchIndex.PlotField, new StandardAnalyzer(LuceneSearchIndex.AppLuceneVersion) }
};
@ -48,7 +49,11 @@ public partial class SearchQueryParser(ISmartCollectionCache smartCollectionCach @@ -48,7 +49,11 @@ public partial class SearchQueryParser(ISmartCollectionCache smartCollectionCach
return new PerFieldAnalyzerWrapper(defaultAnalyzer, customAnalyzers);
}
public async Task<Query> ParseQuery(string query, string smartCollectionName, CancellationToken cancellationToken)
public async Task<Query> ParseQuery(
string query,
string smartCollectionName,
List<string> defaultFields,
CancellationToken cancellationToken)
{
string parsedQuery = query;
@ -92,7 +97,7 @@ public partial class SearchQueryParser(ISmartCollectionCache smartCollectionCach @@ -92,7 +97,7 @@ public partial class SearchQueryParser(ISmartCollectionCache smartCollectionCach
var parser = new CustomMultiFieldQueryParser(
LuceneSearchIndex.AppLuceneVersion,
[LuceneSearchIndex.TitleField],
defaultFields.ToArray(),
analyzerWrapper)
{
AllowLeadingWildcard = true

8
ErsatzTV/Services/RunOnce/LoadLoggingLevelService.cs

@ -54,6 +54,14 @@ public class LoadLoggingLevelService : BackgroundService @@ -54,6 +54,14 @@ public class LoadLoggingLevelService : BackgroundService
loggingLevelSwitches.SchedulingLevelSwitch.MinimumLevel = logLevel;
}
foreach (LogEventLevel logLevel in await configElementRepository.GetValue<LogEventLevel>(
ConfigElementKey.MinimumLogLevelSearching, stoppingToken))
{
LoggingLevelSwitches loggingLevelSwitches =
scope.ServiceProvider.GetRequiredService<LoggingLevelSwitches>();
loggingLevelSwitches.SearchingLevelSwitch.MinimumLevel = logLevel;
}
foreach (LogEventLevel logLevel in await configElementRepository.GetValue<LogEventLevel>(
ConfigElementKey.MinimumLogLevelStreaming, stoppingToken))
{

Loading…
Cancel
Save