diff --git a/sharp/content.Tests/DomainTest.cs b/sharp/content.Tests/DomainTest.cs index 0c3413d..1647248 100644 --- a/sharp/content.Tests/DomainTest.cs +++ b/sharp/content.Tests/DomainTest.cs @@ -22,7 +22,7 @@ public class DomainTest { private (Mock, Mock userRepo, Mock voteRepo, Mock searchClient) Setup() => - (new Mock(), new Mock(), new Mock(), new Mock(null!)); + (new Mock(), new Mock(), new Mock(), new Mock(null!,null!)); [Fact] public async Task FindById_ReturnsVideoDto_WhenVideoExists() diff --git a/sharp/content.Tests/repository/ClientTest.cs b/sharp/content.Tests/repository/ClientTest.cs index 41e3215..2c45c40 100644 --- a/sharp/content.Tests/repository/ClientTest.cs +++ b/sharp/content.Tests/repository/ClientTest.cs @@ -4,6 +4,7 @@ using content.repository; using Moq; using Moq.Protected; +using StackExchange.Redis; namespace content.Tests.repository; @@ -183,7 +184,7 @@ public async Task SimilarSearch_ReturnsListOfVideoIds() }; mockFactory.Setup(_ => _.CreateClient("Search")).Returns(client); - var searchClient = new SearchClient(mockFactory.Object); + var searchClient = new SearchClient(mockFactory.Object, ConnectionMultiplexer.Connect("localhost").GetDatabase()); mockHttpMessageHandler.Protected() .Setup>( @@ -203,6 +204,12 @@ public async Task SimilarSearch_ReturnsListOfVideoIds() // Assert Assert.Equal(expectedResponse.Hits.Select(h => h.Id).ToList(), result); + + // Again + result = await searchClient.SimilarSearch(videoId); + + // Assert + Assert.Equal(expectedResponse.Hits.Select(h => h.Id).ToList(), result); } } diff --git a/sharp/content/appsettings.json b/sharp/content/appsettings.json index 342f949..25d0254 100644 --- a/sharp/content/appsettings.json +++ b/sharp/content/appsettings.json @@ -10,7 +10,8 @@ "Default": "Server=db; User ID=name; Password=passwd; Database=db;", "User": "http://user:8080", "Vote": "http://graph:8088", - "Search": "http://localhost:7700" + "Search": "http://localhost:7700", + "Redis": "localhost:6379" }, "Secret": "secret", "Token": "this is a token for search" diff --git a/sharp/content/content.csproj b/sharp/content/content.csproj index 315cbf7..093954d 100644 --- a/sharp/content/content.csproj +++ b/sharp/content/content.csproj @@ -44,6 +44,7 @@ + diff --git a/sharp/content/repository/Client.cs b/sharp/content/repository/Client.cs index 9fdc004..b35d62d 100644 --- a/sharp/content/repository/Client.cs +++ b/sharp/content/repository/Client.cs @@ -14,7 +14,9 @@ using System.Net.Http.Headers; using System.Security.Claims; +using System.Text.Json; using System.Text.Json.Serialization; +using StackExchange.Redis; namespace content.repository; @@ -57,7 +59,7 @@ public async Task FindById(long id) var req = new HttpRequestMessage(HttpMethod.Get, $"/users/{id}"); req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token); Console.WriteLine(client.BaseAddress); - var resp = await client.SendAsync(req); + using var resp = await client.SendAsync(req); resp.EnsureSuccessStatusCode(); return await resp.Content.ReadFromJsonAsync(UserJsonContext.Default.User) ?? new(); } @@ -67,7 +69,7 @@ public async Task> FindAllByIds(IEnumerable ids) var req = new HttpRequestMessage(HttpMethod.Get, $"/users?{string.Join("&", ids.Distinct().Select(id => $"ids={id}"))}"); req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token); - var resp = await client.SendAsync(req); + using var resp = await client.SendAsync(req); resp.EnsureSuccessStatusCode(); return await resp.Content.ReadFromJsonAsync(UserJsonContext.Default.IReadOnlyListUser) ?? []; } @@ -113,7 +115,7 @@ public async Task> VotedOfVideos(List videoIds) } - var resp = await client.SendAsync(req); + using var resp = await client.SendAsync(req); resp.EnsureSuccessStatusCode(); var result = await resp.Content.ReadFromJsonAsync(VoteJsonContext.Default.ListInt64) ?? []; @@ -128,7 +130,7 @@ public async Task> VotedOfVideos(List videoIds) req.Headers.Authorization = auth; } - var resp = await client.SendAsync(req); + using var resp = await client.SendAsync(req); resp.EnsureSuccessStatusCode(); @@ -150,20 +152,29 @@ public record InQuery(List ObjectIds); [JsonSerializable(typeof(InQuery))] internal partial class VoteJsonContext : JsonSerializerContext; -public class SearchClient(IHttpClientFactory clientFactory) +public class SearchClient(IHttpClientFactory clientFactory, IDatabase db) { public async Task> SimilarSearch(long videoId) { + var cache = await db.StringGetAsync($"similar:{videoId}"); + if (cache.HasValue) + { + return JsonSerializer.Deserialize(cache!, SearchContext.Default.ListInt64) ?? []; + } + using var client = clientFactory.CreateClient("Search"); var body = new RequestBody(videoId, ["id"]); var content = JsonContent.Create(body, SearchContext.Default.RequestBody); var req = new HttpRequestMessage(HttpMethod.Post, "/indexes/videos/similar") { Content = content }; - var resp = await client.SendAsync(req); + using var resp = await client.SendAsync(req); resp.EnsureSuccessStatusCode(); var result = await resp.Content.ReadFromJsonAsync(SearchContext.Default.Response) ?? new Response(); - return result.Hits.Select(h => h.Id).ToList(); + var ids = result.Hits.Select(h => h.Id).ToList(); + await db.StringSetAsync($"similar:{videoId}", JsonSerializer.Serialize(ids, SearchContext.Default.ListInt64), TimeSpan.FromSeconds(60)); + + return ids; } } @@ -179,8 +190,10 @@ public record Response() public record SimilarVideo([property: JsonPropertyName("id")] long Id); + [JsonSerializable(typeof(Response))] [JsonSerializable(typeof(RequestBody))] +[JsonSerializable(typeof(List))] public partial class SearchContext : JsonSerializerContext; public static class Extension @@ -193,11 +206,13 @@ public static IServiceCollection AddVoteRepository(this IServiceCollection servi .GetConnectionString("Vote").EnsureNotNull("Vote connection string is null").TrimEnd('/'))).Services; public static IServiceCollection AddSearchClient(this IServiceCollection services) => services + .AddSingleton(sp => ConnectionMultiplexer.Connect(sp.GetRequiredService().GetConnectionString("Redis").EnsureNotNull("Redis connection string is null"))) + .AddSingleton(sp => sp.GetRequiredService().GetDatabase()) .AddScoped().AddHttpClient("Search", (sp, client) => { var baseAddress = sp.GetRequiredService().GetConnectionString("Search") .EnsureNotNull("Search connection string is null"); - var token = sp.GetRequiredService().GetConnectionString("Token"); + var token = sp.GetRequiredService()["Token"]; client.BaseAddress = new Uri(baseAddress.TrimEnd('/')); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); }).Services;