List all inline keys used in the Vault with notes that use them

Things I have tried

dv.list(dv.pages(`"people"`).groupBy(p => p.Team).key)

However, this only targets one folder instead of the whole Vault (otherwise I get a maximum stack size exceeded error, probably because there are too many keys? But I have only a handful though…)

What I’m trying to do

I would like to maintain a consistent schema of keys in my Vault. It should look like this:
image

2 Likes

Hi @DeutscheGabanna, I tried to use your question to teach myself some Javascript and ended up spending some quality hours typing dataviewjs into the console (Ctrl+Shift+i on Windows). But I made a little progress: I can get a list of all the keys used in the vault! Maybe you or others can progress from here. Tagging the prolific dataview helper @mnvwvnm in hopes of some further insight - I hope that is ok, mnvwvnm!

The trick I thought of was to explicitly exclude many of the Obsidian-default keys associated with a file: lots of them are arrays or large nested objects. I think my vault might have an infinite loop of pages via inlinks. Thus, the first step inside the dataviewjs block:

const excludes = ["file", "aliases", "tags", "position", "links", "inlinks", "outlinks", "etags", "frontmatter", "lists", "tasks"];

Next was just a whole bunch of ugly trial and error, which maybe someone knows how to express more elegantly than this mess of code. But it does work. To continue the same dataviewjs block:

let myFields = new Set();
Object.entries(dv.pages('"/"').values)
.forEach(([thing1, thing2]) => 
		 Object.keys(thing2)
			 .forEach(k => excludes.contains(k) ? 0 : myFields.add(k))
		);
//console.log(myFields);
let myFieldsDV = dv.array(Array.from(myFields));
dv.list(myFieldsDV);

I do not know exactly what “thing1” and “thing2” represent conceptually; “k” is keys.

So, this will get you a list of inline keys from your vault that are mostly ones you have created. (I’m sure I missed filtering out some Obsidian-default ones!)

Where I got stuck: I could not figure out how to associate a file.path or even a count of notes with each key. If you see anything suspicious in the list, you’d have to do an additional search using a .where(<suspicious key>) or something like that. Something I noticed in my vault: many of my inline Dataview fields show up twice because they are not in the standard Dataview format in my notes (e.g. my list includes both “Bed Time” and “bed-time”, even though only “Bed Time” exists explicitly in my notes).

Good luck!
PS: If anyone more fluent in Javascript/dataviewjs can chime in on better techniques/style here, I would really appreciate the learning opportunity. :memo:

3 Likes

Hi, thanks @scholarInTraining !
My low-effort improvement to your great code would be to slugify the inline keys. What it does it deletes duplicates like “Project” and “project” - Dataview likes to create these sort of variants just in case a user referes to the key in different ways each time.


function convertToSlug(Text) {
  return Text.toLowerCase().replace(/ /g,'-').replace(/[-]+/g, '-').replace(/[^\w-]+/g,'');
}

const excludes = ["file", "aliases", "tags", "position", "links", "inlinks", "outlinks", "etags", "frontmatter", "lists", "tasks"];
let myFields = new Set();
Object.entries(dv.pages('"/"').values)
.forEach(([thing1, thing2]) => 
		 Object.keys(thing2)
			 .forEach(k => excludes.contains(k) ? 0 : myFields.add(convertToSlug(k)))
		);

let myFieldsDV = dv.array(Array.from(myFields));
dv.list(myFieldsDV);
2 Likes

This code cut my key list in half, so I’d count it as a success. Next stop is figuring out how to count the instances of each key.

1 Like

Your “slug” (new term for me, thanks!) cut MY list in half! That seems like excellent progress.

I still hope there is a more “dataviewjs” way to do this, rather than going through the underlying objects the way my code did. I suspect that would help with the actual query aspects of this (counting notes etc.).

1 Like

Upgrading from a Set to a Map
I changed myFields from a Set to a Map where the value field is just a count of notes where a key is seen. Full dataviewjs codeblock:

function convertToSlug(str) {
    return str.toLowerCase().replace(/ /g,'-')
		  .replace(/[-]+/g, '-').replace(/[^\w-]+/g,'');
}
const excludes = ["file", "aliases", "tags", "position", "links", "inlinks", "outlinks", "etags", "frontmatter", "lists", "tasks"];
let myFields = new Map();
Object.entries(dv.pages('"/"').values)
	.forEach(([thing1, thing2]) => 
		Object.keys(thing2)
		.map(k => convertToSlug(k))
		.filter(k => !excludes.contains(k))
		.forEach(k => 
			myFields.set(k, (myFields.has(k) ? 1 + myFields.get(k) : 1))
		)
	);
let myFieldsDV = dv.array(Array.from(myFields));
//console.log(myFieldsDV);
dv.table(
	["Slugged Field Name", "Count in Vault"], 
	myFieldsDV.sort(([sluggedField, howManyNotes]) => howManyNotes, "desc")
);

The structure of the resulting myFieldsDV is many rows of size 2, where the items in the row are the slugged field name and the count, respectively. I tried to make that clear in the sort at the bottom.
Now I think we can construct queries to get the actual list of notes associated with small counts!

1 Like

I figured it out! With enough staring and experimentation on the dataviewjs DataArray methods, I was able to get rid of most of the ugly parsing code that was bothering me.
Here’s my final dataviewjs block. Shows a table with all fields and their counts, then a table with rare fields, counts, and notes.

function convertToSlug(str) {
    return str.toLowerCase().replace(/ /g,'-')
		  .replace(/[-]+/g, '-').replace(/[^\w-]+/g,'');
}
const excludes = ["file", "aliases", "tags", "position", "links", "inlinks", "outlinks", "etags", "frontmatter", "lists", "tasks"];
let myFields = new Map();
dv.pages('"/"').forEach((page) => 
	Object.keys(page)
		.map(k => convertToSlug(k))
		.filter(k => !excludes.contains(k))
		.forEach(k => 
			myFields.set(k, (myFields.has(k) ? 1 + myFields.get(k) : 1))
		)
);
let myFieldsDV = dv.array(Array.from(myFields))
	.sort(([sluggedField, howManyNotes]) => howManyNotes, "desc");
let myDVHeaders = ["Slugged Field", "Count"];
dv.table(myDVHeaders, myFieldsDV);

dv.header(3, "Rare Fields:");
dv.table(myDVHeaders.concat(["Notes"]), 
		 myFieldsDV
		 .filter(([ignore, howManyNotes]) => howManyNotes < 3)
		 .map(([sluggedField, howManyNotes]) => 
			 [
			 sluggedField, 
			 howManyNotes, 
			 dv.pages('"/"').filter(page => 
				 Object.hasOwn(page, sluggedField)).file.link
			 ])
		 .sort(([sluggedField, hm, notes]) => sluggedField)
		);	

The vault I used to test this is small (approximately 100 notes) and dataview takes a few seconds to process the query. Note that the second table iterates through all your notes as many times as you have rare fields, so you may be able to speed it up by changing the filter on howManyNotes to be 1 or 2 rather than 3 or by limiting the bottom dv.pages('"/"') by folder as you did in your OP.

I’m curious to hear how the performance is on your vault! Does it avoid the “stack size exceeded” error?

4 Likes

Hi again! I would love to “dissect” the code and learn from it, but the code fails at hasOwn() which is supposedly not a function at eval. From the JS documentation it seems like a standard function built into the framework, so I was wondering whether it’s my vault’s problem?

Wild guess: Object.hasOwn() needs the Object in front of it; that’s not a changeable field name. I notice the syntax highlighting in my browser’s display of the forum makes the Object nearly invisible, so just checking on that.
If that’s not the issue, are you using the code from my most recent post (before this one) and what is the exact error message?

I did just check that the query still works in my vault, with the latest public (non-insider) Obsidian and up-to-date dataview. Not sure what else to check!

I’m using the 0.14.15 version of Obsidian and the 0.5.36 version of Dataview.
I did use your latest version of the code, but the error remains:
Evaluation Error: TypeError: Object.hasOwn is not a function at eval (eval at <anonymous> (plugin:dataview)

1 Like

I have no idea why it is complaining, but if Object.keys works then let’s replace the Object.hasOwn with that?

Here’s a re-written version of that bit - below the line that has just howManyNotes, and above the line with just ]):

dv.pages('"/"')
.filter(page => Object.keys(page).contains(sluggedField))
.map(page => page.file.link)

I separated out the part doing the filtering from the part that picks (via map) the file link as the field we want from the page. I nearly crashed my Obsidian when I forgot the .map line, and would have if my vault wasn’t small, so beware!

Also I found a bug in the first part of the code as I was looking through it just now!
Fields written in a non-slugged format were being counted twice per page, since dataview stores them in both non-slugged and slugged format. A quick fix is to change the .map(k=> convertToSlug(k)) line into:

.filter(k => k === convertToSlug(k))

(Triple equals gave me the right result, but I do not totally understand the difference between double and triple equals. Also do not understand why the triple equals is causing the rest of the line to be italicized on this forum!)

1 Like

I copied the latest code (adding the .filter((k) => k === convertToSlug(k)) correction but leaving in the Object.hasOwn method), and I do not get any error messages. I want to spend more time with your snippet, but just wanted to pop in to say that first :slightly_smiling_face:.

I love what you guys are working on here!

The list gets a little ridiculously long when I run it, because I don’t have a large vault but I do have a TON of unique inline keys since I used that as workaround for Obsidian not supporting description lists…I might have to script something to convert all of those to html now.

I realized after reviewing Map objects that my last encounters with JavaScript were back when ES6 had just been released, and none of my study materials/instructors covered them yet! I will definitely be playing around with my frontmatter keys snippet to use a Map object instead, like you suggested!

3 Likes

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