obsidian:~/vault$ search --hybrid obsidian

Example vault location

#

words: 9649 read_time: 37m updated: 2026-03-02 07:52
$ retriever search --hybrid obsidian

Kluczowe wnioski

Inżynieria kontekstu, nie robienie notatek. Wartość skarbca Obsidian dla AI nie tkwi w samych notatkach, lecz w warstwie wyszukiwania, która czyni je możliwymi do odpytywania. Skarbiec zawierający 16 000 plików bez wyszukiwania to baza danych, do której tylko się zapisuje. Skarbiec z 200 plikami wyposażony w wyszukiwanie hybrydowe i integrację z MCP to baza wiedzy AI. Infrastruktura wyszukiwania jest produktem. Notatki to surowiec.

Wyszukiwanie hybrydowe przewyższa czyste wyszukiwanie słów kluczowych lub czyste wyszukiwanie semantyczne. BM25 wyłapuje dokładne identyfikatory i nazwy funkcji. Wyszukiwanie wektorowe wyłapuje synonimy i dopasowania koncepcyjne w różnej terminologii. Reciprocal Rank Fusion (RRF) łączy oba podejścia bez konieczności kalibracji wyników. Żadna z metod samodzielnie nie pokrywa obu trybów awaryjnych. Badania nad rankingiem fragmentów MS MARCO potwierdzają ten wzorzec: wyszukiwanie hybrydowe konsekwentnie przewyższa każdą z metod stosowaną osobno.1 Szczegółowe omówienie retrievera hybrydowego zawiera matematykę RRF, przykłady z rzeczywistymi liczbami, analizę trybów awaryjnych oraz interaktywny kalkulator fuzji.

MCP zapewnia narzędziom AI bezpośredni dostęp do skarbca. Serwery Model Context Protocol (MCP) udostępniają retriever jako narzędzie, które Claude Code, Codex CLI, Cursor i inne narzędzia AI mogą wywoływać bezpośrednio. Agent odpytuje skarbiec, otrzymuje wyniki z rankingiem i atrybucją źródła, a następnie wykorzystuje kontekst bez ładowania całych plików. Serwer MCP to cienka warstwa opakowująca silnik wyszukiwania.

Podejście local-first oznacza zerowe koszty API i pełną prywatność. Cały stos działa na jednej maszynie: SQLite do przechowywania danych, Model2Vec do embeddingów, FTS5 do wyszukiwania słów kluczowych, sqlite-vec do wektorowego KNN. Żadnych usług chmurowych, żadnych wywołań API, żadnej zależności od sieci. Prywatne notatki nigdy nie opuszczają maszyny. Pełne ponowne generowanie embeddingów dla 49 746 fragmentów kosztowałoby około 0,30 USD po cenach API OpenAI, ale rzeczywistymi kosztami są opóźnienia, narażenie prywatności i zależność od sieci w systemie, który powinien działać offline.2

Inkrementalne indeksowanie utrzymuje system na bieżąco w mniej niż 10 sekund. Porównanie czasu modyfikacji plików wykrywa zmiany. Tylko zmodyfikowane pliki są ponownie dzielone na fragmenty i ponownie embeddowane. Pełne reindeksowanie zajmuje około czterech minut na sprzęcie Apple z serii M. Inkrementalne aktualizacje po typowym dniu edycji wykonują się w mniej niż dziesięć sekund. System pozostaje aktualny bez ręcznej interwencji.

Architektura skaluje się od 200 do ponad 20 000 notatek. Ten sam trzywarstwowy projekt (pozyskiwanie, wyszukiwanie, integracja) działa przy dowolnym rozmiarze skarbca. Można zacząć od wyszukiwania wyłącznie BM25 w małym skarbcu. Wyszukiwanie wektorowe dodaje się, gdy kolizje słów kluczowych stają się problemem. Fuzję RRF dodaje się, gdy potrzebne są zarówno dokładne, jak i semantyczne dopasowania. Każda warstwa jest niezależnie użyteczna i niezależnie usuwalna.


Jak korzystać z tego przewodnika

Ten przewodnik obejmuje kompletny system. Punkt startowy zależy od aktualnej sytuacji:

Opis sytuacji Zacznij tutaj Następnie eksploruj
Nowy w Obsidian + AI Dlaczego Obsidian jako infrastruktura AI, Szybki start Architektura skarbca, Architektura serwera MCP
Istniejący skarbiec, potrzebny dostęp AI Architektura serwera MCP, Integracja z Claude Code Modele embeddingów, Wyszukiwanie pełnotekstowe z FTS5
Budowanie systemu wyszukiwania Kompletny pipeline wyszukiwania, Reciprocal Rank Fusion Optymalizacja wydajności, Rozwiązywanie problemów
Kontekst zespołowy lub korporacyjny Framework decyzyjny, Wzorce grafu wiedzy Przepisy dla programistów, Przewodnik migracji

Sekcje oznaczone jako Kontrakt zawierają szczegóły implementacji, bloki konfiguracyjne i tryby awaryjne. Sekcje oznaczone jako Narracja koncentrują się na koncepcjach, decyzjach architektonicznych i uzasadnieniu wyborów projektowych. Sekcje oznaczone jako Przepis dostarczają procedury krok po kroku.


Dlaczego Obsidian jako infrastruktura AI

Teza tego przewodnika: Skarbce Obsidian są najlepszym podłożem dla osobistych baz wiedzy AI, ponieważ są local-first, oparte na czystym tekście, mają strukturę grafową, a użytkownik kontroluje każdą warstwę stosu.

Co Obsidian daje AI, a alternatywy nie

Pliki markdown w czystym tekście. Każda notatka to plik .md w systemie plików. Brak formatu własnościowego, brak eksportu z bazy danych, brak wymagań dotyczących API do odczytania treści. Każde narzędzie odczytujące pliki może odczytać skarbiec. grep, ripgrep, pathlib z Python, SQLite FTS5 — wszystkie działają bezpośrednio na plikach źródłowych. Podczas budowania systemu wyszukiwania indeksowane są pliki, a nie odpowiedzi API. Indeks jest zawsze spójny ze źródłem, ponieważ źródłem jest system plików.

Architektura local-first. Skarbiec znajduje się na lokalnej maszynie. Brak serwera, brak zależności od synchronizacji w chmurze, brak limitów API, brak warunków korzystania z usługi regulujących przetwarzanie własnych treści. Można generować embeddingi, indeksować, dzielić na fragmenty i przeszukiwać notatki bez żadnej zewnętrznej usługi. Ma to znaczenie dla infrastruktury AI, ponieważ pipeline wyszukiwania działa tak szybko, jak pozwala dysk, a nie tak szybko, jak odpowiada endpoint API. Ma to również znaczenie dla prywatności: prywatne notatki zawierające dane uwierzytelniające, dane medyczne, informacje finansowe i osobiste refleksje nigdy nie opuszczają maszyny.

Struktura grafowa dzięki wiki-linkom. Składnia [[wiki-link]] w Obsidian tworzy graf skierowany pomiędzy notatkami. Notatka o implementacji OAuth zawiera odnośniki do notatek o rotacji tokenów, zarządzaniu sesjami i bezpieczeństwie API. Struktura grafowa koduje relacje między koncepcjami kuratorowane przez człowieka. Embeddingi wektorowe wychwytują podobieństwo semantyczne, ale wiki-linki wychwytują celowe połączenia, które autor stworzył podczas myślenia o danym temacie. Graf jest sygnałem, którego embeddingi nie są w stanie odtworzyć.

Ekosystem wtyczek. Obsidian dysponuje ponad 1800 wtyczkami społecznościowymi. Dataview odpytuje skarbiec jak bazę danych. Templater generuje notatki z szablonów z logiką JavaScript. Integracja z Git synchronizuje skarbiec z repozytorium. Linter wymusza spójność formatowania. Wtyczki te dodają strukturę do skarbca bez zmiany bazowego formatu czystego tekstu. System wyszukiwania indeksuje wyniki działania tych wtyczek, a nie same wtyczki.

Ponad 5 milionów użytkowników. Obsidian posiada dużą, aktywną społeczność tworzącą szablony, przepływy pracy, wtyczki i dokumentację. W przypadku napotkania problemu z organizacją skarbca lub konfiguracją wtyczki istnieje duże prawdopodobieństwo, że ktoś już udokumentował rozwiązanie. Społeczność tworzy również narzędzia powiązane z Obsidian: serwery MCP, skrypty indeksujące, pipeline publikacyjne i wrappery API.

Czego sam system plików nie zapewnia

Katalog plików markdown ma przewagę czystego tekstu, ale brakuje mu trzech rzeczy, które dodaje Obsidian:

  1. Linki dwukierunkowe. Obsidian automatycznie śledzi backlinki. Gdy notatka A zawiera odnośnik do notatki B, notatka B pokazuje, że notatka A się do niej odwołuje. Panel grafu wizualizuje klastry połączeń. Ta dwukierunkowa świadomość to metadane, których surowy system plików nie zapewnia.

  2. Podgląd na żywo z renderowaniem wtyczek. Zapytania Dataview, diagramy Mermaid i bloki callout renderują się w czasie rzeczywistym. Doświadczenie pisania jest bogatsze niż w edytorze tekstu, a format przechowywania pozostaje czystym tekstem. Pisanie i organizacja odbywają się w bogatym środowisku; system wyszukiwania indeksuje surowy markdown.

  3. Infrastruktura społecznościowa. Odkrywanie wtyczek, marketplace motywów, usługa synchronizacji (opcjonalna), usługa publikacji (opcjonalna) oraz ekosystem dokumentacji. Każdą pojedynczą funkcję można odtworzyć za pomocą samodzielnych narzędzi, ale Obsidian łączy je w spójny przepływ pracy.

Czego Obsidian NIE robi (i co trzeba zbudować)

Obsidian nie zawiera infrastruktury wyszukiwania. Posiada podstawowe wyszukiwanie (pełnotekstowe, po nazwie pliku, po tagach), ale brak w nim pipeline embeddingów, wyszukiwania wektorowego, rankingu fuzyjnego, serwera MCP, filtrowania danych uwierzytelniających, strategii dzielenia na fragmenty oraz hooków integracyjnych dla zewnętrznych narzędzi AI. Ten przewodnik obejmuje infrastrukturę budowaną na wierzchu Obsidian. Skarbiec jest podłożem. Pipeline wyszukiwania, serwer MCP i hooki integracyjne to infrastruktura.

Opisana tutaj architektura jest markdown-first, a nie ekskluzywna dla Obsidian. W przypadku korzystania z Logseq, Foam, Dendron lub zwykłego katalogu plików markdown, pipeline wyszukiwania działa identycznie. Chunker odczytuje pliki .md. Embedder przetwarza ciągi tekstowe. Indekser zapisuje do SQLite. Żaden z tych komponentów nie zależy od funkcji specyficznych dla Obsidian. Wkładem Obsidian jest środowisko pisania i organizacji, które tworzy pliki markdown indeksowane przez retriever.


Szybki start: Pierwszy vault połączony z AI

Ta sekcja pozwala połączyć vault z narzędziem AI w pięć minut. Instalacja Obsidian, utworzenie vault, zainstalowanie serwera MCP i uruchomienie pierwszego zapytania. Szybki start wykorzystuje społecznościowy serwer MCP dla natychmiastowych rezultatów. Dalsze sekcje omawiają budowanie własnego pipeline’u wyszukiwania do zastosowań produkcyjnych.

Wymagania wstępne

  • macOS, Linux lub Windows
  • Node.js 18+ (dla serwera MCP)
  • Zainstalowany Claude Code, Codex CLI lub Cursor

Krok 1: Utworzenie vault

Należy pobrać Obsidian ze strony obsidian.md i utworzyć nowy vault. Warto wybrać lokalizację łatwą do zapamiętania — serwer MCP wymaga ścieżki bezwzględnej.

# Example vault location
~/Documents/knowledge-base/

Następnie warto dodać kilka notatek, aby mechanizm wyszukiwania miał z czym pracować. Nawet 10–20 notatek wystarczy, żeby zobaczyć wyniki. Każda notatka powinna być plikiem .md z opisowym tytułem i co najmniej jednym akapitem treści.

Krok 2: Instalacja serwera MCP

Społecznościowy serwer obsidian-mcp zapewnia natychmiastowy dostęp do vault. Instalacja:

npm install -g obsidian-mcp-server

Krok 3: Konfiguracja narzędzia AI

Claude Code — dodać do ~/.claude/settings.json:

{
  "mcpServers": {
    "obsidian": {
      "command": "obsidian-mcp-server",
      "args": ["--vault", "/absolute/path/to/your/vault"]
    }
  }
}

Codex CLI — dodać do .codex/config.toml:

[mcp_servers.obsidian]
command = "obsidian-mcp-server"
args = ["--vault", "/absolute/path/to/your/vault"]

Cursor — dodać do .cursor/mcp.json:

{
  "mcpServers": {
    "obsidian": {
      "command": "obsidian-mcp-server",
      "args": ["--vault", "/absolute/path/to/your/vault"]
    }
  }
}

Krok 4: Uruchomienie pierwszego zapytania

Należy otworzyć narzędzie AI i zadać pytanie, na które notatki w vault mogą odpowiedzieć:

Search my Obsidian vault for notes about [topic you wrote about]

Narzędzie AI wywołuje serwer MCP, który przeszukuje vault i zwraca pasujące treści. W wynikach powinny pojawić się ścieżki plików oraz odpowiednie fragmenty.

Co właśnie zostało zbudowane

Lokalna baza wiedzy została połączona z narzędziem AI poprzez standardowy protokół. Serwer MCP odczytuje pliki z vault, wykonuje podstawowe wyszukiwanie i zwraca wyniki. To minimalna działająca wersja.

Czego ten szybki start NIE zapewnia: - Wyszukiwanie hybrydowe (BM25 + wyszukiwanie wektorowe + fuzja RRF) - Wyszukiwanie semantyczne oparte na embeddingach - Filtrowanie poświadczeń - Inkrementalne indeksowanie - Automatyczne wstrzykiwanie kontekstu oparte na hookach

Dalsza część tego przewodnika omawia budowanie każdej z tych funkcjonalności. Szybki start potwierdza koncepcję. Pełny pipeline dostarcza wyszukiwanie o jakości produkcyjnej.


Schemat decyzyjny: Obsidian a alternatywy

Nie każdy przypadek użycia wymaga Obsidian. Ta sekcja opisuje, kiedy Obsidian jest właściwym podłożem, kiedy jest nadmiarowy i kiedy lepiej sprawdzi się inne rozwiązanie.

Drzewo decyzyjne

START: What is your primary content type?

├─ Structured data (tables, records, schemas)
   Use a database. SQLite, PostgreSQL, or a spreadsheet.
   Obsidian is for prose, not tabular data.

├─ Ephemeral context (current project, temporary notes)
   Use CLAUDE.md / AGENTS.md in the project repo.
   These travel with the code and reset per project.

├─ Team wiki (shared documentation, onboarding)
   Evaluate Notion, Confluence, or a shared git repo.
   Obsidian vaults are personal-first. Team sync is possible
    but not native.

└─ Growing personal knowledge corpus
   
   ├─ < 50 notes
      A folder of markdown files + grep is sufficient.
      Obsidian adds value mainly through the link graph,
       which needs density to be useful.
   
   ├─ 50 - 500 notes
      Obsidian adds value. Wiki-links create a navigable graph.
      BM25-only search (FTS5) is sufficient at this scale.
      Skip vector search and RRF until keyword collisions appear.
   
   ├─ 500 - 5,000 notes
      Full hybrid retrieval becomes valuable. Keyword collisions
       increase. Semantic search catches queries that BM25 misses.
      Add vector search + RRF fusion at this scale.
   
   └─ 5,000+ notes
       Full pipeline is essential. BM25-only returns too much noise.
       Credential filtering becomes critical (more notes = more
        accidentally pasted secrets).
       Incremental indexing matters (full reindex takes minutes).
       MCP integration pays dividends on every AI interaction.

Macierz porównawcza

Kryterium Obsidian Notion Apple Notes Zwykły system plików CLAUDE.md
Lokalna praca Tak Nie (chmura) Częściowo (iCloud) Tak Tak
Czysty tekst Tak (markdown) Nie (bloki) Nie (format własnościowy) Tak Tak
Struktura grafowa Tak (wiki-links) Częściowo (wzmianki) Nie Nie Nie
Indeksowanie przez AI Bezpośredni dostęp do plików Wymaga API Wymaga eksportu Bezpośredni dostęp do plików Już w kontekście
Ekosystem wtyczek 1800+ wtyczek Integracje Brak Nie dotyczy Nie dotyczy
Praca offline Pełna Tylko odczyt z pamięci podręcznej Częściowa Pełna Pełna
Skalowalność do 10 000+ notatek Tak Tak (z API) Pogarsza się Tak Nie (pojedynczy plik)
Koszt Darmowy (rdzeń) Od 10 $/mies. Darmowy Darmowy Darmowy

Kiedy Obsidian jest nadmiarowy

  • Kontekst pojedynczego projektu. Jeśli AI potrzebuje kontekstu wyłącznie o bieżącej bazie kodu, wystarczy umieścić go w CLAUDE.md, AGENTS.md lub dokumentacji na poziomie projektu. Te pliki są częścią repozytorium i są automatycznie ładowane.
  • Dane strukturalne. Jeśli treść to tabele, rekordy lub schematy, lepiej użyć bazy danych. Notatki w Obsidian są nastawione na tekst ciągły. Dataview umożliwia odpytywanie pól frontmatter, ale prawdziwa baza danych radzi sobie z zapytaniami strukturalnymi znacznie lepiej.
  • Tymczasowy research. Jeśli notatki zostaną usunięte po zakończeniu projektu, prostszy będzie katalog roboczy z plikami markdown. Nie warto budować infrastruktury wyszukiwania dla treści tymczasowych.

Kiedy Obsidian jest właściwym wyborem

  • Gromadzenie wiedzy przez miesiące lub lata. Wartość rośnie wraz z powiększaniem się korpusu. Vault liczący 200 notatek, odpytywany codziennie przez sześć miesięcy, daje więcej wartości niż vault z 5000 notatek odpytywany jednokrotnie.
  • Wiele dziedzin w jednym korpusie. Vault zawierający notatki o programowaniu, architekturze, bezpieczeństwie, projektowaniu i osobistych projektach korzysta z wyszukiwania międzydomenowego, którego CLAUDE.md dla pojedynczego projektu nie jest w stanie zapewnić.
  • Treści wrażliwe pod względem prywatności. Praca lokalna oznacza, że pipeline wyszukiwania nigdy nie wysyła treści do usług zewnętrznych. Vault zawiera wszystko, co zostanie w nim umieszczone, łącznie z treściami, których nie należałoby przesyłać do usługi chmurowej.

Model mentalny: Trzy warstwy

System składa się z trzech warstw, które działają niezależnie, ale wzajemnie się wzmacniają. Każda warstwa odpowiada za inny aspekt i ma inny tryb awarii.

┌─────────────────────────────────────────────────────┐
                 INTEGRATION LAYER                     
  MCP servers, hooks, skills, context injection        
  Concern: delivering context to AI tools              
  Failure: wrong context, too much context, stale      
└──────────────────────┬──────────────────────────────┘
                        query + ranked results
┌──────────────────────┴──────────────────────────────┐
                  RETRIEVAL LAYER                      
  BM25, vector KNN, RRF fusion, token budget           
  Concern: finding the right content for any query     
  Failure: wrong ranking, missed results, slow queries 
└──────────────────────┬──────────────────────────────┘
                        chunked, embedded, indexed
┌──────────────────────┴──────────────────────────────┐
                   INTAKE LAYER                        
  Note creation, signal triage, vault organization     
  Concern: what enters the vault and how it's stored   │
  Failure: noise, duplicates, missing structure        
└─────────────────────────────────────────────────────┘

Intake (warstwa przyjmowania) określa, co trafia do vault. Bez kuracji vault gromadzi szum: zrzuty ekranu tweetów, skopiowane artykuły bez adnotacji, niedokończone myśli pozbawione kontekstu. Warstwa intake odpowiada za kontrolę jakości w momencie wprowadzania danych. Pipeline oceniający, konwencja tagowania lub ręczny proces weryfikacji — dowolny mechanizm zapewniający, że vault zawiera treści warte wyszukania.

Retrieval (warstwa wyszukiwania) sprawia, że vault staje się odpytywalny. To silnik systemu: dzielenie notatek na jednostki wyszukiwania (chunking), osadzanie fragmentów w przestrzeni wektorowej (embeddings), indeksowanie pod kątem wyszukiwania słów kluczowych i semantycznego, fuzja wyników za pomocą RRF. Warstwa retrieval przekształca katalog plików w odpytywalną bazę wiedzy. Bez niej vault jest nawigowany jedynie przez ręczne przeglądanie i podstawowe wyszukiwanie, ale nie jest programowo dostępny dla narzędzi AI.

Integration (warstwa integracji) łączy warstwę wyszukiwania z narzędziami AI. Serwer MCP udostępnia wyszukiwanie jako wywoływalne narzędzie. Hooki automatycznie wstrzykują kontekst. Skille przechwytują nową wiedzę z powrotem do vault. Warstwa integracji stanowi interfejs między bazą wiedzy a agentami AI, które z niej korzystają.

Warstwy są celowo oddzielone. Pipeline oceniający w warstwie intake nie wie nic o embeddingach. Mechanizm wyszukiwania nie wie nic o regułach routingu sygnałów. Serwer MCP nie wie nic o sposobie tworzenia notatek. To rozdzielenie oznacza, że każdą warstwę można ulepszać niezależnie. Można wymienić model embeddingów bez zmiany pipeline’u intake. Można dodać nową funkcjonalność MCP bez modyfikacji mechanizmu wyszukiwania. Można zmienić heurystyki oceniania sygnałów bez dotykania indeksu.


Architektura vault dla konsumpcji przez AI

Vault zoptymalizowany pod kątem wyszukiwania przez AI kieruje się innymi konwencjami niż vault zoptymalizowany pod kątem osobistego przeglądania. Ta sekcja obejmuje strukturę folderów, schemat notatek, konwencje frontmatter oraz konkretne wzorce poprawiające jakość wyszukiwania.

Struktura folderów

Należy stosować numerowane prefiksy dla folderów najwyższego poziomu, aby stworzyć przewidywalną hierarchię organizacyjną. Numery nie oznaczają priorytetu — grupują powiązane domeny i ułatwiają przegląd struktury.

vault/
├── 00-inbox/              # Unsorted captures, pending triage
├── 01-projects/           # Active project notes
├── 02-areas/              # Ongoing areas of responsibility
├── 03-resources/          # Reference material by topic
   ├── programming/
   ├── security/
   ├── ai-engineering/
   ├── design/
   └── devops/
├── 04-archive/            # Completed projects, old references
├── 05-signals/            # Scored signal intake
   ├── ai-tooling/
   ├── security/
   ├── systems/
   └── ...12 domain folders
├── 06-daily/              # Daily notes (if used)
├── 07-templates/          # Note templates (excluded from index)
├── 08-attachments/        # Images, PDFs (excluded from index)
├── .obsidian/             # Obsidian config (excluded from index)
└── .indexignore            # Paths to exclude from retrieval index

Foldery, które powinny być indeksowane: Wszystko, co zawiera tekst w formacie markdown — projekty, obszary, zasoby, sygnały, notatki dzienne.

Foldery, które powinny być wykluczone z indeksowania: Szablony (zawierają zmienne zastępcze, a nie treść), załączniki (pliki binarne), konfiguracja Obsidian oraz każdy folder zawierający wrażliwe dane, które nie powinny znaleźć się w indeksie wyszukiwania.

Plik .indexignore

Należy utworzyć plik .indexignore w katalogu głównym vault, aby jawnie wykluczyć ścieżki z indeksu wyszukiwania. Składnia jest zgodna z .gitignore:

# Obsidian internal
.obsidian/

# Templates contain placeholders, not content
07-templates/

# Binary attachments
08-attachments/

# Personal health/medical notes
02-areas/health/

# Financial records
02-areas/finance/personal/

# Career documents (resumes, salary data)
02-areas/career/private/

Indekser odczytuje ten plik przed skanowaniem i całkowicie pomija pasujące ścieżki. Pliki w wykluczonych ścieżkach nigdy nie są dzielone na fragmenty (chunking), nigdy nie są przekształcane w embeddings i nigdy nie pojawiają się w wynikach wyszukiwania.

Schemat notatek

Każda notatka powinna mieć YAML frontmatter. Retriever wykorzystuje pola frontmatter do filtrowania i wzbogacania kontekstu:

---
title: "OAuth Token Rotation Patterns"
type: note           # note | signal | project | moc | daily
domain: security     # primary domain for routing
tags:
  - authentication
  - oauth
  - token-management
created: 2026-01-15
updated: 2026-02-28
source: ""           # URL if captured from external source
status: active       # active | archived | draft
---

Wymagane pola dla wyszukiwania:

  • title — Używany w wyświetlaniu wyników wyszukiwania oraz jako kontekst nagłówkowy dla BM25
  • type — Umożliwia zapytania filtrowane według typu („pokaż tylko MOC” lub „tylko sygnały”)
  • tags — Indeksowane w kontekście nagłówkowym FTS5 z wagą 0.3, zapewniające dopasowania słów kluczowych nawet gdy treść główna używa innej terminologii

Opcjonalne, ale wartościowe pola:

  • domain — Umożliwia zapytania ograniczone do domeny („szukaj tylko w notatkach o bezpieczeństwie”)
  • source — Atrybucja dla pobranej treści; retriever może dołączać źródłowe adresy URL do wyników
  • status — Pozwala wykluczać zarchiwizowane lub szkicowe notatki z aktywnego wyszukiwania

Konwencje podziału na fragmenty (chunking)

Retriever dzieli tekst na granicach nagłówków H2 (##). Oznacza to, że struktura notatki bezpośrednio wpływa na granularność wyszukiwania:

Dobre dla wyszukiwania:

## Token Rotation Strategy

The rotation interval depends on the threat model...

## Implementation with refresh_token

The OAuth 2.0 refresh token flow requires...

## Error Handling: Expired Tokens

When a token expires mid-request...

Trzy sekcje H2 tworzą trzy niezależnie przeszukiwalne fragmenty. Każdy fragment ma wystarczający kontekst, aby embedding uchwycił jego znaczenie. Zapytanie o „obsługę wygasłych tokenów” trafia konkretnie w trzeci fragment.

Słabe dla wyszukiwania:

# OAuth Notes

Token rotation depends on threat model. The OAuth 2.0 refresh
token flow requires storing the refresh token securely. When a
token expires mid-request, the client should retry after refresh.
The rotation interval is typically 15-30 minutes for access tokens
and 7-30 days for refresh tokens...

Jedna długa sekcja bez nagłówków H2 tworzy jeden duży fragment. Embedding uśrednia się po wszystkich tematach w sekcji. Zapytanie o dowolny podtemat pasuje do całej notatki w równym stopniu.

Zasada ogólna: Jeśli sekcja obejmuje więcej niż jedną koncepcję, należy ją podzielić na podsekcje H2. Chunker zajmie się resztą.

Czego nie umieszczać w notatkach

Treści obniżające jakość wyszukiwania:

  • Surowe kopie całych artykułów bez adnotacji. Retriever indeksuje słowa kluczowe oryginalnego artykułu, rozmywając vault treścią, której nie napisaliśmy. Zamiast tego lepiej dodać podsumowanie, wyodrębnić kluczowe punkty lub umieścić link do źródłowego adresu URL.
  • Zrzuty ekranu bez opisu tekstowego. Retriever indeksuje tekst w formacie markdown. Obraz bez tekstu alternatywnego lub otaczającego opisu jest niewidoczny zarówno dla BM25, jak i wyszukiwania wektorowego.
  • Ciągi uwierzytelniające. Klucze API, tokeny, hasła, ciągi połączeń. Nawet przy filtrowaniu danych uwierzytelniających najbezpieczniejszym podejściem jest nigdy nie wklejać sekretów do notatek. Zamiast tego należy odwoływać się do nich po nazwie („token API dla Cloudflare w ~/.env”).
  • Automatycznie generowana treść bez kuracji. Jeśli narzędzie generuje notatkę (transkrypcja spotkania, wyróżnienia z Readwise, import RSS), należy ją przejrzeć i opatrzyć adnotacjami, zanim trafi do stałego vault. Niekurowane automatyczne importy zwiększają objętość, nie dodając wartości wyszukiwawczej.

Ekosystem wtyczek dla przepływów pracy z AI

Wtyczki Obsidian poprawiające jakość vault pod kątem wyszukiwania przez AI dzielą się na trzy kategorie: strukturalne (wymuszające spójność), zapytaniowe (eksponujące metadane) i synchronizacyjne (utrzymujące aktualność vault).

Niezbędne wtyczki

Dataview. Umożliwia odpytywanie vault jak bazy danych z wykorzystaniem pól frontmatter. Pozwala tworzyć dynamiczne indeksy: „wszystkie notatki oznaczone tagiem security zaktualizowane w ciągu ostatnich 30 dni” lub „wszystkie notatki projektowe ze statusem active.” Dataview nie pomaga bezpośrednio w wyszukiwaniu, ale pozwala identyfikować luki w pokryciu vault i znajdować notatki wymagające aktualizacji.

TABLE type, domain, updated
FROM "03-resources"
WHERE status = "active"
SORT updated DESC
LIMIT 20

Templater. Tworzy notatki z szablonów z dynamicznymi polami. Zapewnia, że każda nowa notatka zaczyna się z poprawnym frontmatter, dzięki użyciu szablonu wstępnie wypełniającego pola created, type i domain. Spójny frontmatter poprawia filtrowanie w wyszukiwaniu.

<%* /* New Resource Note Template */ %>
---
title: "<% tp.file.cursor() %>"
type: note
domain: <% tp.system.suggester(["programming", "security", "ai-engineering", "design", "devops"], ["programming", "security", "ai-engineering", "design", "devops"]) %>
tags: []
created: <% tp.date.now("YYYY-MM-DD") %>
updated: <% tp.date.now("YYYY-MM-DD") %>
source: ""
status: active
---

## Key Points

## Details

## Źródła

Linter. Wymusza reguły formatowania w całym sejfie. Spójna hierarchia nagłówków (H1 dla tytułu, H2 dla sekcji, H3 dla podsekcji) zapewnia, że chunker generuje przewidywalne wyniki. Reguły Lintera istotne dla wyszukiwania:

  • Inkrementacja nagłówków: wymuszanie sekwencyjnych poziomów nagłówków (bez przeskakiwania z H1 do H3)
  • YAML tytułu: zgodność z nazwą pliku
  • Końcowe spacje: usuwanie (eliminuje artefakty tokenizacji FTS5)
  • Kolejne puste wiersze: ograniczenie do 1 (czystsze fragmenty)

Integracja z Git. Kontrola wersji dla sejfu. Umożliwia śledzenie zmian w czasie, synchronizację między maszynami oraz odzyskiwanie po przypadkowych usunięciach. Git dostarcza również dane mtime, które indekser wykorzystuje do przyrostowego wykrywania zmian.

Wtyczki wspierające indeksowanie

Smart Connections. Wtyczka Obsidian zapewniająca semantyczne wyszukiwanie wspierane przez AI bezpośrednio w Obsidian. Tworzy własny indeks embeddingów. Choć system wyszukiwania opisany w tym przewodniku jest zewnętrzny wobec Obsidian (działa jako pipeline Python), Smart Connections jest przydatny do eksplorowania relacji semantycznych podczas pisania. Oba systemy indeksują tę samą treść, ale służą różnym celom: Smart Connections do odkrywania w edytorze, zewnętrzny retriever do integracji z narzędziami AI.

Metadata Menu. Zapewnia strukturalną edycję frontmatter z autouzupełnianiem wartości pól. Redukuje literówki w polach type, domain i tags. Spójna metadana poprawia dokładność filtrowania przy wyszukiwaniu.

Wtyczki szkodzące indeksowaniu

Excalidraw. Przechowuje rysunki jako JSON osadzony w plikach markdown. JSON jest składniowo poprawnym markdownem, ale po podziale na fragmenty i wygenerowaniu embeddingów daje bezużyteczne wyniki. Należy wykluczyć pliki Excalidraw z indeksu za pomocą .indexignore lub filtrowania po rozszerzeniu pliku.

Kanban. Przechowuje stan tablicy jako specjalnie sformatowany markdown. Format jest zaprojektowany pod renderowanie Kanban, nie pod wyszukiwanie tekstu. Chunker generuje fragmenty tytułów kart i metadanych, które źle się embeddują. Należy wykluczyć tablice Kanban z indeksu.

Calendar. Tworzy notatki dzienne z minimalną treścią (często tylko nagłówek z datą). Puste lub niemal puste notatki generują fragmenty niskiej jakości. Jeśli korzysta się z notatek dziennych, warto pisać w nich treści merytoryczne lub wykluczyć folder notatek dziennych z indeksu.

Konfiguracja wtyczek, która ma znaczenie

Odzyskiwanie plików → Włączone. Chroni przed przypadkowym usunięciem notatek. Nie jest bezpośrednio związane z wyszukiwaniem, ale krytyczne dla bazy wiedzy, na której się polega.

Ścisłe łamanie wierszy → Wyłączone. Standardowe łamanie wierszy markdown (podwójny nowy wiersz dla akapitu) generuje czystsze fragmenty niż tryb ścisły Obsidian (pojedynczy nowy wiersz dla <br>).

Domyślna lokalizacja nowych plików → Wyznaczony folder. Kierowanie nowych plików do 00-inbox/, aby nieskategoryzowane notatki nie zaśmiecały folderów domenowych. Skrzynka odbiorcza to obszar przejściowy; pliki przenoszone są do folderów domenowych po segregacji.

Format wiki-link → Najkrótsza ścieżka, gdy to możliwe. Krótsze cele linków są łatwiejsze do rozwiązania przez retriever podczas indeksowania struktury linków.


Modele embeddingów: wybór i konfiguracja

Model embeddingów konwertuje fragmenty tekstu na wektory numeryczne służące do wyszukiwania semantycznego. Wybór modelu determinuje jakość wyszukiwania, rozmiar indeksu, szybkość generowania embeddingów oraz zależności uruchomieniowe. Ta sekcja wyjaśnia, dlaczego Model2Vec potion-base-8M jest domyślnym wyborem i kiedy warto rozważyć alternatywy.

Dlaczego Model2Vec potion-base-8M

Model: minishlab/potion-base-8M Parametry: 7,6 miliona Wymiary: 256 Rozmiar: ~30 MB Zależności: model2vec (tylko numpy, bez PyTorch) Wnioskowanie: tylko CPU, statyczne embeddingi słów (bez warstw atencji)

Model2Vec destyluje wiedzę transformera zdaniowego do statycznych embeddingów tokenów. Zamiast uruchamiania warstw atencji na danych wejściowych (jak robią to BERT, MiniLM i inne modele transformerowe), Model2Vec generuje wektory poprzez ważone uśrednianie wstępnie obliczonych embeddingów tokenów.3 Praktyczna konsekwencja: szybkość generowania embeddingów jest 50-500x większa niż w przypadku modeli opartych na transformerach, ponieważ nie ma sekwencyjnych obliczeń.

W zestawie benchmarków MTEB potion-base-8M osiąga 89% wydajności all-MiniLM-L6-v2 (50,03 vs 56,09 średnio).4 Różnica jakości wynosząca 11% to kompromis za zalety w szybkości i prostocie. Dla krótkich fragmentów markdown (średnio 200-400 słów w typowym sejfie) różnica jakości jest mniej wyraźna niż przy dłuższych dokumentach, ponieważ oba modele zbiegają się do podobnych reprezentacji dla krótkich, skoncentrowanych tekstów.

Konfiguracja

# embedder.py
DEFAULT_MODEL = "minishlab/potion-base-8M"
EMBEDDING_DIM = 256

class Model2VecEmbedder:
    def __init__(self, model_name=DEFAULT_MODEL):
        self._model_name = model_name
        self._model = None

    def _ensure_model(self):
        if self._model is not None:
            return
        _activate_venv()  # Add isolated venv to sys.path
        from model2vec import StaticModel
        self._model = StaticModel.from_pretrained(self._model_name)

    def embed_batch(self, texts):
        self._ensure_model()
        vecs = self._model.encode(texts)
        return [v.tolist() for v in vecs]

Leniwe ładowanie. Model ładuje się przy pierwszym użyciu, nie podczas importu. Zaimportowanie modułu embeddera nie generuje żadnych kosztów, gdy retriever działa w trybie awaryjnym BM25 (np. gdy środowisko venv dla embeddingów nie jest zainstalowane).

Izolowane środowisko wirtualne. Model działa w dedykowanym venv (np. ~/.claude/venvs/memory/), aby uniknąć konfliktów zależności z resztą zestawu narzędzi. Funkcja _activate_venv() dodaje site-packages środowiska venv do sys.path w czasie wykonywania.

# Create isolated venv
python3 -m venv ~/.claude/venvs/memory
~/.claude/venvs/memory/bin/pip install model2vec

Przetwarzanie wsadowe. Embedder przetwarza teksty w partiach po 64, aby zamortyzować narzut Model2Vec. Indekser przekazuje fragmenty do embed_batch() zamiast generować embedding po jednym fragmencie na raz.

Kiedy wybrać alternatywy

Model Wym. Rozmiar Szybkość Jakość (MTEB) Najlepszy do
potion-base-8M 256 30 MB 500x 50,03 Domyślny: lokalny, szybki, bez GPU
all-MiniLM-L6-v2 384 80 MB 1x 56,09 Wyższa jakość, nadal lokalny
nomic-embed-text-v1.5 768 270 MB 0,5x 62,28 Najlepsza lokalna jakość
text-embedding-3-small 1536 API N/A 62,30 Oparty na API, najwyższa jakość

Wybierz all-MiniLM-L6-v2, gdy jakość wyszukiwania jest ważniejsza niż szybkość i PyTorch jest zainstalowany. Wektory 384-wymiarowe zwiększają rozmiar bazy danych SQLite o ~50% w porównaniu z wektorami 256-wymiarowymi. Szybkość generowania embeddingów spada z <1 minuty do ~10 minut przy pełnym reindeksowaniu 15 000 plików na sprzęcie z serii M.

Wybierz nomic-embed-text-v1.5, gdy potrzebna jest najlepsza możliwa lokalna jakość wyszukiwania i akceptowane jest wolniejsze indeksowanie. Wektory 768-wymiarowe mniej więcej trzykrotnie zwiększają rozmiar bazy danych. Wymaga PyTorch i nowoczesnego CPU lub GPU.

Wybierz text-embedding-3-small, gdy opóźnienia sieciowe i kwestie prywatności są akceptowalnymi kompromisami. API generuje embeddingi najwyższej jakości, ale wprowadza zależność od chmury, koszt per token (0,02 USD/milion tokenów) i wysyła treści na serwery OpenAI.

Pozostań przy potion-base-8M we wszystkich pozostałych przypadkach. Przewaga szybkości jest kluczowa dla iteracyjnego indeksowania (reindeksowanie podczas pracy), zależność wyłącznie od numpy eliminuje złożoność instalacji PyTorch, a wektory 256-wymiarowe utrzymują kompaktowy rozmiar bazy danych.

Śledzenie hasza modelu

Indekser przechowuje hasz wyprowadzony z nazwy modelu i rozmiaru słownika. Jeśli model embeddingów zostanie zmieniony, indekser wykrywa niezgodność przy następnym przyrostowym uruchomieniu i automatycznie wyzwala pełne reindeksowanie.

def _compute_model_hash(self):
    """Hash model name + vocab size for compatibility tracking."""
    key = f"{self._model_name}:{self._model.vocab_size}"
    return hashlib.sha256(key.encode()).hexdigest()[:16]

Zapobiega to mieszaniu wektorów z różnych modeli w tej samej bazie danych, co generowałoby bezsensowne wyniki cosine similarity.

Tryby awarii

Niepowodzenie pobierania modelu. Pierwsze uruchomienie pobiera model z Hugging Face. Jeśli pobieranie się nie powiedzie (problem z siecią, firmowy firewall), retriever przechodzi w tryb awaryjny BM25. Model jest buforowany lokalnie po pierwszym pobraniu.

Niezgodność wymiarów. Jeśli model zostanie zmieniony bez wyczyszczenia bazy danych, przechowywane wektory mają inny wymiar niż nowe embeddingi. Indekser wykrywa to za pomocą hasza modelu i wyzwala pełne reindeksowanie. Jeśli sprawdzanie hasza zawiedzie (niestandardowy model bez odpowiedniego hasza), sqlite-vec zwróci błąd przy zapytaniach KNN z niezgodnymi wymiarami.

Presja pamięciowa przy dużych sejfach. Generowanie embeddingów dla ponad 50 000 fragmentów w jednej partii może zużyć znaczną ilość pamięci. Indekser przetwarza dane w partiach po 64, aby ograniczyć szczytowe zużycie pamięci. Jeśli pamięć nadal stanowi problem, należy zmniejszyć rozmiar partii.


Wyszukiwanie pełnotekstowe z FTS5

Rozszerzenie FTS5 w SQLite zapewnia wyszukiwanie pełnotekstowe z rankingiem BM25. FTS5 stanowi komponent wyszukiwania słów kluczowych w potoku hybrid retrieval. Ta sekcja obejmuje konfigurację FTS5, sytuacje, w których BM25 sprawdza się najlepiej, oraz jego specyficzne tryby awarii.

Wirtualna tabela FTS5

CREATE VIRTUAL TABLE chunks_fts USING fts5(
    chunk_text,
    section,
    heading_context,
    content=chunks,
    content_rowid=id
);

Tryb synchronizacji zawartości. Parametr content=chunks informuje FTS5, aby odwoływał się bezpośrednio do tabeli chunks, zamiast przechowywać zduplikowaną kopię tekstu. Zmniejsza to zapotrzebowanie na pamięć o połowę, ale oznacza, że FTS5 musi być ręcznie synchronizowany przy wstawianiu, aktualizowaniu lub usuwaniu fragmentów.

Kolumny. Trzy kolumny są indeksowane: - chunk_text — Główna treść każdego fragmentu (waga BM25: 1,0) - section — Tekst nagłówka H2 (waga BM25: 0,5) - heading_context — Tytuł notatki, tagi i metadane (waga BM25: 0,3)

Ranking BM25

BM25 ocenia dokumenty na podstawie częstotliwości terminu, odwrotnej częstotliwości dokumentowej oraz normalizacji długości dokumentu. Funkcja pomocnicza bm25() w FTS5 przyjmuje wagi dla poszczególnych kolumn:

SELECT
    c.id, c.file_path, c.section, c.chunk_text,
    bm25(chunks_fts, 1.0, 0.5, 0.3) AS score
FROM chunks_fts
JOIN chunks c ON chunks_fts.rowid = c.id
WHERE chunks_fts MATCH ?
ORDER BY score
LIMIT 30;

Wagi kolumn (1,0; 0,5; 0,3) oznaczają: - Dopasowanie słowa kluczowego w chunk_text ma największy wpływ na wynik - Dopasowanie w section (nagłówku) ma o połowę mniejszy wpływ - Dopasowanie w heading_context (tytuł, tagi) ma 30% wpływu

Wagi te można dostrajać. Jeśli sejf (vault) zawiera opisowe nagłówki, które dobrze przewidują jakość treści, warto zwiększyć wagę section. Jeśli tagi są kompleksowe i dokładne, warto zwiększyć wagę heading_context.

Kiedy BM25 wygrywa

BM25 sprawdza się najlepiej przy zapytaniach zawierających dokładne identyfikatory:

  • Nazwy funkcji: _rrf_fuse, embed_batch, get_stale_files
  • Flagi CLI: --incremental, --vault, --model
  • Klucze konfiguracyjne: bm25_weight, max_tokens, batch_size
  • Komunikaty błędów: SQLITE_LOCKED, ConnectionRefusedError
  • Specyficzne terminy techniczne: PostToolUse, PreToolUse, AGENTS.md

Przy tego typu zapytaniach BM25 natychmiast znajduje dokładne dopasowanie. Wyszukiwanie wektorowe zwróciłoby semantycznie powiązaną treść, ale mogłoby uszeregować dokładne dopasowanie niżej niż dyskusję koncepcyjną.

Kiedy BM25 zawodzi

BM25 zawodzi przy zapytaniach, które używają innej terminologii niż przechowywana treść:

  • Zapytanie: „how to handle authentication failures” → Sejf (vault) zawiera notatki o „login error recovery” i „session expiration handling”. BM25 nie dopasuje, ponieważ słowa kluczowe się różnią.
  • Zapytanie: „what is the best way to manage state” → Sejf (vault) zawiera notatki o „Redux store patterns” i „context providers”. BM25 nie trafia, ponieważ „zarządzanie stanem” jest wyrażone poprzez nazwy konkretnych technologii.

BM25 zawodzi również przy kolizji słów kluczowych w dużej skali. W sejfie (vault) zawierającym 15 000 plików wyszukiwanie frazy „configuration” dopasowuje setki notatek, ponieważ niemal każda notatka projektowa wspomina o konfiguracji. Wyniki są technicznie poprawne, ale praktycznie bezużyteczne — ranking nie jest w stanie określić, która notatka o „konfiguracji” jest istotna dla bieżącego zapytania.

Tokenizer FTS5

FTS5 domyślnie używa tokenizera unicode61, który obsługuje tekst ASCII i Unicode. W przypadku sejfów (vault) z dużą ilością treści CJK (chińskich, japońskich, koreańskich) warto rozważyć tokenizer trigram:

-- For CJK-heavy vaults
CREATE VIRTUAL TABLE chunks_fts USING fts5(
    chunk_text, section, heading_context,
    content=chunks, content_rowid=id,
    tokenize='trigram'
);

Domyślny tokenizer unicode61 dzieli tekst na granicach wyrazów, co słabo działa w przypadku języków bez spacji między wyrazami. Tokenizer trigram dzieli na co trzy znaki, umożliwiając dopasowywanie podciągów kosztem rozmiaru indeksu (około 3× większy).

Konserwacja

FTS5 wymaga jawnej synchronizacji, gdy zmienia się bazowa tabela chunks:

# After inserting chunks
cursor.execute("""
    INSERT INTO chunks_fts(chunks_fts)
    VALUES('rebuild')
""")

Polecenie rebuild odbudowuje indeks FTS5 z tabeli zawartości. Należy je uruchamiać po masowych wstawieniach (pełna reindeksacja), ale nie po pojedynczych aktualizacjach przyrostowych — w tym przypadku należy użyć INSERT INTO chunks_fts(rowid, chunk_text, section, heading_context) do synchronizacji poszczególnych wierszy.


Wyszukiwanie wektorowe z sqlite-vec

Rozszerzenie sqlite-vec wprowadza wyszukiwanie wektorowe KNN (K-Nearest Neighbors) do SQLite. Ta sekcja obejmuje konfigurację sqlite-vec, potok embeddingów od notatki do wektora przeszukiwalnego oraz specyficzne wzorce zapytań.

Wirtualna tabela sqlite-vec

CREATE VIRTUAL TABLE chunk_vecs USING vec0(
    id INTEGER PRIMARY KEY,
    embedding float[256]
);

Moduł vec0 przechowuje 256-wymiarowe wektory zmiennoprzecinkowe jako upakowane dane binarne. Kolumna id mapuje się 1:1 do tabeli chunks, umożliwiając złączenia między wynikami wektorowymi a metadanymi fragmentów.

Potok embeddingów

Potok przebiega od notatki do wektora przeszukiwalnego:

Note (.md file)
   Chunker: split at H2 boundaries
     Chunks (30-2000 chars each)
       Credential filter: scrub secrets
         Embedder: Model2Vec encode
           Vectors (256-dim float arrays)
             sqlite-vec: store as packed binary
               Ready for KNN queries

Serializacja wektorów

Moduł struct Python serializuje wektory zmiennoprzecinkowe do formatu pamięci sqlite-vec:

import struct

def _serialize_vector(vec):
    """Pack float list into binary for sqlite-vec."""
    return struct.pack(f"{len(vec)}f", *vec)

def _deserialize_vector(blob, dim=256):
    """Unpack binary blob to float list."""
    return list(struct.unpack(f"{dim}f", blob))

Zapytanie KNN

Zapytanie wyszukiwania wektorowego osadza zapytanie wejściowe, a następnie znajduje K najbliższych fragmentów według odległości kosinusowej:

def _vector_search(self, query_text, limit=30):
    query_vec = self.embedder.embed_batch([query_text])[0]
    packed = _serialize_vector(query_vec)

    results = self.db.execute("""
        SELECT
            cv.id,
            cv.distance,
            c.file_path,
            c.section,
            c.chunk_text
        FROM chunk_vecs cv
        JOIN chunks c ON cv.id = c.id
        WHERE embedding MATCH ?
            AND k = ?
        ORDER BY distance
    """, [packed, limit]).fetchall()

    return results

Operator MATCH w sqlite-vec wykonuje przybliżone wyszukiwanie najbliższego sąsiada. Parametr k kontroluje liczbę zwracanych wyników. Kolumna distance zawiera odległość kosinusową (0 = identyczne, 2 = przeciwne).

Kiedy wyszukiwanie wektorowe wygrywa

Wyszukiwanie wektorowe sprawdza się najlepiej przy zapytaniach, w których koncepcja jest ważniejsza niż konkretne słowa:

  • Zapytanie: „how to handle authentication failures” → Znajduje notatki o „login error recovery” (ta sama przestrzeń semantyczna, inne słowa kluczowe)
  • Zapytanie: „what patterns exist for caching” → Znajduje notatki o „memoization”, „Redis TTL strategies” i „HTTP cache headers” (powiązane koncepcje, zróżnicowana terminologia)
  • Zapytanie: „approaches to testing asynchronous code” → Znajduje notatki o „pytest-asyncio fixtures”, „mock event loops” i „async test patterns” (ta sama koncepcja wyrażona poprzez szczegóły implementacji)

Kiedy wyszukiwanie wektorowe zawodzi

Wyszukiwanie wektorowe ma trudności z dokładnymi identyfikatorami:

  • Zapytanie: _rrf_fuse → Zwraca notatki o „fusion algorithms” i „rank merging”, ale może uszeregować właściwą definicję funkcji niżej niż dyskusje koncepcyjne
  • Zapytanie: PostToolUse → Zwraca notatki o „tool lifecycle hooks” i „post-execution handlers” zamiast konkretnej nazwy hooka

Wyszukiwanie wektorowe ma również trudności z danymi strukturalnymi. Pliki konfiguracyjne JSON, bloki YAML i fragmenty kodu generują embeddingi, które chwytają wzorce strukturalne, a nie znaczenie semantyczne. Plik JSON z "review": true jest osadzany inaczej niż prozaiczna dyskusja o przeglądzie kodu.

Łagodna degradacja

Jeśli sqlite-vec nie uda się załadować (brak rozszerzenia, niekompatybilna platforma, uszkodzona biblioteka), retriever przełącza się na tryb wyszukiwania wyłącznie BM25:

class VectorIndex:
    def __init__(self, db_path):
        self.db = sqlite3.connect(db_path)
        self._vec_available = False
        try:
            self.db.enable_load_extension(True)
            self.db.load_extension("vec0")
            self._vec_available = True
        except Exception:
            pass  # BM25-only mode

    @property
    def vec_available(self):
        return self._vec_available

Retriever sprawdza vec_available przed próbą wykonania zapytań wektorowych. Gdy jest wyłączony, wszystkie wyszukiwania korzystają wyłącznie z BM25, a krok fuzji RRF jest pomijany.


Reciprocal Rank Fusion (RRF)

RRF łączy dwie listy rankingowe bez konieczności kalibracji wyników. Ta sekcja obejmuje algorytm, szczegółowy ślad zapytania, dostrajanie parametru k oraz uzasadnienie wyboru RRF zamiast alternatywnych metod. Interaktywny kalkulator z edytowalnymi rangami, gotowymi scenariuszami i wizualnym eksploratorem architektury znajduje się w pogłębionej analizie hybrid retriever.

Algorytm

RRF przypisuje każdemu dokumentowi wynik oparty wyłącznie na jego pozycji rankingowej w każdej liście:

score(d) = Σ (weight_i / (k + rank_i))

Gdzie: - k to stała wygładzająca (60, zgodnie z Cormack i in.1) - rank_i to pozycja dokumentu w liście wyników i (indeksowana od 1) - weight_i to opcjonalny mnożnik dla danej listy (domyślnie 1.0)

Dokumenty, które zajmują wysokie pozycje w wielu listach, otrzymują wyższe wyniki po fuzji. Dokumenty pojawiające się tylko w jednej liście otrzymują wynik wyłącznie z tego jednego źródła.

Dlaczego RRF zamiast alternatyw

Ważona kombinacja liniowa wymaga kalibracji wyników BM25 względem odległości cosinusowych. Wyniki BM25 są nieograniczone i skalują się z rozmiarem korpusu. Odległości cosinusowe są ograniczone do przedziału [0, 2]. Ich łączenie wymaga normalizacji, a parametry normalizacji zależą od zbioru danych. RRF wykorzystuje wyłącznie pozycje rankingowe, które zawsze są liczbami całkowitymi zaczynającymi się od 1, niezależnie od metody punktacji.

Modele fuzji z uczeniem wymagają oznaczonych danych treningowych — par zapytanie-dokument z oceną trafności. W przypadku osobistej bazy wiedzy takie dane treningowe nie istnieją. Konieczne byłoby ręczne ocenianie setek par zapytanie-dokument, aby wytrenować użyteczny model. RRF działa bez jakichkolwiek danych treningowych.

Metody głosowania Condorceta (metoda Bordy, metoda Schulzego) są teoretycznie eleganckie, ale bardziej złożone w implementacji i dostrajaniu. Oryginalny artykuł o RRF wykazał, że RRF przewyższa metody Condorceta na danych ewaluacyjnych TREC.1

Fuzja w praktyce

Zapytanie: „how does the review aggregator handle disagreements”

BM25 umieszcza review-aggregator.py na pozycji 3 (dokładne dopasowanie słów kluczowych „review”, „aggregator”, „disagreements”), ale dwa pliki konfiguracyjne plasuje wyżej (słowo „review” występuje w nich bardziej prominentnie). Wyszukiwanie wektorowe umieszcza ten sam fragment na pozycji 1 (dopasowanie semantyczne dotyczące rozwiązywania konfliktów). Po fuzji RRF:

Fragment BM25 Vec Wynik fuzji
review-aggregator.py „Disagreement Resolution” #3 #1 0,0323
code-review-patterns.md „Multi-Reviewer” #4 #2 0,0317
deliberation-config.json „Review Weights” #1 0,0164

Fragmenty, które zajmują wysokie pozycje w obu listach, trafiają na szczyt. Fragmenty pojawiające się tylko w jednej liście otrzymują wynik z jednego źródła i spadają poniżej wyników z podwójnym rankingiem. Właściwa logika rozwiązywania sporów wygrywa, ponieważ obie metody ją znalazły — BM25 przez słowa kluczowe, wyszukiwanie wektorowe przez semantykę.

Pełny ślad krok po kroku z obliczeniami RRF dla poszczególnych rang oraz możliwością testowania różnych wartości k znajduje się w interaktywnym kalkulatorze RRF.

Implementacja

RRF_K = 60

def _rrf_fuse(self, bm25_results, vec_results,
              bm25_weight=1.0, vec_weight=1.0):
    """Fuse BM25 and vector results using Reciprocal Rank Fusion."""
    scores = {}

    for rank, r in enumerate(bm25_results, start=1):
        cid = r["id"]
        if cid not in scores:
            scores[cid] = {
                "rrf_score": 0.0,
                "file_path": r["file_path"],
                "section": r["section"],
                "chunk_text": r["chunk_text"],
                "bm25_rank": None,
                "vec_rank": None,
            }
        scores[cid]["rrf_score"] += bm25_weight / (self._rrf_k + rank)
        scores[cid]["bm25_rank"] = rank

    for rank, r in enumerate(vec_results, start=1):
        cid = r["id"]
        if cid not in scores:
            scores[cid] = {
                "rrf_score": 0.0,
                "file_path": r["file_path"],
                "section": r["section"],
                "chunk_text": r["chunk_text"],
                "bm25_rank": None,
                "vec_rank": None,
            }
        scores[cid]["rrf_score"] += vec_weight / (self._rrf_k + rank)
        scores[cid]["vec_rank"] = rank

    fused = sorted(
        scores.values(),
        key=lambda x: x["rrf_score"],
        reverse=True,
    )
    return fused

Dostrajanie k

Stała k kontroluje, ile wagi przypisuje się najwyżej pozycjonowanym wynikom w porównaniu z wynikami na niższych pozycjach:

  • Niskie k (np. 10): Najwyżej pozycjonowane wyniki dominują. Pozycja 1 uzyskuje 1/11 = 0,091, pozycja 10 uzyskuje 1/20 = 0,050 (różnica 1,8x). Sprawdza się, gdy poszczególne metody rankingowe trafnie wskazują najlepszy wynik.
  • Domyślne k (60): Zrównoważone. Pozycja 1 uzyskuje 1/61 = 0,0164, pozycja 10 uzyskuje 1/70 = 0,0143 (różnica 1,15x). Różnice rang są skompresowane, co przyznaje większą wagę obecności w wielu listach.
  • Wysokie k (np. 200): Obecność w obu listach ma znacznie większe znaczenie niż pozycja rankingowa. Pozycja 1 uzyskuje 1/201, pozycja 10 uzyskuje 1/210 — niemal identyczne. Stosować, gdy poszczególne metody rankingowe generują zaszumione rankingi, ale zgodność między listami jest wiarygodna.

Należy zacząć od k=60. Oryginalny artykuł o RRF wykazał, że ta wartość jest stabilna na zróżnicowanych zbiorach danych TREC. Dostrajanie ma sens dopiero po zmierzeniu przypadków niepowodzeń na własnym rozkładzie zapytań.

Rozstrzyganie remisów

Gdy dwa fragmenty mają identyczne wyniki RRF (rzadko, ale możliwe przy tej samej randze w jednej liście i braku obecności w drugiej), remisy rozstrzyga się następująco:

  1. Preferowane są fragmenty obecne w obu listach nad fragmentami z tylko jednej listy
  2. Wśród fragmentów w obu listach preferowany jest ten z niższą łączną rangą
  3. Wśród fragmentów z tylko jednej listy preferowany jest ten z niższą rangą w tej liście

Kompletny potok wyszukiwania

Ta sekcja śledzi zapytanie od wejścia do wyjścia przez cały potok: wyszukiwanie BM25, wyszukiwanie wektorowe, fuzja RRF, obcinanie budżetu tokenów i składanie kontekstu.

Przepływ od początku do końca

User query: "PostToolUse hook for context compression"
  │
  ├─ BM25 Search (FTS5)
  │    → MATCH "PostToolUse hook context compression"
  │    → Top 30 results ranked by BM25 score
  │    → 12ms
  │
  ├─ Vector Search (sqlite-vec)
  │    → Embed query with Model2Vec
  │    → KNN k=30 on chunk_vecs
  │    → Top 30 results ranked by cosine distance
  │    → 8ms
  │
  └─ RRF Fusion
       → Merge 60 candidates (may overlap)
       → Score by rank position
       → Top 10 results
       → 3ms
       │
       └─ Token Budget
            → Truncate to max_tokens (default 4000)
            → Estimate at 4 chars per token
            → Return results with metadata
            → <1ms

Całkowite opóźnienie: ~23ms dla bazy danych zawierającej 49 746 fragmentów na sprzęcie Apple M3 Pro.

API wyszukiwania

class HybridRetriever:
    def search(self, query, limit=10, max_tokens=4000,
               bm25_weight=1.0, vec_weight=1.0):
        """
        Search the vault using hybrid BM25 + vector retrieval.

        Args:
            query: Search query text
            limit: Maximum results to return
            max_tokens: Token budget for total result text
            bm25_weight: Weight for BM25 results in RRF
            vec_weight: Weight for vector results in RRF

        Returns:
            List of SearchResult with file_path, section,
            chunk_text, rrf_score, bm25_rank, vec_rank
        """
        # BM25 search
        bm25_results = self._bm25_search(query, limit=30)

        # Vector search (if available)
        if self.index.vec_available:
            vec_results = self._vector_search(query, limit=30)
            fused = self._rrf_fuse(
                bm25_results, vec_results,
                bm25_weight, vec_weight,
            )
        else:
            fused = bm25_results  # BM25-only fallback

        # Token budget truncation
        results = []
        token_count = 0
        for r in fused[:limit]:
            chunk_tokens = len(r["chunk_text"]) // 4
            if token_count + chunk_tokens > max_tokens:
                break
            results.append(r)
            token_count += chunk_tokens

        return results

Obcinanie budżetu tokenów

Parametr max_tokens zapobiega zwracaniu przez retriever większej ilości kontekstu, niż narzędzie AI jest w stanie wykorzystać. Szacowanie opiera się na 4 znakach na token (rozsądne przybliżenie dla prozy w języku angielskim). Wyniki są obcinane zachłannie: kolejne wyniki są dodawane według rankingu, dopóki budżet nie zostanie wyczerpany.

Jest to strategia konserwatywna. Bardziej zaawansowane podejście uwzględniałoby oceny jakości poszczególnych wyników i preferowało krótsze, wyższej jakości wyniki nad dłuższymi, niższej jakości. Podejście zachłanne jest prostsze i dobrze sprawdza się w praktyce, ponieważ ranking RRF już porządkuje wyniki według trafności.

Schemat bazy danych (kompletny)

-- Chunk content and metadata
CREATE TABLE chunks (
    id INTEGER PRIMARY KEY,
    file_path TEXT NOT NULL,
    section TEXT NOT NULL,
    chunk_text TEXT NOT NULL,
    heading_context TEXT DEFAULT '',
    mtime_ns INTEGER NOT NULL,
    embedded_at REAL NOT NULL
);

CREATE INDEX idx_chunks_file ON chunks(file_path);
CREATE INDEX idx_chunks_mtime ON chunks(mtime_ns);

-- FTS5 for BM25 search (content-synced to chunks table)
CREATE VIRTUAL TABLE chunks_fts USING fts5(
    chunk_text, section, heading_context,
    content=chunks, content_rowid=id
);

-- sqlite-vec for vector KNN search
CREATE VIRTUAL TABLE chunk_vecs USING vec0(
    id INTEGER PRIMARY KEY,
    embedding float[256]
);

-- Model metadata for compatibility tracking
CREATE TABLE model_meta (
    key TEXT PRIMARY KEY,
    value TEXT
);

Ścieżka łagodnej degradacji

Full pipeline:     BM25 + Vector + RRF    Best results
No sqlite-vec:     BM25 only              Good results (no semantic)
No model download:  BM25 only              Good results (no semantic)
No FTS5:           Vector only             Decent results (no keyword)
No database:       Error                   Prompt user to run indexer

Retriever sprawdza dostępne możliwości podczas inicjalizacji i dostosowuje strategię zapytań. Brakujący komponent obniża jakość, ale nie powoduje błędów. Jedynym krytycznym niepowodzeniem jest brak pliku bazy danych.

Statystyki produkcyjne

Pomiary na bazie wiedzy zawierającej 16 894 pliki, 49 746 fragmentów, bazę danych SQLite o rozmiarze 83 MB, Apple M3 Pro:

Metryka Wartość
Łączna liczba plików 16 894
Łączna liczba fragmentów 49 746
Rozmiar bazy danych 83 MB
Opóźnienie zapytania BM25 (p50) 12ms
Opóźnienie zapytania wektorowego (p50) 8ms
Opóźnienie fuzji RRF 3ms
Opóźnienie wyszukiwania od początku do końca (p50) 23ms
Czas pełnego reindeksowania ~4 minuty
Czas przyrostowego reindeksowania <10 sekund
Model embeddingów potion-base-8M (256-dim)
Pula kandydatów BM25 30
Pula kandydatów wektorowych 30
Domyślny limit wyników 10
Domyślny budżet tokenów 4 000 tokenów

Haszowanie treści i wykrywanie zmian

Indekser musi wiedzieć, które pliki zmieniły się od ostatniego uruchomienia indeksowania. Ta sekcja opisuje mechanizm wykrywania zmian i strategię haszowania.

Porównywanie czasu modyfikacji plików

Indekser przechowuje mtime_ns (czas modyfikacji pliku w nanosekundach) dla każdego fragmentu w tabeli chunks. Podczas przyrostowego uruchomienia indekser:

  1. Skanuje bazę wiedzy w poszukiwaniu wszystkich plików .md w dozwolonych folderach
  2. Odczytuje mtime_ns każdego pliku z systemu plików
  3. Porównuje z zapisanym mtime_ns w bazie danych
  4. Identyfikuje trzy kategorie:
  5. Nowe pliki: ścieżka istnieje w systemie plików, ale nie w bazie danych
  6. Zmienione pliki: ścieżka istnieje w obu, ale mtime_ns się różni
  7. Usunięte pliki: ścieżka istnieje w bazie danych, ale nie w systemie plików
def get_stale_files(self, vault_mtimes):
    """Find files whose mtime changed or are new."""
    stored = dict(self.db.execute(
        "SELECT DISTINCT file_path, mtime_ns FROM chunks"
    ).fetchall())

    stale = []
    for path, mtime in vault_mtimes.items():
        if path not in stored or stored[path] != mtime:
            stale.append(path)
    return stale

def get_deleted_files(self, vault_paths):
    """Find files in database that no longer exist in vault."""
    stored_paths = set(r[0] for r in self.db.execute(
        "SELECT DISTINCT file_path FROM chunks"
    ).fetchall())
    return stored_paths - set(vault_paths)

Dlaczego mtime, a nie hasz treści

Haszowanie treści (SHA-256 zawartości pliku) byłoby bardziej niezawodne niż porównywanie mtime — wykryłoby przypadki, gdy plik został „dotknięty” bez faktycznej zmiany (np. git checkout przywracający oryginalny mtime). Jednak haszowanie wymaga odczytania każdego pliku przy każdym przyrostowym uruchomieniu. Dla 16 894 plików odczyt zawartości zajmuje 2–3 sekundy. Odczyt mtime z systemu plików zajmuje <100ms.

Kompromis: porównywanie mtime czasami wyzwala niepotrzebne ponowne indeksowanie niezmienionych plików (fałszywe pozytywy), ale nigdy nie pomija faktycznych zmian. Fałszywe pozytywy kosztują kilka dodatkowych wywołań embeddingów na uruchomienie. Różnica w szybkości (100ms vs 3 sekundy) sprawia, że mtime jest pragmatycznym wyborem dla systemu uruchamianego przy każdej interakcji z AI.

Obsługa usuwania plików

Gdy plik zostanie usunięty z bazy wiedzy, indekser usuwa wszystkie jego fragmenty z bazy danych:

def remove_file(self, file_path):
    """Remove all chunks and vectors for a file."""
    chunk_ids = [r[0] for r in self.db.execute(
        "SELECT id FROM chunks WHERE file_path = ?",
        [file_path],
    ).fetchall()]

    for cid in chunk_ids:
        self.db.execute(
            "DELETE FROM chunk_vecs WHERE id = ?", [cid]
        )
    self.db.execute(
        "DELETE FROM chunks WHERE file_path = ?",
        [file_path],
    )

Tabele FTS5 z synchronizacją treści wymagają jawnego usuwania za pomocą INSERT INTO chunks_fts(chunks_fts, rowid, ...) VALUES('delete', ?, ...) dla każdego usuniętego wiersza. Indekser obsługuje to jako część procesu usuwania pliku.


Indeksowanie przyrostowe a pełne

Indekser obsługuje dwa tryby: przyrostowy (szybki, do codziennego użytku) i pełny (wolny, okazjonalny). Ta sekcja opisuje, kiedy stosować każdy z nich, gwarancje idempotentności oraz odzyskiwanie po uszkodzeniu bazy danych.

Indeksowanie przyrostowe

Kiedy stosować: Codzienne indeksowanie po edycji notatek. Tryb domyślny.

Co robi: 1. Skanuje vault w poszukiwaniu zmian w plikach (porównanie mtime) 2. Usuwa chunki dla skasowanych plików 3. Ponownie dzieli na chunki i generuje embeddingi dla zmienionych plików 4. Wstawia nowe chunki dla nowych plików 5. Synchronizuje indeks FTS5

Typowy czas trwania: <10 sekund dla dziennych edycji w vaulcie zawierającym 16 000 plików.

python index_vault.py --incremental

Pełne indeksowanie

Kiedy stosować: - Po zmianie modelu embeddingów (wykryto niezgodność hasza modelu) - Po migracji schematu (nowe kolumny, zmienione indeksy) - Po uszkodzeniu bazy danych (kontrola integralności kończy się błędem) - Gdy indeksowanie przyrostowe daje nieoczekiwane wyniki

Co robi: 1. Usuwa wszystkie istniejące dane (chunki, wektory, wpisy FTS5) 2. Skanuje cały vault 3. Dzieli wszystkie pliki na chunki 4. Generuje embeddingi dla wszystkich chunków 5. Buduje indeks FTS5 od podstaw

Typowy czas trwania: ~4 minuty dla 16 894 plików na Apple M3 Pro.

python index_vault.py --full

Idempotentność

Oba tryby są idempotentne: dwukrotne uruchomienie tego samego polecenia daje identyczny wynik. Indekser usuwa istniejące chunki dla pliku przed wstawieniem nowych, więc ponowne uruchomienie indeksowania przyrostowego na aktualnej bazie danych nie wprowadza żadnych zmian. Ponowne uruchomienie pełnego indeksowania daje identyczną bazę danych.

Odzyskiwanie po uszkodzeniu

Jeśli baza danych SQLite ulegnie uszkodzeniu (utrata zasilania podczas zapisu, błąd dysku, przerwanie procesu w trakcie transakcji):

# Check integrity
sqlite3 vectors.db "PRAGMA integrity_check;"

# If corruption detected, full reindex rebuilds from source files
python index_vault.py --full

Źródłem prawdy są zawsze pliki w vaulcie, nie baza danych. Baza danych jest artefaktem pochodnym, który można odbudować w dowolnym momencie. To kluczowa właściwość projektowa: nigdy nie trzeba tworzyć kopii zapasowej bazy danych.

Flaga --incremental

Gdy indekser uruchamiany jest z flagą --incremental:

  1. Sprawdzenie hasza modelu. Porównuje zapisany hasz modelu z aktualnym. Jeśli się różnią, automatycznie przełącza na tryb pełnego indeksowania i ostrzega użytkownika.
  2. Skanowanie plików. Przechodzi dozwolone foldery, zbiera ścieżki plików i wartości mtime.
  3. Wykrywanie zmian. Porównuje z zapisanymi danymi.
  4. Przetwarzanie wsadowe. Ponownie dzieli na chunki i generuje embeddingi dla zmienionych plików w partiach po 64.
  5. Raportowanie postępu. Wyświetla liczbę przetworzonych plików i czas, jaki upłynął.
  6. Łagodne zamykanie. Obsługuje SIGINT, kończąc przetwarzanie bieżącego pliku przed zatrzymaniem.

Filtrowanie danych uwierzytelniających i granice danych

Notatki osobiste zawierają sekrety: klucze API, tokeny bearer, ciągi połączeń z bazami danych, klucze prywatne wklejone podczas sesji debugowania. Filtr danych uwierzytelniających zapobiega ich przedostaniu się do indeksu wyszukiwania.

Problem

Notatka dotycząca debugowania integracji z OAuth może zawierać:

The token was: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
I used this curl command:
  curl -H "Authorization: Bearer sk-ant-api03-abc123..."

Bez filtrowania zarówno JWT, jak i klucz API zostałyby podzielone na chunki, przekształcone w embeddingi i zapisane w bazie danych. Wyszukiwanie frazy „uwierzytelnianie” zwróciłoby chunk zawierający prawdziwe sekrety. Co gorsza, jeśli retriever przekazuje wyniki do narzędzia AI przez MCP, sekrety pojawiają się w oknie kontekstu AI i potencjalnie w logach narzędzia.

Filtrowanie oparte na wzorcach

Filtr danych uwierzytelniających uruchamia się na każdym chunku przed zapisem, dopasowując 25 wzorców specyficznych dla dostawców oraz wzorce generyczne:

Wzorce specyficzne dla dostawców:

Wzorzec Przykład Regex
Klucz API OpenAI sk-... sk-[a-zA-Z0-9_-]{20,}
Klucz API Anthropic sk-ant-api03-... sk-ant-api\d{2}-[a-zA-Z0-9_-]{20,}
PAT GitHub ghp_... gh[ps]_[a-zA-Z0-9]{36,}
Klucz dostępu AWS AKIA... AKIA[0-9A-Z]{16}
Klucz Stripe sk_live_... [sr]k_(live\|test)_[a-zA-Z0-9]{24,}
Token Cloudflare ... Różne wzorce

Wzorce generyczne:

Wzorzec Wykrywanie
Tokeny JWT eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+
Tokeny Bearer Bearer\s+[a-zA-Z0-9_\-\.]+
Klucze prywatne -----BEGIN (RSA\|EC\|OPENSSH) PRIVATE KEY-----
Ciągi base64 o wysokiej entropii Ciągi z entropią >4,5 bita/znak, 40+ znaków
Przypisania haseł password\s*[:=]\s*["'][^"']+["']

Implementacja filtra

def clean_content(text):
    """Scrub credentials from text before indexing."""
    result = ScanResult(is_clean=True, match_count=0, patterns=[])

    for pattern in CREDENTIAL_PATTERNS:
        matches = pattern.regex.findall(text)
        if matches:
            text = pattern.regex.sub(
                f"[REDACTED:{pattern.name}]", text
            )
            result.is_clean = False
            result.match_count += len(matches)
            result.patterns.append(pattern.name)

    return text, result

Kluczowe decyzje projektowe:

  1. Filtrowanie przed generowaniem embeddingów. Oczyszczony tekst jest tym, co zostaje przekształcone w embedding. Reprezentacja wektorowa nigdy nie koduje wzorców danych uwierzytelniających. Zapytanie o „klucz API” zwraca notatki omawiające zarządzanie kluczami API, a nie notatki zawierające prawdziwe klucze.

  2. Zastępowanie, nie usuwanie. Token [REDACTED:pattern-name] zachowuje kontekst semantyczny otaczającego tekstu. Embedding oddaje, że „w tym miejscu znajdowało się coś przypominającego dane uwierzytelniające”, bez kodowania samych danych.

  3. Logowanie wzorców, nie wartości. Filtr loguje, które wzorce zostały dopasowane (np. „Scrubbed 2 credential(s) from oauth-debug.md [jwt, bearer-token]”), ale nigdy nie loguje wartości danych uwierzytelniających.

Wykluczanie na podstawie ścieżek

Plik .indexignore zapewnia wykluczanie zgrubne na poziomie ścieżek. Filtr danych uwierzytelniających zapewnia precyzyjne oczyszczanie wewnątrz indeksowanych plików. Oba mechanizmy są niezbędne:

  • .indexignore dla całych folderów, o których wiadomo, że zawierają wrażliwe treści (notatki zdrowotne, dokumenty finansowe, dokumenty dotyczące kariery)
  • Filtr danych uwierzytelniających dla sekretów przypadkowo osadzonych w treściach przeznaczonych do indeksowania

Klasyfikacja danych

W przypadku vaultów zawierających zróżnicowane treści warto rozważyć klasyfikację notatek według poziomu wrażliwości:

Poziom Przykłady Indeksować? Filtrować?
Publiczne Szkice wpisów na bloga, notatki techniczne Tak Tak
Wewnętrzne Plany projektów, decyzje architektoniczne Tak Tak
Wrażliwe Dane o wynagrodzeniach, dokumentacja medyczna Nie (.indexignore) Nie dotyczy
Zastrzeżone Dane uwierzytelniające, klucze prywatne Nie (.indexignore) Nie dotyczy

Architektura serwera MCP

Model Context Protocol (MCP) pozwala udostępnić retriever jako narzędzie wywoływane przez agentów AI. Ta sekcja obejmuje projekt serwera, powierzchnię możliwości oraz granice uprawnień.

Wybór protokołu: STDIO vs HTTP

MCP obsługuje dwa tryby transportu:

STDIO — Narzędzie AI uruchamia serwer MCP jako proces potomny i komunikuje się przez stdin/stdout. Jest to standardowy tryb dla narzędzi lokalnych. Claude Code, Codex CLI oraz Cursor obsługują serwery MCP w trybie STDIO.

{
  "mcpServers": {
    "obsidian": {
      "command": "python",
      "args": ["/path/to/obsidian_mcp.py"],
      "env": {
        "VAULT_PATH": "/path/to/vault",
        "DB_PATH": "/path/to/vectors.db"
      }
    }
  }
}

HTTP — Serwer MCP działa jako samodzielna usługa HTTP. Przydatne w przypadku zdalnego dostępu, konfiguracji wieloklientowych lub zespołowych, gdy skarbiec znajduje się na współdzielonym serwerze.

{
  "mcpServers": {
    "obsidian": {
      "url": "http://localhost:3333/mcp"
    }
  }
}

Zalecenie: Dla osobistych skarbców należy używać STDIO. Jest prostszy, bezpieczniejszy (brak ekspozycji sieciowej), a cyklem życia serwera zarządza narzędzie AI. HTTP należy stosować wyłącznie wtedy, gdy wiele narzędzi lub maszyn potrzebuje jednoczesnego dostępu do tego samego skarbca.

Projektowanie możliwości

Serwer MCP powinien udostępniać minimalny zestaw narzędzi:

search — Główne narzędzie. Uruchamia wyszukiwanie hybrydowe i zwraca uporządkowane wyniki.

{
  "name": "obsidian_search",
  "description": "Search the Obsidian vault using hybrid BM25 + vector retrieval",
  "parameters": {
    "query": { "type": "string", "description": "Search query" },
    "limit": { "type": "integer", "default": 5 },
    "max_tokens": { "type": "integer", "default": 2000 }
  }
}

read_note — Odczyt pełnej zawartości konkretnej notatki na podstawie ścieżki. Przydatne, gdy agent chce zobaczyć pełny kontekst wyniku wyszukiwania.

{
  "name": "obsidian_read_note",
  "description": "Read the full content of a note by file path",
  "parameters": {
    "file_path": { "type": "string", "description": "Relative path within vault" }
  }
}

list_notes — Lista notatek pasujących do filtra (według folderu, tagu, typu lub zakresu dat). Przydatne do eksploracji, gdy agent nie ma konkretnego zapytania.

{
  "name": "obsidian_list_notes",
  "description": "List notes matching filters",
  "parameters": {
    "folder": { "type": "string", "description": "Folder path within vault" },
    "tag": { "type": "string", "description": "Tag to filter by" },
    "limit": { "type": "integer", "default": 20 }
  }
}

get_context — Wygodne narzędzie, które uruchamia wyszukiwanie i formatuje wyniki jako blok kontekstu gotowy do wstrzyknięcia do konwersacji.

{
  "name": "obsidian_get_context",
  "description": "Get formatted context from vault for a topic",
  "parameters": {
    "topic": { "type": "string", "description": "Topic to get context for" },
    "max_tokens": { "type": "integer", "default": 2000 }
  }
}

Granice uprawnień

Serwer MCP powinien egzekwować ścisłe granice:

  1. Tylko odczyt. Serwer odczytuje skarbiec i bazę indeksu. Nie tworzy, nie modyfikuje ani nie usuwa notatek. Operacje zapisu (przechwytywanie nowych notatek) są obsługiwane przez osobne hooki lub umiejętności, a nie przez serwer MCP.

  2. Zakres ograniczony do skarbca. Serwer odczytuje wyłącznie pliki w ramach skonfigurowanej ścieżki skarbca. Próby path traversal (../../etc/passwd) muszą być odrzucane.

  3. Filtrowanie poświadczeń na wyjściu. Nawet jeśli baza danych zawiera wstępnie przefiltrowaną treść, należy stosować filtrowanie poświadczeń na wyjściu jako mechanizm obrony w głąb (defense-in-depth).

  4. Ograniczenie tokenów w odpowiedziach. Wymuszanie max_tokens we wszystkich odpowiedziach narzędzi, aby zapobiec otrzymywaniu przez narzędzie AI nadmiernie dużych bloków kontekstu.

Obsługa błędów

Narzędzia MCP powinny zwracać ustrukturyzowane komunikaty o błędach, które pomagają narzędziu AI w odzyskiwaniu:

def search(self, query, limit=5, max_tokens=2000):
    if not self.db_path.exists():
        return {
            "error": "Index database not found. Run the indexer first.",
            "suggestion": "python index_vault.py --full"
        }

    results = self.retriever.search(query, limit, max_tokens)

    if not results:
        return {
            "results": [],
            "message": f"No results found for '{query}'. Try broader terms."
        }

    return {
        "results": [
            {
                "file_path": r["file_path"],
                "section": r["section"],
                "text": r["chunk_text"],
                "score": round(r["rrf_score"], 4),
            }
            for r in results
        ],
        "count": len(results),
        "query": query,
    }

Integracja z Claude Code

Claude Code jest głównym konsumentem systemu wyszukiwania Obsidian. Ta sekcja obejmuje konfigurację MCP, integrację z hookami oraz wzorzec obsidian_bridge.py.

Konfiguracja MCP

Serwer MCP Obsidian należy dodać do ~/.claude/settings.json:

{
  "mcpServers": {
    "obsidian": {
      "command": "python",
      "args": ["/path/to/obsidian_mcp.py"],
      "env": {
        "VAULT_PATH": "/absolute/path/to/vault",
        "DB_PATH": "/absolute/path/to/vectors.db"
      }
    }
  }
}

Po dodaniu konfiguracji należy ponownie uruchomić Claude Code. Serwer MCP zostanie uruchomiony jako proces potomny. Aby zweryfikować, czy działa:

> What tools do you have from the obsidian MCP server?

Claude Code powinien wyświetlić listę dostępnych narzędzi (obsidian_search, obsidian_read_note itp.).

Integracja z hookami

Hooki rozszerzają zachowanie Claude Code w zdefiniowanych punktach cyklu życia. Dwa hooki są istotne dla integracji z Obsidian:

Hook PreToolUse — Odpytuje skarbiec przed przetworzeniem wywołania narzędzia przez agenta. Automatycznie wstrzykuje odpowiedni kontekst.

#!/bin/bash
# ~/.claude/hooks/pre-tool-use/obsidian-context.sh
# Automatically inject vault context before tool execution

TOOL_NAME="$1"
PROMPT="$2"

# Only inject context for code-related tools
case "$TOOL_NAME" in
    Edit|Write|Bash)
        # Query the vault
        CONTEXT=$(python /path/to/retriever.py search "$PROMPT" --limit 3 --max-tokens 1500)
        if [ -n "$CONTEXT" ]; then
            echo "---"
            echo "Relevant vault context:"
            echo "$CONTEXT"
            echo "---"
        fi
        ;;
esac

Hook PostToolUse — Przechwytuje znaczące wyniki narzędzi z powrotem do skarbca na potrzeby przyszłego wyszukiwania.

#!/bin/bash
# ~/.claude/hooks/post-tool-use/capture-insight.sh
# Capture significant outputs to vault (selective)

TOOL_NAME="$1"
OUTPUT="$2"

# Only capture substantial outputs
if [ ${#OUTPUT} -gt 500 ]; then
    python /path/to/capture.py --text "$OUTPUT" --source "claude-code-$TOOL_NAME"
fi

Wzorzec obsidian_bridge.py

Moduł mostowy udostępnia Python API, który mogą wywoływać hooki i umiejętności:

# obsidian_bridge.py
from retriever import HybridRetriever

_retriever = None

def get_retriever():
    global _retriever
    if _retriever is None:
        _retriever = HybridRetriever(
            db_path="/path/to/vectors.db",
            vault_path="/path/to/vault",
        )
    return _retriever

def search_vault(query, limit=5, max_tokens=2000):
    """Search vault and return formatted context."""
    retriever = get_retriever()
    results = retriever.search(query, limit, max_tokens)

    if not results:
        return ""

    lines = ["## Vault Context\n"]
    for r in results:
        lines.append(f"**{r['file_path']}** — {r['section']}")
        lines.append(f"> {r['chunk_text'][:500]}")
        lines.append("")

    return "\n".join(lines)

Umiejętność /capture

Umiejętność Claude Code służąca do przechwytywania spostrzeżeń z powrotem do skarbca:

/capture "OAuth token rotation requires both access and refresh token invalidation"
  --domain security
  --tags oauth,tokens

Umiejętność tworzy nową notatkę w 00-inbox/ z odpowiednim frontmatter i uruchamia przyrostowe reindeksowanie, dzięki czemu nowa notatka jest natychmiast dostępna w wyszukiwaniu.

Zarządzanie oknem kontekstu

Integracja powinna uwzględniać okno kontekstu Claude Code:

  • Wstrzykiwany kontekst należy ograniczyć do 1500–2000 tokenów na zapytanie. Więcej konkuruje z pamięcią roboczą agenta.
  • Dołączać atrybucję źródła. Zawsze należy uwzględnić ścieżkę pliku i nagłówek sekcji, aby agent mógł odwołać się do źródła.
  • Przycinać tekst fragmentów. Długie fragmenty powinny być przycinane ze znakiem ... zamiast całkowicie pomijane. Pierwsze 300–500 znaków zazwyczaj zawiera kluczowe informacje.
  • Nie wstrzykiwać przy każdym wywołaniu narzędzia. Hook PreToolUse powinien selektywnie wstrzykiwać kontekst w zależności od wywoływanego narzędzia. Operacje odczytu nie potrzebują kontekstu ze skarbca. Operacje zapisu i edycji korzystają z niego.

Integracja z Codex CLI

Codex CLI łączy się z serwerami MCP poprzez config.toml. Wzorzec integracji różni się od Claude Code składnią konfiguracji i sposobem dostarczania instrukcji.

Konfiguracja MCP

Należy dodać do .codex/config.toml lub ~/.codex/config.toml:

[mcp_servers.obsidian]
command = "python"
args = ["/path/to/obsidian_mcp.py"]

[mcp_servers.obsidian.env]
VAULT_PATH = "/absolute/path/to/vault"
DB_PATH = "/absolute/path/to/vectors.db"

Wzorce AGENTS.md

Codex CLI odczytuje AGENTS.md w celu uzyskania instrukcji na poziomie projektu. Należy uwzględnić wskazówki dotyczące wyszukiwania w skarbcu:

## Dostępne narzędzia

### Obsidian Vault (MCP: obsidian)
Narzędzie `obsidian_search` służy do wyszukiwania odpowiedniego kontekstu z bazy wiedzy.
Przeszukiwanie vault jest przydatne, gdy potrzebne są:
- Informacje o danym koncepcie lub wzorcu
- Wcześniejsze decyzje lub ich uzasadnienie
- Materiały referencyjne do implementacji

Przykładowe zapytania:
- "authentication patterns in FastAPI"
- "how does the review aggregator work"
- "sqlite-vec configuration"

Różnice w stosunku do Claude Code

Funkcja Claude Code Codex CLI
Konfiguracja MCP settings.json config.toml
Hooki ~/.claude/hooks/ Nieobsługiwane
Umiejętności ~/.claude/skills/ Nieobsługiwane
Plik instrukcji CLAUDE.md AGENTS.md
Tryby zatwierdzania --dangerously-skip-permissions suggest / auto-edit / full-auto

Kluczowa różnica: Codex CLI nie obsługuje hooków. Wzorzec automatycznego wstrzykiwania kontekstu (hook PreToolUse) nie jest dostępny. Zamiast tego należy umieścić jawne instrukcje w pliku AGENTS.md, nakazujące agentowi przeszukanie vault przed rozpoczęciem pracy.


Cursor i inne narzędzia

Cursor oraz inne narzędzia AI obsługujące MCP mogą łączyć się z tym samym serwerem Obsidian MCP. Ta sekcja obejmuje konfigurację dla popularnych narzędzi.

Cursor

Należy dodać do pliku .cursor/mcp.json w katalogu głównym projektu:

{
  "mcpServers": {
    "obsidian": {
      "command": "python",
      "args": ["/path/to/obsidian_mcp.py"],
      "env": {
        "VAULT_PATH": "/absolute/path/to/vault",
        "DB_PATH": "/absolute/path/to/vectors.db"
      }
    }
  }
}

Plik .cursorrules Cursora może zawierać instrukcje dotyczące korzystania z vault:

When working on implementation tasks, search the Obsidian vault
for relevant context before writing code. Use the obsidian_search
tool with descriptive queries about the concept you're implementing.

Macierz kompatybilności

Narzędzie Obsługa MCP Transport Lokalizacja konfiguracji
Claude Code Pełna STDIO ~/.claude/settings.json
Codex CLI Pełna STDIO .codex/config.toml
Cursor Pełna STDIO .cursor/mcp.json
Windsurf Pełna STDIO .windsurf/mcp.json
Continue.dev Częściowa HTTP ~/.continue/config.json
Zed W trakcie STDIO Interfejs ustawień

Rozwiązanie awaryjne dla narzędzi bez MCP

W przypadku narzędzi nieobsługujących MCP retriever można opakować jako CLI:

# Search from command line
python retriever_cli.py search "query text" --limit 5

# Output formatted for copy-paste into any tool
python retriever_cli.py context "query text" --format markdown

CLI generuje ustrukturyzowany tekst, który można ręcznie wkleić do dowolnego narzędzia AI. Jest to mniej eleganckie rozwiązanie niż integracja MCP, ale działa uniwersalnie.


Buforowanie promptów z notatek strukturalnych

Ustrukturyzowane notatki w vault mogą służyć jako wielokrotnie wykorzystywane bloki kontekstu, zmniejszające zużycie tokenów w interakcjach z AI. Ta sekcja obejmuje projektowanie kluczy cache oraz zarządzanie budżetem tokenów.

Wzorzec

Zamiast wyszukiwać kontekst przy każdej interakcji, można wstępnie zbudować bloki kontekstu z dobrze ustrukturyzowanych notatek vault i je zbuforować:

# cache_keys.py
CONTEXT_BLOCKS = {
    "auth-patterns": {
        "vault_query": "authentication patterns implementation",
        "max_tokens": 1500,
        "ttl_hours": 24,  # Rebuild daily
    },
    "api-conventions": {
        "vault_query": "API design conventions REST patterns",
        "max_tokens": 1000,
        "ttl_hours": 168,  # Rebuild weekly
    },
    "project-architecture": {
        "vault_query": "current project architecture decisions",
        "max_tokens": 2000,
        "ttl_hours": 12,  # Rebuild twice daily
    },
}

Inwalidacja cache

Inwalidacja cache opiera się na dwóch sygnałach:

  1. Wygaśnięcie TTL. Każdy blok kontekstu ma czas życia (time-to-live). Po wygaśnięciu TTL blok jest odbudowywany poprzez ponowne zapytanie do vault.
  2. Wykrywanie zmian w vault. Gdy indekser wykryje zmiany w plikach, które przyczyniły się do zbuforowanego bloku kontekstu, blok jest natychmiast unieważniany.

Zarządzanie budżetem tokenów

Sesja rozpoczyna się z całkowitym budżetem kontekstu. Zbuforowane bloki zużywają część tego budżetu:

Total context budget:    8,000 tokens
├─ System prompt:        1,500 tokens
├─ Cached blocks:        3,000 tokens (pre-loaded)
├─ Dynamic search:       2,000 tokens (on-demand)
└─ Conversation:         1,500 tokens (remaining)

Zbuforowane bloki ładują się na początku sesji. Dynamiczne wyniki wyszukiwania wypełniają pozostały budżet na zasadzie per-zapytanie. To hybrydowe podejście zapewnia agentowi bazę często potrzebnego kontekstu, jednocześnie zachowując budżet na konkretne zapytania.

Porównanie zużycia tokenów

Bez buforowania: Każde istotne zapytanie wyzwala przeszukiwanie vault, zwracając 1500–2000 tokenów kontekstu. W ciągu 10 zapytań w sesji agent zużywa 15 000–20 000 tokenów kontekstu z vault.

Z buforowaniem: Trzy wstępnie zbudowane bloki kontekstu zużywają łącznie 4500 tokenów. Dodatkowe wyszukiwania dodają 1500–2000 tokenów na unikalne zapytanie. W ciągu 10 zapytań, z których 6 jest pokrytych przez zbuforowane bloki, agent zużywa 4500 + (4 × 1500) = 10 500 tokenów — mniej więcej połowę zużycia bez buforowania.


Hooki PostToolUse do kompresji kontekstu

Dane wyjściowe narzędzi mogą być rozbudowane: ślady stosu, listy plików, wyniki testów. Hook PostToolUse może skompresować te dane wyjściowe, zanim zajmą miejsce w oknie kontekstu.

Problem

Wywołanie narzędzia Bash uruchamiające testy może zwrócić:

PASSED tests/test_auth.py::test_login_success
PASSED tests/test_auth.py::test_login_failure
PASSED tests/test_auth.py::test_token_refresh
PASSED tests/test_auth.py::test_session_expiry
... (200 more lines)
FAILED tests/test_api.py::test_rate_limit_exceeded

Pełne dane wyjściowe to 5000 tokenów, ale istotna informacja mieści się w 2 liniach: 200 zaliczonych, 1 niezaliczony.

Implementacja hooka

#!/bin/bash
# ~/.claude/hooks/post-tool-use/compress-output.sh
# Compress verbose tool outputs to preserve context window

TOOL_NAME="$1"
OUTPUT="$2"
OUTPUT_LEN=${#OUTPUT}

# Only compress large outputs
if [ "$OUTPUT_LEN" -lt 2000 ]; then
    exit 0  # Pass through unchanged
fi

case "$TOOL_NAME" in
    Bash)
        # Compress test output
        if echo "$OUTPUT" | grep -q "PASSED\|FAILED"; then
            PASSED=$(echo "$OUTPUT" | grep -c "PASSED")
            FAILED=$(echo "$OUTPUT" | grep -c "FAILED")
            FAILURES=$(echo "$OUTPUT" | grep "FAILED")
            echo "Tests: $PASSED passed, $FAILED failed"
            if [ "$FAILED" -gt 0 ]; then
                echo "Failures:"
                echo "$FAILURES"
            fi
        fi
        ;;
esac

Zapobieganie rekurencyjnemu wyzwalaniu

Hook kompresji generujący dane wyjściowe może wyzwolić sam siebie, jeśli nie zostanie odpowiednio zabezpieczony:

# Guard against recursive invocation
if [ -n "$COMPRESS_HOOK_ACTIVE" ]; then
    exit 0
fi
export COMPRESS_HOOK_ACTIVE=1

Heurystyki kompresji

Typ danych wyjściowych Wykrywanie Strategia kompresji
Wyniki testów Słowa kluczowe PASSED / FAILED Zliczenie zaliczonych/niezaliczonych, wyświetlenie tylko niepowodzeń
Listy plików ls lub find w poleceniu Obcięcie do pierwszych 20 pozycji + liczba całkowita
Ślady stosu Słowo kluczowe Traceback Zachowanie pierwszej i ostatniej ramki + komunikat błędu
Status Git modified: / new file: Podsumowanie liczb według statusu
Dane wyjściowe kompilacji warning: / error: Usunięcie linii informacyjnych, zachowanie ostrzeżeń/błędów

Pipeline przyjmowania i triażu sygnałów

Warstwa przyjmowania określa, co trafia do vault. Bez kuracji vault gromadzi szum informacyjny. Ta sekcja opisuje pipeline oceniania, który kieruje sygnały do folderów domenowych.

Źródła

Sygnały pochodzą z wielu kanałów:

  • Kanały RSS: Blogi techniczne, biuletyny bezpieczeństwa, informacje o wydaniach
  • Zakładki: Zakładki przeglądarki zapisane za pomocą Obsidian Web Clipper lub bookmarkletu
  • Newslettery: Kluczowe fragmenty z newsletterów e-mailowych
  • Ręczne przechwytywanie: Notatki sporządzane podczas czytania, rozmów lub badań
  • Dane wyjściowe narzędzi: Istotne wyniki narzędzi AI przechwytywane za pomocą hooków

Wymiary oceniania

Każdy sygnał jest oceniany w czterech wymiarach (od 0,0 do 1,0 każdy):

Wymiar Pytanie Niska ocena (0,0-0,3) Wysoka ocena (0,7-1,0)
Trafność Czy to dotyczy moich aktywnych domen? Poboczne, poza zakresem Bezpośrednio związane z bieżącą pracą
Wykonalność Czy mogę wykorzystać tę informację? Czysta teoria, brak zastosowania Konkretna technika lub wzorzec do zastosowania
Głębokość Jak merytoryczna jest treść? Nagłówki, płytkie podsumowanie Szczegółowa analiza z przykładami
Autorytet Jak wiarygodne jest źródło? Anonimowy blog, niezweryfikowane Źródło pierwotne, recenzowane, uznany ekspert

Ocena złożona i kierowanie

composite = (relevance * 0.35) + (actionability * 0.25) +
            (depth * 0.25) + (authority * 0.15)
Zakres oceny Działanie
0,55+ Automatyczne kierowanie do folderu domenowego
0,40 - 0,55 Kolejka do ręcznego przeglądu
< 0,40 Odrzucenie (nie przechowywać)

Kierowanie domenowe

Sygnały z oceną powyżej 0,55 trafiają do jednego z 12 folderów domenowych na podstawie dopasowania słów kluczowych i klasyfikacji tematycznej:

05-signals/
├── ai-tooling/        # Claude, LLMs, AI development tools
├── security/          # Vulnerabilities, auth, cryptography
├── systems/           # Architecture, distributed systems
├── programming/       # Languages, patterns, algorithms
├── web/               # Frontend, backends, APIs
├── data/              # Databases, data engineering
├── devops/            # CI/CD, containers, infrastructure
├── design/            # UI/UX, product design
├── mobile/            # iOS, Android, cross-platform
├── career/            # Industry trends, hiring, growth
├── research/          # Academic papers, whitepapers
└── other/             # Signals that don't fit a domain

Statystyki produkcyjne

Na przestrzeni 14 miesięcy działania:

Metryka Wartość
Łączna liczba przetworzonych sygnałów 7 771
Automatycznie skierowane (>0,55) 4 832 (62%)
W kolejce do przeglądu (0,40-0,55) 1 543 (20%)
Odrzucone (<0,40) 1 396 (18%)
Aktywne foldery domenowe 12
Średnia liczba sygnałów dziennie ~18

Wzorce grafu wiedzy

Graf wiki-link w Obsidian koduje relacje między notatkami. Ta sekcja opisuje semantykę linków, przechodzenie grafu w celu rozszerzania kontekstu oraz antywzorce obniżające jakość grafu.

Semantyka backlinków

Każdy wiki-link tworzy krawędź skierowaną w grafie. Obsidian śledzi zarówno linki do przodu, jak i backlinki:

  • Link do przodu: Notatka A zawiera [[Notatka B]] → A linkuje do B
  • Backlink: Notatka B pokazuje, że Notatka A się do niej odwołuje

Graf koduje różne typy relacji w zależności od kontekstu:

Wzorzec linku Semantyka Przykład
Link w tekście „Jest powiązany z” „Zobacz [[OAuth Token Rotation]] po szczegóły”
Link w nagłówku „Ma podtemat” „## Powiązane\n- [[Token Rotation]]\n- [[Session Management]]”
Link jako tag „Jest skategoryzowany jako” „[[type/reference]]”
Link MOC „Jest częścią” Notatka Maps of Content zawierająca listę powiązanych notatek

Maps of Content (MOC)

MOC to notatki indeksujące, które organizują powiązane notatki w nawigacyjną strukturę:

---
title: "Authentication & Security MOC"
type: moc
domain: security
---

## Core Concepts
- [[OAuth 2.0 Overview]]
- [[JWT Token Anatomy]]
- [[Session Management Patterns]]

## Implementation Patterns
- [[OAuth Token Rotation]]
- [[Refresh Token Security]]
- [[PKCE Flow Implementation]]

## Failure Modes
- [[Token Expiry Handling]]
- [[Session Fixation Prevention]]
- [[CSRF Defense Strategies]]

MOC wspierają wyszukiwanie na dwa sposoby:

  1. Bezpośrednie dopasowanie. Zapytanie „przegląd uwierzytelniania” dopasowuje sam MOC, dostarczając agentowi wyselekcjonowaną listę powiązanych notatek.
  2. Rozszerzanie kontekstu. Po znalezieniu konkretnej notatki retriever może sprawdzić, czy notatka pojawia się w jakimkolwiek MOC, i uwzględnić strukturę MOC w wynikach, dając agentowi mapę szerszego tematu.

Przechodzenie grafu w celu rozszerzania kontekstu

Przyszłe rozszerzenie retrievera: po znalezieniu najlepszych wyników rozszerzenie kontekstu poprzez podążanie za linkami:

def expand_context(results, depth=1):
    """Follow wiki-links from top results to find related context."""
    expanded = set()
    for result in results:
        # Parse wiki-links from chunk text
        links = extract_wiki_links(result["chunk_text"])
        for link_target in links:
            # Resolve link to file path
            target_path = resolve_wiki_link(link_target)
            if target_path and target_path not in expanded:
                expanded.add(target_path)
                # Include target's most relevant chunk
                target_chunks = get_chunks_for_file(target_path)
                # ... rank and include best chunk
    return results + list(expanded_results)

Nie jest to zaimplementowane w obecnym retrieverze, ale stanowi naturalne rozszerzenie struktury grafu.

Antywzorce

Klastry sieroce. Grupy notatek, które linkują do siebie nawzajem, ale nie mają połączeń z resztą vault. Panel grafu w Obsidian uwidacznia je jako odłączone wyspy. Klastry sieroce wskazują na brakujące MOC lub brakujące linki międzydomenowe.

Rozrost tagów. Niespójne używanie tagów lub tworzenie zbyt wielu szczegółowych tagów. Vault z 500 unikalnymi tagami na 5 000 notatek ma średnio 1 notatkę na 10 tagów — tagi nie są przydatne do filtrowania. Należy skonsolidować do 20-50 tagów wysokiego poziomu odpowiadających folderom domenowym.

Notatki bogate w linki, ubogie w treść. Notatki składające się wyłącznie z wiki-linków bez tekstu opisowego. Takie notatki indeksują się słabo, ponieważ chunker nie ma tekstu do wygenerowania embeddingów. Należy dodać przynajmniej akapit kontekstu wyjaśniający, dlaczego powiązane notatki są ze sobą związane.

Dwukierunkowe linki do wszystkiego. Nie każde odniesienie wymaga wiki-linku. Wzmianka o „OAuth” mimochodem nie wymaga [[OAuth 2.0 Overview]]. Wiki-linki należy rezerwować dla intencjonalnych, nawigacyjnych relacji, w których kliknięcie linku dostarczyłoby przydatny kontekst.


Przepisy na przepływy pracy programisty

Praktyczne przepływy pracy łączące wyszukiwanie w vault z codziennymi zadaniami programistycznymi.

Poranne ładowanie kontekstu

Rozpocznij dzień od załadowania odpowiedniego kontekstu:

Search my vault for notes about [current project] updated in the last week

Retriever zwraca ostatnie notatki dotyczące aktywnego projektu, zapewniając szybkie przypomnienie, na czym skończyłeś pracę. Skuteczniejsze niż ponowne czytanie wczorajszych komunikatów commitów.

Przechwytywanie badań podczas kodowania

Podczas implementacji funkcji można utrwalać spostrzeżenia bez opuszczania edytora:

/capture "FastAPI dependency injection with async generators requires yield,
not return. The generator is the dependency lifecycle."
  --domain programming
  --tags fastapi,dependency-injection

Przechwycone spostrzeżenie jest natychmiast indeksowane i dostępne do przyszłego wyszukiwania. Na przestrzeni miesięcy te mikroprzechwycenia budują korpus wiedzy specyficznej dla implementacji.

Rozpoczynanie projektu

Przy rozpoczynaniu nowego projektu lub funkcji:

  1. Przeszukaj vault: „Co wiem o [technologia/wzorzec]?”
  2. Przejrzyj 5 najlepszych wyników pod kątem wcześniejszych decyzji i pułapek
  3. Sprawdź, czy istnieje MOC dla danej domeny; jeśli nie, utwórz go
  4. Wyszukaj tryby awaryjne: „problemy z [technologia]”

Debugowanie z wyszukiwaniem w vault

W przypadku napotkania błędu lub nieoczekiwanego zachowania:

Search my vault for [error message or symptom]

Wcześniejsze notatki z debugowania często zawierają przyczynę źródłową i rozwiązanie. Jest to szczególnie wartościowe przy powtarzających się problemach między projektami — vault pamięta to, co się zapomina.

Przygotowanie do przeglądu kodu

Przed przeglądem PR:

Search my vault for patterns and conventions about [module being changed]

Vault zwraca wcześniejsze decyzje, ograniczenia architektoniczne i standardy kodowania istotne dla przeglądanego kodu. Przegląd opiera się na wiedzy instytucjonalnej, a nie tylko na diffie.

Dostrajanie wydajności

Ta sekcja obejmuje strategie optymalizacji dla różnych rozmiarów vault i wzorców użytkowania.

Zarządzanie rozmiarem indeksu

Rozmiar vault Fragmenty Rozmiar DB Pełna reindeksacja Przyrostowa
500 notatek ~1 500 3 MB 15 sekund <1 sekunda
2 000 notatek ~6 000 12 MB 45 sekund 2 sekundy
5 000 notatek ~15 000 30 MB 2 minuty 4 sekundy
15 000 notatek ~50 000 83 MB 4 minuty <10 sekund
50 000 notatek ~150 000 250 MB 15 minut 30 sekund

Przy ponad 50 000 notatek warto rozważyć: - Zwiększenie batch_size z 64 do 128 w celu szybszego generowania embeddings - Użycie trybu WAL (domyślnie) do współbieżnego dostępu - Uruchamianie pełnej reindeksacji poza godzinami pracy

Optymalizacja zapytań

Tryb WAL. Tryb Write-Ahead Logging w SQLite umożliwia współbieżne odczyty podczas zapisu przez indekser:

db.execute("PRAGMA journal_mode=WAL")

Jest to kluczowe, gdy serwer MCP obsługuje zapytania, podczas gdy indekser wykonuje aktualizację przyrostową.

Pula połączeń. Serwer MCP powinien ponownie wykorzystywać połączenia z bazą danych zamiast otwierać nowe połączenie dla każdego zapytania. Pojedyncze długotrwałe połączenie w trybie WAL obsługuje współbieżne odczyty.

# MCP server initialization
db = sqlite3.connect(DB_PATH, check_same_thread=False)
db.execute("PRAGMA journal_mode=WAL")
db.execute("PRAGMA mmap_size=268435456")  # 256 MB mmap

Mapowanie pamięci (memory-mapped I/O). Pragma mmap_size instruuje SQLite, aby używał mapowanego wejścia/wyjścia dla pliku bazy danych. Dla bazy o rozmiarze 83 MB mapowanie całego pliku do pamięci eliminuje większość odczytów z dysku.

Optymalizacja FTS5. Po pełnej reindeksacji należy wykonać:

INSERT INTO chunks_fts(chunks_fts) VALUES('optimize');

To scala wewnętrzne segmenty b-tree w FTS5, zmniejszając opóźnienie zapytań przy kolejnych wyszukiwaniach.

Benchmarki skalowalności

Pomiary na Apple M3 Pro, 36 GB RAM, dysk NVMe SSD:

Operacja 500 notatek 5 tys. notatek 15 tys. notatek 50 tys. notatek
Zapytanie BM25 2ms 5ms 12ms 25ms
Zapytanie wektorowe 1ms 3ms 8ms 20ms
Fuzja RRF <1ms <1ms 3ms 5ms
Pełne wyszukiwanie 3ms 8ms 23ms 50ms

Wszystkie benchmarki obejmują dostęp do bazy danych, wykonanie zapytania i formatowanie wyników. Opóźnienie sieciowe komunikacji MCP STDIO dodaje 1–2 ms.


Rozwiązywanie problemów

Dryf indeksu

Objaw: Wyszukiwanie zwraca nieaktualne wyniki lub pomija niedawno dodane notatki.

Przyczyna: Indekser przyrostowy nie uruchomił się po dodaniu notatek lub znacznik czasu modyfikacji pliku (mtime) nie został zaktualizowany (np. plik zsynchronizowany z innego urządzenia z zachowanymi znacznikami czasu).

Rozwiązanie: Uruchomienie pełnej reindeksacji: python index_vault.py --full

Zmiana modelu embeddings

Objaw: Po zmianie modelu embeddings wyszukiwanie wektorowe zwraca bezsensowne wyniki.

Przyczyna: Stare wektory (z poprzedniego modelu) są porównywane z nowymi wektorami zapytań. Wymiary lub semantyka przestrzeni wektorowej są niekompatybilne.

Rozwiązanie: Indekser powinien wykryć niezgodność hasza modelu i automatycznie uruchomić pełną reindeksację. Jeśli tak się nie stanie, należy ręcznie wyczyścić bazę danych i wykonać reindeksację:

rm vectors.db
python index_vault.py --full

Konserwacja FTS5

Objaw: Zapytania FTS5 zwracają nieprawidłowe lub niekompletne wyniki po wielu aktualizacjach przyrostowych.

Przyczyna: Wewnętrzne segmenty FTS5 mogą ulec fragmentacji po wielu małych aktualizacjach.

Rozwiązanie: Przebudowa i optymalizacja:

INSERT INTO chunks_fts(chunks_fts) VALUES('rebuild');
INSERT INTO chunks_fts(chunks_fts) VALUES('optimize');

Limit czasu MCP

Objaw: Narzędzie AI zgłasza, że serwer MCP przekroczył limit czasu.

Przyczyna: Pierwsze zapytanie uruchamia ładowanie modelu (leniwa inicjalizacja), co zajmuje 2–5 sekund. Domyślny limit czasu MCP w narzędziu AI może być krótszy.

Rozwiązanie: Wstępne załadowanie modelu przy uruchomieniu serwera:

# In MCP server initialization
retriever = HybridRetriever(db_path, vault_path)
retriever.search("warmup", limit=1)  # Trigger model load

Blokady plików SQLite

Objaw: Błędy SQLITE_BUSY lub SQLITE_LOCKED.

Przyczyna: Wiele procesów jednocześnie zapisuje do bazy danych. Tryb WAL umożliwia współbieżne odczyty, ale dopuszcza tylko jednego zapisującego.

Rozwiązanie: Należy upewnić się, że tylko jeden proces (indekser) zapisuje do bazy danych. Serwer MCP i hooki powinny jedynie odczytywać. Jeśli potrzebne są współbieżne zapisy, należy użyć trybu WAL i ustawić limit oczekiwania:

db.execute("PRAGMA busy_timeout=5000")  # Wait up to 5 seconds

sqlite-vec nie ładuje się

Objaw: Wyszukiwanie wektorowe jest wyłączone; retriever działa wyłącznie w trybie BM25.

Przyczyna: Rozszerzenie sqlite-vec nie jest zainstalowane, nie zostało znalezione w ścieżce bibliotek lub jest niekompatybilne z wersją SQLite.

Rozwiązanie:

# Install via pip
pip install sqlite-vec

# Or compile from source
git clone https://github.com/asg017/sqlite-vec
cd sqlite-vec && make

Weryfikacja, czy rozszerzenie się ładuje:

import sqlite3
db = sqlite3.connect(":memory:")
db.enable_load_extension(True)
db.load_extension("vec0")
print("sqlite-vec loaded successfully")

Problemy z pamięcią przy dużych vault

Objaw: Błędy braku pamięci podczas pełnej reindeksacji dużego vault (ponad 50 000 notatek).

Przyczyna: Rozmiar partii embeddings jest zbyt duży lub cała zawartość plików jest ładowana do pamięci jednocześnie.

Rozwiązanie: Zmniejszenie rozmiaru partii i przetwarzanie plików przyrostowo:

BATCH_SIZE = 32  # Reduce from 64

Należy również upewnić się, że indekser przetwarza pliki pojedynczo (odczytując, dzieląc na fragmenty i generując embeddings dla każdego pliku przed przejściem do następnego) zamiast ładować wszystkie pliki do pamięci.


Przewodnik po migracji

Z Apple Notes

  1. Eksport Apple Notes za pomocą opcji „Eksportuj wszystko” (macOS) lub przy użyciu narzędzia migracyjnego, takiego jak apple-notes-liberator
  2. Konwersja eksportów HTML do markdown za pomocą markdownify lub pandoc
  3. Przeniesienie skonwertowanych plików do folderu 00-inbox/ w vault
  4. Przegląd i dodanie frontmatter do każdej notatki
  5. Przeniesienie notatek do odpowiednich folderów domenowych

Z Notion

  1. Eksport z Notion: Ustawienia → Eksport → Markdown & CSV
  2. Rozpakowanie eksportu do folderu 00-inbox/ w vault
  3. Naprawienie artefaktów markdown specyficznych dla Notion:
  4. Notion używa - [ ] dla list kontrolnych — to standardowy markdown
  5. Notion zawiera tabele właściwości jako HTML — należy je przekonwertować na YAML frontmatter
  6. Notion osadza obrazy jako ścieżki względne — należy skopiować obrazy do folderu załączników
  7. Dodanie standardowego frontmatter (type, domain, tags)
  8. Zastąpienie linków do stron Notion wiki-linkami Obsidian

Z Google Docs

  1. Użycie Google Takeout do eksportu wszystkich dokumentów
  2. Konwersja plików .docx do markdown: pandoc -f docx -t markdown input.docx -o output.md
  3. Konwersja zbiorcza: for f in *.docx; do pandoc -f docx -t markdown "$f" -o "${f%.docx}.md"; done
  4. Przeniesienie do vault, dodanie frontmatter, organizacja w foldery

Z czystego markdown (bez Obsidian)

Jeśli istnieje już katalog z plikami markdown:

  1. Otwarcie katalogu jako vault Obsidian (Obsidian → Open Vault → Open folder)
  2. Dodanie .obsidian/ do .gitignore, jeśli katalog jest wersjonowany
  3. Utworzenie szablonów frontmatter i zastosowanie ich do istniejących plików
  4. Rozpoczęcie łączenia notatek za pomocą [[wiki-links]] podczas czytania i organizowania
  5. Natychmiastowe uruchomienie indeksera — system wyszukiwania działa od pierwszego dnia

Z innego systemu wyszukiwania

W przypadku migracji z innego systemu embeddings/wyszukiwania:

  1. Nie należy próbować migrować wektorów. Różne modele tworzą niekompatybilne przestrzenie wektorowe. Należy uruchomić pełną reindeksację z nowym modelem.
  2. Migracja treści, nie indeksu. Pliki vault są źródłem prawdy. Indeks jest artefaktem pochodnym.
  3. Weryfikacja po migracji. Należy uruchomić 10–20 zapytań, na które znana jest odpowiedź, i sprawdzić, czy wyniki odpowiadają oczekiwaniom.

Dziennik zmian

Data Zmiana
2026-03-01 Pierwsze wydanie

Przypisy


  1. Cormack, G.V., Clarke, C.L.A., and Buettcher, S. Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods. SIGIR, 2009. Wprowadza RRF z k=60 jako bezparametrową metodę łączenia rankingowanych list. 

  2. OpenAI Embeddings Pricing. text-embedding-3-small: 0,02 USD za milion tokenów. Szacowany koszt pełnej reindeksacji skarbca: ~0,30 USD. 

  3. van Dongen, T. et al. Model2Vec: Turn any Sentence Transformer into a Small Fast Model. arXiv, 2025. Opisuje podejście destylacji tworzące statyczne embeddingi z modeli sentence transformer. 

  4. MTEB: Massive Text Embedding Benchmark. potion-base-8M uzyskuje średnio 50,03 wobec 56,09 dla all-MiniLM-L6-v2 (89% zachowanej jakości). 

  5. SQLite FTS5 Extension. FTS5 zapewnia wyszukiwanie pełnotekstowe z rankingiem BM25 i konfigurowalnymi wagami kolumn. 

  6. sqlite-vec: A vector search SQLite extension. Udostępnia tabele wirtualne vec0 do wyszukiwania wektorowego KNN w ramach SQLite. 

  7. Robertson, S. and Zaragoza, H. The Probabilistic Relevance Framework: BM25 and Beyond. Foundations and Trends in Information Retrieval, 2009. 

  8. Karpukhin, V. et al. Dense Passage Retrieval for Open-Domain Question Answering. EMNLP, 2020. Gęste reprezentacje przewyższają BM25 o 9–19% w zadaniach otwartego wyszukiwania odpowiedzi. 

  9. Reimers, N. and Gurevych, I. Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks. EMNLP, 2019. Fundamentalna praca dotycząca gęstego podobieństwa semantycznego. 

  10. Luan, Y. et al. Sparse, Dense, and Attentional Representations for Text Retrieval. TACL, 2021. Wyszukiwanie hybrydowe konsekwentnie przewyższa podejścia jednomodalne na MS MARCO. 

  11. SQLite Write-Ahead Logging. Tryb WAL umożliwiający współbieżne odczyty z jednym procesem zapisu. 

  12. Gao, Y. et al. Retrieval-Augmented Generation for Large Language Models: A Survey. arXiv, 2024. Przegląd architektur RAG i strategii podziału na fragmenty. 

  13. Thakur, N. et al. BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models. NeurIPS, 2021. 

  14. Model2Vec: Distill a Small Fast Model from any Sentence Transformer. Minish Lab, 2024. 

  15. Obsidian Documentation. Oficjalna dokumentacja Obsidian. 

  16. Model Context Protocol Specification. Standard MCP do łączenia narzędzi AI ze źródłami danych. 

  17. Dane produkcyjne autora. 16 894 plików, 49 746 fragmentów, baza danych SQLite o rozmiarze 83,56 MB, 7 771 sygnałów przetworzonych w ciągu 14 miesięcy. Opóźnienie zapytań mierzone za pomocą time.perf_counter()

VAULT obsidian.md INDEXED