Skip to content

Commit 153fe54

Browse files
committed
songs search by title and artists
1 parent 9009355 commit 153fe54

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+552
-160
lines changed

HarmonyDB and OneShelf.sln

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HarmonyDB.Source", "Harmony
169169
EndProject
170170
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HarmonyDB.Playground", "HarmonyDB.Playground", "{47FED0C3-B336-4FC6-86FE-51A78872C2DE}"
171171
EndProject
172-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HarmonyDB.Playground.Web", "HarmonyDB.Playground\HarmonyDB.Playground.Web\HarmonyDB.Playground.Web.csproj", "{C63D176C-03AB-4403-BE88-E8FE4EFE736D}"
172+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HarmonyDB.Playground.Web", "HarmonyDB.Playground\HarmonyDB.Playground.Web\HarmonyDB.Playground.Web.csproj", "{C63D176C-03AB-4403-BE88-E8FE4EFE736D}"
173+
EndProject
174+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HarmonyDB.Common.FullTextSearch", "HarmonyDB.Common\HarmonyDB.Common.FullTextSearch\HarmonyDB.Common.FullTextSearch.csproj", "{21D494EB-BDD0-49B0-A79D-57AE6A529F4D}"
173175
EndProject
174176
Global
175177
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -421,6 +423,10 @@ Global
421423
{C63D176C-03AB-4403-BE88-E8FE4EFE736D}.Debug|Any CPU.Build.0 = Debug|Any CPU
422424
{C63D176C-03AB-4403-BE88-E8FE4EFE736D}.Release|Any CPU.ActiveCfg = Release|Any CPU
423425
{C63D176C-03AB-4403-BE88-E8FE4EFE736D}.Release|Any CPU.Build.0 = Release|Any CPU
426+
{21D494EB-BDD0-49B0-A79D-57AE6A529F4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
427+
{21D494EB-BDD0-49B0-A79D-57AE6A529F4D}.Debug|Any CPU.Build.0 = Debug|Any CPU
428+
{21D494EB-BDD0-49B0-A79D-57AE6A529F4D}.Release|Any CPU.ActiveCfg = Release|Any CPU
429+
{21D494EB-BDD0-49B0-A79D-57AE6A529F4D}.Release|Any CPU.Build.0 = Release|Any CPU
424430
EndGlobalSection
425431
GlobalSection(SolutionProperties) = preSolution
426432
HideSolutionNode = FALSE
@@ -488,6 +494,7 @@ Global
488494
{92969108-D9C8-4FF8-B083-B10463EB0FEC} = {2AD8DB5A-A1C3-4116-B8C5-D8DA505B1CC5}
489495
{F2CFF546-5D77-4E7D-BE8B-611022AD6ECA} = {7548FF8E-E1B9-4939-96E9-03DBD5236709}
490496
{C63D176C-03AB-4403-BE88-E8FE4EFE736D} = {47FED0C3-B336-4FC6-86FE-51A78872C2DE}
497+
{21D494EB-BDD0-49B0-A79D-57AE6A529F4D} = {2950865E-9D1A-406E-B0C6-4C249E2E4D61}
491498
EndGlobalSection
492499
GlobalSection(ExtensibilityGlobals) = postSolution
493500
SolutionGuid = {59093261-FDDA-411A-852D-EA21AEF83E07}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace HarmonyDB.Common.FullTextSearch;
2+
3+
public static class FullTextSearchExtensions
4+
{
5+
public static readonly char[] Separators =
6+
{
7+
'-',
8+
',',
9+
' ',
10+
'\r',
11+
'\n',
12+
'\'',
13+
};
14+
15+
public static string ToSearchSyntax(this string text) => text.ToLowerInvariant().Replace("ё", "е");
16+
17+
public static string SearchSyntaxRemoveSeparators(this string text) => text.Replace("'", "");
18+
19+
public static bool SearchSyntaxAnySeparatorsToRemove(this string text) => text.Contains('\'');
20+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<PackageReadmeFile>readme.md</PackageReadmeFile>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<None Include="..\..\nuget readme.md" Pack="true" Link="readme.md" PackagePath="\readme.md" />
12+
</ItemGroup>
13+
14+
</Project>
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
namespace HarmonyDB.Common.FullTextSearch;
2+
3+
public static class SearchHighlightingTools
4+
{
5+
public static IEnumerable<(string fragment, bool isHighlighted)> GetFragments(string? query, string text)
6+
{
7+
if (string.IsNullOrWhiteSpace(text)) yield break;
8+
var split = query?.ToLowerInvariant().ToSearchSyntax().Split(FullTextSearchExtensions.Separators, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
9+
if (split?.Length is null or 0)
10+
{
11+
yield return (text, false);
12+
yield break;
13+
}
14+
15+
var remaining = text.ToLowerInvariant().ToSearchSyntax().SearchSyntaxRemoveSeparators();
16+
var rendering = text;
17+
var anyToRemove = rendering.SearchSyntaxAnySeparatorsToRemove();
18+
19+
while (true)
20+
{
21+
var first = split
22+
.Select(x => (x, index: (int?)remaining.IndexOf(x, StringComparison.Ordinal)))
23+
.Where(x => x.index > -1)
24+
.OrderBy(x => x.index)
25+
.FirstOrDefault();
26+
27+
var index = first.index;
28+
29+
if (index == null)
30+
{
31+
yield return (remaining, false);
32+
yield break;
33+
}
34+
35+
var updatedIndex = index.Value;
36+
37+
if (updatedIndex > 0 && anyToRemove)
38+
{
39+
var expected = remaining.Substring(0, index.Value);
40+
var renderingTests = rendering.ToLowerInvariant().ToSearchSyntax();
41+
while (true)
42+
{
43+
var test = renderingTests.Substring(0, updatedIndex).SearchSyntaxRemoveSeparators();
44+
if (test == expected) break;
45+
46+
updatedIndex++;
47+
if (updatedIndex > 100) throw new("Protection. Failed.");
48+
}
49+
}
50+
51+
var matchLength = first.x.Length;
52+
var updatedMatchLength = matchLength;
53+
54+
if (anyToRemove)
55+
{
56+
var expected = remaining.Substring(index.Value, matchLength);
57+
var renderingTests = rendering.Substring(updatedIndex).ToLowerInvariant().ToSearchSyntax();
58+
while (true)
59+
{
60+
var test = renderingTests.Substring(0, updatedMatchLength).SearchSyntaxRemoveSeparators();
61+
if (test == expected) break;
62+
63+
updatedMatchLength++;
64+
if (updatedMatchLength > 100) throw new("Protection. Failed.");
65+
}
66+
}
67+
68+
if (index == 0)
69+
{
70+
yield return (rendering.Substring(0, updatedMatchLength), true);
71+
}
72+
else
73+
{
74+
yield return (rendering.Substring(0, updatedIndex), false);
75+
yield return (rendering.Substring(updatedIndex, updatedMatchLength), true);
76+
}
77+
78+
remaining = remaining.Substring(index.Value + matchLength);
79+
rendering = rendering.Substring(updatedIndex + updatedMatchLength);
80+
if (remaining == string.Empty) yield break;
81+
}
82+
}
83+
}

HarmonyDB.Common/HarmonyDB.Common/Tools/TranspositionExtensions.cs renamed to HarmonyDB.Common/HarmonyDB.Common/Transposition/TranspositionExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace HarmonyDB.Common.Tools;
1+
namespace HarmonyDB.Common.Transposition;
22

33
public static class TranspositionExtensions
44
{

HarmonyDB.Index/HarmonyDB.Index.Api.Client/IndexApiClient.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@ public async Task<TryImportResponse> TryImport(string url)
2525
Url = url,
2626
});
2727

28-
public async Task<SearchResponse> Search(SearchRequest request, ApiTraceBag? apiTraceBag = null)
29-
=> await PostWithCode<SearchRequest, SearchResponse>(IndexApiUrls.VExternal1Search, request, apiTraceBag: apiTraceBag);
28+
public async Task<SongsByChordsResponse> SongsByChords(SongsByChordsRequest request, ApiTraceBag? apiTraceBag = null)
29+
=> await PostWithCode<SongsByChordsRequest, SongsByChordsResponse>(IndexApiUrls.VExternal1SongsByChords, request, apiTraceBag: apiTraceBag);
30+
31+
public async Task<SongsByHeaderResponse> SongsByHeader(SongsByHeaderRequest request, ApiTraceBag? apiTraceBag = null)
32+
=> await PostWithCode<SongsByHeaderRequest, SongsByHeaderResponse>(IndexApiUrls.VExternal1SongsByHeader, request, apiTraceBag: apiTraceBag);
3033

3134
public async Task<LoopsResponse> Loops(LoopsRequest request, ApiTraceBag? apiTraceBag = null)
3235
=> await PostWithCode<LoopsRequest, LoopsResponse>(IndexApiUrls.VExternal1Loops, request, apiTraceBag: apiTraceBag);

HarmonyDB.Index/HarmonyDB.Index.Api.Model/IndexApiUrls.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ public static class IndexApiUrls
77
public const string VInternalTryImport = nameof(VInternalTryImport);
88
public const string VInternalGetLyrics = nameof(VInternalGetLyrics);
99

10-
public const string VExternal1Search = nameof(VExternal1Search);
10+
public const string VExternal1SongsByChords = nameof(VExternal1SongsByChords);
11+
public const string VExternal1SongsByHeader = nameof(VExternal1SongsByHeader);
1112
public const string VExternal1Loops = nameof(VExternal1Loops);
1213
}

HarmonyDB.Index/HarmonyDB.Index.Api.Model/VExternal1/SearchResponse.cs

Lines changed: 0 additions & 6 deletions
This file was deleted.

HarmonyDB.Index/HarmonyDB.Index.Api.Model/VExternal1/SearchRequest.cs renamed to HarmonyDB.Index/HarmonyDB.Index.Api.Model/VExternal1/SongsByChordsRequest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace HarmonyDB.Index.Api.Model.VExternal1;
44

5-
public record SearchRequest : PagedRequestBase
5+
public record SongsByChordsRequest : PagedRequestBase
66
{
77
public required string Query { get; init; }
88

@@ -13,5 +13,5 @@ public record SearchRequest : PagedRequestBase
1313
public int SongsPerPage { get; init; } = 100;
1414

1515
[JsonConverter(typeof(JsonStringEnumConverter))]
16-
public SearchRequestOrdering Ordering { get; init; } = SearchRequestOrdering.ByRating;
16+
public SongsByChordsRequestOrdering Ordering { get; init; } = SongsByChordsRequestOrdering.ByRating;
1717
}

HarmonyDB.Index/HarmonyDB.Index.Api.Model/VExternal1/SearchRequestOrdering.cs renamed to HarmonyDB.Index/HarmonyDB.Index.Api.Model/VExternal1/SongsByChordsRequestOrdering.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
namespace HarmonyDB.Index.Api.Model.VExternal1;
22

3-
public enum SearchRequestOrdering
3+
public enum SongsByChordsRequestOrdering
44
{
55
ByCoverage,
66
ByRating,
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace HarmonyDB.Index.Api.Model.VExternal1;
2+
3+
public record SongsByChordsResponse : PagedResponseBase
4+
{
5+
public required List<SongsByChordsResponseSong> Songs { get; init; }
6+
}

HarmonyDB.Index/HarmonyDB.Index.Api.Model/VExternal1/SearchResponseSong.cs renamed to HarmonyDB.Index/HarmonyDB.Index.Api.Model/VExternal1/SongsByChordsResponseSong.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace HarmonyDB.Index.Api.Model.VExternal1;
44

5-
public class SearchResponseSong
5+
public class SongsByChordsResponseSong
66
{
77
public required IndexHeader Header { get; set; }
88

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace HarmonyDB.Index.Api.Model.VExternal1;
2+
3+
public record SongsByHeaderRequest : PagedRequestBase
4+
{
5+
public required string Query { get; init; }
6+
7+
public int MinRating { get; init; } = 70;
8+
9+
public int SongsPerPage { get; init; } = 100;
10+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using HarmonyDB.Source.Api.Model.V1;
2+
3+
namespace HarmonyDB.Index.Api.Model.VExternal1;
4+
5+
public record SongsByHeaderResponse : PagedResponseBase
6+
{
7+
public required List<IndexHeader> Songs { get; init; }
8+
}

HarmonyDB.Index/HarmonyDB.Index.Api/Functions/VDev/IndexFunctions.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ public class IndexFunctions
2424
private readonly IndexHeadersCache _indexHeadersCache;
2525
private readonly InputParser _inputParser;
2626
private readonly ProgressionsSearch _progressionsSearch;
27+
private readonly FullTextSearchCache _fullTextSearchCache;
2728

28-
public IndexFunctions(ILogger<IndexFunctions> logger, DownstreamApiClient downstreamApiClient, ProgressionsCache progressionsCache, LoopsStatisticsCache loopsStatisticsCache, SecurityContext securityContext, IndexHeadersCache indexHeadersCache, InputParser inputParser, ProgressionsSearch progressionsSearch)
29+
public IndexFunctions(ILogger<IndexFunctions> logger, DownstreamApiClient downstreamApiClient, ProgressionsCache progressionsCache, LoopsStatisticsCache loopsStatisticsCache, SecurityContext securityContext, IndexHeadersCache indexHeadersCache, InputParser inputParser, ProgressionsSearch progressionsSearch, FullTextSearchCache fullTextSearchCache)
2930
{
3031
_logger = logger;
3132
_downstreamApiClient = downstreamApiClient;
@@ -34,6 +35,7 @@ public IndexFunctions(ILogger<IndexFunctions> logger, DownstreamApiClient downst
3435
_indexHeadersCache = indexHeadersCache;
3536
_inputParser = inputParser;
3637
_progressionsSearch = progressionsSearch;
38+
_fullTextSearchCache = fullTextSearchCache;
3739

3840
securityContext.InitService();
3941
}
@@ -163,4 +165,16 @@ public async Task<IActionResult> VDevFindAndCount([HttpTrigger(AuthorizationLeve
163165
{
164166
return new OkObjectResult(_progressionsSearch.Search((await _progressionsCache.Get()).Values, _inputParser.Parse(searchQuery)).Count);
165167
}
168+
169+
[Function(nameof(VDevFindHeaderAndCount))]
170+
public async Task<IActionResult> VDevFindHeaderAndCount([HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req, string searchQuery)
171+
{
172+
var fullTextSearch = await _fullTextSearchCache.Get();
173+
var found = fullTextSearch.Find(searchQuery);
174+
return new OkObjectResult(new
175+
{
176+
found.Count,
177+
Top100 = found.Take(100).ToList(),
178+
});
179+
}
166180
}

HarmonyDB.Index/HarmonyDB.Index.Api/Functions/VExternal1/Search.cs renamed to HarmonyDB.Index/HarmonyDB.Index.Api/Functions/VExternal1/SongsByChords.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
namespace HarmonyDB.Index.Api.Functions.VExternal1;
2020

21-
public class Search : ServiceFunctionBase<SearchRequest, SearchResponse>
21+
public class SongsByChords : ServiceFunctionBase<SongsByChordsRequest, SongsByChordsResponse>
2222
{
2323
private readonly ProgressionsCache _progressionsCache;
2424
private readonly IndexHeadersCache _indexHeadersCache;
@@ -28,7 +28,7 @@ public class Search : ServiceFunctionBase<SearchRequest, SearchResponse>
2828
private readonly IndexApiOptions _options;
2929
private readonly IndexApiClient _indexApiClient;
3030

31-
public Search(ILoggerFactory loggerFactory, SecurityContext securityContext, ProgressionsCache progressionsCache, IndexHeadersCache indexHeadersCache, ProgressionsSearch progressionsSearch, InputParser inputParser, DownstreamApiClient downstreamApiClient, ConcurrencyLimiter concurrencyLimiter, IOptions<IndexApiOptions> options, IndexApiClient indexApiClient)
31+
public SongsByChords(ILoggerFactory loggerFactory, SecurityContext securityContext, ProgressionsCache progressionsCache, IndexHeadersCache indexHeadersCache, ProgressionsSearch progressionsSearch, InputParser inputParser, DownstreamApiClient downstreamApiClient, ConcurrencyLimiter concurrencyLimiter, IOptions<IndexApiOptions> options, IndexApiClient indexApiClient)
3232
: base(loggerFactory, securityContext, concurrencyLimiter, options.Value.RedirectCachesToIndex)
3333
{
3434
_progressionsCache = progressionsCache;
@@ -40,15 +40,15 @@ public Search(ILoggerFactory loggerFactory, SecurityContext securityContext, Pro
4040
_options = options.Value;
4141
}
4242

43-
[Function(IndexApiUrls.VExternal1Search)]
44-
public Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, [FromBody] SearchRequest request)
43+
[Function(IndexApiUrls.VExternal1SongsByChords)]
44+
public Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, [FromBody] SongsByChordsRequest request)
4545
=> RunHandler(request);
4646

47-
protected override async Task<SearchResponse> Execute(SearchRequest request)
47+
protected override async Task<SongsByChordsResponse> Execute(SongsByChordsRequest request)
4848
{
4949
if (_options.RedirectCachesToIndex)
5050
{
51-
return await _indexApiClient.Search(request);
51+
return await _indexApiClient.SongsByChords(request);
5252
}
5353

5454
var progressions = await _progressionsCache.Get();
@@ -68,17 +68,17 @@ protected override async Task<SearchResponse> Execute(SearchRequest request)
6868
.Where(x => x.coverage >= request.MinCoverage && x.h.Value.Rating >= request.MinRating)
6969
.OrderByDescending<(KeyValuePair<string, IndexHeader> h, float coverage), int>(request.Ordering switch
7070
{
71-
SearchRequestOrdering.ByRating => x =>
71+
SongsByChordsRequestOrdering.ByRating => x =>
7272
(int)(x.h.Value.Rating * 10 ?? 0) * 10 + (int)(x.coverage * 1000),
73-
SearchRequestOrdering.ByCoverage => x =>
73+
SongsByChordsRequestOrdering.ByCoverage => x =>
7474
(int)(x.h.Value.Rating * 10 ?? 0) + (int)(x.coverage * 1000) * 10,
7575
_ => throw new ArgumentOutOfRangeException(),
7676
})
7777
.ToList();
7878

7979
return new()
8080
{
81-
Songs = results.Skip((request.PageNumber - 1) * request.SongsPerPage).Take(request.SongsPerPage).Select(x => new SearchResponseSong
81+
Songs = results.Skip((request.PageNumber - 1) * request.SongsPerPage).Take(request.SongsPerPage).Select(x => new SongsByChordsResponseSong
8282
{
8383
Header = x.h.Value with
8484
{

0 commit comments

Comments
 (0)