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 FROM mcr.microsoft.com/dotnet/aspnet:6.0
EXPOSE 5000 EXPOSE 5000
EXPOSE 43000
WORKDIR /app WORKDIR /app
COPY --from=build /app/out . COPY --from=build /app/out .
ENV ASPNETCORE_ENVIRONMENT Production 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 namespace RhSolutions.Api.Controllers
{ {
[Route("api/[controller]")] [Route("api/[controller]")]
public class ProductsController : ControllerBase public class ProductsController : ControllerBase
{ {
private RhSolutionsContext dbContext; private RhSolutionsContext dbContext;
private IPricelistParser parser; private IPricelistParser parser;
public ProductsController(RhSolutionsContext dbContext, IPricelistParser parser) public ProductsController(RhSolutionsContext dbContext, IPricelistParser parser)
{ {
this.dbContext = dbContext; this.dbContext = dbContext;
this.parser = parser; this.parser = parser;
} }
[HttpGet] [HttpGet]
public IAsyncEnumerable<Product> GetProducts() public IAsyncEnumerable<Product> GetProducts()
{ {
return dbContext.Products return dbContext.Products
.AsAsyncEnumerable(); .AsAsyncEnumerable();
} }
[HttpGet("{id}")] [HttpGet("{id}")]
public IEnumerable<Product> GetProduct(string id) public IEnumerable<Product> GetProduct(string id)
{ {
return dbContext.Products return dbContext.Products
.Where(p => p.Id.Equals(id)); .Where(p => p.Id.Equals(id));
} }
[HttpPost] [HttpPost]
public IActionResult PostProductsFromXls() public IActionResult PostProductsFromXls()
{ {
try try
{ {
var products = parser.GetProducts(HttpContext).GroupBy(p => p.ProductSku) var products = parser.GetProducts(HttpContext).GroupBy(p => p.ProductSku)
.Select(g => new Product(g.Key) .Select(g => new Product(g.Key)
{ {
Name = g.First().Name, Name = g.First().Name,
DeprecatedSkus = g.SelectMany(p => p.DeprecatedSkus).Distinct().ToList(), DeprecatedSkus = g.SelectMany(p => p.DeprecatedSkus).Distinct().ToList(),
ProductLines = g.SelectMany(p => p.ProductLines).Distinct().ToList(), ProductLines = g.SelectMany(p => p.ProductLines).Distinct().ToList(),
IsOnWarehouse = g.Any(p => p.IsOnWarehouse == true), IsOnWarehouse = g.Any(p => p.IsOnWarehouse == true),
ProductMeasure = g.First().ProductMeasure, ProductMeasure = g.First().ProductMeasure,
DeliveryMakeUp = g.First().DeliveryMakeUp, DeliveryMakeUp = g.First().DeliveryMakeUp,
Price = g.First().Price Price = g.First().Price
}); });
foreach (var p in products) foreach (var p in products)
{ {
dbContext.Add<Product>(p); dbContext.Add<Product>(p);
} }
dbContext.SaveChanges(); dbContext.SaveChanges();
return Ok(); return Ok();
} }
catch (Exception ex) catch (Exception ex)
{ {
return BadRequest(ex.Message); return BadRequest(ex.Message);
} }
} }
[HttpDelete] [HttpDelete]
public IActionResult DeleteAllProducts() public IActionResult DeleteAllProducts()
{ {
List<Product> deleted = new(); List<Product> deleted = new();
if (dbContext.Products.Count() > 0) if (dbContext.Products.Count() > 0)
{ {
foreach (Product p in dbContext.Products) foreach (Product p in dbContext.Products)
{ {
deleted.Add(p); deleted.Add(p);
dbContext.Remove(p); dbContext.Remove(p);
} }
dbContext.SaveChanges(); dbContext.SaveChanges();
return Ok(deleted); return Ok(deleted);
} }
else return Ok("Empty db"); else return Ok("Empty db");
} }
} }
} }

View File

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

View File

@ -4,7 +4,7 @@
"anonymousAuthentication": true, "anonymousAuthentication": true,
"launchBrowser": false, "launchBrowser": false,
"iisExpress": { "iisExpress": {
"applicationUrl": "http://localhost:5000", "applicationUrl": "http://localhost:5000;http://localhost:43000",
"sslPort": 0 "sslPort": 0
} }
}, },
@ -13,7 +13,7 @@
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": false, "launchBrowser": false,
"applicationUrl": "http://localhost:5000", "applicationUrl": "http://localhost:5000;http://localhost:43000",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "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> <ItemGroup>
<PackageReference Include="ClosedXML" Version="0.100.0" /> <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"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
@ -29,4 +34,9 @@
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Protobuf Include="Protos\product.proto" GrpcServices="Server" />
</ItemGroup>
</Project> </Project>

View File

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