From 3b61a3c8647f9eca7b8ff0ebdde9f9326d7ac829 Mon Sep 17 00:00:00 2001 From: Patrick Ackermann Date: Mon, 8 Aug 2022 11:50:25 +0200 Subject: [PATCH 1/3] Enable Nqgsql logging for Debuging --- src/Program.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Program.cs b/src/Program.cs index 23d6987..1928479 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,6 +1,10 @@ using Microsoft.EntityFrameworkCore; using ModelRepoBrowser; using ModelRepoBrowser.Crawler; +using Npgsql.Logging; + +NpgsqlLogManager.Provider = new ConsoleLoggingProvider(NpgsqlLogLevel.Debug, true, false); +NpgsqlLogManager.IsParameterLoggingEnabled = true; var builder = WebApplication.CreateBuilder(args); From 8ca5697e4126e1a359d33298d371002f3fb6ff1e Mon Sep 17 00:00:00 2001 From: Patrick Ackermann Date: Wed, 17 Aug 2022 12:07:40 +0200 Subject: [PATCH 2/3] Move search to separate method --- src/Controllers/SearchController.cs | 46 ++++++++++++++++------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/src/Controllers/SearchController.cs b/src/Controllers/SearchController.cs index c1b37d8..f40cc7d 100644 --- a/src/Controllers/SearchController.cs +++ b/src/Controllers/SearchController.cs @@ -49,7 +49,28 @@ public SearchController(ILogger logger, RepoBrowserContext con return null; } - var searchPattern = $"%{EscapeLikePattern(trimmedQuery)}%"; + var repositories = await SearchRepositories(trimmedQuery).ConfigureAwait(false); + + // Create repository tree + foreach (var repository in repositories.Values) + { + repository.SubsidiarySites = repository.SubsidiarySites.Select(c => repositories[c.HostNameId]).ToHashSet(); + } + + var root = repositories.Values.SingleOrDefault(r => !r.ParentSites.Any()); + if (root != null && PruneEmptyRepositories(root)) + { + return root; + } + else + { + return null; + } + } + + private Task> SearchRepositories(string query) + { + var searchPattern = $"%{EscapeLikePattern(query)}%"; var modelsNamesFoundFromCatalogs = context.Catalogs .Where(c => EF.Functions.ILike(c.Identifier, searchPattern, @"\")) @@ -59,7 +80,7 @@ public SearchController(ILogger logger, RepoBrowserContext con .Distinct() .ToList(); - var repositories = await context.Repositories + return context.Repositories .Include(r => r.SubsidiarySites) .Include(r => r.ParentSites) .Include(r => r.Models @@ -69,26 +90,9 @@ public SearchController(ILogger logger, RepoBrowserContext con || EF.Functions.ILike(m.Version, searchPattern, @"\") || EF.Functions.ILike(m.File, searchPattern, @"\") || modelsNamesFoundFromCatalogs.Contains(m.Name) - || m.Tags.Contains(trimmedQuery))) + || m.Tags.Contains(query))) .AsNoTracking() - .ToDictionaryAsync(r => r.HostNameId) - .ConfigureAwait(false); - - // Create repository tree - foreach (var repository in repositories.Values) - { - repository.SubsidiarySites = repository.SubsidiarySites.Select(c => repositories[c.HostNameId]).ToHashSet(); - } - - var root = repositories.Values.SingleOrDefault(r => !r.ParentSites.Any()); - if (root != null && PruneEmptyRepositories(root)) - { - return root; - } - else - { - return null; - } + .ToDictionaryAsync(r => r.HostNameId); } /// From f901c490a85fd5e565c97370f804767422c6dbe4 Mon Sep 17 00:00:00 2001 From: Patrick Ackermann Date: Wed, 17 Aug 2022 12:09:36 +0200 Subject: [PATCH 3/3] Add controller action for autocomplete --- src/ClientApp/src/components/Home.js | 2 +- src/Controllers/SearchController.cs | 25 +++++++++++++ tests/SearchControllerTest.cs | 53 ++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/ClientApp/src/components/Home.js b/src/ClientApp/src/components/Home.js index a1fa66e..e6d3c49 100644 --- a/src/ClientApp/src/components/Home.js +++ b/src/ClientApp/src/components/Home.js @@ -41,7 +41,7 @@ export function Home() { if (searchString.length < 3) { setSearchOptions([]); } else { - const response = await fetch("/search?query=" + searchString); + const response = await fetch("/search/suggest/" + searchString); if (response.ok && response.status !== 204 /* No Content */) { const repositoryTree = await response.json(); setSearchOptions([...new Set(getAllModels(repositoryTree).map((m) => m.name))]); diff --git a/src/Controllers/SearchController.cs b/src/Controllers/SearchController.cs index f40cc7d..108a5e5 100644 --- a/src/Controllers/SearchController.cs +++ b/src/Controllers/SearchController.cs @@ -68,6 +68,31 @@ public SearchController(ILogger logger, RepoBrowserContext con } } + /// + /// Get search query suggestions based on . + /// + /// The query string to search for. + /// A sequence of related to . + [HttpGet("suggest/{query}")] + public async Task> GetSearchSuggestions(string query) + { + logger.LogDebug("Get search options for <{SearchQuery}>", query); + + var trimmedQuery = query?.Trim(); + if (string.IsNullOrEmpty(trimmedQuery)) + { + return Enumerable.Empty(); + } + + var repositories = await SearchRepositories(trimmedQuery).ConfigureAwait(false); + + return repositories + .Values + .SelectMany(r => r.Models) + .Select(m => m.Name) + .ToList(); + } + private Task> SearchRepositories(string query) { var searchPattern = $"%{EscapeLikePattern(query)}%"; diff --git a/tests/SearchControllerTest.cs b/tests/SearchControllerTest.cs index 8244f86..fedf9d7 100644 --- a/tests/SearchControllerTest.cs +++ b/tests/SearchControllerTest.cs @@ -184,4 +184,57 @@ public async Task SearchRepositoryTreeResult() .AssertSingleItem("jaquelin.com", 1) .AssertSingleItem("kelsi.biz", 2)); } + + [TestMethod] + public async Task GetSearchSuggestions() + { + const string query = "and"; + var suggestions = await controller.GetSearchSuggestions(query); + var search = await controller.Search(query); + Assert.IsNotNull(search); + + CollectionAssert.AreEquivalent(new[] + { + "Handcrafted Rubber Tuna_Sudanese Pound_syndicate", + "Engineer_Hill_Solutions_Practical_Michigan", + "Iceland Krona_New Israeli Sheqel_matrix_Oklahoma", + "Handcrafted Granite Ball_Associate_haptic_Money Market Account_Beauty", + "deposit", + "Virgin Islands, U.S._withdrawal_CFA Franc BCEAO_THX", + "indigo_Sleek Granite Salad_Practical Concrete Ball_moderator_interface", + "back-end_benchmark_Legacy_Future_Crescent", + "capability_Unbranded Granite Table_Intelligent Cotton Table_static", + "Global_Licensed", + "bandwidth_Refined Fresh Shoes", + "back-end_grey_JBOD", + "bandwidth_auxiliary_Incredible", + "Handcrafted Fresh Hat_metrics_invoice", + "Iowa_Junctions", + "Via_West Virginia_withdrawal", + }, + suggestions.ToArray()); + + CollectionAssert.AreEquivalent(search.GetAllModels().Select(x => x.Name).ToList(), suggestions.ToList()); + } + + [TestMethod] + public async Task GetSearchSuggestionsNull() + { + var suggestions = await controller.GetSearchSuggestions(null); + suggestions.AssertCount(0); + } + + [TestMethod] + public async Task GetSearchSuggestionsEmptyString() + { + var suggestions = await controller.GetSearchSuggestions(string.Empty); + suggestions.AssertCount(0); + } + + [TestMethod] + public async Task GetSearchSuggestionsWhitespace() + { + var suggestions = await controller.GetSearchSuggestions(" "); + suggestions.AssertCount(0); + } }