Using Dataview (or something similar) to sort and filter existing notes

Hey, y’all.

What I’m trying to do

I’m currently working on a (pretty sizable) database of mixed-drink recipes. As it stands, I’ve sorted recipes by the source I got them from - this includes books, web sites, conversations with bartenders, and others.

For each recipe (currently north of 5,000), I’m going through and tagging them with two “family” properties: “root” (what drink are they fundamentally?) and “parent” (what are they riffing on?

For example, the Pete’s Word is, by family, a riff on the Last Word (I’ve got many Last Word riffs in here), but, at its root, it’s a Sidecar. There are six “root” sections; the “family” sections will expand and contract as I find recipes that riff on recipes that riff on recipes / et cetera.

Ultimately, I’d like to make a note that takes all of these recipes and sorts them into a tree: Root first; families (and recipes without a family); then recipes after families.

For example:

Sidecar
--- Bird of Paradise
--- Delicate Refusal
--- Last Word
------ Crop Top
------ Pete's Word
--- Margarita
------ Forgive and Forget

I’d also like for recipes without “root” or “parent” tags to go in their own section so I can update them accordingly.

I’m thinking the best path here would be to use a combination of Dataview, Folder Note, and Waypoint: Dataview to filter things down, Folder Note to create a hub note of sorts, and Waypoint for the way it automatically creates trees of links. But I’m open to being totally wrong here – I’m quite new to Obsidian.

Things I have tried

I’ve started trying to learn Dataview to pull out recipes here, using the LIST functionality, but I’ve not figured out how to specifically call ones with certain properties. I’ve also tried out messing with Waypoint to see if I can get it to call what I need, but, there, I’m running into a simple file-organization issue - Waypoint generates trees based off of the existing folder tree, and I want to generate a new tree rather than use the one I’ve currently got set up. I’ve spent quite a while digging around on forums and help centers, largely with a focus on doubling folder trees and filtering by properties, but to little luck. To be frank, I’m just not certain how possible what I’m looking for is (or how to make it possible in the first place).

I’d love any advice here, or even just a point in the right direction - fully realize it’s possible I’ve missed something elementary here.

I’m not familiar with the problem domain (mixed drinks) and thus I’m not sure about the ontology you described. For example:

  • In your example “Bird of Paradise” and “Delicate refusal” have “Sidecar” as root, but… no family? In this case, will you like them to be grouped in a “Sidecar → No-family” group?
  • “Last Word” is a recipe? Or it is only the familiy for “Crop Top” and “Pete’s Word”?
  • The hierarchy is apparently “root->family->recipe”, so, in this regard, I would have used a single property “parent”. Recpies have families as parents, families have roots as parents. Why do you use two separate properties? As I understand it, this could lead to contradictions. Assume for example that I introduce a new recipe “Foo”, with family=“Last Word”, root=“Foobar”. This (I think) contradicts the hierarchy you were buiding, because this “Foo” recipe would imply that “Last Word” is a “child” of the root “Foobar”, while your previous examples implied that “Last Word” is a child of root “Sidecar”.
  • What I meant in the last point is that having two separate properties introduces the possibility of having one same familiy with two or more different roots. Is this possible in your problem domain?

Perhaps most of my doubts could be cleared if you expand your example by listing the recipes and the values for “root” and “family” of each one (only for the recipes in your previous example, of course)

Despite the possible inconsistencies signaled in my previous post, I think the following dataviewjs snippet could work. Unfortunately I cannot test it, because I don’t have the appropriate data.

const recipes_folder = "Recipes" // Change this

const all = dv.pages(`"${recipes_folder}"`).where(p => p.root || p.family);

// Unique roots, sorted
const roots = dv.array(all.where(p => p.root).map(p => p.root))
  .distinct()
  .sort(r => String(r).toLowerCase());

// List to build the markdown output
let out = [];

for (const root of roots) {
  out.push(`## ${root}`);
  const subset = all.where(p => p.root === root);

  // Group by family
  const groups = dv.array(subset.groupBy(p => p.family ?? "__NO_FAMILY__"))
                   .sort(g => g.key === "__NO_FAMILY__" ? "zzz" : String(g.key).toLowerCase());

  for (const g of groups) {
    const familyName = g.key === "__NO_FAMILY__" ? "— No family —" : g.key;
    out.push(`- **${familyName}**`);
    const items = dv.array(g.rows).sort(t => t.file.name.toLowerCase());
    for (const it of items) {
      out.push(`  - [[${it.file.path}]]`);
    }
  }
  out.push(""); // empty line
}

// Section: recipes without root
const missingRoot = dv.array(dv.pages(`"${recipes_folder}"`).where(p => !p.root)).sort(t => t.file.name.toLowerCase());
if (missingRoot.length) {
  out.push("## Missing root");
  for (const it of missingRoot) {
    out.push(`- [[${it.file.path}]]`);
  }
}

dv.paragraph(out.join("\n"));

Hey! I appreciate your help and questions here.

In this example, yes, Bird of Paradise and Delicate Refusal both have “Sidecar” as their root but no family. At risk of getting into things too deeply here, a sidecar is a style of mixed drink in itself. I’m only intending to introduce families when drinks spawn their own riffs, i.e. the Last Word variants I provided.

Here, let me provide a fleshed-out example:

Sidecar [root]
--- Bird of Paradise [root: sidecar; no family]
--- Delicate Refusal [root: sidecar; no family]
--- Last Word [root: sidecar; no family]
------ Crop Top [root: sidecar; family: last word]
------ Pete's Word [root: sidecar; family: last word]
------ Dutch Kills' Last Word [root: sidecar; family: last word]
--- Margarita [root: sidecar; no family]
------ Forgive and Forget [root: sidecar; family: margarita]

As it stands, I’m imagining the parents as folders rather than recipes – they’re archetypes to riff on. It’s entirely possible that I’d include a recipe called “Last Word”; in that case, it would be a child of the “Last Word” parent. See below. If that’s not possible, I’m happy to rename the “Last Word” recipe to something different from the folder name, i.e. “Last Word (PUNCH)”.

--- Last Word [FOLDER; root: sidecar; no family]
------ Last Word [RECIPE; root: sidecar; family: last word]

It’s possible for one drink to fall under multiple families, not they can only have one root. (Some drinks, for example, are somewhere between a Margarita and a Last Word; in that case, I might want to tag it with [root: sidecar; parent: last word, margarita].

The intention of having and “root” vs. “family” tag is to offer me more in-the-moment knowledge when I’m looking at a note. A “Sidecar” can be a lot of things; a “Last Word” is much more specific, so I’ve got a better idea of what to expect from the flavor profile of a recipe when I see that.

I realize you specified wanting only the original example, but just in case this is of value, I’ve provided an annotated example of what this ultimately may look like - in the most complicated single example I’m able to find.

--- Martini [FOLDER]
----- Negroni [FOLDER; root: martini]
-------- Boulevardier [FOLDER; root: martini; parent: negroni]
----------- Boulevardier (Dead Rabbit) [RECIPE; root: martini; parent: boulevardier]
----------- Boulevardier (Death & Co.) [RECIPE; root: martini; parent: boulevardier]
-------- White Negroni [FOLDER; root: martini; parent: negroni]
----------- Trick Dog's White Negroni [RECIPE; root: martini; parent: white negroni]
----------- Dr. Fantasy [RECIPE; root: martini: parent: white negroni]
-------- Bitter End [RECIPE; root: martini; parent: negroni]
-------- De Ville [RECIPE; root: martini; parent: negroni]

This isn’t quite related, but I figured you should know: I’m currently running into issues running the code you sent me. At the start of line two, I’m getting: “Expected one of the following: Not a comment, TABLE or LIST or TASK or CALENDAR, whitespace” I’ll see if I can’t pick it apart tomorrow morning.

Again - thanks so much for this. I’m really grateful you’re digging into this with me, and I hope I’m being fairly clear with what I’m trying to do.

Ok, two quick notes (I have no time now to address your complete example, I’ll come back later).

  1. The code I provided must be put in a dataviewjs block, not dataview (an you need to allow dataviewjs in dataview settings)
  2. My code does not allow for multiple, comma separated, values in family. I don’t see how the output hierarchy would look in this case.

Can you test the code again and look if, as it is now, is useful enough?

Hey!

Thanks so much. The code provided does work well - I’m trying to fiddle with it and see if there’s any way I can get it to have sub-families, like I described in the Negroni / White Negroni example, but that’s not as critical.

I’m also trying to find out if there’s any way I can make the headers (i.e. Daiquiri / Last Word / etc) collapsible, but I think that’s more of a UX/UI thing than a JS/Dataview thing.

Glad to know that the script works.

Adapting it to use subfamilies, or multiple families per recipe is not trivial.

And for your second request, unfortunately this seems to be a limitation of dataview/obsidian. When a dataviewjs block generates content, that content is not treated by obsidian the same way than the content you actually wrote in a note. In particular, the headings or list items present if the content generated by dataviewjs are not collapsible.

The code I provided builds a long string containing the markdown of the result. Then uses dv.paragraph() to output that markdown in the note. But as you have seen, the items in that output are not collapsible.

The only possible workaround that I can think of is, to replace the last line in the script dv.paragraph(out.join("\n")); by:

dv.paragraph("```markdown\n" + out.join("\n")+ "```\n");

This causes the markdown to be output in a code block. You can then copy the content of that block (it has a “copy” button) and paste it into a new note. That note would be “normal” (i.e. contain markdown instead of a dataviewjs block), so Obsidian will treat it normally, allowing you to fold at each header or bullet (if those options are enabled at Settings → Editor)

Of course this means that whenever you alter your recipes, you need to copy&paste again the result.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.