dmderelyn105 downloadsBuild local vectorization indexes for RAG-style retrieval and relation discovery.
A local RAG (Retrieval-Augmented Generation) indexing plugin for Obsidian. Smart Relations builds deterministic vectorization-like indexes over your vault's markdown notes, enabling semantic-style retrieval and relation discovery — entirely offline, with zero external API calls.
Smart Relations reads every markdown note in your vault, extracts its content and YAML frontmatter (including a UUID identifier), and builds six interconnected indexes that power fast, ranked retrieval:
| Index | Purpose |
|---|---|
| UUID Index | Maps each note's UUID to its path, title, type, tags, and modification time |
| Term Index | Inverted index mapping every stemmed term to the documents it appears in, with frequency and position data |
| Tag Co-occurrence | Tracks which tags appear together across your notes, enabling tag-based similarity |
| N-gram Index | Character-level trigram index for fuzzy title and summary matching |
| Relation Graph | Bidirectional graph of note-to-note connections from related: frontmatter fields |
| Document Stats | Per-document statistics including word count, unique terms, and TF-IDF vector norms |
When you open a note, the plugin scores every other note in your vault using four signals:
These four scores are normalized to a 0–1 range and combined using configurable weights to produce a single ranked list of related notes.
id frontmatter field (UUID v4) so renaming a file never breaks a link. Smart Relations can auto-add IDs to every note if you opt in, or you can add them one at a time with the "Add UUID to current note" command. The legacy uuid field is still accepted for existing vaults.Every indexed note must have an id field in its YAML frontmatter. This is the canonical identifier used for all cross-referencing — it lets Smart Relations track notes through renames and moves without breaking links. Notes without an id are simply skipped during indexing (not deleted, not modified).
For backward compatibility, Smart Relations also reads the legacy uuid field if id is absent. New notes written by the plugin always use id. The value format is the same either way: UUID v4.
You have two ways to get IDs into your notes:
id to any note that lacks one. This writes to your files, which is why it's disabled by default.id to the active note only. Use this if you want manual control.Existing notes are never overwritten — if a valid id (or legacy uuid) is already present, Smart Relations leaves it alone. The generated values are standard UUID v4 format:
---
id: "550e8400-e29b-41d4-a716-446655440000"
kind: knowledge
status: raw
created: "2026-04-07"
modified: "2026-04-07"
tags: [topic-ai, domain-research]
related: []
summary: "One-sentence description of this note."
---
# Your Note Title
Your content here.
The kind field (previously type) is an optional free-form string used for filtering and display. Smart Relations accepts either kind or type for backward compatibility and does not validate the value against any enum — non-TTRPG vaults are free to use whatever vocabulary fits.
ID rules:
id (or legacy uuid) are logged as warnings and excluded from indexingThe plugin supports two formats for the related: field, configurable in settings:
Simple format (array of UUID strings):
related:
- "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
- "f47ac10b-58cc-4372-a567-0e02b2c3d479"
Rich format (objects with relation type and auto-detection flag):
related:
- id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
rel: "references"
auto: true
- id: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
rel: "supports"
auto: false
The plugin reads both formats regardless of the setting, and within the rich format it accepts either id (preferred) or legacy uuid as the sub-key. The setting only controls which format is used when writing new entries via the "Suggest relations" command; new rich entries are always written with id.
No build tools, no dependencies — it just works.
main.js, manifest.json, and styles.css.obsidian/plugins/smart-relations/The plugin will automatically build its indexes the first time it loads. You'll see a notice: "Smart Relations: Building index for the first time..."
If you want to modify the plugin or contribute:
Clone the repository into your vault's plugins folder:
cd /path/to/your/vault/.obsidian/plugins
git clone https://github.com/DMDerelyn/Obsidian-smart-relations.git smart-relations
cd smart-relations
Install dependencies and build:
npm install
npm run build
Enable the plugin in Obsidian Settings > Community plugins
For live-reloading during development:
npm run dev
This watches for file changes and rebuilds automatically.
When the plugin loads for the first time (or when no saved indexes exist), it automatically performs a full vault reindex. This reads every markdown file, extracts frontmatter and content, and builds all six indexes. For a vault with ~1,000 notes, this takes roughly 5–10 seconds.
If your notes don't have UUIDs yet, the first reindex will report zero indexed notes. Either enable "Auto-add UUIDs to notes" in settings and reindex (adds UUIDs to every note in one pass) or run "Smart Relations: Add UUID to current note" on each note manually.
Open the related notes panel via:
Smart Relations: Find related notesThe panel automatically updates when you switch between notes. For each related note, it shows:
To add a related note to the current note's frontmatter:
related: field in your frontmatterThe format used (simple or rich) depends on your settings.
If you've made bulk changes or want to force a full rebuild:
The status bar at the bottom shows indexing progress and current state:
SR: 234 notes | just now — Indexes are loaded, 234 notes indexedSR: Building term index... — Reindexing in progressSR: Not indexed — No indexes loaded yetYou don't need to manually reindex after normal edits. The plugin listens to vault events:
| Event | What Happens |
|---|---|
Create a new .md file |
Indexed automatically (if it has a UUID) |
| Modify a file | Re-indexed after a 500ms debounce window |
| Delete a file | Removed from all six indexes, dangling references logged |
| Rename a file | Path updated in UUID index, full re-index queued |
Open Settings > Smart Relations to configure:
| Setting | Default | Description |
|---|---|---|
| Excluded folders | (empty) | Comma-separated folder paths to skip during indexing (e.g., templates, archive) |
| Auto-add UUIDs to notes | Off | When on, Smart Relations writes a uuid field into the frontmatter of any note that lacks one during indexing. Disabled by default because it modifies your files. |
| Rich related format | On | Use {uuid, rel, auto} objects instead of plain UUID strings when writing to related: |
| BM25 weight | 0.40 | Weight of BM25 text relevance in the combined score |
| Jaccard (tag) weight | 0.20 | Weight of tag overlap similarity |
| Term overlap weight | 0.20 | Weight of vocabulary overlap (unique terms shared) |
| Graph proximity weight | 0.20 | Weight of relation graph distance |
| Min similarity threshold | 0.10 | Results below this combined score are filtered out |
| Max related notes | 20 | Maximum number of results shown in the panel |
| N-gram size | 3 | Character n-gram size for fuzzy matching (2–5) |
Tip: The four scoring weights should sum to 1.0 for balanced results. If you primarily organize by tags, increase the Jaccard weight. If your notes have rich related: fields, increase graph proximity.
Smart Relations indexes are designed to work with Claude Code for RAG-style queries over your vault. The plugin builds the indexes; Claude Code reads them to efficiently find and answer questions about your notes.
See CLAUDE.md in this repository for detailed instructions on how Claude Code uses the indexes.
A standalone CLI query tool is included for users who have Node.js installed. This is entirely optional — the plugin works without it, and Claude Code can read the index files directly.
node query.mjs /path/to/vault "your search query" --top 10
The tool performs BM25 + tag + term overlap scoring against the pre-built indexes and outputs ranked results. Add --json for machine-readable output, or --tags "tag1,tag2" to boost results matching specific tags.
This tool has zero dependencies — it embeds its own Porter stemmer and stopword list.
src/
├── main.ts # Plugin entry point — wires everything together
├── settings.ts # Settings interface, defaults, and settings tab
├── nlp/
│ ├── tokenizer.ts # Text tokenization, markdown stripping, position tracking
│ ├── stemmer.ts # Porter Stemmer (embedded, zero dependencies)
│ └── stopwords.ts # 192 English stopwords
├── utils/
│ ├── uuid.ts # UUID v4 validation and generation
│ ├── frontmatter.ts # Parse frontmatter from Obsidian's MetadataCache
│ └── cache.ts # Index persistence via vault adapter
├── indexer/
│ ├── types.ts # Shared type definitions for all index artifacts
│ ├── IndexManager.ts # Orchestrates all indexers, incremental updates
│ ├── UuidIndexer.ts # UUID → metadata lookup
│ ├── TermIndexer.ts # Inverted term index with BM25 IDF
│ ├── TagIndexer.ts # Tag co-occurrence matrix
│ ├── NgramIndexer.ts # Character n-gram index
│ ├── RelationGraphBuilder.ts # Bidirectional relation graph with BFS
│ └── DocumentStats.ts # Per-document statistics and TF-IDF norms
├── scoring/
│ ├── types.ts # ScoredResult and QuerySource types
│ ├── BM25Scorer.ts # Okapi BM25 ranking (k1=1.5, b=0.75)
│ ├── JaccardScorer.ts # Set similarity for tags and terms
│ └── CombinedScorer.ts # Weighted multi-signal scoring with normalization
└── views/
├── RelatedNotesView.ts # Side panel with ranked results
└── SuggestionModal.ts # Fuzzy search modal for adding relations
Given a source note (or text query), the combined scorer:
combined = w1*bm25 + w2*jaccard + w3*termOverlap + w4*graphProximityscore(Q, D) = Σ IDF(qi) × (tf(qi,D) × (k1+1)) / (tf(qi,D) + k1 × (1 - b + b × |D|/avgdl))
IDF(qi) = log((N - n(qi) + 0.5) / (n(qi) + 0.5) + 1)
Where N = total documents, n(qi) = documents containing term qi, tf = term frequency, |D| = document length, avgdl = average document length, k1 = 1.5, b = 0.75.
| Operation | Target |
|---|---|
| Full index rebuild (1,000 notes) | < 10 seconds |
| Full index rebuild (5,000 notes) | < 30 seconds |
| Incremental update (single file) | < 200ms |
| Query scoring (1,000 documents) | < 100ms |
| Index load from disk | < 1 second |
| Bundle size | 67 KB |
The plugin processes files in batches during full rebuilds, yielding to the UI thread between batches to prevent freezing.
The plugin handles common edge cases gracefully:
related: pointing to a deleted note is logged but doesn't crashMIT