50 lines
1.8 KiB
JavaScript
50 lines
1.8 KiB
JavaScript
function normalizeSearchText(value) {
|
|
return String(value ?? '').trim().toLocaleLowerCase().replace(/\s+/g, ' ');
|
|
}
|
|
|
|
function ingredientNames(recipe) {
|
|
return (recipe.ingredientGroups ?? [])
|
|
.flatMap((group) => group.items ?? [])
|
|
.map((item) => item.name)
|
|
.filter(Boolean);
|
|
}
|
|
|
|
export function rankRecipeMatch(recipe, query) {
|
|
const normalizedQuery = normalizeSearchText(query);
|
|
if (!normalizedQuery) return { score: 0, matchedIngredients: [] };
|
|
|
|
const ingredients = ingredientNames(recipe);
|
|
const matchedIngredients = ingredients.filter((name) => (
|
|
normalizeSearchText(name).includes(normalizedQuery)
|
|
));
|
|
const hasExactIngredient = matchedIngredients.some((name) => (
|
|
normalizeSearchText(name) === normalizedQuery
|
|
));
|
|
const title = normalizeSearchText(recipe.title);
|
|
const tags = (recipe.tags ?? []).map(normalizeSearchText);
|
|
const summary = normalizeSearchText(recipe.summary);
|
|
|
|
let score = 0;
|
|
if (hasExactIngredient) score = 500;
|
|
else if (matchedIngredients.length > 0) score = 400;
|
|
else if (title === normalizedQuery) score = 350;
|
|
else if (title.includes(normalizedQuery)) score = 300;
|
|
else if (tags.includes(normalizedQuery)) score = 250;
|
|
else if (tags.some((tag) => tag.includes(normalizedQuery))) score = 200;
|
|
else if (summary.includes(normalizedQuery)) score = 100;
|
|
|
|
return { score, matchedIngredients };
|
|
}
|
|
|
|
export function searchRecipes(recipes, query) {
|
|
if (!normalizeSearchText(query)) {
|
|
return recipes.map((recipe) => ({ recipe, score: 0, matchedIngredients: [] }));
|
|
}
|
|
|
|
return recipes
|
|
.map((recipe, index) => ({ recipe, index, ...rankRecipeMatch(recipe, query) }))
|
|
.filter(({ score }) => score > 0)
|
|
.sort((a, b) => b.score - a.score || a.index - b.index)
|
|
.map(({ recipe, score, matchedIngredients }) => ({ recipe, score, matchedIngredients }));
|
|
}
|