Added vector search
This commit is contained in:
@@ -6,6 +6,8 @@ using System.Security.Claims;
|
|||||||
using Seasoned.Backend.Models;
|
using Seasoned.Backend.Models;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Pgvector;
|
||||||
|
using Pgvector.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Seasoned.Backend.Controllers;
|
namespace Seasoned.Backend.Controllers;
|
||||||
|
|
||||||
@@ -55,6 +57,9 @@ public class RecipeController : ControllerBase
|
|||||||
UserId = userId
|
UserId = userId
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var fullText = $"{recipeDto.Title} {string.Join(" ", recipeDto.Ingredients)} {string.Join(" ", recipeDto.Instructions)}";
|
||||||
|
recipe.Embedding = await _recipeService.GetEmbeddingAsync(fullText);
|
||||||
|
|
||||||
_context.Recipes.Add(recipe);
|
_context.Recipes.Add(recipe);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
@@ -110,4 +115,23 @@ public class RecipeController : ControllerBase
|
|||||||
var result = await _recipeService.ConsultChefAsync(request.Prompt);
|
var result = await _recipeService.ConsultChefAsync(request.Prompt);
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("search")]
|
||||||
|
public async Task<ActionResult<IEnumerable<Recipe>>> SearchRecipes([FromQuery] string query)
|
||||||
|
{
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(query))
|
||||||
|
return await GetMyRecipes();
|
||||||
|
|
||||||
|
var queryVector = await _recipeService.GetEmbeddingAsync(query);
|
||||||
|
|
||||||
|
var results = await _context.Recipes
|
||||||
|
.Where(r => r.UserId == userId)
|
||||||
|
.OrderBy(r => r.Embedding!.CosineDistance(queryVector))
|
||||||
|
.Take(15)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Ok(results);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using Pgvector;
|
||||||
using Seasoned.Backend.Data;
|
using Seasoned.Backend.Data;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
@@ -13,8 +14,8 @@ using Seasoned.Backend.Data;
|
|||||||
namespace Seasoned.Backend.Migrations
|
namespace Seasoned.Backend.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(ApplicationDbContext))]
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
[Migration("20260318044626_ChangeIconToImageUrl")]
|
[Migration("20260318201722_InitialCreateWith768Vector")]
|
||||||
partial class ChangeIconToImageUrl
|
partial class InitialCreateWith768Vector
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
@@ -234,6 +235,9 @@ namespace Seasoned.Backend.Migrations
|
|||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Vector>("Embedding")
|
||||||
|
.HasColumnType("vector(768)");
|
||||||
|
|
||||||
b.Property<string>("ImageUrl")
|
b.Property<string>("ImageUrl")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
@@ -2,13 +2,14 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using Pgvector;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Seasoned.Backend.Migrations
|
namespace Seasoned.Backend.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public partial class ChangeIconToImageUrl : Migration
|
public partial class InitialCreateWith768Vector : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
@@ -172,7 +173,8 @@ namespace Seasoned.Backend.Migrations
|
|||||||
Ingredients = table.Column<List<string>>(type: "text[]", nullable: false),
|
Ingredients = table.Column<List<string>>(type: "text[]", nullable: false),
|
||||||
Instructions = table.Column<List<string>>(type: "text[]", nullable: false),
|
Instructions = table.Column<List<string>>(type: "text[]", nullable: false),
|
||||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
UserId = table.Column<string>(type: "text", nullable: true)
|
UserId = table.Column<string>(type: "text", nullable: true),
|
||||||
|
Embedding = table.Column<Vector>(type: "vector(768)", nullable: true)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
@@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using Pgvector;
|
||||||
using Seasoned.Backend.Data;
|
using Seasoned.Backend.Data;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
@@ -231,6 +232,9 @@ namespace Seasoned.Backend.Migrations
|
|||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Vector>("Embedding")
|
||||||
|
.HasColumnType("vector(768)");
|
||||||
|
|
||||||
b.Property<string>("ImageUrl")
|
b.Property<string>("ImageUrl")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
using Pgvector;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace Seasoned.Backend.Models;
|
namespace Seasoned.Backend.Models;
|
||||||
|
|
||||||
public class Recipe {
|
public class Recipe {
|
||||||
@@ -8,4 +11,7 @@ public class Recipe {
|
|||||||
public List<string> Instructions { get; set; } = new();
|
public List<string> Instructions { get; set; } = new();
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
public string? UserId { get; set; }
|
public string? UserId { get; set; }
|
||||||
|
|
||||||
|
[Column(TypeName = "vector(768)")]
|
||||||
|
public Vector? Embedding { get; set; }
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.2" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.13" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.13" />
|
||||||
<PackageReference Include="Mscc.GenerativeAI" Version="2.2.8" />
|
<PackageReference Include="Mscc.GenerativeAI" Version="2.2.8" />
|
||||||
|
<PackageReference Include="pgvector" Version="0.3.2" />
|
||||||
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.3.0" />
|
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.3.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ public interface IRecipeService
|
|||||||
{
|
{
|
||||||
Task<RecipeResponseDto> ParseRecipeImageAsync(IFormFile image);
|
Task<RecipeResponseDto> ParseRecipeImageAsync(IFormFile image);
|
||||||
Task<ChefConsultResponseDto> ConsultChefAsync(string userPrompt);
|
Task<ChefConsultResponseDto> ConsultChefAsync(string userPrompt);
|
||||||
|
Task<Pgvector.Vector> GetEmbeddingAsync(string text);
|
||||||
}
|
}
|
||||||
@@ -5,16 +5,33 @@ using System.IO;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Pgvector;
|
||||||
|
|
||||||
namespace Seasoned.Backend.Services;
|
namespace Seasoned.Backend.Services;
|
||||||
|
|
||||||
public class RecipeService : IRecipeService
|
public class RecipeService : IRecipeService
|
||||||
{
|
{
|
||||||
private readonly string _apiKey;
|
private readonly string _apiKey;
|
||||||
|
private readonly GoogleAI _googleAI;
|
||||||
|
|
||||||
public RecipeService(IConfiguration config)
|
public RecipeService(IConfiguration config)
|
||||||
{
|
{
|
||||||
_apiKey = config["GEMINI_API_KEY"] ?? throw new ArgumentNullException("API Key missing");
|
_apiKey = config["GEMINI_API_KEY"] ?? throw new ArgumentNullException("API Key missing");
|
||||||
|
_googleAI = new GoogleAI(_apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Vector> GetEmbeddingAsync(string text)
|
||||||
|
{
|
||||||
|
var model = _googleAI.GenerativeModel("text-embedding-004");
|
||||||
|
|
||||||
|
var response = await model.EmbedContent(text);
|
||||||
|
|
||||||
|
if (response.Embedding?.Values != null)
|
||||||
|
{
|
||||||
|
return new Vector(response.Embedding.Values.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception("The Chef couldn't extract the meaning from that recipe text.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<RecipeResponseDto> ParseRecipeImageAsync(IFormFile image)
|
public async Task<RecipeResponseDto> ParseRecipeImageAsync(IFormFile image)
|
||||||
@@ -75,6 +92,7 @@ public class RecipeService : IRecipeService
|
|||||||
}
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException ex)
|
||||||
{
|
{
|
||||||
|
Console.WriteLine($"Chef's Error: JSON Parsing failed. Message: {ex.Message}");
|
||||||
Console.WriteLine($"Raw AI Output: {rawText}");
|
Console.WriteLine($"Raw AI Output: {rawText}");
|
||||||
return new RecipeResponseDto { Title = "Parsing Error" };
|
return new RecipeResponseDto { Title = "Parsing Error" };
|
||||||
}
|
}
|
||||||
@@ -145,4 +163,5 @@ public class RecipeService : IRecipeService
|
|||||||
if (start == -1 || end == -1) return string.Empty;
|
if (start == -1 || end == -1) return string.Empty;
|
||||||
return rawText.Substring(start, (end - start) + 1);
|
return rawText.Substring(start, (end - start) + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user