diff --git a/.gitignore b/.gitignore index 2afa2e2..9692748 100644 --- a/.gitignore +++ b/.gitignore @@ -452,3 +452,4 @@ $RECYCLE.BIN/ !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json +/RhSolutions.Api/MLModels diff --git a/RhSolutions.Api/Middleware/QueryModifier.cs b/RhSolutions.Api/Middleware/QueryModifier.cs new file mode 100644 index 0000000..810c7dd --- /dev/null +++ b/RhSolutions.Api/Middleware/QueryModifier.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Http.Extensions; +using RhSolutions.Api.Services; + +namespace RhSolutions.Api.Middleware; + +public class QueryModifier +{ + private RequestDelegate _next; + + public QueryModifier(RequestDelegate nextDelegate) + { + _next = nextDelegate; + } + + public async Task Invoke(HttpContext context, IProductTypePredicter typePredicter, ProductQueryModifierFactory productQueryModifierFactory) + { + if (context.Request.Method == HttpMethods.Get + && context.Request.Path == "/api/search") + { + string query = context.Request.Query["query"].ToString(); + var productType = typePredicter.GetPredictedProductType(query); + var modifier = productQueryModifierFactory.GetModifier(productType!); + if (modifier.TryQueryModify(context.Request.Query, out var newQuery)) + { + context.Request.QueryString = newQuery; + } + } + await _next(context); + } +} diff --git a/RhSolutions.Api/Program.cs b/RhSolutions.Api/Program.cs index 00a8c84..530be7f 100644 --- a/RhSolutions.Api/Program.cs +++ b/RhSolutions.Api/Program.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using RhSolutions.Models; using RhSolutions.Api.Services; +using RhSolutions.Api.Middleware; var builder = WebApplication.CreateBuilder(args); @@ -21,12 +22,15 @@ builder.Services.AddDbContext(opts => opts.EnableSensitiveDataLogging(true); } }); -builder.Services.AddScoped(); +builder.Services.AddScoped() + .AddScoped() + .AddSingleton(); builder.Services.AddControllers(); var app = builder.Build(); app.MapControllers(); +app.UseMiddleware(); var context = app.Services.CreateScope().ServiceProvider .GetRequiredService(); diff --git a/RhSolutions.Api/RhSolutions.Api.csproj b/RhSolutions.Api/RhSolutions.Api.csproj index 270c488..5f122f2 100644 --- a/RhSolutions.Api/RhSolutions.Api.csproj +++ b/RhSolutions.Api/RhSolutions.Api.csproj @@ -13,9 +13,16 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + + + PreserveNewest + + + diff --git a/RhSolutions.Api/Services/BypassQueryModifier.cs b/RhSolutions.Api/Services/BypassQueryModifier.cs new file mode 100644 index 0000000..8ba3826 --- /dev/null +++ b/RhSolutions.Api/Services/BypassQueryModifier.cs @@ -0,0 +1,11 @@ +namespace RhSolutions.Api.Services +{ + public class BypassQueryModifier : IProductQueryModifier + { + public bool TryQueryModify(IQueryCollection collection, out QueryString queryString) + { + queryString = QueryString.Empty; + return false; + } + } +} diff --git a/RhSolutions.Api/Services/IProductQueryModifier.cs b/RhSolutions.Api/Services/IProductQueryModifier.cs new file mode 100644 index 0000000..3892a39 --- /dev/null +++ b/RhSolutions.Api/Services/IProductQueryModifier.cs @@ -0,0 +1,7 @@ +namespace RhSolutions.Api.Services +{ + public interface IProductQueryModifier + { + public bool TryQueryModify(IQueryCollection collection, out QueryString queryString); + } +} diff --git a/RhSolutions.Api/Services/IProductTypePredicter.cs b/RhSolutions.Api/Services/IProductTypePredicter.cs new file mode 100644 index 0000000..b4625ec --- /dev/null +++ b/RhSolutions.Api/Services/IProductTypePredicter.cs @@ -0,0 +1,6 @@ +namespace RhSolutions.Api.Services; + +public interface IProductTypePredicter +{ + public string? GetPredictedProductType(string productName); +} \ No newline at end of file diff --git a/RhSolutions.Api/Services/ProductQueryModifierFactory.cs b/RhSolutions.Api/Services/ProductQueryModifierFactory.cs new file mode 100644 index 0000000..8f587a8 --- /dev/null +++ b/RhSolutions.Api/Services/ProductQueryModifierFactory.cs @@ -0,0 +1,15 @@ +namespace RhSolutions.Api.Services; + +public class ProductQueryModifierFactory +{ + public IProductQueryModifier GetModifier(string productTypeName) + { + switch (productTypeName) + { + case "Тройник RAUTITAN": + return new TPieceQueryModifier(); + default: + return new BypassQueryModifier(); + } + } +} diff --git a/RhSolutions.Api/Services/ProductTypePredicter.cs b/RhSolutions.Api/Services/ProductTypePredicter.cs new file mode 100644 index 0000000..2c2c226 --- /dev/null +++ b/RhSolutions.Api/Services/ProductTypePredicter.cs @@ -0,0 +1,44 @@ +using Microsoft.ML; +using Microsoft.ML.Data; + +namespace RhSolutions.Api.Services; + +public class ProductTypePredicter : IProductTypePredicter +{ + private readonly string _modelPath = @"./MLModels/model.zip"; + private MLContext _mlContext; + private ITransformer _loadedModel; + private PredictionEngine _predEngine; + + public ProductTypePredicter() + { + _mlContext = new MLContext(seed: 0); + _loadedModel = _mlContext.Model.Load(_modelPath, out var _); + _predEngine = _mlContext.Model.CreatePredictionEngine(_loadedModel); + } + + public string? GetPredictedProductType(string productName) + { + Product p = new() + { + Name = productName + }; + var prediction = _predEngine.Predict(p); + return prediction.Type; + } + + public class Product + { + [LoadColumn(0)] + public string? Name { get; set; } + [LoadColumn(1)] + public string? Type { get; set; } + } + + public class TypePrediction + { + [ColumnName("PredictedLabel")] + public string? Type { get; set; } + } +} + diff --git a/RhSolutions.Api/Services/TPieceQueryModifier.cs b/RhSolutions.Api/Services/TPieceQueryModifier.cs new file mode 100644 index 0000000..7ec6439 --- /dev/null +++ b/RhSolutions.Api/Services/TPieceQueryModifier.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Http.Extensions; +using System.Text; +using System.Text.RegularExpressions; + +namespace RhSolutions.Api.Services +{ + public class TPieceQueryModifier : IProductQueryModifier + { + private readonly string pattern = @"(\b16|20|25|32|40|50|63\b)+"; + + public bool TryQueryModify(IQueryCollection collection, out QueryString queryString) + { + queryString = QueryString.Empty; + var query = collection["query"].ToString(); + if (string.IsNullOrEmpty(query)) + { + return false; + } + var matches = Regex.Matches(query, pattern); + StringBuilder sb = new(); + sb.Append("Тройник RAUTITAN -PLATINUM"); + if (matches.Count == 1) + { + sb.Append($" {matches.First().Value}-{matches.First().Value}-{matches.First().Value}"); + } + else if (matches.Count >= 3) + { + sb.Append($" {matches[0].Value}-{matches[1].Value}-{matches[2].Value}"); + } + else + { + return false; + } + QueryBuilder qb = new() + { + { "query", sb.ToString() } + }; + queryString = qb.ToQueryString(); + return true; + } + } +}