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:
parent
084ad4228b
commit
2dfbe9f08d
17 changed files with 738 additions and 97 deletions
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue