This artwork is grown from the article's title — unique to this note. Move through it; click to plant light.
productivity · knowledge‑management · embeddings

Cross‑Platform Idea Portals: Linking Notion, Roam, and Obsidian with Vector Embeddings

A step‑by‑step guide to unify Notion, Roam, and Obsidian through shared vector embeddings for instant, context‑rich search.

July 5, 2026 · 5 min read · Generated by the AI Gardener under public quality rules

Imagine a single query that surfaces a relevant paragraph from a Notion page, a linked thought in Roam, and a code snippet stored in Obsidian—all without opening each app.

Why Connect Your Knowledge Bases?

Most creators end up using multiple tools because each excels at a particular habit: Notion for structured databases, Roam for bi‑directional linking, Obsidian for local markdown vaults. The downside is a fragmented mental model. When ideas are scattered, retrieval becomes a chore and synthesis stalls.

Vector embeddings turn raw text into high‑dimensional numbers that capture meaning. By storing the same embedding space for every note, you create a lingua franca that any application can query. The result is:

  • Instant cross‑app discovery: a single search surface returns matches from all three tools.
  • Contextual recommendations: related ideas appear even if they were never linked manually.
  • Future‑proof extensibility: add new apps or datasets without redesigning the query logic.

Preparing Your Vaults for Vector Export

Before you generate embeddings, you need a clean, exportable text representation from each platform. The goal is a stable identifier (a UUID or file path) paired with the raw content.

1. Notion – Export as Markdown

  • Open Settings & Members → Export.
  • Select Markdown & CSV and include sub‑pages.
  • Save the zip, unzip it, and keep the folder structure; each .md file’s relative path will serve as its unique ID.

2. Roam – Pull the Graph JSON

  • Navigate to https://roamresearch.com/#/app/your‑graph/settings (you’ll need a paid plan).
  • Click “Export Graph” → “JSON”.
  • The resulting roam.json contains an array of blocks; each block already has a uid field that can be used as the identifier.

3. Obsidian – Use the Vault Folder Directly

  • Locate your vault folder on disk.
  • Every .md file represents a note; the full path (relative to the vault root) is a reliable ID.
  • If you use plugins that store attachments, keep those files alongside the markdown to preserve context.

At this stage you have three collections of (id, text) pairs. The next step is to turn the text into vectors.

Generating and Storing Embeddings

Open‑source transformer models such as sentence‑transformers/all‑MiniLM‑L6‑v2 provide a good balance of speed and semantic quality. You can run them locally or on a modest cloud instance.

Step‑by‑step script (Python)

  1. Install the required packages: pip install sentence-transformers tqdm
  2. Load the model once: from sentence_transformers import SentenceTransformer model = SentenceTransformer('all-MiniLM-L6-v2')
  3. Iterate over each export, compute embeddings, and write them to a simple SQLite database: import sqlite3, json, os, glob conn = sqlite3.connect('embeddings.db') c = conn.cursor() c.execute('CREATE TABLE IF NOT EXISTS docs (id TEXT PRIMARY KEY, source TEXT, embedding BLOB)') def embed_and_store(id_, source, text): emb = model.encode(text, show_progress_bar=False) c.execute('INSERT OR REPLACE INTO docs (id, source, embedding) VALUES (?,?,?)', (id_, source, emb.tobytes())) conn.commit()
  4. Process Notion markdown files: for path in glob.glob('notion_export/**/*.md', recursive=True): with open(path, encoding='utf-8') as f: txt = f.read() embed_and_store(path, 'notion', txt)
  5. Process Roam JSON blocks: with open('roam.json', encoding='utf-8') as f: data = json.load(f) for block in data['blocks']: uid = block['uid'] txt = block.get('string', '') embed_and_store(uid, 'roam', txt)
  6. Process Obsidian vault files: for path in glob.glob('obsidian_vault/**/*.md', recursive=True): with open(path, encoding='utf-8') as f: txt = f.read() embed_and_store(path, 'obsidian', txt)

The database now holds a uniform embedding space for every piece of knowledge, regardless of origin.

Building a Cross‑Platform Query Layer

With embeddings stored, you need a service that accepts a natural‑language query, converts it to a vector, and returns the top‑k most similar documents. The service can be a lightweight Flask API, a FastAPI endpoint, or even a local command‑line tool.

Similarity Search Basics

  • Convert the query string to a vector using the same model.
  • Compute cosine similarity between the query vector and each stored embedding.
  • Rank results and retrieve the original IDs.

SQLite alone is not optimal for large‑scale similarity search, but for a personal knowledge base of a few thousand notes it works fine. For larger collections, consider an approximate nearest‑neighbor library such as FAISS or Annoy. Below is a minimal Flask example using SQLite.

from flask import Flask, request, jsonify import numpy as np, sqlite3, heapq app = Flask(__name__) conn = sqlite3.connect('embeddings.db', check_same_thread=False) def cosine_sim(a, b): return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) @app.route('/search') def search(): q = request.args.get('q', '') if not q: return jsonify({'error': 'missing query'}), 400 q_vec = model.encode(q) cur = conn.cursor() cur.execute('SELECT id, source, embedding FROM docs') heap = [] for row in cur: emb = np.frombuffer(row[2], dtype=np.float32) sim = cosine_sim(q_vec, emb) if len(heap) < 5: heapq.heappush(heap, (sim, row[0], row[1])) else: heapq.heappushpop(heap, (sim, row[0], row[1])) results = sorted(heap, reverse=True) return jsonify([{'id': r[1], 'source': r[2], 'score': r[0]} for r in results])

Running this server gives you an HTTP endpoint that can be called from any tool: a browser bookmarklet, a custom command in Obsidian, or a Notion integration that fetches results and injects them into a page.

Embedding the Query into Each App

  • Obsidian: Use the “Templater” or “Obsidian‑Commander” plugin to run a shell command that hits /search?q=… and paste the JSON response into a note.
  • Roam: