Merge pull request #25679 from peppy/song-select-search-performance

Improve song select search performance
This commit is contained in:
Bartłomiej Dach 2023-12-05 11:06:31 +01:00 committed by GitHub
commit 566d336470
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 59 additions and 56 deletions

View File

@ -1,8 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Localisation;
using osu.Game.Screens.Select;
namespace osu.Game.Beatmaps
{
@ -29,20 +29,22 @@ namespace osu.Game.Beatmaps
return new RomanisableString($"{metadata.GetPreferred(true)}".Trim(), $"{metadata.GetPreferred(false)}".Trim());
}
public static List<string> GetSearchableTerms(this IBeatmapInfo beatmapInfo)
public static bool Match(this IBeatmapInfo beatmapInfo, params FilterCriteria.OptionalTextFilter[] filters)
{
var termsList = new List<string>(BeatmapMetadataInfoExtensions.MAX_SEARCHABLE_TERM_COUNT + 1);
addIfNotNull(beatmapInfo.DifficultyName);
BeatmapMetadataInfoExtensions.CollectSearchableTerms(beatmapInfo.Metadata, termsList);
return termsList;
void addIfNotNull(string? s)
foreach (var filter in filters)
{
if (!string.IsNullOrEmpty(s))
termsList.Add(s);
if (filter.Matches(beatmapInfo.DifficultyName))
continue;
if (BeatmapMetadataInfoExtensions.Match(beatmapInfo.Metadata, filter))
continue;
// failed to match a single filter at all - fail the whole match.
return false;
}
// got through all filters without failing any - pass the whole match.
return true;
}
private static string getVersionString(IBeatmapInfo beatmapInfo) => string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? string.Empty : $"[{beatmapInfo.DifficultyName}]";

View File

@ -3,11 +3,14 @@
using System.Collections.Generic;
using osu.Framework.Localisation;
using osu.Game.Screens.Select;
namespace osu.Game.Beatmaps
{
public static class BeatmapMetadataInfoExtensions
{
internal const int MAX_SEARCHABLE_TERM_COUNT = 7;
/// <summary>
/// An array of all searchable terms provided in contained metadata.
/// </summary>
@ -18,7 +21,18 @@ namespace osu.Game.Beatmaps
return termsList.ToArray();
}
internal const int MAX_SEARCHABLE_TERM_COUNT = 7;
public static bool Match(IBeatmapMetadataInfo metadataInfo, FilterCriteria.OptionalTextFilter filter)
{
if (filter.Matches(metadataInfo.Author.Username)) return true;
if (filter.Matches(metadataInfo.Artist)) return true;
if (filter.Matches(metadataInfo.ArtistUnicode)) return true;
if (filter.Matches(metadataInfo.Title)) return true;
if (filter.Matches(metadataInfo.TitleUnicode)) return true;
if (filter.Matches(metadataInfo.Source)) return true;
if (filter.Matches(metadataInfo.Tags)) return true;
return false;
}
internal static void CollectSearchableTerms(IBeatmapMetadataInfo metadataInfo, IList<string> termsList)
{

View File

@ -41,6 +41,21 @@ namespace osu.Game.Screens.Select.Carousel
return match;
}
if (criteria.SearchTerms.Length > 0)
{
match = BeatmapInfo.Match(criteria.SearchTerms);
// if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs.
// this should be done after text matching so we can prioritise matching numbers in metadata.
if (!match && criteria.SearchNumber.HasValue)
{
match = (BeatmapInfo.OnlineID == criteria.SearchNumber.Value) ||
(BeatmapInfo.BeatmapSet?.OnlineID == criteria.SearchNumber.Value);
}
}
if (!match) return false;
match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(BeatmapInfo.StarRating);
match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(BeatmapInfo.Difficulty.ApproachRate);
match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(BeatmapInfo.Difficulty.DrainRate);
@ -64,40 +79,6 @@ namespace osu.Game.Screens.Select.Carousel
if (!match) return false;
if (criteria.SearchTerms.Length > 0)
{
var searchableTerms = BeatmapInfo.GetSearchableTerms();
foreach (FilterCriteria.OptionalTextFilter criteriaTerm in criteria.SearchTerms)
{
bool any = false;
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
foreach (string searchTerm in searchableTerms)
{
if (!criteriaTerm.Matches(searchTerm)) continue;
any = true;
break;
}
if (any) continue;
match = false;
break;
}
// if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs.
// this should be done after text matching so we can prioritise matching numbers in metadata.
if (!match && criteria.SearchNumber.HasValue)
{
match = (BeatmapInfo.OnlineID == criteria.SearchNumber.Value) ||
(BeatmapInfo.BeatmapSet?.OnlineID == criteria.SearchNumber.Value);
}
}
if (!match) return false;
match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true;
if (match && criteria.RulesetCriteria != null)
match &= criteria.RulesetCriteria.Matches(BeatmapInfo);

View File

@ -86,16 +86,20 @@ namespace osu.Game.Screens.Select.Carousel
items.ForEach(c => c.Filter(criteria));
criteriaComparer = Comparer<CarouselItem>.Create((x, y) =>
// Sorting is expensive, so only perform if it's actually changed.
if (lastCriteria?.Sort != criteria.Sort)
{
int comparison = x.CompareTo(criteria, y);
if (comparison != 0)
return comparison;
criteriaComparer = Comparer<CarouselItem>.Create((x, y) =>
{
int comparison = x.CompareTo(criteria, y);
if (comparison != 0)
return comparison;
return x.ItemID.CompareTo(y.ItemID);
});
return x.ItemID.CompareTo(y.ItemID);
});
items.Sort(criteriaComparer);
items.Sort(criteriaComparer);
}
lastCriteria = criteria;
}

View File

@ -176,13 +176,15 @@ namespace osu.Game.Screens.Select
{
default:
case MatchMode.Substring:
return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase);
// Note that we are using ordinal here to avoid performance issues caused by globalisation concerns.
// See https://github.com/ppy/osu/issues/11571 / https://github.com/dotnet/docs/issues/18423.
return value.Contains(SearchTerm, StringComparison.OrdinalIgnoreCase);
case MatchMode.IsolatedPhrase:
return Regex.IsMatch(value, $@"(^|\s){Regex.Escape(searchTerm)}($|\s)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
case MatchMode.FullPhrase:
return CultureInfo.InvariantCulture.CompareInfo.Compare(value, searchTerm, CompareOptions.IgnoreCase) == 0;
return CultureInfo.InvariantCulture.CompareInfo.Compare(value, searchTerm, CompareOptions.OrdinalIgnoreCase) == 0;
}
}