# Obsidian for Meal Planning _February 29, 2024_ I'm beginning to use Obsidian for meal planning, part of which includes selecting recipes to cook every week. One of the goals is to simplify and avoid food waste by picking recipes that use overlapping ingredients. I realized that ==Obsidian, with its note-linking feature and its [Dataview plugin](https://github.com/blacksmithgu/obsidian-dataview), can automatically display recipes that have the most ingredients in common,== provided that each recipe is stored as a separate note and follows a simple format. ## Recipe Notes Each recipe has its own Markdown note. All recipes are stored in a dedicated "Recipes" folder, but that's not a hard requirement. The format of the notes is flexible, in fact it can be whatever you want, ==the only requirement is that ingredients should be links.== I like to use \[[Wikilinks]], but what's important is that the same ingredient should always link to the same file path across recipe notes. For example: ``` ## Ingredients - [[Rice (white)]] - [[Cucumbers]] - [[Spicy mayo]] - … ``` If another recipe used `[[Rice]]` instead of `[[Rice (white)]]`, then those two ingredients wouldn't be considered the same. `[[Rice]]` and `[[Rice|Rice (white)]]`, however, does work. To ensure consistency I opted to use the plural form, Obsidian's autocomplete takes care of the rest. ## Ingredient Notes There doesn't need to be actual notes for each ingredient, but if there are then it becomes possible to ==see all the recipes that use a particular ingredient by enabling the Backlinks core plugin:== ![[Carrots backlinks.png]] It also makes it possible to use aliases (carrots & carrot, for example). ## Find and Display Similar Recipes 1. ==Make sure the [[Obsidian Dataview|Dataview plugin]] is installed.== 2. Copy this code snippet into its own file, name it `similar-recipes.js` or something. ```js const RECIPES_FOLDER = 'Recipes'; const MAX_SIMILAR_RECIPES = 7; function getUniqueOutlinks(file) { return file.outlinks.values.filter((link, i, arr) => { return arr.find(l => l.path === link.path) === link; }); } /** * arr1, arr2 and exclude must be arrays of objects with a 'path' property. */ function getCommonByPath(arr1, arr2, exclude) { return arr1 // Filter out paths that are not common to arr1 and arr2. .filter(i1 => arr2.find(i2 => i1.path === i2.path)) // Filter out paths that are part of the exclude list. .filter(i => !exclude.find(e => e.path === i.path)); } function displaySimilarRecipes(recipeFolder, maxCount) { // Get outlinks (which we assume are ingredients) from the current recipe // file. const currentRecipe = dv.current().file; const currentIngredients = getUniqueOutlinks(currentRecipe); // Get recipe files from the recipe folder (configurable). const recipes = dv.pages(`"${recipeFolder}"`).values.map(p => p.file); let similarRecipes = []; for (const recipe of recipes) { // No need to compare a recipe to itself. if (currentRecipe.path === recipe.path) continue; // If the recipe has at least one ingredient in common, add it to the // list of similar recipes. const ingredients = getUniqueOutlinks(recipe); const commonIngredients = getCommonByPath(currentIngredients, ingredients, recipes); if (commonIngredients.length > 0) { similarRecipes.push([recipe, commonIngredients]); } } // Sort similar recipes based on the number of ingredients in common, or // alphabetically if equal. similarRecipes.sort((a, b) => { const compareCommon = b[1].length - a[1].length; if (compareCommon === 0) { return a[0].name.localeCompare(b[0].name); } else { return compareCommon; } }); // Only display the top recipes (configurable). similarRecipes = similarRecipes.slice(0, maxCount); // If there's at least one similar recipe, display them as a list under // a header. if (similarRecipes.length > 0) { const similarRecipesList = similarRecipes.map(r => { const ingredients = r[1].map(i => i.display.toLowerCase()).join(', '); return `${r[0].link} — also references ${ingredients}`; }); return `## Similar Recipes\n\n${dv.markdownList(similarRecipesList)}`; } } return displaySimilarRecipes(RECIPES_FOLDER, MAX_SIMILAR_RECIPES); ``` 3. Edit the folder path and the maximum number of recipes to display, as needed. 4. Use this code block inside your recipe notes: ```` ```dataviewjs // The '.js' extension must be omitted. await dv.view('path/to/similar-recipes'); ``` ```` > [!tip] For more information about `dv.view(…)` see [[Reuse JavaScript Code Across Obsidian Notes#Dataview and Custom Views]] With a few recipes, the code block will render as something like: > ## Similar Recipes > > - [[Bibimbap|Bibimbap]] — also references carrots, cilantro, cucumbers, soy sauce > - [[Thai Mango Curry|Thai Mango Curry]] — also references tofu, chili garlic paste, red curry paste > - [[Sausage Pitas (Vegan)|Sausage Pitas (Vegan)]] — also references carrots, mint > - [[Pumpkin Kale Pasta|Pumpkin Kale Pasta]] — also references garlic > - [[Mushroom Leek Soup|Mushroom Leek Soup]] — also references garlic ## Caveat: All Shared Links Are Considered Ingredients If recipe notes share links that are not related to ingredients, those will show up as well, for example: > - [[Bibimbap|Bibimbap]] — also references carrots, _note title that has nothing to do about ingredients,_ cilantro, sausage pitas (vegan) Depending on your use case, the code can be modified to address this issue. For example, if you _don't_ use [[#Ingredient Notes|ingredient notes]], then you could exclude links that don't point to an existing note. On the other hand, if you _do_ use [[#Ingredient Notes|ingredient notes]], they can be placed in a dedicated folder and you can exclude links that point to notes outside of that folder. ## Technical Details Tested with Obsidian v1.5.6 and Dataview v0.5.64.