From 7de2e831575f531b7aa742f4db6a1f5b693220aa Mon Sep 17 00:00:00 2001 From: Jesse Brault Date: Fri, 30 Jan 2026 16:26:22 -0600 Subject: [PATCH] MME-3 Add recipe embedding job. --- .../api/BackfillRecipeEmbeddings.java | 39 +++---------- .../app/mealsmadeeasy/api/job/JobService.java | 5 ++ .../api/recipe/RecipeService.java | 6 +- .../recipe/job/RecipeEmbeddingJobHandler.java | 58 +++++++++++++++++++ .../recipe/job/RecipeEmbeddingJobPayload.java | 3 + src/main/resources/application.properties | 1 - 6 files changed, 78 insertions(+), 34 deletions(-) create mode 100644 src/main/java/app/mealsmadeeasy/api/recipe/job/RecipeEmbeddingJobHandler.java create mode 100644 src/main/java/app/mealsmadeeasy/api/recipe/job/RecipeEmbeddingJobPayload.java diff --git a/src/main/java/app/mealsmadeeasy/api/BackfillRecipeEmbeddings.java b/src/main/java/app/mealsmadeeasy/api/BackfillRecipeEmbeddings.java index 1e999b9..c729787 100644 --- a/src/main/java/app/mealsmadeeasy/api/BackfillRecipeEmbeddings.java +++ b/src/main/java/app/mealsmadeeasy/api/BackfillRecipeEmbeddings.java @@ -1,56 +1,31 @@ package app.mealsmadeeasy.api; +import app.mealsmadeeasy.api.job.JobService; import app.mealsmadeeasy.api.recipe.Recipe; -import app.mealsmadeeasy.api.recipe.RecipeEmbedding; import app.mealsmadeeasy.api.recipe.RecipeRepository; -import app.mealsmadeeasy.api.recipe.RecipeService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.ai.embedding.EmbeddingModel; +import app.mealsmadeeasy.api.recipe.job.RecipeEmbeddingJobHandler; +import app.mealsmadeeasy.api.recipe.job.RecipeEmbeddingJobPayload; +import lombok.RequiredArgsConstructor; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; -import java.time.OffsetDateTime; import java.util.List; @Component +@RequiredArgsConstructor @ConditionalOnProperty(name = "backfill.recipe-embeddings.enabled", havingValue = "true") public class BackfillRecipeEmbeddings implements ApplicationRunner { - private static final Logger logger = LoggerFactory.getLogger(BackfillRecipeEmbeddings.class); - private final RecipeRepository recipeRepository; - private final RecipeService recipeService; - private final EmbeddingModel embeddingModel; - - public BackfillRecipeEmbeddings( - RecipeRepository recipeRepository, - RecipeService recipeService, - EmbeddingModel embeddingModel - ) { - this.recipeRepository = recipeRepository; - this.recipeService = recipeService; - this.embeddingModel = embeddingModel; - } + private final JobService jobService; @Override public void run(ApplicationArguments args) { final List recipeEntities = this.recipeRepository.findAllByEmbeddingIsNull(); for (final Recipe recipe : recipeEntities) { - logger.info("Calculating embedding for {}", recipe); - final String renderedMarkdown = this.recipeService.getRenderedMarkdown(recipe); - final String toEmbed = "

" + recipe.getTitle() + "

" + renderedMarkdown; - final float[] embedding = this.embeddingModel.embed(toEmbed); - - final RecipeEmbedding recipeEmbedding = new RecipeEmbedding(); - recipeEmbedding.setRecipe(recipe); - recipeEmbedding.setEmbedding(embedding); - recipeEmbedding.setTimestamp(OffsetDateTime.now()); - recipe.setEmbedding(recipeEmbedding); - - this.recipeRepository.save(recipe); + this.jobService.create(RecipeEmbeddingJobHandler.JOB_KEY, new RecipeEmbeddingJobPayload(recipe.getId())); } this.recipeRepository.flush(); } diff --git a/src/main/java/app/mealsmadeeasy/api/job/JobService.java b/src/main/java/app/mealsmadeeasy/api/job/JobService.java index f3a07c9..e65b139 100644 --- a/src/main/java/app/mealsmadeeasy/api/job/JobService.java +++ b/src/main/java/app/mealsmadeeasy/api/job/JobService.java @@ -4,6 +4,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.transaction.Transactional; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -19,6 +21,8 @@ import java.util.concurrent.ThreadLocalRandom; @Service public class JobService { + private static final Logger logger = LoggerFactory.getLogger(JobService.class); + private final ApplicationContext applicationContext; private final JobRepository jobRepository; private final ObjectMapper objectMapper; @@ -83,6 +87,7 @@ public class JobService { job.setLockedAt(null); this.jobRepository.save(job); } catch (Exception e) { + logger.error("Job {} {} threw an exception: {}", job.getId(), job.getJobKey(), e.getMessage()); final int attemptCount = job.getAttempts() + 1; final boolean isDead = attemptCount >= job.getMaxAttempts(); final OffsetDateTime runAfter = isDead diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeService.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeService.java index 8d6251e..fdd5a16 100644 --- a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeService.java +++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeService.java @@ -6,6 +6,8 @@ import app.mealsmadeeasy.api.image.ImageService; import app.mealsmadeeasy.api.job.JobService; import app.mealsmadeeasy.api.markdown.MarkdownService; import app.mealsmadeeasy.api.recipe.body.RecipeAiSearchBody; +import app.mealsmadeeasy.api.recipe.job.RecipeEmbeddingJobHandler; +import app.mealsmadeeasy.api.recipe.job.RecipeEmbeddingJobPayload; import app.mealsmadeeasy.api.recipe.job.RecipeInferJobHandler; import app.mealsmadeeasy.api.recipe.job.RecipeInferJobPayload; import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec; @@ -52,7 +54,9 @@ public class RecipeService { draft.setRawText(spec.getRawText()); draft.setMainImage(spec.getMainImage()); draft.setIsPublic(spec.isPublic()); - return this.recipeRepository.save(draft); + final Recipe saved = this.recipeRepository.save(draft); + this.jobService.create(RecipeEmbeddingJobHandler.JOB_KEY, new RecipeEmbeddingJobPayload(saved.getId())); + return saved; } private Recipe getById(Integer id) { diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/job/RecipeEmbeddingJobHandler.java b/src/main/java/app/mealsmadeeasy/api/recipe/job/RecipeEmbeddingJobHandler.java new file mode 100644 index 0000000..8c62907 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/recipe/job/RecipeEmbeddingJobHandler.java @@ -0,0 +1,58 @@ +package app.mealsmadeeasy.api.recipe.job; + +import app.mealsmadeeasy.api.job.Job; +import app.mealsmadeeasy.api.job.JobHandler; +import app.mealsmadeeasy.api.recipe.Recipe; +import app.mealsmadeeasy.api.recipe.RecipeEmbedding; +import app.mealsmadeeasy.api.recipe.RecipeRepository; +import app.mealsmadeeasy.api.recipe.RecipeService; +import app.mealsmadeeasy.api.util.NoSuchEntityWithIdException; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.stereotype.Component; + +import java.time.OffsetDateTime; + +@Component +@RequiredArgsConstructor +public class RecipeEmbeddingJobHandler implements JobHandler { + + public static final String JOB_KEY = "RECIPE_EMBEDDING"; + + private static final Logger logger = LoggerFactory.getLogger(RecipeEmbeddingJobHandler.class); + + private final RecipeRepository recipeRepository; + private final RecipeService recipeService; + private final EmbeddingModel embeddingModel; + + @Override + public Class getPayloadType() { + return RecipeEmbeddingJobPayload.class; + } + + @Override + public String getJobKey() { + return JOB_KEY; + } + + @Override + public void handle(Job job, RecipeEmbeddingJobPayload payload) { + logger.info("Calculating embedding for recipeId {}", payload.recipeId()); + final Recipe recipe = this.recipeRepository.findById(payload.recipeId()) + .orElseThrow(() -> new NoSuchEntityWithIdException(Recipe.class, payload.recipeId())); + final String renderedMarkdown = this.recipeService.getRenderedMarkdown(recipe); + final String toEmbed = "

" + recipe.getTitle() + "

\n" + renderedMarkdown; + final float[] embedding = this.embeddingModel.embed(toEmbed); + + final RecipeEmbedding recipeEmbedding = new RecipeEmbedding(); + recipeEmbedding.setRecipe(recipe); + recipeEmbedding.setEmbedding(embedding); + recipeEmbedding.setTimestamp(OffsetDateTime.now()); + recipe.setEmbedding(recipeEmbedding); + this.recipeRepository.save(recipe); + logger.info("Finished calculating embedding for recipeId {}", payload.recipeId()); + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/job/RecipeEmbeddingJobPayload.java b/src/main/java/app/mealsmadeeasy/api/recipe/job/RecipeEmbeddingJobPayload.java new file mode 100644 index 0000000..caf7337 --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/recipe/job/RecipeEmbeddingJobPayload.java @@ -0,0 +1,3 @@ +package app.mealsmadeeasy.api.recipe.job; + +public record RecipeEmbeddingJobPayload(Integer recipeId) {} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 92218bb..98a3781 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -20,5 +20,4 @@ app.mealsmadeeasy.api.files.bucketName=files # AI spring.ai.vectorstore.pgvector.dimensions=1024 -spring.ai.ollama.chat.options.model=deepseek-ocr:latest spring.ai.ollama.init.pull-model-strategy=never \ No newline at end of file