From 6a293e842e8ff7f557c624c7531f85ad2e96e6a5 Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 9 Sep 2025 11:56:26 +0200 Subject: [PATCH] docs/chunking_strategy.md aktualisiert --- docs/chunking_strategy.md | 257 ++++++++------------------------------ 1 file changed, 51 insertions(+), 206 deletions(-) diff --git a/docs/chunking_strategy.md b/docs/chunking_strategy.md index d9e2f40..543f92e 100644 --- a/docs/chunking_strategy.md +++ b/docs/chunking_strategy.md @@ -1,214 +1,59 @@ -# WP-02 Chunking & Embedding-Strategie -**Projekt:** mindnet Wissensnetzwerk -**Bezug:** `knowledge_design.md` (IDs, Dateinamen, Tags, Edge-Typen, Abschnitts-Konventionen) +# Chunking-Strategie für mindnet ---- +## Ziel +Die Chunking-Strategie sorgt dafür, dass Inhalte aus Markdown-Dateien verlustfrei, sinnvoll segmentiert und mit stabilen IDs in Qdrant abgelegt werden. Jeder Chunk soll semantisch sinnvoll abgegrenzt sein und eine optimale Balance aus Kontexttiefe und Abrufbarkeit für LLM-Abfragen bieten. -## 1) Ziel & Rahmen -Entwickle eine robuste, parser-freundliche **Chunking-Strategie** für Markdown-Notizen, die: -- die **Frontmatter** (YAML) als Meta nutzt, aber nicht (standardmäßig) embedden lässt, -- den **Body** in semantisch sinnvolle Chunks zerlegt (Überschriften/Absätze/Listen), -- **Edges** zwischen Chunks und Notizen/Links erzeugt (u. a. `belongs_to`, `references`, `backlink`). +## Grundprinzipien +- **Chunkgrößen**: Zielbereich 250–400 Tokens; Overlap 40–60 Tokens. +- **Semantische Schnitte**: Chunks enden an Absätzen, Überschriften, Listenpunkten, Codeblöcken oder Zitaten. +- **Frontmatter**: YAML-Frontmatter wird **nicht** eingebettet, sondern vollständig im Note-Payload gespeichert. +- **Verlustfreiheit**: Originaltext bleibt durch `note.payload.fulltext` erhalten; Chunks enthalten zusätzlich `chunk.payload.text`. ---- +## Typ-Spezifische Regeln +- **Notes (Konzepttexte, Pläne, Tagebücher)**: Chunk an Überschriften + Absätzen. +- **Checklisten / Listen**: jeder Abschnitt wird zu eigenem Chunk. +- **Codeblöcke**: immer geschlossen innerhalb eines Chunks. +- **Zitate**: als eigener Chunk, wenn mehr als 2 Zeilen. -## 2) Annahmen aus `knowledge_design.md` -- **Stabile IDs** & Dateinamen-Schema (z. B. `YYYYMMDD-HHMM-type-slug` bzw. `type-slug`). -- **Pflichtfelder** in YAML: `title`, `id`, `type`, `status`, `created`, `tags` (empfohlene Felder ggf. mehr). -- **Abschnitts-Konventionen** je `type` (z. B. `## Zusammenfassung`, `## Kontext` …) → ideale **Chunk-Grenzen**. -- **Edge-Typen**: `belongs_to`, `references`, `backlink`, `depends_on`, `assigned_to`, `discussed_in`, `authored_by`, `related`. Für Chunking primär `belongs_to` & `references` (+ optionale `prev`/`next`). +## Overlaps +- Overlap von 40–60 Tokens verhindert semantische Brüche zwischen Chunks. +- Overlap gilt **nur für den Body**, nicht für die YAML-Frontmatter. ---- +## Edge-Modell (Graph) +- **Chunk → Note**: `belongs_to(note_id)` (immer; Owner-Note im Payload) +- **Chunk → Chunk**: `prev`, `next` (symmetrisch, für lineare Navigation) +- **Chunk → Note**: `references(target_id)` aus `[[Wikilinks]]` oder Links im Text +- **Note → Note**: `backlink(target_id)` automatisch generiert, dedupliziert +- **Optional**: `references:note` (nur wenn per Flag/ENV aktiviert, Default = aus) +- **Unresolved**: Kanten mit `status=unresolved`, wenn Ziel noch nicht existiert -## 3) Chunking-Prinzipien -1. **Frontmatter extrahieren** → als strukturiertes `metadata` (nicht in `text` des Chunks). -2. **Markdown AST** (oder strukturierter Parser) nutzen: - - Primäre Grenzen: **Überschriftenebene H2/H3** (H1 = Dokumenttitel). - - Sekundär: **Absätze**, **nummerierte/unnummerierte Listen** als atomare Einheiten, **Codeblöcke** als zusammenhängender Block (nicht splitten). -3. **Längensteuerung**: Semantik vor harter Länge. Wenn Abschnitt > Zielgröße, **weich segmentieren** entlang von Absätzen/Listenpunkten. -4. **Referenz-Extraktion**: `[[Wikilinks]]` + Markdown-Links → `references`. Backlinks werden downstream generiert. -5. **Kontext-Overlap**: Chunks erhalten **Überlappung** über Satz-/Absatzgrenzen, um Kohärenz beim Retrieval zu erhöhen. +## Payload-Erweiterungen +- Jeder Chunk speichert: + - `text` (Originalchunk) + - `note_id` (Owner) + - Metadaten (`chunk_index`, `section_title`, `neighbors.prev/next`) +- Jede Note speichert: + - `fulltext` (verlustfreier Body) + - `path` (relativ zum Vault) + - `references` (Liste von Slugs, als Fallback für Kantenbildung) ---- +## Performance +- Qdrant-Payload-Indizes für `note_id`, `kind`, `scope`, `source_id`, `target_id`. +- Materialisierte Nachbarschaften optional in `note.payload.neighbors` für direkte 1-Hop-Abfragen. -## 4) Optimale Chunkgrößen & Overlaps -> Ziel: gutes Recall@k, wenig Fragmentierung, stabile Antwortqualität. - -| Notiz-Typ (`type`) | Inhalt | Zielgröße (Tokens) | Max. Größe (Tokens) | Overlap (Tokens) | Hinweise | -|---|---|---:|---:|---:|---| -| `thought` | kurze Thesen/Ideen | 150–250 | 300 | 30–40 | meist 1–2 Chunks | -| `experience` | Kontext/Beobachtung/Interpretation | 250–350 | 450 | 40–60 | Trenne entlang der Template-Sektionen | -| `journal` | Tagebuch, episodisch | 200–300 | 400 | 30–50 | Tagesabschnitte bündeln | -| `task` | knapp, Checklisten | 120–200 | 250 | 20–30 | Listen nicht splitten | -| `project` | Scope/WPs/Risiken/Status | 300–450 | 600 | 50–70 | Pro Hauptsektion ein Chunk | -| `concept` | definierte Begriffe, Erklärungen | 250–400 | 550 | 40–60 | Definition separat halten | -| `source` | Metadaten + Auszüge/Notizen | 200–350 | 500 | 30–50 | Zitate als eigene Chunks (Urheberrecht) | - -## 4.1 Overlap-Regeln -- Overlap **nie mitten im Satz** beenden → mindestens **ein kompletter Satz** wandert in den nächsten Chunk. -- An **Absatzgrenzen** ausrichten, wenn möglich. -- Bei **Listen** ggf. den vorherigen Punkt als Overlap mitführen (falls < 50 Tokens). -- Codeblöcke und Tabellen **nicht splitten**; wenn sie größer als `Max-Tokens` sind, als **eigener Chunk** belassen (Ausnahmefall zulassen). -**Daumenregel (zeichenzentriert):** ~4 Zeichen ≈ 1 Token → 800–1600 Zeichen Ziel; Overlap ~120–240 Zeichen (≈ 30–60 Tokens). -**Mindestanforderung:** Keine Trennung innerhalb von Sätzen. Sicherstellung, dass immer an Satzenden gechunct wird -**Default für mindnet:** **~300 Tokens** + **~50 Tokens Overlap**; per `type` feinjustieren (siehe Tabelle). - ---- - -## 5) Semantische Regeln je Strukturelement -- **Überschriften (H2/H3)**: new chunk start; speichere `section_path` (z. B. `/Kontext/Beobachtung`). -- **Absätze**: Primäre Untereinheiten; wenn Zusammenfassung + Beispiel direkt folgen, zusammen lassen. -- **Listen**: Jeder **Listeneintrag** bleibt zusammen mit seinem einleitenden Satz (falls vorhanden). Große Listen → in **logische Blöcke** (5–8 Items) splitten. -- **Codeblöcke**: Nicht splitten; ggf. separater Chunk, **aber** im Retrieval **downweighten** (optional). -- **Zitate/Blockquotes**: zusammenhalten; Quelle im `references_meta` anreichern (falls Link vorhanden). -- **Tabellen**: als ein Chunk; zusätzlich **Plain-Text-Extrakt** (Header + 1–2 Zeilen) in `summary` für Retriever, falls Modelle Tabellen schlechter verarbeiten. - ---- - -## 6) Normalisierung & Preprocessing -- **Nicht embedden:** YAML-Frontmatter (aber spezifische Felder als `metadata.*` mitschreiben: `type`, `tags`, `area`, `project`, `priority`, `people`, `aliases`). -- **Bereinigen:** führende/trailing Spaces, Mehrfach-Leerzeilen → 1; Unicode-NFKC; Zeilenenden normalisieren. -- **Bewahren:** Markdown-Semantik (Überschriften-Hashes, Listenpräfixe, Codefences) **im Text beibehalten**, damit RAG-Antworten sauber zitieren können. -- **Sprachhinweis:** `lang` heuristisch bestimmen (DE/EN/…); in `metadata.lang` ablegen → später für sprachspezifische Embeddings nützlich. -- **Stop-Abschnitte:** „Mögliche Verbindungen“ am Ende **separat** chunken (nur Links), um Link-Graph sauber zu extrahieren. - ---- - -## 7) Edge-Modell (Graph) -- **Chunk → Note:** `belongs_to(note_id)` (obligatorisch). -- **Chunk → Chunk (gleiche Note):** `prev`, `next` (lineare Leserichtung). -- **Note/Chunk → Note:** `references(target_id)` aus `[[Wikilinks]]` & Markdown-Links; optional `related` (schwach). -- **Backlink:** systemisch generiert: inverse Kante `backlink`. -- **Sektionen:** `has_section(note_id, section_path)` (optional, für Navigations-UI). -- **Task-Spezialfälle:** `depends_on`, `assigned_to(person-id)` bleiben **Note-Level** (nicht Chunk-Level). - ---- - -## 8) Datenmodell (Qdrant-tauglich) - -**Point (Chunk)** - - { - "id": "20250902-1830-thought-yaml-recall#c02", - "vector": [/* embedding */], - "payload": { - "note_id": "20250902-1830-thought-yaml-recall", - "note_title": "Gedanke: Einheitliche YAML-Standards steigern Recall", - "path": "10_thoughts/20250902-1830-thought-yaml-recall.md", - "type": "thought", - "area": "mindnet", - "project": "project-mindnet", - "tags": ["area/mindnet","type/thought","topic/yaml"], - "chunk_index": 2, - "char_start": 1240, - "char_end": 2012, - "token_count": 305, - "section_title": "Begründung / Details", - "section_path": "/Begründung / Details", - "lang": "de", - "wikilinks": ["concept-vektorsuche-qdrant","source-obsidian-properties"], - "external_links": [], - "references": [ - {"target_id": "concept-vektorsuche-qdrant", "kind": "wikilink"} - ], - "neighbors": {"prev": "#c01", "next": "#c03"}, - "created_at": "2025-09-02T18:35:00+02:00" - } - } - -**Collection-Einstellungen (Empfehlung)** -- `distance`: `Cosine` (gemischte Textlängen) -- `vectors`: 384–1024 Dim (abh. Embedding-Modell) -- `hnsw_ef_construct`: 128–256; `m`: 16–32 (Workload-abhängig) - ---- - -## 9) Algorithmus (Python-ready Outline) - - def chunk_markdown_note(md_text: str, file_path: str) -> list[Chunk]: - fm, body = split_frontmatter(md_text) # YAML -> dict - meta = normalize_frontmatter(fm) # enforce schema defaults - ast = parse_markdown_to_ast(body) # any mdast-compatible lib - - sections = split_by_headings(ast, levels=(2,3)) # H2/H3 primary - raw_chunks = [] - for sec in sections: - blocks = group_blocks(sec, keep_code=True, keep_lists=True) - for group in soft_wrap(blocks, target_tokens=target_size(meta["type"]), - max_tokens=max_size(meta["type"])): - raw_chunks.append(render_markdown(group)) - - chunks = apply_overlap(raw_chunks, overlap_tokens=overlap_size(meta["type"])) - chunks = trim_whitespace(chunks) - - # link extraction - for i, ch in enumerate(chunks): - ch.payload.update({ - "wikilinks": extract_wikilinks(ch.text), - "external_links": extract_md_links(ch.text), - "references": [{"target_id": t, "kind": "wikilink"} for t in extract_wikilinks(ch.text)], - "neighbors": {"prev": f"#c{i-1}" if i>0 else None, - "next": f"#c{i+1}" if i