0
0

Implement gRPC search service

This commit is contained in:
Serghei Cebotari 2023-10-30 21:49:58 +03:00
parent b91d8fbe99
commit 36cd74a959
9 changed files with 194 additions and 107 deletions

View File

@ -7,7 +7,8 @@ RUN dotnet publish -c Release -o out
FROM mcr.microsoft.com/dotnet/aspnet:6.0
EXPOSE 5000
EXPOSE 43000
WORKDIR /app
COPY --from=build /app/out .
ENV ASPNETCORE_ENVIRONMENT Production
ENTRYPOINT [ "dotnet", "RhSolutions.Api.dll", "--urls=http://0.0.0.0:5000" ]
ENTRYPOINT [ "dotnet", "RhSolutions.Api.dll" ]

View File

@ -5,78 +5,78 @@ using System.Linq;
namespace RhSolutions.Api.Controllers
{
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private RhSolutionsContext dbContext;
private IPricelistParser parser;
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private RhSolutionsContext dbContext;
private IPricelistParser parser;
public ProductsController(RhSolutionsContext dbContext, IPricelistParser parser)
{
this.dbContext = dbContext;
this.parser = parser;
}
public ProductsController(RhSolutionsContext dbContext, IPricelistParser parser)
{
this.dbContext = dbContext;
this.parser = parser;
}
[HttpGet]
public IAsyncEnumerable<Product> GetProducts()
{
return dbContext.Products
.AsAsyncEnumerable();
}
[HttpGet]
public IAsyncEnumerable<Product> GetProducts()
{
return dbContext.Products
.AsAsyncEnumerable();
}
[HttpGet("{id}")]
public IEnumerable<Product> GetProduct(string id)
{
return dbContext.Products
.Where(p => p.Id.Equals(id));
}
[HttpGet("{id}")]
public IEnumerable<Product> GetProduct(string id)
{
return dbContext.Products
.Where(p => p.Id.Equals(id));
}
[HttpPost]
public IActionResult PostProductsFromXls()
{
try
{
var products = parser.GetProducts(HttpContext).GroupBy(p => p.ProductSku)
.Select(g => new Product(g.Key)
{
Name = g.First().Name,
DeprecatedSkus = g.SelectMany(p => p.DeprecatedSkus).Distinct().ToList(),
ProductLines = g.SelectMany(p => p.ProductLines).Distinct().ToList(),
IsOnWarehouse = g.Any(p => p.IsOnWarehouse == true),
ProductMeasure = g.First().ProductMeasure,
DeliveryMakeUp = g.First().DeliveryMakeUp,
Price = g.First().Price
});
[HttpPost]
public IActionResult PostProductsFromXls()
{
try
{
var products = parser.GetProducts(HttpContext).GroupBy(p => p.ProductSku)
.Select(g => new Product(g.Key)
{
Name = g.First().Name,
DeprecatedSkus = g.SelectMany(p => p.DeprecatedSkus).Distinct().ToList(),
ProductLines = g.SelectMany(p => p.ProductLines).Distinct().ToList(),
IsOnWarehouse = g.Any(p => p.IsOnWarehouse == true),
ProductMeasure = g.First().ProductMeasure,
DeliveryMakeUp = g.First().DeliveryMakeUp,
Price = g.First().Price
});
foreach (var p in products)
{
dbContext.Add<Product>(p);
}
foreach (var p in products)
{
dbContext.Add<Product>(p);
}
dbContext.SaveChanges();
return Ok();
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
dbContext.SaveChanges();
return Ok();
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
[HttpDelete]
public IActionResult DeleteAllProducts()
{
List<Product> deleted = new();
if (dbContext.Products.Count() > 0)
{
foreach (Product p in dbContext.Products)
{
deleted.Add(p);
dbContext.Remove(p);
}
dbContext.SaveChanges();
return Ok(deleted);
}
else return Ok("Empty db");
}
}
[HttpDelete]
public IActionResult DeleteAllProducts()
{
List<Product> deleted = new();
if (dbContext.Products.Count() > 0)
{
foreach (Product p in dbContext.Products)
{
deleted.Add(p);
dbContext.Remove(p);
}
dbContext.SaveChanges();
return Ok(deleted);
}
else return Ok("Empty db");
}
}
}

View File

@ -4,7 +4,7 @@ using RhSolutions.Api.Services;
using RhSolutions.Api.Middleware;
using RhSolutions.QueryModifiers;
var builder = WebApplication.CreateBuilder(args);
var builder = WebApplication.CreateBuilder();
string dbHost = builder.Configuration["DB_HOST"],
dbPort = builder.Configuration["DB_PORT"],
@ -23,17 +23,18 @@ builder.Services.AddDbContext<RhSolutionsContext>(opts =>
opts.EnableSensitiveDataLogging(true);
}
});
builder.Services.AddScoped<IPricelistParser, ClosedXMLParser>()
.AddScoped<IProductTypePredicter, ProductTypePredicter>()
.AddSingleton<ProductQueryModifierFactory>();
.AddSingleton<ProductQueryModifierFactory>()
.AddGrpc();
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.MapGrpcService<SearchService>();
app.UseMiddleware<QueryModifier>();
var context = app.Services.CreateScope().ServiceProvider
.GetRequiredService<RhSolutionsContext>();
app.Run();

View File

@ -4,7 +4,7 @@
"anonymousAuthentication": true,
"launchBrowser": false,
"iisExpress": {
"applicationUrl": "http://localhost:5000",
"applicationUrl": "http://localhost:5000;http://localhost:43000",
"sslPort": 0
}
},
@ -13,7 +13,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5000",
"applicationUrl": "http://localhost:5000;http://localhost:43000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}

View File

@ -0,0 +1,15 @@
syntax = "proto3";
service ProductSearch {
rpc GetProduct (ProductRequest) returns (ProductReply);
}
message ProductRequest {
string query = 1;
}
message ProductReply {
string id = 1;
string name = 2;
double price = 3;
}

View File

@ -9,6 +9,11 @@
<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.100.0" />
<PackageReference Include="Grpc.AspnetCore" Version="2.58.0" />
<PackageReference Include="Grpc.Tools" Version="2.59.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
@ -29,4 +34,9 @@
</None>
</ItemGroup>
<ItemGroup>
<Protobuf Include="Protos\product.proto" GrpcServices="Server" />
</ItemGroup>
</Project>

View File

@ -5,40 +5,40 @@ namespace RhSolutions.Api.Services;
public class ProductTypePredicter : IProductTypePredicter
{
private readonly string _modelPath = @"./MLModels/model.zip";
private MLContext _mlContext;
private ITransformer _loadedModel;
private PredictionEngine<Product, TypePrediction> _predEngine;
private readonly string _modelPath = @"./MLModels/model.zip";
private MLContext _mlContext;
private ITransformer _loadedModel;
private PredictionEngine<Product, TypePrediction> _predEngine;
public ProductTypePredicter()
{
_mlContext = new MLContext(seed: 0);
_loadedModel = _mlContext.Model.Load(_modelPath, out var _);
_predEngine = _mlContext.Model.CreatePredictionEngine<Product, TypePrediction>(_loadedModel);
}
public ProductTypePredicter()
{
_mlContext = new MLContext(seed: 0);
_loadedModel = _mlContext.Model.Load(_modelPath, out var _);
_predEngine = _mlContext.Model.CreatePredictionEngine<Product, TypePrediction>(_loadedModel);
}
public string? GetPredictedProductType(string productName)
{
Product p = new()
{
Name = productName
};
var prediction = _predEngine.Predict(p);
return prediction.Type;
}
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 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; }
}
public class TypePrediction
{
[ColumnName("PredictedLabel")]
public string? Type { get; set; }
}
}

View File

@ -0,0 +1,48 @@
using Grpc.Core;
using RhSolutions.Models;
using Microsoft.EntityFrameworkCore;
using RhSolutions.QueryModifiers;
namespace RhSolutions.Api.Services;
public class SearchService : ProductSearch.ProductSearchBase
{
private RhSolutionsContext _dbContext;
private IProductTypePredicter _typePredicter;
private ProductQueryModifierFactory _productQueryModifierFactory;
public SearchService(RhSolutionsContext dbContext, IProductTypePredicter typePredicter, ProductQueryModifierFactory productQueryModifierFactory)
{
_dbContext = dbContext;
_typePredicter = typePredicter;
_productQueryModifierFactory = productQueryModifierFactory;
}
public override async Task<ProductReply?> GetProduct(ProductRequest request, ServerCallContext context)
{
var productType = _typePredicter.GetPredictedProductType(request.Query);
var modifier = _productQueryModifierFactory.GetModifier(productType!);
string query = request.Query;
if (modifier.TryQueryModify(query, out var modified))
{
query = modified;
}
var product = await _dbContext.Products
.Where(p => EF.Functions.ToTsVector(
"russian", string.Join(' ', new[] { p.Name, string.Join(' ', p.ProductLines) }))
.Matches(EF.Functions.WebSearchToTsQuery("russian", query)))
.OrderByDescending(p => p.IsOnWarehouse)
.FirstOrDefaultAsync();
if (product != null)
{
return new ProductReply()
{
Id = product.Id,
Name = product.Name,
Price = (double)product.Price
};
}
return null;
}
}

View File

@ -6,5 +6,17 @@
"Microsoft.EntityFrameworkCore": "Information"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://0.0.0.0:5000",
"Protocols": "Http1AndHttp2"
},
"gRPC": {
"Url": "http://0.0.0.0:43000",
"Protocols": "Http2"
}
}
}
}