1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-07-24 15:49:42 +02:00

feat: Improved Ingredient Matching (#2535)

* added normalization to foods and units

* changed search to reference new normalized fields

* fix tests

* added parsed food matching to backend

* prevent pagination from ordering when searching

* added extra fuzzy matching to sqlite ing matching

* added tests

* only apply search ordering when order_by is null

* enabled post-search fuzzy matching for postgres

* fixed postgres fuzzy search test

* idk why this is failing

* 🤦

* simplified frontend ing matching
and restored automatic unit creation

* tightened food fuzzy threshold

* change to rapidfuzz

* sped up fuzzy matching with process

* fixed units not matching by abbreviation

* fast return for exact matches

* replace db searching with pure fuzz

* added fuzzy normalization

* tightened unit fuzzy matching thresh

* cleaned up comments/var names

* ran matching logic through the dryer

* oops

* simplified order by application logic
This commit is contained in:
Michael Genson 2023-09-15 12:19:34 -05:00 committed by GitHub
parent 084ad4228b
commit 2dfbe9f08d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 738 additions and 97 deletions

View file

@ -68,7 +68,15 @@
<RecipeIngredientEditor v-model="parsedIng[index].ingredient" allow-insert-ingredient @insert-ingredient="insertIngredient(index)" @delete="deleteIngredient(index)" />
{{ ing.input }}
<v-card-actions>
<v-spacer></v-spacer>
<v-spacer />
<BaseButton
v-if="errors[index].unitError && errors[index].unitErrorMessage !== ''"
color="warning"
small
@click="createUnit(ing.ingredient.unit, index)"
>
{{ errors[index].unitErrorMessage }}
</BaseButton>
<BaseButton
v-if="errors[index].foodError && errors[index].foodErrorMessage !== ''"
color="warning"
@ -99,7 +107,7 @@ import {
import RecipeIngredientEditor from "~/components/Domain/Recipe/RecipeIngredientEditor.vue";
import { useUserApi } from "~/composables/api";
import { useRecipe } from "~/composables/recipes";
import { useFoodData, useFoodStore, useUnitStore } from "~/composables/store";
import { useFoodData, useFoodStore, useUnitStore, useUnitData } from "~/composables/store";
import { Parser } from "~/lib/api/user/recipes/recipe";
import { uuid4 } from "~/composables/use-utils";
@ -215,30 +223,19 @@ export default defineComponent({
const foodStore = useFoodStore();
const foodData = useFoodData();
const { units } = useUnitStore();
const unitStore = useUnitStore();
const unitData = useUnitData();
const errors = ref<Error[]>([]);
function checkForUnit(unit?: IngredientUnit | CreateIngredientUnit) {
if (!unit) {
return false;
}
if (units.value && unit?.name) {
const lower = unit.name.toLowerCase();
return units.value.some((u) => u.name.toLowerCase() === lower);
}
return false;
// @ts-expect-error; we're just checking if there's an id on this unit and returning a boolean
return !!unit?.id;
}
function checkForFood(food?: IngredientFood | CreateIngredientFood) {
if (!food) {
return false;
}
if (foodStore.foods.value && food?.name) {
const lower = food.name.toLowerCase();
return foodStore.foods.value.some((f) => f.name.toLowerCase() === lower);
}
return false;
// @ts-expect-error; we're just checking if there's an id on this food and returning a boolean
return !!food?.id;
}
async function createFood(food: CreateIngredientFood | undefined, index: number) {
@ -247,11 +244,24 @@ export default defineComponent({
}
foodData.data.name = food.name;
await foodStore.actions.createOne(foodData.data);
parsedIng.value[index].ingredient.food = await foodStore.actions.createOne(foodData.data) || undefined;
errors.value[index].foodError = false;
foodData.reset();
}
async function createUnit(unit: CreateIngredientUnit | undefined, index: number) {
if (!unit) {
return;
}
unitData.data.name = unit.name;
parsedIng.value[index].ingredient.unit = await unitStore.actions.createOne(unitData.data) || undefined;
errors.value[index].unitError = false;
unitData.reset();
}
function insertIngredient(index: number) {
if (!recipe.value?.recipeIngredient) {
return;
@ -287,27 +297,21 @@ export default defineComponent({
// =========================================================
// Save All Logic
async function saveAll() {
let ingredients = parsedIng.value.map((ing) => {
const ingredients = parsedIng.value.map((ing) => {
if (!checkForFood(ing.ingredient.food)) {
ing.ingredient.food = undefined;
}
if (!checkForUnit(ing.ingredient.unit)) {
ing.ingredient.unit = undefined;
}
return {
...ing.ingredient,
originalText: ing.input,
} as RecipeIngredient;
});
ingredients = ingredients.map((ing) => {
if (!foodStore.foods.value || !units.value) {
return ing;
}
// Get food from foods
const lowerFood = ing.food?.name?.toLowerCase();
ing.food = foodStore.foods.value.find((f) => f.name.toLowerCase() === lowerFood);
// Get unit from units
const lowerUnit = ing.unit?.name?.toLowerCase();
ing.unit = units.value.find((u) => u.name.toLowerCase() === lowerUnit);
return ing;
});
if (!recipe.value || !recipe.value.slug) {
return;
}
@ -328,6 +332,7 @@ export default defineComponent({
parser,
saveAll,
createFood,
createUnit,
deleteIngredient,
insertIngredient,
errors,