Add sorts to search items

This commit is contained in:
Zoe Roux
2023-10-31 21:10:35 +01:00
parent 68a3af0b52
commit 377d85c7f1
9 changed files with 198 additions and 159 deletions
+25 -20
View File
@@ -16,7 +16,6 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
@@ -33,7 +32,7 @@ namespace Kyoo.Core.Api
/// An endpoint to search for every resources of kyoo. Searching for only a specific type of resource
/// is available on the said endpoint.
/// </summary>
[Route("search/{query?}")]
[Route("search")]
[ApiController]
[ResourceView]
[ApiDefinition("Search", Group = ResourcesGroup)]
@@ -54,7 +53,7 @@ namespace Kyoo.Core.Api
/// <remarks>
/// Search for collections
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <param name="q">The query to search for.</param>
/// <param name="sortBy">Sort information about the query (sort by, sort order).</param>
/// <param name="pagination">How many items per page should be returned, where should the page start...</param>
/// <param name="fields">The aditional fields to include in the result.</param>
@@ -64,12 +63,13 @@ namespace Kyoo.Core.Api
[Permission(nameof(Collection), Kind.Read)]
[ApiDefinition("Collections")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<SearchPage<Collection>> SearchCollections(string? query,
public async Task<SearchPage<Collection>> SearchCollections(
[FromQuery] string? q,
[FromQuery] Sort<Collection> sortBy,
[FromQuery] SearchPagination pagination,
[FromQuery] Include<Collection> fields)
{
return SearchPage(await _searchManager.SearchCollections(query, sortBy, pagination, fields));
return SearchPage(await _searchManager.SearchCollections(q, sortBy, pagination, fields));
}
/// <summary>
@@ -78,7 +78,7 @@ namespace Kyoo.Core.Api
/// <remarks>
/// Search for shows
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <param name="q">The query to search for.</param>
/// <param name="sortBy">Sort information about the query (sort by, sort order).</param>
/// <param name="pagination">How many items per page should be returned, where should the page start...</param>
/// <param name="fields">The aditional fields to include in the result.</param>
@@ -88,12 +88,13 @@ namespace Kyoo.Core.Api
[Permission(nameof(Show), Kind.Read)]
[ApiDefinition("Show")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<SearchPage<Show>> SearchShows(string? query,
public async Task<SearchPage<Show>> SearchShows(
[FromQuery] string? q,
[FromQuery] Sort<Show> sortBy,
[FromQuery] SearchPagination pagination,
[FromQuery] Include<Show> fields)
{
return SearchPage(await _searchManager.SearchShows(query, sortBy, pagination, fields));
return SearchPage(await _searchManager.SearchShows(q, sortBy, pagination, fields));
}
/// <summary>
@@ -102,7 +103,7 @@ namespace Kyoo.Core.Api
/// <remarks>
/// Search for movie
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <param name="q">The query to search for.</param>
/// <param name="sortBy">Sort information about the query (sort by, sort order).</param>
/// <param name="pagination">How many items per page should be returned, where should the page start...</param>
/// <param name="fields">The aditional fields to include in the result.</param>
@@ -112,12 +113,13 @@ namespace Kyoo.Core.Api
[Permission(nameof(Movie), Kind.Read)]
[ApiDefinition("Movie")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<SearchPage<Movie>> SearchMovies(string? query,
public async Task<SearchPage<Movie>> SearchMovies(
[FromQuery] string? q,
[FromQuery] Sort<Movie> sortBy,
[FromQuery] SearchPagination pagination,
[FromQuery] Include<Movie> fields)
{
return SearchPage(await _searchManager.SearchMovies(query, sortBy, pagination, fields));
return SearchPage(await _searchManager.SearchMovies(q, sortBy, pagination, fields));
}
/// <summary>
@@ -126,7 +128,7 @@ namespace Kyoo.Core.Api
/// <remarks>
/// Search for items
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <param name="q">The query to search for.</param>
/// <param name="sortBy">Sort information about the query (sort by, sort order).</param>
/// <param name="pagination">How many items per page should be returned, where should the page start...</param>
/// <param name="fields">The aditional fields to include in the result.</param>
@@ -136,12 +138,13 @@ namespace Kyoo.Core.Api
[Permission(nameof(LibraryItem), Kind.Read)]
[ApiDefinition("Item")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<SearchPage<LibraryItem>> SearchItems(string? query,
public async Task<SearchPage<LibraryItem>> SearchItems(
[FromQuery] string? q,
[FromQuery] Sort<LibraryItem> sortBy,
[FromQuery] SearchPagination pagination,
[FromQuery] Include<LibraryItem> fields)
{
return SearchPage(await _searchManager.SearchItems(query, sortBy, pagination, fields));
return SearchPage(await _searchManager.SearchItems(q, sortBy, pagination, fields));
}
/// <summary>
@@ -150,7 +153,7 @@ namespace Kyoo.Core.Api
/// <remarks>
/// Search for episodes
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <param name="q">The query to search for.</param>
/// <param name="sortBy">Sort information about the query (sort by, sort order).</param>
/// <param name="pagination">How many items per page should be returned, where should the page start...</param>
/// <param name="fields">The aditional fields to include in the result.</param>
@@ -160,12 +163,13 @@ namespace Kyoo.Core.Api
[Permission(nameof(Episode), Kind.Read)]
[ApiDefinition("Episodes")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<SearchPage<Episode>> SearchEpisodes(string? query,
public async Task<SearchPage<Episode>> SearchEpisodes(
[FromQuery] string? q,
[FromQuery] Sort<Episode> sortBy,
[FromQuery] SearchPagination pagination,
[FromQuery] Include<Episode> fields)
{
return SearchPage(await _searchManager.SearchEpisodes(query, sortBy, pagination, fields));
return SearchPage(await _searchManager.SearchEpisodes(q, sortBy, pagination, fields));
}
/// <summary>
@@ -174,7 +178,7 @@ namespace Kyoo.Core.Api
/// <remarks>
/// Search for studios
/// </remarks>
/// <param name="query">The query to search for.</param>
/// <param name="q">The query to search for.</param>
/// <param name="sortBy">Sort information about the query (sort by, sort order).</param>
/// <param name="pagination">How many items per page should be returned, where should the page start...</param>
/// <param name="fields">The aditional fields to include in the result.</param>
@@ -184,12 +188,13 @@ namespace Kyoo.Core.Api
[Permission(nameof(Studio), Kind.Read)]
[ApiDefinition("Studios")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<SearchPage<Studio>> SearchStudios(string? query,
public async Task<SearchPage<Studio>> SearchStudios(
[FromQuery] string? q,
[FromQuery] Sort<Studio> sortBy,
[FromQuery] SearchPagination pagination,
[FromQuery] Include<Studio> fields)
{
return SearchPage(await _searchManager.SearchStudios(query, sortBy, pagination, fields));
return SearchPage(await _searchManager.SearchStudios(q, sortBy, pagination, fields));
}
}
}
+92 -78
View File
@@ -33,6 +33,93 @@ namespace Kyoo.Meiliseach
private readonly IConfiguration _configuration;
public static Dictionary<string, Settings> IndexSettings => new()
{
{
"items",
new Settings()
{
SearchableAttributes = new[]
{
CamelCase.ConvertName(nameof(LibraryItem.Name)),
CamelCase.ConvertName(nameof(LibraryItem.Slug)),
CamelCase.ConvertName(nameof(LibraryItem.Aliases)),
CamelCase.ConvertName(nameof(LibraryItem.Path)),
CamelCase.ConvertName(nameof(LibraryItem.Tags)),
CamelCase.ConvertName(nameof(LibraryItem.Overview)),
},
FilterableAttributes = new[]
{
CamelCase.ConvertName(nameof(LibraryItem.Genres)),
CamelCase.ConvertName(nameof(LibraryItem.Status)),
CamelCase.ConvertName(nameof(LibraryItem.AirDate)),
CamelCase.ConvertName(nameof(Movie.StudioID)),
CamelCase.ConvertName(nameof(LibraryItem.Kind)),
},
SortableAttributes = new[]
{
CamelCase.ConvertName(nameof(LibraryItem.AirDate)),
CamelCase.ConvertName(nameof(LibraryItem.AddedDate)),
},
DisplayedAttributes = new[]
{
CamelCase.ConvertName(nameof(LibraryItem.Id)),
CamelCase.ConvertName(nameof(LibraryItem.Kind)),
},
// TODO: Add stopwords
// TODO: Extend default ranking to add ratings.
}
},
{
nameof(Episode),
new Settings()
{
SearchableAttributes = new[]
{
CamelCase.ConvertName(nameof(Episode.Name)),
CamelCase.ConvertName(nameof(Episode.Overview)),
CamelCase.ConvertName(nameof(Episode.Slug)),
CamelCase.ConvertName(nameof(Episode.Path)),
},
FilterableAttributes = new[]
{
CamelCase.ConvertName(nameof(Episode.SeasonNumber)),
},
SortableAttributes = new[]
{
CamelCase.ConvertName(nameof(Episode.ReleaseDate)),
CamelCase.ConvertName(nameof(Episode.AddedDate)),
CamelCase.ConvertName(nameof(Episode.SeasonNumber)),
CamelCase.ConvertName(nameof(Episode.EpisodeNumber)),
CamelCase.ConvertName(nameof(Episode.AbsoluteNumber)),
},
DisplayedAttributes = new[]
{
CamelCase.ConvertName(nameof(Episode.Id)),
},
// TODO: Add stopwords
}
},
{
nameof(Studio),
new Settings()
{
SearchableAttributes = new[]
{
CamelCase.ConvertName(nameof(Studio.Name)),
CamelCase.ConvertName(nameof(Studio.Slug)),
},
FilterableAttributes = Array.Empty<string>(),
SortableAttributes = Array.Empty<string>(),
DisplayedAttributes = new[]
{
CamelCase.ConvertName(nameof(Studio.Id)),
},
// TODO: Add stopwords
}
},
};
public MeilisearchModule(IConfiguration configuration)
{
_configuration = configuration;
@@ -47,89 +134,16 @@ namespace Kyoo.Meiliseach
{
MeilisearchClient client = provider.GetRequiredService<MeilisearchClient>();
await _CreateIndex(client, "items", true, new Settings()
{
SearchableAttributes = new[]
{
CamelCase.ConvertName(nameof(LibraryItem.Name)),
CamelCase.ConvertName(nameof(LibraryItem.Slug)),
CamelCase.ConvertName(nameof(LibraryItem.Aliases)),
CamelCase.ConvertName(nameof(LibraryItem.Path)),
CamelCase.ConvertName(nameof(LibraryItem.Tags)),
// Overview could be included as well but I think it would be better without.
},
FilterableAttributes = new[]
{
CamelCase.ConvertName(nameof(LibraryItem.Genres)),
CamelCase.ConvertName(nameof(LibraryItem.Status)),
CamelCase.ConvertName(nameof(LibraryItem.AirDate)),
CamelCase.ConvertName(nameof(Movie.StudioID)),
CamelCase.ConvertName(nameof(LibraryItem.Kind)),
},
SortableAttributes = new[]
{
CamelCase.ConvertName(nameof(LibraryItem.AirDate)),
CamelCase.ConvertName(nameof(LibraryItem.AddedDate)),
},
DisplayedAttributes = new[]
{
CamelCase.ConvertName(nameof(LibraryItem.Id)),
CamelCase.ConvertName(nameof(LibraryItem.Kind)),
},
// TODO: Add stopwords
// TODO: Extend default ranking to add ratings.
});
await _CreateIndex(client, nameof(Episode), false, new Settings()
{
SearchableAttributes = new[]
{
CamelCase.ConvertName(nameof(Episode.Name)),
CamelCase.ConvertName(nameof(Episode.Overview)),
CamelCase.ConvertName(nameof(Episode.Slug)),
CamelCase.ConvertName(nameof(Episode.Path)),
},
FilterableAttributes = new[]
{
CamelCase.ConvertName(nameof(Episode.SeasonNumber)),
},
SortableAttributes = new[]
{
CamelCase.ConvertName(nameof(Episode.ReleaseDate)),
CamelCase.ConvertName(nameof(Episode.AddedDate)),
CamelCase.ConvertName(nameof(Episode.SeasonNumber)),
CamelCase.ConvertName(nameof(Episode.EpisodeNumber)),
CamelCase.ConvertName(nameof(Episode.AbsoluteNumber)),
},
DisplayedAttributes = new[]
{
CamelCase.ConvertName(nameof(Episode.Id)),
},
// TODO: Add stopwords
});
await _CreateIndex(client, nameof(Studio), false, new Settings()
{
SearchableAttributes = new[]
{
CamelCase.ConvertName(nameof(Studio.Name)),
CamelCase.ConvertName(nameof(Studio.Slug)),
},
FilterableAttributes = Array.Empty<string>(),
SortableAttributes = Array.Empty<string>(),
DisplayedAttributes = new[]
{
CamelCase.ConvertName(nameof(Studio.Id)),
},
// TODO: Add stopwords
});
await _CreateIndex(client, "items", true);
await _CreateIndex(client, nameof(Episode), false);
await _CreateIndex(client, nameof(Studio), false);
}
private static async Task _CreateIndex(MeilisearchClient client, string index, bool hasKind, Settings opts)
private static async Task _CreateIndex(MeilisearchClient client, string index, bool hasKind)
{
TaskInfo task = await client.CreateIndexAsync(index, hasKind ? "ref" : CamelCase.ConvertName(nameof(IResource.Id)));
await client.WaitForTaskAsync(task.TaskUid);
await client.Index(index).UpdateSettingsAsync(opts);
await client.Index(index).UpdateSettingsAsync(IndexSettings[index]);
}
/// <inheritdoc />
+7 -5
View File
@@ -32,13 +32,15 @@ public class SearchManager : ISearchManager
private readonly MeilisearchClient _client;
private readonly ILibraryManager _libraryManager;
private static IEnumerable<string> _GetSortsBy<T>(Sort<T>? sort)
private static IEnumerable<string> _GetSortsBy<T>(string index, Sort<T>? sort)
{
return sort switch
{
Sort<T>.Default => Array.Empty<string>(),
Sort<T>.By @sortBy => new[] { $"{sortBy.Key}:{(sortBy.Desendant ? "desc" : "asc")}" },
Sort<T>.Conglomerate(var list) => list.SelectMany(_GetSortsBy),
Sort<T>.By @sortBy => MeilisearchModule.IndexSettings[index].SortableAttributes.Contains(sortBy.Key, StringComparer.InvariantCultureIgnoreCase)
? new[] { $"{CamelCase.ConvertName(sortBy.Key)}:{(sortBy.Desendant ? "desc" : "asc")}" }
: throw new ValidationException($"Invalid sorting mode: {sortBy.Key}"),
Sort<T>.Conglomerate(var list) => list.SelectMany(x => _GetSortsBy(index, x)),
Sort<T>.Random => throw new ValidationException("Random sorting is not supported while searching."),
_ => Array.Empty<string>(),
};
@@ -106,7 +108,7 @@ public class SearchManager : ISearchManager
ISearchable<IdResource> res = await _client.Index(index).SearchAsync<IdResource>(query, new SearchQuery()
{
Filter = where,
Sort = _GetSortsBy(sortBy),
Sort = _GetSortsBy(index, sortBy),
Limit = pagination?.Limit ?? 50,
Offset = pagination?.Skip ?? 0,
});
@@ -126,7 +128,7 @@ public class SearchManager : ISearchManager
// TODO: add filters and facets
ISearchable<IdResource> res = await _client.Index("items").SearchAsync<IdResource>(query, new SearchQuery()
{
Sort = _GetSortsBy(sortBy),
Sort = _GetSortsBy("items", sortBy),
Limit = pagination?.Limit ?? 50,
Offset = pagination?.Skip ?? 0,
});