LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Running a Local Knowledge Base: Obsidian, Logseq, and AI-Powered Search Over Your Notes

obsidianlogseqknowledge-managementpkmaiollamaragproductivityself-hosting

Most knowledge management systems fail for the same reason: they’re optimized for capturing information and terrible at retrieving it. You spend years faithfully taking notes, building a second brain, and then when you actually need something you either can’t find it or don’t remember it exists.

The promise of AI-powered search is that it flips this. Instead of trying to remember which folder you filed something in three years ago, you ask “what do I know about rate limiting strategies?” and get a synthesized answer drawn from everything you’ve ever written.

This guide covers building a local knowledge base from the ground up: choosing between Obsidian and Logseq, organizing notes in ways that scale, syncing without trusting your data to a third party, and layering in local AI search using Ollama and vector embeddings — all running on your own machine.


The Case for Local-First

Before picking a tool, it’s worth being clear about what “local-first” means and why it matters for a knowledge base specifically.

Your notes are a map of how you think. They contain half-formed ideas, private reflections, work strategy, client details, personal struggles, career plans. Handing that to a cloud service means it’s processed on their servers, stored under their security model, accessible to their employees, subject to their pricing changes, and gone if they shut down or you stop paying.

Local-first means:

  • Plain text files — Markdown, readable and writable by any tool now and in 50 years
  • Your storage — files on your disk, backed up how you want
  • Your sync — Syncthing, git, or a private Nextcloud, not a proprietary service
  • Your AI — local models via Ollama, not API calls to OpenAI with your notes as context

Both Obsidian and Logseq store notes as plain Markdown files on disk. That’s the foundation everything else builds on.


Obsidian vs. Logseq: Choosing Your Tool

Both tools have passionate communities and genuine strengths. Neither is universally better.

Obsidian

Obsidian is a document-first note-taking app. Notes are discrete Markdown files in a “vault” (a folder). You link between them with [[wiki-links]], visualize connections in a graph view, and extend everything with a massive plugin ecosystem.

Strengths:

  • Fastest and most polished UI of any local note tool
  • Best-in-class plugin ecosystem (1,000+ community plugins)
  • Excellent for long-form writing, project notes, reference material
  • Flexible: you can impose any structure you want, or none
  • Canvases for visual thinking
  • Strong mobile apps (iOS and Android)

Weaknesses:

  • Not open source (free for personal use, paid for commercial)
  • Sync is paid ($10/month) unless you DIY it
  • Less structured for daily journaling — you build the system yourself

Best for: Engineers, writers, researchers who want flexible document storage with powerful linking and plugin options.

Logseq

Logseq is an outliner-first tool. Every page is a collection of bullet points (blocks), and you can embed, reference, and query blocks across pages. Daily notes are first-class, not an afterthought.

Strengths:

  • Fully open source (AGPL-3.0)
  • Block-level references and queries — incredibly powerful for connecting ideas
  • Built-in daily journaling with automatic date pages
  • Tasks and scheduling integrated into the outliner
  • Better for capturing fleeting thoughts quickly

Weaknesses:

  • Outliner paradigm is polarizing — some love it, some hate it
  • Performance degrades with very large graphs (thousands of pages)
  • Plugin ecosystem is smaller than Obsidian’s
  • Mobile apps lag behind desktop

Best for: People who journal daily, think in bullet points, and want the most powerful block-level querying available.

The Practical Answer

If you’re starting fresh and aren’t sure: start with Obsidian. The flexibility means you can grow into any system, the plugins can add almost any feature, and it’s faster. Switch to Logseq if you find yourself drawn to the block-reference model after living with Obsidian for a few months.

Many people run both: Logseq for daily capture and fleeting notes, Obsidian for polished reference notes and long-form writing. The same Markdown files work in both.


Setting Up Obsidian

Vault Structure

The most important decision in Obsidian is vault structure. There’s no single right answer, but a few patterns work well:

PARA Method (Projects, Areas, Resources, Archives):

vault/
├── Projects/           # Active projects with a defined outcome
│   ├── Blog redesign/
│   └── K8s migration/
├── Areas/              # Ongoing responsibilities without a finish line
│   ├── Work/
│   ├── Health/
│   └── Finances/
├── Resources/          # Reference material by topic
│   ├── Kubernetes/
│   ├── Python/
│   └── Networking/
├── Archives/           # Completed projects, old reference material
└── Inbox/              # Unprocessed captures

Zettelkasten (flat, linked):

vault/
├── Notes/              # All notes, flat structure
│   ├── 202603250847 Rate limiting strategies.md
│   ├── 202603251122 Token bucket algorithm.md
│   └── 202603251245 Leaky bucket vs token bucket.md
├── MOC/                # Maps of Content — index notes
│   └── Networking MOC.md
└── Inbox/

The Zettelkasten approach treats each note as an atomic idea with a unique ID, linked heavily to related notes. The PARA approach mimics a file system for active work.

Pick whichever matches how you actually think. A system you use beats a perfect system you don’t.

Essential Obsidian Plugins

Install via Settings → Community Plugins → Browse.

Dataview — query your notes like a database:

TABLE file.mtime as "Modified", tags
FROM #project
WHERE status = "active"
SORT file.mtime DESC

Templater — powerful templates with JavaScript:

1
2
3
4
5
---
created: <% tp.date.now("YYYY-MM-DD") %>
tags: []
---
# <% tp.file.title %>

Calendar — visual calendar linking to daily notes.

Periodic Notes — daily, weekly, monthly note templates with calendar integration.

Excalidraw — draw diagrams directly in your vault.

Omnisearch — full-text search that’s much faster and smarter than the built-in search.

Git — auto-commit your vault to a local git repo.

Tasks — query and manage tasks across all notes.

YAML Frontmatter Conventions

Consistent frontmatter makes Dataview queries powerful:

1
2
3
4
5
6
7
8
9
---
title: Rate Limiting Strategies
created: 2026-03-25
modified: 2026-03-25
tags: [networking, api, backend]
status: evergreen     # seed | budding | evergreen
source: https://...   # if derived from a source
type: note            # note | moc | project | reference
---

Setting Up Logseq

Installation

Download from logseq.com — desktop app for Windows, macOS, Linux, and mobile.

Point it at a directory that will become your graph:

~/notes/logseq-graph/
├── journals/       # Auto-created daily note pages
├── pages/          # Named pages
└── logseq/         # Logseq config, plugins

The Daily Note Workflow

Logseq defaults to opening today’s daily note. This is intentional — the daily journal is the inbox:

1
2
3
4
5
6
7
- [[standup]]
  - Yesterday: finished rate limiter PR
  - Today: write tests
  - Blockers: none
- Read [[202603251122 Token bucket algorithm]] — interesting point about burst capacity
  - The burst window needs to account for downstream service recovery time #insight
- TODO Review [[K8s migration]] checklist before EOW

Everything goes here first. Links ([[page name]]) create or connect to other pages. Block references (((block-id))) embed specific blocks from other pages.

Queries in Logseq

Logseq’s block-level database enables powerful queries:

1
{{query (and (todo TODO) (not (done)))}}
1
{{query (and [[kubernetes]] [[production]])}}

Advanced queries use Datalog:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#+BEGIN_QUERY
{:title "Recently modified notes about Kubernetes"
 :query [:find (pull ?b [*])
         :where
         [?b :block/refs ?p]
         [?p :block/name "kubernetes"]
         [?b :block/page ?pg]
         [?pg :block/updated-at ?t]
         [(> ?t 1700000000000)]]}
#+END_QUERY

Syncing Without the Cloud

Both Obsidian and Logseq store plain files — sync them any way you’d sync files.

Syncthing is peer-to-peer, encrypted, open source, and free. No server required, no third party involved.

1
2
3
4
5
6
7
8
# Install on Linux
sudo apt install syncthing

# Start and enable
systemctl enable --now syncthing@$USER

# Open web UI
xdg-open http://localhost:8384

In the Syncthing UI:

  1. Add a device: share the device ID between your machines
  2. Add a folder: point it at your vault directory
  3. Share the folder with your other device
  4. Done — changes sync within seconds on the same LAN, minutes over the internet

For Android: install Syncthing-Fork from F-Droid. For iOS: Möbius Sync (paid, ~$5) or use git sync instead.

Git Sync

Obsidian’s community Git plugin can auto-commit and push/pull on a schedule:

1
2
3
4
# Initialize vault as git repo
cd ~/notes/obsidian-vault
git init
git remote add origin git@github.com:yourname/private-notes.git

In Obsidian Git settings:

  • Auto backup interval: 10 minutes
  • Auto pull interval: 5 minutes
  • Commit message: vault backup: {{date}}

For private repos on a self-hosted Gitea: set the remote to your Gitea instance. Your notes never leave infrastructure you control.

Nextcloud

If you’re already running Nextcloud, just point your vault at a synced Nextcloud folder. Nextcloud Desktop Client does the rest.

For Logseq on iOS/Android, Nextcloud Files app can sync the graph directory, though it’s less seamless than Syncthing.


Local AI Search with Ollama

This is where a knowledge base transforms from a sophisticated file system into something genuinely useful. Instead of keyword search, you get semantic understanding: “what did I write about handling flaky tests?” finds relevant notes even if they never use those exact words.

Architecture

Your notes (Markdown files)
        │
        ▼
  Text chunking
        │
        ▼
  Embedding model (runs locally via Ollama)
        │
        ▼
  Vector database (ChromaDB / SQLite-vec)
        │
        ▼
  Similarity search
        │
        ▼
  LLM synthesis (Ollama)
        │
        ▼
  Answer with source citations

Install Ollama

1
2
3
4
5
6
7
8
# Linux/macOS
curl -fsSL https://ollama.com/install.sh | sh

# Pull models
ollama pull nomic-embed-text   # fast, 274M embedding model
ollama pull llama3.2           # 3B — fast reasoning for query synthesis
# or for better quality:
ollama pull qwen2.5:7b

nomic-embed-text is the workhorse — a dedicated embedding model that converts text to vectors. Use it instead of a general-purpose model for embeddings.

Building the Indexer

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#!/usr/bin/env python3
# index_notes.py — index your Markdown vault into ChromaDB

import os
import re
import hashlib
from pathlib import Path
import chromadb
import ollama

VAULT_PATH = Path.home() / "notes" / "obsidian-vault"
CHROMA_PATH = Path.home() / ".local" / "share" / "notes-index"
EMBED_MODEL = "nomic-embed-text"
CHUNK_SIZE = 500   # characters
CHUNK_OVERLAP = 100


def chunk_text(text: str, size: int = CHUNK_SIZE, overlap: int = CHUNK_OVERLAP) -> list[str]:
    """Split text into overlapping chunks."""
    chunks = []
    start = 0
    while start < len(text):
        end = start + size
        chunk = text[start:end]
        # Try to break at sentence boundary
        if end < len(text):
            last_period = chunk.rfind('. ')
            if last_period > size // 2:
                chunk = chunk[:last_period + 1]
                end = start + last_period + 1
        chunks.append(chunk.strip())
        start = end - overlap
    return [c for c in chunks if len(c) > 50]


def extract_frontmatter(content: str) -> tuple[dict, str]:
    """Extract YAML frontmatter and return (metadata, body)."""
    if not content.startswith('---'):
        return {}, content
    end = content.find('\n---\n', 4)
    if end == -1:
        return {}, content
    frontmatter = content[4:end]
    body = content[end + 5:]
    metadata = {}
    for line in frontmatter.split('\n'):
        if ':' in line:
            key, _, value = line.partition(':')
            metadata[key.strip()] = value.strip()
    return metadata, body


def get_file_hash(path: Path) -> str:
    return hashlib.md5(path.read_bytes()).hexdigest()


def index_vault():
    client = chromadb.PersistentClient(path=str(CHROMA_PATH))
    collection = client.get_or_create_collection(
        name="notes",
        metadata={"hnsw:space": "cosine"}
    )

    # Load existing index to skip unchanged files
    existing = {}
    results = collection.get(include=["metadatas"])
    for meta in results["metadatas"]:
        if meta and "file_hash" in meta:
            existing[meta["source"]] = meta["file_hash"]

    md_files = list(VAULT_PATH.rglob("*.md"))
    print(f"Found {len(md_files)} Markdown files")

    added = 0
    skipped = 0

    for filepath in md_files:
        # Skip hidden files and Logseq/Obsidian internals
        if any(part.startswith('.') for part in filepath.parts):
            continue
        if filepath.parent.name in ('logseq', '.obsidian', 'templates'):
            continue

        rel_path = str(filepath.relative_to(VAULT_PATH))
        file_hash = get_file_hash(filepath)

        # Skip if unchanged
        if existing.get(rel_path) == file_hash:
            skipped += 1
            continue

        content = filepath.read_text(encoding='utf-8', errors='ignore')
        metadata, body = extract_frontmatter(content)

        # Remove wiki links and clean up
        body = re.sub(r'\[\[([^\]|]+)(?:\|[^\]]+)?\]\]', r'\1', body)
        body = re.sub(r'!?\[([^\]]*)\]\([^\)]*\)', r'\1', body)

        chunks = chunk_text(body)
        if not chunks:
            continue

        # Delete old version of this file
        try:
            old_ids = [r for r in results["ids"]
                       if results["metadatas"][results["ids"].index(r)].get("source") == rel_path]
            if old_ids:
                collection.delete(ids=old_ids)
        except (ValueError, KeyError):
            pass

        # Embed and store each chunk
        for i, chunk in enumerate(chunks):
            chunk_id = f"{rel_path}::{i}"
            response = ollama.embeddings(model=EMBED_MODEL, prompt=chunk)
            embedding = response["embedding"]

            collection.add(
                ids=[chunk_id],
                embeddings=[embedding],
                documents=[chunk],
                metadatas=[{
                    "source": rel_path,
                    "file_hash": file_hash,
                    "chunk": i,
                    "title": metadata.get("title", filepath.stem),
                    "tags": metadata.get("tags", ""),
                }]
            )

        added += 1
        print(f"  Indexed: {rel_path} ({len(chunks)} chunks)")

    print(f"\nDone — {added} files indexed, {skipped} unchanged")


if __name__ == "__main__":
    index_vault()

Install dependencies:

1
pip install chromadb ollama

Run:

1
python index_notes.py

For a vault of 500 notes, initial indexing takes 5–10 minutes. Re-runs are fast — only changed files are re-indexed.

The Query Interface

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#!/usr/bin/env python3
# query_notes.py — semantic search and QA over your notes

import sys
from pathlib import Path
import chromadb
import ollama

CHROMA_PATH = Path.home() / ".local" / "share" / "notes-index"
EMBED_MODEL = "nomic-embed-text"
REASONING_MODEL = "llama3.2"  # or qwen2.5:7b for better quality
TOP_K = 6


def search(query: str) -> list[dict]:
    client = chromadb.PersistentClient(path=str(CHROMA_PATH))
    collection = client.get_collection("notes")

    response = ollama.embeddings(model=EMBED_MODEL, prompt=query)
    query_embedding = response["embedding"]

    results = collection.query(
        query_embeddings=[query_embedding],
        n_results=TOP_K,
        include=["documents", "metadatas", "distances"]
    )

    hits = []
    for i in range(len(results["ids"][0])):
        hits.append({
            "id": results["ids"][0][i],
            "text": results["documents"][0][i],
            "source": results["metadatas"][0][i]["source"],
            "title": results["metadatas"][0][i].get("title", ""),
            "distance": results["distances"][0][i],
        })
    return hits


def answer(query: str, hits: list[dict]) -> str:
    context = "\n\n---\n\n".join(
        f"From: {h['source']}\n{h['text']}" for h in hits
    )

    prompt = f"""You are a helpful assistant answering questions based on the user's personal notes.

Answer the question using ONLY the provided notes as context. If the notes don't contain
enough information to answer, say so. Always cite the source file(s) you drew from.

Question: {query}

Notes context:
{context}

Answer:"""

    response = ollama.chat(
        model=REASONING_MODEL,
        messages=[{"role": "user", "content": prompt}]
    )
    return response["message"]["content"]


def main():
    if len(sys.argv) < 2:
        print("Usage: python query_notes.py \"your question\"")
        sys.exit(1)

    query = " ".join(sys.argv[1:])
    print(f"\nSearching: {query}\n")

    hits = search(query)

    print("Most relevant notes:")
    for h in hits:
        score = 1 - h["distance"]  # cosine similarity
        print(f"  [{score:.2f}] {h['source']}")

    print("\n--- Answer ---")
    print(answer(query, hits))


if __name__ == "__main__":
    main()

Usage:

1
2
3
python query_notes.py "what do I know about rate limiting strategies?"
python query_notes.py "what were my thoughts on the K8s migration?"
python query_notes.py "summarize my notes on system design interviews"

Wrapping It in a Simple Web UI

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env python3
# app.py — minimal Gradio UI for note search

import gradio as gr
from query_notes import search, answer

def ask_notes(question: str) -> tuple[str, str]:
    if not question.strip():
        return "", ""
    hits = search(question)
    response = answer(question, hits)
    sources = "\n".join(f"- {h['source']} (score: {1 - h['distance']:.2f})" for h in hits)
    return response, sources

with gr.Blocks(title="Notes AI") as demo:
    gr.Markdown("## Ask Your Notes")
    with gr.Row():
        question = gr.Textbox(label="Question", placeholder="What do I know about...?", scale=4)
        submit = gr.Button("Ask", variant="primary")
    answer_box = gr.Textbox(label="Answer", lines=10)
    sources_box = gr.Textbox(label="Sources", lines=6)
    submit.click(ask_notes, inputs=question, outputs=[answer_box, sources_box])
    question.submit(ask_notes, inputs=question, outputs=[answer_box, sources_box])

if __name__ == "__main__":
    demo.launch(server_name="0.0.0.0", server_port=7860)
1
2
3
pip install gradio
python app.py
# Open http://localhost:7860

Auto-Reindexing with a Watcher

Keep the index fresh without manual runs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# watch_and_index.py
import time
from pathlib import Path
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from index_notes import index_vault

VAULT_PATH = Path.home() / "notes" / "obsidian-vault"

class VaultHandler(FileSystemEventHandler):
    def __init__(self):
        self.pending = False

    def on_modified(self, event):
        if event.src_path.endswith('.md'):
            self.pending = True

    def on_created(self, event):
        if event.src_path.endswith('.md'):
            self.pending = True

handler = VaultHandler()
observer = Observer()
observer.schedule(handler, str(VAULT_PATH), recursive=True)
observer.start()

print(f"Watching {VAULT_PATH} for changes...")

try:
    while True:
        time.sleep(30)
        if handler.pending:
            print("Changes detected — reindexing...")
            index_vault()
            handler.pending = False
except KeyboardInterrupt:
    observer.stop()
observer.join()
1
2
pip install watchdog
python watch_and_index.py &  # run in background

As a systemd user service:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# ~/.config/systemd/user/notes-watcher.service
[Unit]
Description=Notes vault watcher and reindexer
After=network.target

[Service]
ExecStart=/usr/bin/python3 /home/jmoon/notes/watch_and_index.py
WorkingDirectory=/home/jmoon/notes
Restart=always
RestartSec=10

[Install]
WantedBy=default.target
1
systemctl --user enable --now notes-watcher

Alternative: Obsidian Smart Connections Plugin

If you’d rather stay entirely in Obsidian without any Python scripting, the Smart Connections plugin does a lot of this for you:

  1. Install from Obsidian Community Plugins: “Smart Connections”
  2. In settings, set the embedding model to “local” and point it at your Ollama endpoint (http://localhost:11434)
  3. The plugin indexes your vault using your local Ollama embedding model
  4. Open the Smart Connections panel (ribbon icon) to chat with your notes
  5. Use the “Smart Search” view for semantic search within Obsidian

This is less customizable but requires zero code and works entirely within the Obsidian UI.


Logseq has its own AI integration via plugins:

Logseq Copilot plugin:

  • Configure it with http://localhost:11434 as the base URL
  • Use llama3.2 or qwen2.5:7b as the model
  • Enables in-line AI completion and chat

For full semantic search in Logseq, the Python indexer approach above works identically — Logseq’s Markdown files are in the same format as Obsidian’s. Point VAULT_PATH at your Logseq graph directory.


Performance and Model Selection

Embedding and querying speed depends on your hardware:

Hardware nomic-embed-text llama3.2 (3B) qwen2.5:7b
M1 MacBook Air ~80ms/chunk ~15 tokens/s ~8 tokens/s
Ryzen 5 + RTX 3060 ~40ms/chunk ~35 tokens/s ~18 tokens/s
Intel i7 (CPU only) ~200ms/chunk ~5 tokens/s ~2 tokens/s

For a 500-note vault (~2,000 chunks), initial indexing:

  • M1 Mac: ~3 minutes
  • RTX 3060: ~1.5 minutes
  • CPU-only i7: ~7 minutes

Query response time (embedding + retrieval + synthesis):

  • M1 Mac: ~8–15 seconds for a thorough answer
  • GPU: ~4–8 seconds

If you need faster responses, use llama3.2 (3B) for synthesis — it’s much faster than 7B with acceptable quality for personal notes. Use qwen2.5:14b or mistral only if you have a strong GPU and want better reasoning.


Privacy and Security

Your notes contain your most private thoughts. A few practices worth adopting:

Encrypt the vault at rest: Use LUKS on Linux or FileVault on macOS to encrypt the partition containing your notes.

Encrypt Syncthing traffic: Syncthing encrypts in transit by default. For extra caution, configure it to sync only over your Tailscale/WireGuard network.

Keep git history clean: If using git for sync, consider squashing history periodically — git log of a notes repo can reveal a lot. Or use a private repo with only you as owner.

Sanitize before sharing: Never share your vault directory or the ChromaDB index. The embeddings don’t leak raw text but could theoretically be reversed with enough effort.

Air-gap sensitive notes: Keep a separate vault for extremely sensitive notes (financial, medical, passwords) that never syncs.


For a new setup, this sequence takes about two hours:

  1. Install Obsidian — download from obsidian.md, create vault at ~/notes/vault
  2. Install essential plugins — Dataview, Templater, Calendar, Periodic Notes, Git, Omnisearch
  3. Set up Syncthing — install on all devices, add vault folder, pair devices
  4. Start taking notes — don’t over-engineer the structure, use the Inbox folder for everything until patterns emerge
  5. After 2–4 weeks — reorganize based on what you actually captured
  6. Add AI search — install Ollama, pull nomic-embed-text + llama3.2, run the indexer
  7. Auto-reindex — deploy the watcher as a systemd service
  8. (Optional) — deploy the Gradio UI if you want a browser interface

The most important step is step 4. The AI layer only helps if there’s something worth searching. A knowledge base with 50 thoughtful notes is more valuable than one with 5,000 poorly captured snippets.


What Makes a Good Note

A brief note on note quality — since none of the tooling helps if the underlying notes are bad.

Atomic notes: One idea per note. If a note grows beyond two screens, ask whether it’s actually two ideas that should be split.

In your own words: Copy-pasting is for reference notes. For ideas you want to internalize, rewrite them. The act of restating is the learning.

Link aggressively: The value of a knowledge graph comes from connections. If a note mentions a concept you’ve written about elsewhere, link it. This is what separates a knowledge base from a folder of documents.

Date everything: The when of an idea matters. Future you will want to know whether something you wrote was current thinking or five-year-old half-formed opinion.

Write for future you: The audience for your notes is yourself six months from now who has forgotten the context. Be explicit.

The tools covered here — Obsidian, Logseq, Syncthing, Ollama — are all means to an end. The end is a system that makes your accumulated thinking accessible and useful, not one that makes you feel productive while capturing it.

Comments