Skip to content

Commit

Permalink
feat: add fuzzy search to series episode endpoint
Browse files Browse the repository at this point in the history
in api v3
  • Loading branch information
revam committed Apr 1, 2023
1 parent e12f34c commit eea3a9b
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 39 deletions.
27 changes: 25 additions & 2 deletions Shoko.Server/API/v3/Controllers/TreeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Shoko.Models.Enums;
using Shoko.Plugin.Abstractions.DataModels;
using Shoko.Plugin.Abstractions.Extensions;
using Shoko.Server.API.Annotations;
using Shoko.Server.API.ModelBinders;
using Shoko.Server.API.v3.Helpers;
Expand All @@ -13,6 +15,7 @@
using Shoko.Server.Models;
using Shoko.Server.Repositories;
using Shoko.Server.Settings;
using Shoko.Server.Utilities;

using EpisodeType = Shoko.Server.API.v3.Models.Shoko.EpisodeType;
using AniDBEpisodeType = Shoko.Models.Enums.EpisodeType;
Expand Down Expand Up @@ -538,14 +541,16 @@ public ActionResult<Series> GetMainSeriesInGroup([FromRoute] int groupID, [FromQ
/// <param name="includeDataFrom">Include data from selected <see cref="DataSource"/>s.</param>
/// <param name="includeWatched">Include watched episodes in the list.</param>
/// <param name="type">Filter episodes by the specified <see cref="EpisodeType"/>s.</param>
/// <param name="search">An optional search query to filter episodes based on their titles.</param>
/// <returns>A list of episodes based on the specified filters.</returns>
[HttpGet("Series/{seriesID}/Episode")]
public ActionResult<ListResult<Episode>> GetEpisodes([FromRoute] int seriesID,
[FromQuery] [Range(0, 1000)] int pageSize = 20, [FromQuery] [Range(1, int.MaxValue)] int page = 1,
[FromQuery] IncludeOnlyFilter includeMissing = IncludeOnlyFilter.False, [FromQuery] IncludeOnlyFilter includeHidden = IncludeOnlyFilter.False,
[FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet<DataSource> includeDataFrom = null,
[FromQuery] IncludeOnlyFilter includeWatched = IncludeOnlyFilter.True,
[FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet<EpisodeType> type = null)
[FromQuery, ModelBinder(typeof(CommaDelimitedModelBinder))] HashSet<EpisodeType> type = null,
[FromQuery] string search = null)
{
var series = RepoFactory.AnimeSeries.GetByID(seriesID);
if (series == null)
Expand All @@ -558,7 +563,25 @@ [FromQuery] [Range(0, 1000)] int pageSize = 20, [FromQuery] [Range(1, int.MaxVal
return Forbid(SeriesController.SeriesForbiddenForUser);
}

return series.GetAnimeEpisodes(orderList: true, includeHidden: includeHidden != IncludeOnlyFilter.False)
IEnumerable<SVR_AnimeEpisode> episodes = series.GetAnimeEpisodes(orderList: true, includeHidden: includeHidden != IncludeOnlyFilter.False);
if (!string.IsNullOrEmpty(search))
{
var languages = SettingsProvider.GetSettings()
.LanguagePreference
.Select(lang => lang.GetTitleLanguage())
.Concat(new TitleLanguage[] { TitleLanguage.English, TitleLanguage.Romaji })
.ToHashSet();
episodes = episodes.FuzzySearch(
"",
ep => RepoFactory.AniDB_Episode_Title.GetByEpisodeID(ep.AniDB_EpisodeID)
.Where(title => title != null && languages.Contains(title.Language))
.Select(title => title.Title)
.ToList()
)
.Select(a => a.Result);
}

return episodes
.Where(a =>
{
// Filter by hidden state, if spesified
Expand Down
95 changes: 58 additions & 37 deletions Shoko.Server/Utilities/SeriesSearch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,54 +141,75 @@ private static SearchResult<List<SVR_AnimeSeries>> CheckTitlesIndexOf(IGrouping<
return dist;
}

public static List<SearchResult<T>> SearchCollection<T>(string query, IEnumerable<T> list,
/// <summary>
/// Searches a collection of items based on a search query using fuzzy search.
/// </summary>
/// <typeparam name="T">The type of the items in the collection.</typeparam>
/// <param name="query">The search query used to filter the collection.</param>
/// <param name="list">The collection of items to be searched.</param>
/// <param name="selector">A function that takes an item of type T and returns a list of strings that represent searchable properties of the item.</param>
/// <returns>A list of search results containing the matched items and their search-related information, such as index, distance, and exact match status.</returns>
public static IOrderedEnumerable<SearchResult<T>> SearchCollection<T>(string query, IEnumerable<T> list,
Func<T, List<string>> selector)
{
var parallelList = list.ToList().AsParallel();
var results = parallelList.Select(a =>
{
var titles = selector(a);
SearchResult<T> dist = null;
foreach (var title in titles)
return list.ToList().AsParallel()
.Select(a =>
{
if (string.IsNullOrEmpty(title))
var titles = selector(a);
SearchResult<T> dist = null;
foreach (var title in titles)
{
continue;
}
if (string.IsNullOrEmpty(title))
{
continue;
}

var k = Math.Max(Math.Min((int)(title.Length / 6D), (int)(query.Length / 6D)), 1);
if (query.Length <= 4 || title.Length <= 4)
{
k = 0;
}
var k = Math.Max(Math.Min((int)(title.Length / 6D), (int)(query.Length / 6D)), 1);
if (query.Length <= 4 || title.Length <= 4)
{
k = 0;
}

var result =
Misc.DiceFuzzySearch(title, query, k, a);
if (result.Index == -1)
{
continue;
}
var result =
Misc.DiceFuzzySearch(title, query, k, a);
if (result.Index == -1)
{
continue;
}

var searchGrouping = new SearchResult<T>
{
Distance = result.Distance,
Index = result.Index,
ExactMatch = result.ExactMatch,
Match = title,
Result = a
};
if (result.Distance < (dist?.Distance ?? int.MaxValue))
{
dist = searchGrouping;
var searchGrouping = new SearchResult<T>
{
Distance = result.Distance,
Index = result.Index,
ExactMatch = result.ExactMatch,
Match = title,
Result = a
};
if (result.Distance < (dist?.Distance ?? int.MaxValue))
{
dist = searchGrouping;
}
}
}

return dist;
}).Where(a => a != null && a.Index != -1).ToList().OrderBy(a => a.Index).ThenBy(a => a.Distance).ToList();

return results;
return dist;
})
.Where(a => a != null && a.Index != -1)
.ToList()
.OrderBy(a => a.Index)
.ThenBy(a => a.Distance);
}

/// <summary>
/// Performs a fuzzy search on an enumerable collection.
/// </summary>
/// <typeparam name="T">The type of the items in the enumerable collection.</typeparam>
/// <param name="enumerable">The enumerable collection to be searched.</param>
/// <param name="query">The search query used to filter the collection.</param>
/// <param name="selector">A function that takes an item of type T and returns a list of strings that represent searchable properties of the item.</param>
/// <returns>An ordered enumerable of search results containing the matched items and their search-related information
public static IOrderedEnumerable<SearchResult<T>> FuzzySearch<T>(this IEnumerable<T> enumerable, string query, Func<T, List<string>> selector)
=> SearchCollection(query, enumerable, selector);

/// <summary>
/// Search for series with given query in name or tag
/// </summary>
Expand Down

0 comments on commit eea3a9b

Please sign in to comment.