Michael Naumov223 downloadsMaintains the vault indexes that are otherwise rebuilt on every call, and answers the built-in lookups from them, so backlinks come from an index instead of a fresh scan of every note.
Some of the questions Obsidian answers about a vault have no index behind them. Asking which notes link to this one means scanning every note, on every call — the Backlinks pane pays that cost, and so does every plugin that asks the same question. Asking what every note is called means the same walk, and the [[ autocomplete pays it every single time you open it. On a large vault it is slow enough to be felt.
This plugin maintains the indexes those questions deserve and answers from them instead. It does so by replacing the method that already asks the question, so nothing has to know the plugin is installed: the Backlinks pane gets faster, and so does anything else that asks.
Each index is a module that is switched on or off on its own, so a vault only pays for the ones it uses. On a small vault you will not notice any of them; that is the point at which you do not need this.
The documentation is an interactive demo vault. Every feature has a note that explains what it does and why you would want it, with buttons that measure the difference for real.
Start reading here — it is plain markdown, so it works on GitHub with nothing installed.
A copy of the vault ships with every release. You can access it via any of the following:
advanced-metadata-cache-demo-vault.zip from the Releases. It unzips into a single advanced-metadata-cache-demo-vault-<version> folder.demo-vault/ in this repository.getCache(), which is otherwise left empty for canvas files. 03 Canvas backlinksFrontmatter Markdown Links plugin is installed. 03 Canvas backlinks[[ autocomplete, so opening it stops rescanning every note in the vault for its name and aliases. 05 Name indextitle property is found under that title — and, if you ask for it, offered under that title by the [[ autocomplete. 06 Titles| Module | What it indexes | Default |
|---|---|---|
| Backlinks | Which notes link to a note, answering getBacklinksForFile(). |
On |
| Names | What each note is called - its name and its aliases - answering getLinkSuggestions(). |
Off |
| Titles | What a note's own frontmatter says it is called, read per note and on demand. | Off |
Switching a module off unloads it completely — its index, its listeners and the commands it registers all go with it, and the built-in implementation answers again. Switching one back on rebuilds its index from scratch.
A note can carry its real name in a frontmatter property rather than in its filename — title is the usual one, and several plugins write it. Obsidian itself does not treat that value as a name, so nothing can find the note by it.
Switch the Titles module on and every property you list there is read as a name. The list defaults to title alone, and it joins the note's own name and its aliases:
---
title: The Real Name
---
Two things then know about it. getPathsByName('The Real Name') finds the note, while the Names module is on — and any other plugin can read the same properties and the same per-note answer, so you name the property here once instead of once per plugin.
It does not change the [[ autocomplete unless you ask it to. Out of the box the list Obsidian offers there is answered faster by this plugin, not differently, so nothing about typing a link moves. Switch Offer titles in the [[ autocomplete on — it appears under Settings once both the Names and Titles modules are on — and every title is offered there too, appended after everything Obsidian itself would have offered rather than ranked against it. Accepting one writes a link like:
[[Notes/some-note|The Real Name]]
which resolves through Obsidian's own machinery, survives a rename, and keeps working if you ever switch this plugin off. A title that already matches the note's own name or one of its aliases is not offered a second time.
This is the reverse direction from Front Matter Title, and the two are independent by design: that plugin takes a note and shows you its title, in the explorer, the tabs and the graph. This one takes a title and finds you the note. If you run both, name the property in each — one setting silently changing what another plugin answers would be worse than typing it twice.
Everything this plugin offers another plugin is declared in one hand-written file — api.d.ts at the repository root. It imports from obsidian and nothing else, so you can copy it into your own code or reference it where it sits, with no build-time dependency on this repository.
There are two kinds of surface in it, and which one you use depends on the module.
This plugin replaces app.metadataCache.getBacklinksForFile() with a faster implementation, adds an overload accepting a vault path as well as a TFile, and keeps the original reachable:
const fast = app.metadataCache.getBacklinksForFile(pathOrFile);
const safe = await app.metadataCache.getBacklinksForFile.safe(pathOrFile);
const original = app.metadataCache.getBacklinksForFile.originalFn(file);
All three members arrived in 1.0.0, and all three are present only while the Backlinks module is on — a consumer that cannot assume it is should fall back to the built-in signature rather than assume the widened one.
While the Names module is on, app.metadataCache.getLinkSuggestions() is answered from the name index instead of from a full vault walk, and carries the same three members plus the reverse lookup that walk cannot do at all:
const fast = app.metadataCache.getLinkSuggestions();
const safe = await app.metadataCache.getLinkSuggestions.safe();
const original = app.metadataCache.getLinkSuggestions.originalFn();
const paths = app.metadataCache.getLinkSuggestions.getPathsByName('Some Alias');
const safePaths = await app.metadataCache.getLinkSuggestions.getPathsByNameSafe('Some Alias');
getPathsByName answers "which notes are called this?" — matching a note's own name or any of its aliases, case-insensitively and with runs of whitespace collapsed, exactly as Obsidian resolves a wikilink. The answer is a list and is deliberately unranked: several notes may declare the same alias, and which one the user meant is a question about your context, not about the vault.
All five of those members arrived in 1.0.0 as well, and like the backlink ones they are present only while their module — Names — is on.
The array getLinkSuggestions() answers with is Obsidian's own, entry for entry — originalFn() is there so you can check. The single exception is the Offer titles in the [[ autocomplete setting described under Titles: with it on, the title entries are appended, so Obsidian's array stays a strict prefix of what you get and the difference is exactly a suffix. It is off by default, so a consumer that has not been told otherwise can treat the two as identical.
Use the safe variants when you may be asked early. The patch is installed as soon as the module loads, but the index is built once the metadata cache can answer; until then a plain call falls through to Obsidian's implementation and getPathsByName answers with nothing. safe() / getPathsByNameSafe() wait for the build.
Both calls are typed in api.d.ts as GetBacklinksForFileFn and GetLinkSuggestionsFn — cast the core method to one of those to reach the added members. There is nothing to fetch and no version to negotiate: the patch is installed or it is not, so pin against the plugin version a member arrived in. 02 Fast, safe, and original backlinks runs all three backlink calls side by side, and 05 Name index does the same for the name calls.
The two modules above answer by replacing a method Obsidian already has, so there is nothing to fetch. The Titles module has no such method to replace — Obsidian has no notion of a name-bearing property — so it publishes an API instead, AdvancedMetadataCacheApi in the same api.d.ts:
import { watchPluginApi } from 'obsidian-dev-utils/obsidian/plugin/plugin-api';
const apiRef = watchPluginApi<AdvancedMetadataCacheApi>({
apiVersionRange: '^1',
app: this.app,
component: this,
pluginId: 'advanced-metadata-cache'
});
// `value` is always current and never stale: `null` while this plugin is not loaded, and non-`null`
// on its own once it is.
const api = apiRef.value;
if (api) {
console.log(api.getTitlePropertyNames()); // ['title']
console.log(api.getTitles('Notes/Some note.md')); // ['The Real Name']
}
The handle comes from the obsidian-dev-utils plugin registry; Cross-plugin APIs covers watchPluginApi and the registry from the library's side.
Both arrived in contract 1.0.0, and the contract version moves independently of the plugin's own, so ask for a range. Both answer empty while the Titles module is off, which is the same answer as "no property is configured" and is meant to be.
getTitles is synchronous and lazily memoized per note, so it is safe on a per-keystroke path. Read getTitlePropertyNames() and show that list rather than offering a property setting of your own — one place to type title is the point of the setting living here.
A plugin that used to own a title property setting hands its value over with migrateSettings, added in contract 1.1.0. It is the envelope of obsidian-dev-utils's SettingsMigrationApi, so SettingsMigrationComponent drives it for you. Watch this plugin's API with the contract { migrateSettings: {} } and the range ^1, then propose { titlePropertyNames: ['subtitle'] }. Against a 1.0.0 provider, the offer waits instead of failing.
This plugin owns the list, so it owns the dialog. The dialog names your plugin, shows the proposed names beside the current list, and suggests the proposed names ADDED to that list (a name already on it in any casing is not added twice), which the user can approve, edit or decline. While the Titles module is off, it also offers to switch it on. The call resolves { isApplied: false } on a cancel, so retire your pending value only on true. When there is nothing to change, it resolves true without showing anything.
If you would rather not depend on obsidian-dev-utils for the handle, the registry is a documented wire protocol you can read directly — see Plugin API protocol.
The plugin is available in the official Community Plugins repository.
To install the latest beta release of this plugin (regardless if it is available in the official Community Plugins repository or not), follow these steps:
Add plugin button once and wait a few seconds for the plugin to install.By default, debug messages for this plugin are hidden.
To show them, run the following command in the DevTools Console:
window.DEBUG.enable('advanced-metadata-cache');
For more details, refer to the documentation.
Contributions are welcome — see CONTRIBUTING to get set up.
See my other Obsidian resources.