Added primitive search mechanism in backend.

Began implementing search mechanism for frontend.
This commit is contained in:
2021-08-05 01:22:19 -05:00
parent f71758ca69
commit c94ea4a624
56 changed files with 1623 additions and 490 deletions

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Props.Models.Search;
using Props.Shop.Framework;
namespace Props.Services.Modules
{
public interface IMetricsManager
{
public IEnumerable<ProductListingInfo> RetrieveTopListings(int max = 10);
public IEnumerable<string> RetrieveCommonKeywords(int max = 50);
public void RegisterSearchQuery(string query);
public void RegisterListing(ProductListing productListing, string shopName);
}
}

View File

@@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Props.Models.Search;
using Props.Shop.Framework;
namespace Props.Services.Modules
{
public interface ISearchManager
{
public IEnumerable<ProductListing> Search(string query, SearchOutline searchOutline);
}
}

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
@@ -6,9 +7,10 @@ using Props.Shop.Framework;
namespace Props.Services.Modules
{
public interface IShopManager
public interface IShopManager : IDisposable
{
public IEnumerable<string> AvailableShops();
public Task<IList<ProductListing>> Search(string query, SearchOutline searchOutline);
public IEnumerable<string> GetAllShopNames();
public IShop GetShop(string name);
public IEnumerable<IShop> GetAllShops();
}
}

View File

@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
using Props.Data;
using Props.Models.Search;
using Props.Shop.Framework;
namespace Props.Services.Modules
{
public class LiveMetricsManager : IMetricsManager
{
private ILogger<LiveMetricsManager> logger;
ApplicationDbContext dbContext;
public LiveMetricsManager(ApplicationDbContext dbContext, ILogger<LiveMetricsManager> logger)
{
this.logger = logger;
this.dbContext = dbContext;
}
public IEnumerable<ProductListingInfo> RetrieveTopListings(int max)
{
if (dbContext.ProductListingInfos == null) return null;
return (from l in dbContext.ProductListingInfos
orderby l.Hits descending
select l).Take(max);
}
public IEnumerable<string> RetrieveCommonKeywords(int max)
{
if (dbContext.Keywords == null) return null;
return (from k in dbContext.Keywords
orderby k.Hits descending
select k.Word).Take(max);
}
public void RegisterSearchQuery(string query)
{
query = query.ToLower();
string[] tokens = query.Split(' ');
QueryWordInfo[] wordInfos = new QueryWordInfo[tokens.Length];
for (int wordIndex = 0; wordIndex < tokens.Length; wordIndex++)
{
QueryWordInfo queryWordInfo = dbContext.Keywords.Where((k) => k.Word.ToLower().Equals(tokens[wordIndex])).SingleOrDefault() ?? new QueryWordInfo();
if (queryWordInfo.Hits == 0)
{
queryWordInfo.Word = tokens[wordIndex];
dbContext.Keywords.Add(queryWordInfo);
}
queryWordInfo.Hits += 1;
wordInfos[wordIndex] = queryWordInfo;
}
for (int wordIndex = 0; wordIndex < tokens.Length; wordIndex++)
{
for (int beforeIndex = 0; beforeIndex < wordIndex; beforeIndex++)
{
wordInfos[wordIndex].Preceding.Add(wordInfos[beforeIndex]);
}
for (int afterIndex = wordIndex; afterIndex < tokens.Length; afterIndex++)
{
wordInfos[wordIndex].Following.Add(wordInfos[afterIndex]);
}
}
dbContext.SaveChanges();
}
public void RegisterListing(ProductListing productListing, string shopName)
{
ProductListingInfo productListingInfo =
(from info in dbContext.ProductListingInfos
where info.ProductUrl.Equals(productListing.URL)
select info).SingleOrDefault() ?? new ProductListingInfo();
if (productListingInfo.Hits == 0)
{
dbContext.Add(productListingInfo);
}
productListingInfo.ShopName = shopName;
productListingInfo.ProductName = productListing.Name;
productListingInfo.ProductUrl = productListing.URL;
productListingInfo.LastUpdated = DateTime.UtcNow;
productListingInfo.Hits += 1;
dbContext.SaveChanges();
}
}
}

View File

@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Castle.Core.Logging;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Props.Models.Search;
using Props.Options;
using Props.Shop.Framework;
namespace Props.Services.Modules
{
public class LiveSearchManager : ISearchManager
{
private ILogger<LiveSearchManager> logger;
private SearchOptions searchOptions;
private IShopManager shopManager;
private IMetricsManager metricsManager;
public LiveSearchManager(IMetricsManager metricsManager, IShopManager shopManager, IConfiguration configuration, ILogger<LiveSearchManager> logger)
{
this.logger = logger;
this.metricsManager = metricsManager;
this.shopManager = shopManager;
this.searchOptions = configuration.GetSection(SearchOptions.Search).Get<SearchOptions>();
}
public IEnumerable<ProductListing> Search(string query, SearchOutline searchOutline)
{
if (string.IsNullOrWhiteSpace(query)) throw new ArgumentException($"Query \"{query}\" is null or whitepsace.");
if (searchOutline == null) throw new ArgumentNullException("searchOutline");
List<ProductListing> results = new List<ProductListing>();
metricsManager.RegisterSearchQuery(query);
logger.LogDebug("Searching for \"{0}\".", query);
foreach (string shopName in shopManager.GetAllShopNames())
{
if (searchOutline.Enabled[shopName])
{
logger.LogDebug("Checking \"{0}\".", shopName);
int amount = 0;
foreach (ProductListing product in shopManager.GetShop(shopName).Search(query, searchOutline.Filters))
{
if (searchOutline.Filters.Validate(product))
{
amount += 1;
metricsManager.RegisterListing(product, shopName);
results.Add(product);
}
if (amount >= searchOptions.MaxResults) break;
}
logger.LogDebug("Found {0} listings that satisfy the search filters from {1}.", amount, shopName);
}
}
return results;
}
}
}

View File

@@ -1,24 +1,30 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Props.Data;
using Props.Models.Search;
using Props.Options;
using Props.Shop.Framework;
namespace Props.Services.Modules
{
public class LocalShopManager : IShopManager
public class ModularShopManager : IShopManager
{
private ILogger<LocalShopManager> logger;
private ILogger<ModularShopManager> logger;
private Dictionary<string, IShop> shops;
private ModulesOptions options;
private IConfiguration configuration;
public LocalShopManager(IConfiguration configuration, ILogger<LocalShopManager> logger)
private bool disposedValue;
public ModularShopManager(IConfiguration configuration, ILogger<ModularShopManager> logger)
{
this.configuration = configuration;
this.logger = logger;
@@ -34,35 +40,25 @@ namespace Props.Services.Modules
}
}
public IEnumerable<string> AvailableShops()
public IEnumerable<string> GetAllShopNames()
{
return shops.Keys;
}
public async Task<IList<ProductListing>> Search(string query, SearchOutline searchOutline)
public IShop GetShop(string name)
{
List<ProductListing> results = new List<ProductListing>();
foreach (string shopName in shops.Keys)
{
if (!searchOutline.Disabled[shopName])
{
int amount = 0;
await foreach (ProductListing product in shops[shopName].Search(query, searchOutline.Filters))
{
if (searchOutline.Filters.Validate(product))
{
amount += 1;
results.Add(product);
}
if (amount >= options.MaxResults) break;
}
}
}
return results;
return shops[name];
}
public IEnumerable<IShop> GetAllShops()
{
return shops.Values;
}
private IEnumerable<IShop> LoadShops(string shopsDir, string shopRegex, bool recursiveLoad)
{
Stack<Task> asyncInitTasks = new Stack<Task>();
Stack<string> directories = new Stack<string>();
directories.Push(shopsDir);
string currentDirectory = null;
@@ -90,7 +86,11 @@ namespace Props.Services.Modules
IShop shop = Activator.CreateInstance(type) as IShop;
if (shop != null)
{
// TODO: load persisted shop data.
shop.Initialize(null);
asyncInitTasks.Push(shop.InitializeAsync(null));
success += 1;
logger.LogDebug("Loaded \"{0}\".", shop.ShopName);
yield return shop;
}
}
@@ -102,6 +102,32 @@ namespace Props.Services.Modules
}
}
}
logger.LogDebug("Waiting for all shops to finish asynchronous initialization.");
Task.WaitAll(asyncInitTasks.ToArray());
logger.LogDebug("All shops finished asynchronous initialization.");
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
foreach (string shopName in shops.Keys)
{
// TODO: Get shop data to persist.
shops[shopName].Dispose();
}
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}