docs/mindnet_technical_architecture.md aktualisiert
All checks were successful
Deploy mindnet to llm-node / deploy (push) Successful in 3s
All checks were successful
Deploy mindnet to llm-node / deploy (push) Successful in 3s
This commit is contained in:
parent
c56a31c6bc
commit
e202a8d88b
|
|
@ -1,699 +1,215 @@
|
|||
# Mindnet – Technische Architektur (V2, Stand: 2025-11-xx)
|
||||
# Mindnet v2.2 – Technische Architektur
|
||||
**Datei:** `docs/mindnet_technical_architecture_v2.2.md`
|
||||
**Stand:** 2025-12-07
|
||||
**Status:** **FINAL** (Integrierter Stand WP01–WP04a)
|
||||
**Quellen:** `mindnet_technical_architecture.md`, `chunking_strategy.md`, `Handbuch.md`, `wp04_retriever_scoring.md`, `Überarbeitungshinweise_WP03.md`, `Überarbeitungshinweise_WP04.md`.
|
||||
|
||||
> **Ziel dieses Dokuments:**
|
||||
> Vollständige, konsolidierte Beschreibung der aktuellen technischen Architektur von **Mindnet V2** – insbesondere der Verarbeitung von Markdown-Notizen, des Chunkings, der Kantenableitung (Edges) inkl. `rule_id` / `confidence` und der Speicherung in Qdrant.
|
||||
> Das Dokument bildet den **aktuellen Implementierungsstand** ab, nicht mehr gültige Annahmen sind bereinigt, neue Konzepte (Typ-Defaults, Inline-Relationen, Edges-Schema) sind integriert.
|
||||
> **Ziel dieses Dokuments:**
|
||||
> Vollständige, konsolidierte Beschreibung der aktuellen technischen Architektur von **Mindnet v2.2**. Es definiert die Datenstrukturen in Qdrant, die Verarbeitungspipelines (Importer, Chunker, Edges) und die Retrieval-Logik. Es bildet den **aktuellen Implementierungsstand** ab.
|
||||
|
||||
---
|
||||
|
||||
## 1. Systemüberblick
|
||||
|
||||
### 1.1 Zielbild
|
||||
|
||||
### 1.1 Architektur-Zielbild
|
||||
Mindnet ist ein **persönliches Wissensnetz**. Technisch bedeutet das:
|
||||
1. **Source:** Markdown-Notizen in einem Vault (Obsidian-kompatibel).
|
||||
2. **Pipeline:** Ein Python-Importer transformiert diese in **Notes**, **Chunks** und **Edges**.
|
||||
3. **Storage:** Speicherung in **Qdrant** (Vektor-Datenbank) in drei getrennten Collections.
|
||||
4. **Service:** Eine FastAPI-Anwendung stellt Endpunkte für **Semantische** und **Hybride Suche** bereit.
|
||||
|
||||
- Markdown-Notizen in einem Vault (Obsidian-kompatibel) werden
|
||||
- geparst (Frontmatter + Body),
|
||||
- in **Notes**, **Chunks** und **Edges** überführt,
|
||||
- in **Qdrant** als drei Collections abgelegt:
|
||||
- `<prefix>_notes`
|
||||
- `<prefix>_chunks`
|
||||
- `<prefix>_edges`.
|
||||
Das System arbeitet **deterministisch** (stabile IDs) und ist **konfigurationsgetrieben** (`types.yaml`, `retriever.yaml`).
|
||||
|
||||
- Das System ist so konzipiert, dass es:
|
||||
- **deterministisch** arbeitet (IDs, Chunks, Kanten),
|
||||
- über `types.yaml` **konfigurierbar** ist,
|
||||
- später von einem LLM-basieren Agenten als **Graph-Retriever** genutzt werden kann.
|
||||
### 1.2 Verzeichnisstruktur & Komponenten
|
||||
Die Codebasis ist modular aufgebaut.
|
||||
|
||||
/mindnet/
|
||||
├── app/
|
||||
│ ├── main.py # FastAPI Einstiegspunkt
|
||||
│ ├── core/
|
||||
│ │ ├── qdrant.py # Client-Factory & Connection
|
||||
│ │ ├── qdrant_points.py # Low-Level Point Operations (Upsert/Delete)
|
||||
│ │ ├── note_payload.py # Bau der Note-Objekte
|
||||
│ │ ├── chunk_payload.py # Bau der Chunk-Objekte
|
||||
│ │ ├── chunker.py # Text-Zerlegung (Profiling)
|
||||
│ │ ├── edges.py # Edge-Datenstrukturen
|
||||
│ │ ├── derive_edges.py # Logik der Kantenableitung (WP03)
|
||||
│ │ └── retriever.py # Scoring & Graph-Expansion (WP04)
|
||||
│ ├── models/ # Pydantic DTOs (QueryRequest, etc.)
|
||||
│ └── routers/ # API Routen
|
||||
├── config/
|
||||
│ ├── types.yaml # Typ-Definitionen (Import-Zeit)
|
||||
│ └── retriever.yaml # Scoring-Gewichte (Laufzeit)
|
||||
├── scripts/
|
||||
│ ├── import_markdown.py # Haupt-Importer CLI
|
||||
│ ├── payload_dryrun.py # Diagnose: JSON-Generierung ohne DB
|
||||
│ └── edges_full_check.py # Diagnose: Graph-Integrität
|
||||
└── tests/ # Pytest Suite
|
||||
|
||||
---
|
||||
|
||||
### 1.2 Verzeichnisstruktur (Server)
|
||||
## 2. Datenmodell & Collections (Qdrant)
|
||||
|
||||
Das Datenmodell verteilt sich auf drei Collections, definiert durch `COLLECTION_PREFIX` (Standard: `mindnet`).
|
||||
|
||||
### 2.1 Notes Collection (`<prefix>_notes`)
|
||||
Repräsentiert die Metadaten einer Datei.
|
||||
* **Zweck:** Filterung, Metadaten-Haltung, Vererbung von Eigenschaften an Chunks.
|
||||
* **Schema (Payload):**
|
||||
|
||||
| Feld | Datentyp | Beschreibung | Herkunft |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `note_id` | Keyword | Stabile ID (UUIDv5 oder Slug). | Frontmatter |
|
||||
| `title` | Text | Titel der Notiz. | Frontmatter |
|
||||
| `type` | Keyword | Logischer Typ (z.B. `concept`). | `types.yaml` Resolver |
|
||||
| `retriever_weight` | Float | Wichtigkeit im Retrieval. | `types.yaml` |
|
||||
| `chunk_profile` | Keyword | Genutztes Chunking-Profil. | `types.yaml` |
|
||||
| `edge_defaults` | List[Str] | Aktive Default-Relationen. | `types.yaml` |
|
||||
| `tags` | List[Kw] | Tags zur Filterung. | Frontmatter |
|
||||
| `updated` | Integer | Zeitstempel (z.B. YYYYMMDD...). | File Stats |
|
||||
| `fulltext` | Text | Gesamter Inhalt (für Export/Rekonstruktion). | Body |
|
||||
|
||||
|
||||
|
||||
### 2.2 Chunks Collection (`<prefix>_chunks`)
|
||||
Die atomaren Sucheinheiten.
|
||||
* **Zweck:** Vektorsuche (Embeddings), Granulares Ergebnis.
|
||||
* **Schema (Payload):**
|
||||
|
||||
| Feld | Datentyp | Beschreibung |
|
||||
| :--- | :--- | :--- |
|
||||
| `chunk_id` | Keyword | Deterministisch: `{note_id}#c{index:02d}`. |
|
||||
| `note_id` | Keyword | Referenz zur Note. |
|
||||
| `text` | Text | **Reiner Inhalt** (ohne Overlap). Anzeige-Text. |
|
||||
| `window` | Text | **Kontext-Fenster** (mit Overlap). Embedding-Basis. |
|
||||
| `ord` | Integer | Sortierreihenfolge (1..N). |
|
||||
| `retriever_weight` | Float | Vererbt von Note. |
|
||||
| `chunk_profile` | Keyword | Vererbt von Note. |
|
||||
| `neighbors_prev` | List[Str] | ID des Vorgänger-Chunks. |
|
||||
| `neighbors_next` | List[Str] | ID des Nachfolger-Chunks. |
|
||||
|
||||
|
||||
|
||||
### 2.3 Edges Collection (`<prefix>_edges`)
|
||||
Gerichtete Kanten. Massiv erweitert in WP03 für Provenienz-Tracking.
|
||||
* **Zweck:** Graph-Traversal, Kontext-Anreicherung.
|
||||
* **Schema (Payload):**
|
||||
|
||||
| Feld | Datentyp | Beschreibung | Wertebereich (Bsp.) |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `edge_id` | Keyword | Hash aus (src, dst, kind). | |
|
||||
| `source_id` | Keyword | ID des Start-Chunks. | |
|
||||
| `target_id` | Keyword | ID des Ziel-Chunks (oder Note-Titel). | |
|
||||
| `note_id` | Keyword | Note, die diese Kante definiert. | |
|
||||
| `kind` | Keyword | Art der Beziehung. | `references`, `depends_on`, `next` |
|
||||
| `scope` | Keyword | Geltungsbereich. | Immer `"chunk"` (v2.2). |
|
||||
| `rule_id` | Keyword | Herkunftsregel. | `explicit:wikilink`, `inline:rel` |
|
||||
| `confidence` | Float | Vertrauenswürdigkeit (0.0-1.0). | 1.0, 0.95, 0.7 |
|
||||
|
||||
/home/llmadmin/mindnet/:
|
||||
|
||||
- **/app**
|
||||
- `main.py` – FastAPI-App / Einstieg
|
||||
- **/app/core**
|
||||
- `qdrant.py` – QdrantConfig, Client-Factory, Collection-Management
|
||||
- `qdrant_points.py` – Upserts / Deletes / Queries auf Punktebene
|
||||
- `note_payload.py` – Bau von Note-Payloads
|
||||
- `chunk_payload.py` – Bau von Chunk-Payloads
|
||||
- `chunker.py` – Chunking-Logik
|
||||
- `edges.py` – Datenstruktur & Hilfsfunktionen für Edges
|
||||
- `derive_edges.py` – Ableitung von Edges aus Text und Typen
|
||||
- **/app/models**
|
||||
- `dto.py` – DTOs für API / Services
|
||||
- **/app/routers**
|
||||
- `qdrant_router.py` – API-Layer Richtung Qdrant
|
||||
- **/app/services**
|
||||
- `llm_ollama.py` – Anbindung an Ollama-LLM
|
||||
- `embeddings_client.py` – Embedding-Service (z. B. Ollama / lokaler Dienst)
|
||||
- **/config**
|
||||
- `types.yaml` – Typdefinitionen inkl. Chunk-Profilen und Edge-Defaults
|
||||
- **/scripts**
|
||||
- `import_markdown.py` – Hauptimporter für Vault
|
||||
- `reset_qdrant.py` – Collections löschen/neu anlegen (inkl. Indizes)
|
||||
- `setup_mindnet_collections.py` – Collection-Setup (historisch / Hilfsskript)
|
||||
- diverse Diagnose- und Testskripte, z. B.
|
||||
- `payload_dryrun.py`
|
||||
- `edges_full_check.py`
|
||||
- **/tests**
|
||||
- `test_edges_smoke.py` – einfache Edge-Smoke-Tests
|
||||
- `test_edges_all.py` – umfassendere Checks (Counts, Plausibilität)
|
||||
- `ensure_indexes_and_show.py`, `assert_payload_schema.py` – Schema/Index-Checks
|
||||
- **/vault**
|
||||
- produktives Vault (Obsidian)
|
||||
- **/mindnet_v2_test_vault**
|
||||
- Test-Vault für V2-Funktionen (Chunking, Edges, Typ-Defaults)
|
||||
|
||||
---
|
||||
|
||||
## 2. Datenmodell & Collections
|
||||
## 3. Konfiguration
|
||||
|
||||
### 2.1 Note, Chunk, Edge – Begriffe
|
||||
|
||||
- **Note**
|
||||
Entspricht einer Markdown-Datei mit YAML-Frontmatter (`id`, `title`, `type`, …).
|
||||
In Qdrant als ein Punkt in `<prefix>_notes`.
|
||||
|
||||
- **Chunk**
|
||||
Ein Abschnitt einer Note (z. B. Absatz, Überschrifts-Block, Sliding Window).
|
||||
In Qdrant als Punkt in `<prefix>_chunks`.
|
||||
|
||||
- **Edge**
|
||||
Eine gerichtete Kante, die eine Beziehung zwischen:
|
||||
- zwei Chunks,
|
||||
- Chunk ↔ Note,
|
||||
- oder (konzeptionell) Note ↔ Note beschreibt.
|
||||
Gespeichert in `<prefix>_edges`.
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Qdrant Collections (Stand V2)
|
||||
|
||||
**Namenskonvention:**
|
||||
Prefix aus ENV (`COLLECTION_PREFIX`, z. B. `mindnet`):
|
||||
|
||||
- `mindnet_notes`
|
||||
- `mindnet_chunks`
|
||||
- `mindnet_edges`
|
||||
|
||||
Die Collections werden über:
|
||||
|
||||
```bash
|
||||
python3 -m scripts.reset_qdrant --mode wipe --prefix "$COLLECTION_PREFIX"
|
||||
```
|
||||
|
||||
**vollständig neu angelegt**, inkl. Payload-Indizes (Details in 6.1).
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Notes – Payload & Felder
|
||||
|
||||
Minimale, indexierte Felder (Auszug, Schema laut `diag_payload_indexes`):
|
||||
|
||||
- `note_id` (keyword) – stabile Note-ID (Frontmatter `id`)
|
||||
- `title` (text) – Notiz-Titel
|
||||
- `type` (keyword) – logischer Typ (z. B. `concept`, `project`, `profile`, `journal`, `source`)
|
||||
- `tags` (keyword[]) – Tags aus Frontmatter
|
||||
- `updated` (integer) – Zeitstempel (z. B. `YYYYMMDDHHMMSS` oder epoch; Implementierungsdetail)
|
||||
|
||||
Weitere Felder (nicht alle indexiert, aber im Payload vorhanden):
|
||||
|
||||
- `retriever_weight` (float) – wie stark diese Note/Chunks im Retrieval gewichtet werden
|
||||
- `chunk_profile` (string) – Name des verwendeten Chunk-Profils
|
||||
- `edge_defaults` (string[]) – Liste von Default-Relationen pro Typ (z. B. `["references","related_to"]`)
|
||||
- `confidentiality`, `visibility`, weitere Metadaten aus Frontmatter
|
||||
|
||||
Diese Felder werden in `app/core/note_payload.py` aus Frontmatter + `types.yaml` konstruiert.
|
||||
|
||||
---
|
||||
|
||||
### 2.4 Chunks – Payload & Felder
|
||||
|
||||
Indexierte Kernfelder:
|
||||
|
||||
- `note_id` (keyword) – Referenz auf die übergeordnete Note
|
||||
- `chunk_id` (keyword) – deterministische Chunk-ID:
|
||||
`"{note_id}#c{index:02d}"` (z. B. `20251110-ollama-llm-9f0a12#c02`)
|
||||
- `index` (integer) – 0-basiert; Position in der Note
|
||||
- `type` (keyword) – Note-Typ (vom Note geerbt)
|
||||
- `tags` (keyword[]) – Tags (vom Note geerbt)
|
||||
|
||||
Weitere wichtige Payload-Felder:
|
||||
|
||||
- `ord` (integer) – 1-basiertes Pendant zu `index` (für menschliche Lesbarkeit)
|
||||
- `text` (string) – eigentlicher Chunk-Text (ggf. gereinigt/normalisiert)
|
||||
- `window` (string) – Kontext-Fenster (z. B. Chunk ± Nachbarn)
|
||||
- `retriever_weight` (float) – gültiger Wert aus `types.yaml` (oder Default)
|
||||
- `chunk_profile` (string) – verwendetes Profil, z. B. `short`, `medium`, `long`, `by_heading`
|
||||
- `neighbors_prev` (string[]) – Liste von `chunk_id`s direkter Vorgänger (i. d. R. 0 oder 1 ID)
|
||||
- `neighbors_next` (string[]) – Liste von `chunk_id`s direkter Nachfolger
|
||||
|
||||
Die Konstruktion erfolgt in `app/core/chunk_payload.py` auf Basis der Chunks aus `chunker.py` und der aufgelösten Typ-Defaults.
|
||||
|
||||
---
|
||||
|
||||
### 2.5 Edges – Payload & Felder
|
||||
|
||||
Indexierte Felder:
|
||||
|
||||
- `note_id` (keyword) – Note, zu der der **Quell-Chunk** gehört
|
||||
- `source_id` (keyword) – i. d. R. `chunk_id` des Quell-Chunks (z. B. `2025…#c00`)
|
||||
- `target_id` (keyword) – Zielobjekt (z. B.:
|
||||
- `chunk_id` eines anderen Chunks
|
||||
- Note-Titel (`"Embeddings 101"`) bei **konzeptionellen Referenzen**
|
||||
- `kind` (keyword) – Kantenart:
|
||||
- Struktur: `belongs_to`, `next`, `prev`
|
||||
- Explizit: `references`
|
||||
- Inline: z. B. `similar_to`, `depends_on`, `related_to`
|
||||
- Typ-Defaults: ebenfalls z. B. `depends_on`, `related_to` (aus `edge_defaults`)
|
||||
- `scope` (keyword) – aktuell immer `"chunk"` (Kante wird auf Chunk-Ebene geführt)
|
||||
- `chunk_id` (keyword) – `chunk_id` des Quell-Chunks
|
||||
|
||||
Weitere Payload-Felder:
|
||||
|
||||
- `rule_id` (string) – Herkunftsregel:
|
||||
- Struktur:
|
||||
- `structure:belongs_to`
|
||||
- `structure:next`
|
||||
- `structure:prev`
|
||||
- Explizite Wiki-Links:
|
||||
- `explicit:wikilink`
|
||||
- Inline-Relationen:
|
||||
- `inline:rel`
|
||||
- Typ-Defaults aus `types.yaml`:
|
||||
- `edge_defaults:<type>:<relation>`
|
||||
z. B. `edge_defaults:concept:related_to`
|
||||
- `confidence` (float) – Stärke / Vertrauensgrad:
|
||||
- Struktur-Kanten: `1.0`
|
||||
- Explizite Wiki-Links: `1.0`
|
||||
- Inline-Relationen (`[[rel:… …]]`): z. B. `0.95`
|
||||
- Typ-Defaults: z. B. `0.7`
|
||||
|
||||
Edges werden in `app/core/derive_edges.py` erzeugt und in `mindnet_edges` gespeichert.
|
||||
|
||||
---
|
||||
|
||||
## 3. Typ-Konfiguration (`config/types.yaml`)
|
||||
|
||||
### 3.1 Struktur des Files
|
||||
|
||||
`types.yaml` (vereinfachtes Beispiel):
|
||||
Die Logik ist ausgelagert in YAML-Dateien.
|
||||
|
||||
### 3.1 Typ-Registry (`config/types.yaml`)
|
||||
Steuert den Import-Prozess.
|
||||
* **Priorität:** Frontmatter > Pfad > Default.
|
||||
* **Inhalt:**
|
||||
```yaml
|
||||
version: 1.0
|
||||
default:
|
||||
retriever_weight: 1.0
|
||||
chunk_profile: default
|
||||
|
||||
types:
|
||||
concept:
|
||||
chunk_profile: medium
|
||||
edge_defaults: ["references", "related_to"]
|
||||
retriever_weight: 0.35
|
||||
|
||||
task:
|
||||
chunk_profile: short
|
||||
edge_defaults: ["depends_on", "belongs_to"]
|
||||
retriever_weight: 0.8
|
||||
|
||||
experience:
|
||||
chunk_profile: medium
|
||||
edge_defaults: ["derived_from", "inspired_by"]
|
||||
retriever_weight: 0.9
|
||||
|
||||
project:
|
||||
chunk_profile: long
|
||||
edge_defaults: ["references", "depends_on"]
|
||||
retriever_weight: 0.97
|
||||
|
||||
profile:
|
||||
chunk_profile: long
|
||||
edge_defaults: ["references", "depends_on"]
|
||||
retriever_weight: 0.66
|
||||
retriever_weight: 0.60
|
||||
```
|
||||
|
||||
### 3.2 Typauflösung – Reihenfolge der Prioritäten
|
||||
|
||||
Für eine Note mit `type` und optionalen Frontmatter Feldern `retriever_weight`, `chunk_profile` gilt aktuell:
|
||||
|
||||
1. **Typ-spezifische Defaults** aus `types.yaml`:
|
||||
- `retriever_weight` ← `types.<type>.retriever_weight` (falls vorhanden)
|
||||
- `chunk_profile` ← `types.<type>.chunk_profile` (falls vorhanden)
|
||||
2. **Globaler Default** `default` in `types.yaml`, falls Typ-Eintrag fehlt.
|
||||
3. **Frontmatter `retriever_weight` / `chunk_profile`** werden aktuell *nicht* mehr als Konfigurationsebene verwendet, sondern eher als historische Relikte. (Dies kann zukünftig wieder aktiviert werden, ist aber im aktuellen Stand bewusst abgeschaltet zugunsten klarer, zentraler Konfiguration über `types.yaml`.)
|
||||
|
||||
Das Ergebnis dieser Auflösung wird im Payload als:
|
||||
|
||||
- `note_payload["retriever_weight"]`
|
||||
- `note_payload["chunk_profile"]`
|
||||
|
||||
gesetzt und **1:1 an alle Chunks vererbt**.
|
||||
### 3.2 Retriever-Config (`config/retriever.yaml`)
|
||||
Steuert das Scoring zur Laufzeit (WP04a).
|
||||
* **Inhalt:**
|
||||
```yaml
|
||||
scoring:
|
||||
semantic_weight: 1.0
|
||||
edge_weight: 0.7
|
||||
centrality_weight: 0.5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Import-Pipeline (Markdown → Qdrant)
|
||||
|
||||
### 4.1 Runbook
|
||||
Das Skript `scripts/import_markdown.py` orchestriert den Prozess.
|
||||
|
||||
Standard-Import (mit Purge-Before-Upsert):
|
||||
### 4.1 Verarbeitungsschritte
|
||||
|
||||
```bash
|
||||
export COLLECTION_PREFIX=mindnet
|
||||
export MINDNET_TYPES_FILE="$(pwd)/config/types.yaml"
|
||||
|
||||
python3 -m scripts.import_markdown \
|
||||
--vault ./mindnet_v2_test_vault \
|
||||
--apply \
|
||||
--purge-before-upsert \
|
||||
--prefix "$COLLECTION_PREFIX"
|
||||
```
|
||||
|
||||
Nur Sync-Deletes (entfernte Dateien in Qdrant aufräumen):
|
||||
|
||||
```bash
|
||||
python3 -m scripts.import_markdown \
|
||||
--vault ./mindnet_v2_test_vault \
|
||||
--sync-deletes \
|
||||
--apply \
|
||||
--prefix "$COLLECTION_PREFIX"
|
||||
```
|
||||
1. **Discovery & Parsing:**
|
||||
* Einlesen der `.md` Dateien. Hash-Vergleich (Body/Frontmatter) zur Erkennung von Änderungen.
|
||||
2. **Typauflösung:**
|
||||
* Laden der `types.yaml`. Bestimmen des effektiven Typs und der `edge_defaults`.
|
||||
3. **Chunking:**
|
||||
* Zerlegung via `chunker.py` basierend auf `chunk_profile` (z.B. `by_heading`, `short`, `long`).
|
||||
* Trennung von `text` (Kern) und `window` (Embedding-Kontext).
|
||||
4. **Kantenableitung (Edge Derivation):**
|
||||
Die `derive_edges.py` erzeugt Kanten in strikter Reihenfolge:
|
||||
1. **Inline-Edges:** `[[rel:depends_on X]]` → `kind=depends_on`, `rule_id=inline:rel`, `conf=0.95`.
|
||||
2. **Callout-Edges:** `> [!edge] related_to: [[X]]` → `kind=related_to`, `rule_id=callout:edge`, `conf=0.90`.
|
||||
3. **Explizite Referenzen:** `[[X]]` → `kind=references`, `rule_id=explicit:wikilink`, `conf=1.0`.
|
||||
4. **Typ-Defaults:** Für jede Referenz werden Zusatzkanten gemäß `edge_defaults` erzeugt (z.B. `project` -> `depends_on`). `rule_id=edge_defaults:...`, `conf=0.7`.
|
||||
5. **Struktur:** `belongs_to`, `next`, `prev` (automatisch).
|
||||
5. **Upsert:**
|
||||
* Schreiben in Qdrant. Nutzung von `--purge-before-upsert` für saubere Updates.
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Verarbeitungsschritte
|
||||
## 5. Retriever-Architektur & Scoring
|
||||
|
||||
**1. Discovery & Parsing**
|
||||
Der Retriever (`app/core/retriever.py`) realisiert die Logik hinter `/query`.
|
||||
|
||||
- `import_markdown.py` traversiert das Vault‐Verzeichnis.
|
||||
- Für jede `.md`:
|
||||
- YAML-Frontmatter extrahieren (id, title, type, created, updated, tags, …).
|
||||
- Body (Markdown) als String.
|
||||
### 5.1 Betriebsmodi
|
||||
* **Semantic:** Reine Vektorsuche. Schnell.
|
||||
* **Hybrid:** Vektorsuche + Graph-Expansion (Tiefe N) + Re-Ranking.
|
||||
|
||||
**2. Typauflösung & Payload-Bau (Note)**
|
||||
### 5.2 Scoring-Formel (WP04a)
|
||||
Die Relevanzberechnung ist nun eine gewichtete Summe:
|
||||
|
||||
- Laden & Auflösen von `types.yaml` (über ENV `MINDNET_TYPES_FILE` oder Default `./config/types.yaml`).
|
||||
- Bestimmung des effektiven Typs (`type` aus Frontmatter; ggf. Fallback).
|
||||
- Anwendung der Typdefaults:
|
||||
- `retriever_weight` und `chunk_profile` werden aus `types.yaml` gelesen.
|
||||
- Konstruktion des Note-Payloads via `make_note_payload` (in `note_payload.py`):
|
||||
- Felder wie `note_id`, `title`, `type`, `tags`, `updated`, `retriever_weight`, `chunk_profile`, `edge_defaults`, …
|
||||
$$
|
||||
TotalScore = (W_{sem} \cdot S_{sem} \cdot \max(W_{type}, 0)) + (W_{edge} \cdot B_{edge}) + (W_{cent} \cdot B_{cent})
|
||||
$$
|
||||
|
||||
**3. Chunking**
|
||||
* **Komponenten:**
|
||||
* $S_{sem}$: Semantic Score (Cosine Similarity).
|
||||
* $W_{type}$: `retriever_weight` (aus Note/Chunk Payload).
|
||||
* $B_{edge}$: Edge-Bonus (Summe der Konfidenzen eingehender relevanter Kanten).
|
||||
* $B_{cent}$: Centrality-Bonus (im lokalen Subgraphen).
|
||||
* **Gewichte ($W$):** Stammen aus `retriever.yaml`.
|
||||
|
||||
- Übergabe des Note-Bodies + `chunk_profile` an `chunker.py`.
|
||||
- `chunker.py` nutzt `chunk_config.py`, das definierte Profile bereitstellt, z. B.:
|
||||
|
||||
```python
|
||||
CHUNK_PROFILES = {
|
||||
"short": {"max_tokens": 128, "overlap": 32},
|
||||
"medium": {"max_tokens": 256, "overlap": 64},
|
||||
"long": {"max_tokens": 512, "overlap": 128},
|
||||
"by_heading": {"strategy": "by_heading", "max_chars": 2000},
|
||||
"default": {"max_tokens": 256, "overlap": 64},
|
||||
}
|
||||
```
|
||||
|
||||
- Ergebnis: Liste geordneter Chunks mit:
|
||||
- `chunk_id` (`note_id#cXX`),
|
||||
- `index` (0..N-1),
|
||||
- `ord` (1..N),
|
||||
- `text`,
|
||||
- `window` (inkl. Nachbartexte),
|
||||
- `neighbors_prev` / `neighbors_next`.
|
||||
|
||||
**4. Chunk-Payloads**
|
||||
|
||||
- In `chunk_payload.py` werden für jeden Chunk die Payloads gebaut:
|
||||
- Vererbung von `note_id`, `type`, `tags`, `retriever_weight`, `chunk_profile`.
|
||||
- Setzen von `index`, `ord`, `neighbors_prev`, `neighbors_next`.
|
||||
- Persistieren von `text` und `window`.
|
||||
|
||||
**5. Kantenableitung (Edges)**
|
||||
|
||||
- `derive_edges.py` wird mit Note + Chunks + Typinformationen aufgerufen.
|
||||
- Es entstehen mehrere Gruppen von Kanten:
|
||||
1. **Struktur-Kanten**:
|
||||
- `belongs_to`: Chunk → Note
|
||||
- `next`: Chunk[i] → Chunk[i+1]
|
||||
- `prev`: Chunk[i] → Chunk[i-1]
|
||||
2. **Explizite Kanten aus Text**:
|
||||
- Wiki-Links: `[[Target Title]]` → `kind="references"`, `rule_id="explicit:wikilink"`, `confidence=1.0`.
|
||||
- Inline-Relationen: `[[rel:depends_on Embeddings 101]]`:
|
||||
- `kind="depends_on"`
|
||||
- `rule_id="inline:rel"`
|
||||
- `confidence≈0.95`
|
||||
- `target_id="Embeddings 101"` (Titel, nicht ID – Auflösung erfolgt später im Agenten).
|
||||
3. **Typ-basierte Defaults (`edge_defaults`)**:
|
||||
- Aus `types.yaml` je `type`: Liste von Relations-Kinds, z. B.:
|
||||
- `concept.edge_defaults = ["references","related_to"]`
|
||||
- `project.edge_defaults = ["references","depends_on"]`
|
||||
- Für jede explizite Referenz Quelle→Ziel (z. B. `references`, `similar_to`) werden **zusätzliche Kanten** erzeugt, z. B.:
|
||||
- `edge_defaults:project:depends_on`
|
||||
- `edge_defaults:concept:related_to`
|
||||
- `confidence≈0.7`.
|
||||
4. **Deduplication**:
|
||||
- Alle Kanten werden in einem Set anhand eines Keys wie
|
||||
- `(kind, scope, source_id, target_id, rule_id)`
|
||||
dedupliziert (idempotent).
|
||||
|
||||
**6. Upsert nach Qdrant**
|
||||
|
||||
- Punkte (Notes, Chunks, Edges) werden via `qdrant_points.py` in die entsprechenden Collections upserted.
|
||||
- Option `--purge-before-upsert` sorgt dafür, dass alte Punkte mit denselben IDs entfernt werden, bevor neue geschrieben werden (stabiler Reimport).
|
||||
- `--sync-deletes` entfernt Punkte, deren zugehörige Dateien im Vault nicht mehr existieren.
|
||||
### 5.3 Graph-Expansion
|
||||
Der Hybrid-Modus lädt dynamisch die Nachbarschaft der Top-K Vektor-Treffer ("Seeds") über `graph_adapter.expand`. Dies baut einen temporären `NetworkX`-Graphen im Speicher, auf dem Boni berechnet werden.
|
||||
|
||||
---
|
||||
|
||||
## 5. Edges im Detail – Semantik & Regeln
|
||||
## 6. Indizes & Performance
|
||||
|
||||
### 5.1 Struktur-Kanten
|
||||
Damit Qdrant performant bleibt, sind Payload-Indizes essenziell.
|
||||
|
||||
Erzeugt für **jede Note** mit >0 Chunks:
|
||||
**Erforderliche Indizes:**
|
||||
* **Notes:** `note_id`, `type`, `tags`.
|
||||
* **Chunks:** `note_id`, `chunk_id`.
|
||||
* **Edges:** `source_id`, `target_id`, `kind`, `scope`, `note_id`.
|
||||
|
||||
- **belongs_to**
|
||||
- pro Chunk genau eine Kante:
|
||||
- `source_id = chunk_id`
|
||||
- `target_id = note_id` *oder* (konzeptionell) ein Note-Identifikator
|
||||
- `kind = "belongs_to"`
|
||||
- `scope = "chunk"`
|
||||
- `rule_id = "structure:belongs_to"`
|
||||
- `confidence = 1.0`
|
||||
- **next / prev**
|
||||
- für Index i (`0..N-2`):
|
||||
- `next`: `chunk_i → chunk_{i+1}`
|
||||
- `prev`: `chunk_{i+1} → chunk_i`
|
||||
- `kind = "next"` bzw. `"prev"`
|
||||
- `scope = "chunk"`
|
||||
- `rule_id = "structure:next"` / `"structure:prev"`
|
||||
- `confidence = 1.0`
|
||||
|
||||
Tests (`tests.test_edges_smoke` / `tests.test_edges_all`) stellen sicher:
|
||||
|
||||
- `belongs_to` == `#chunks`
|
||||
- `next` == `prev` == `#chunks - 1`
|
||||
- keine Duplikate der Strukturkanten.
|
||||
Validierung erfolgt über `tests/ensure_indexes_and_show.py`.
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Explizite Wiki-Links (`[[Target Title]]`)
|
||||
|
||||
- Parser in `derive_edges.py` sucht im Chunk-Text nach Mustern:
|
||||
- `[[Some Page]]` (kein `rel:` Präfix)
|
||||
- Erzeugt Kanten:
|
||||
- `kind = "references"`
|
||||
- `scope = "chunk"`
|
||||
- `source_id = chunk_id`
|
||||
- `target_id = "<Titel aus dem Link>"` (z. B. `"Vector DB Basics"`)
|
||||
- `rule_id = "explicit:wikilink"`
|
||||
- `confidence = 1.0`
|
||||
|
||||
Diese Kanten bilden das **explizite Referenznetz** des Nutzers ab (Obsidian-typische Wikilinks).
|
||||
|
||||
---
|
||||
|
||||
### 5.3 Inline-Relationen (`[[rel:… …]]`)
|
||||
|
||||
Aktuell **implementiert und funktional** ist folgende Form:
|
||||
|
||||
- **Format (aktuell unterstützt):**
|
||||
|
||||
```md
|
||||
[[rel:depends_on Embeddings 101]]
|
||||
[[rel:similar_to Vector DB Basics]]
|
||||
[[rel:related_to Qdrant Vektordatenbank]]
|
||||
```
|
||||
|
||||
- Parsing-Strategie:
|
||||
- Link-Inhalt wird gesplittet:
|
||||
- Erster Token: `rel:depends_on`
|
||||
- Rest: Zielbezeichnung (`Embeddings 101`)
|
||||
- `kind` wird aus dem Teil `depends_on` extrahiert (z. B. `similar_to`, `related_to`).
|
||||
- `target_id` wird aus dem Rest (meist ein Titel) abgeleitet.
|
||||
|
||||
- Ergebnis-Kante:
|
||||
- `kind = "<Relation aus rel:…>"`
|
||||
- `scope = "chunk"`
|
||||
- `source_id = chunk_id`
|
||||
- `target_id = "<Ziel-Titel>"`
|
||||
- `rule_id = "inline:rel"`
|
||||
- `confidence ≈ 0.95`
|
||||
|
||||
- **Nicht unterstützt (Stand jetzt):**
|
||||
|
||||
```md
|
||||
rel: depends_on [[Embeddings 101]] [[Vector DB Basics]]
|
||||
```
|
||||
|
||||
Diese Syntax wird derzeit **nicht** erkannt.
|
||||
Multi-Target-Inline-Relationen (`rel: similar_to [[A]] [[B]]`) sind ein **offener Task**.
|
||||
|
||||
---
|
||||
|
||||
### 5.4 Typ-basierte Default-Edges (`edge_defaults`)
|
||||
|
||||
Zweck:
|
||||
|
||||
- Für bestimmte Typen (vor allem `concept`, `project`, `profile`) sollen zusätzliche Kanten automatisch ergänzt werden, um das **Semantiknetz zu verdichten**, ohne dass der Nutzer jede Relation explizit pflegen muss.
|
||||
|
||||
Konfiguration:
|
||||
|
||||
- In `types.yaml` definiert, z. B.:
|
||||
|
||||
```yaml
|
||||
types:
|
||||
concept:
|
||||
edge_defaults: ["references", "related_to"]
|
||||
project:
|
||||
edge_defaults: ["references", "depends_on"]
|
||||
```
|
||||
|
||||
Aktuelle Implementierung:
|
||||
|
||||
- Für jede **explizite** Referenz (Wiki-Link oder Inline-Relation) von Chunk → Ziel:
|
||||
- `kind="references"` (Wiki) oder die Inline-Relation (`depends_on`, `similar_to`, …)
|
||||
- Auf Basis des Note-Typs (`type`) wird pro Default-Relation eine zusätzliche Kante erzeugt:
|
||||
|
||||
Beispiel:
|
||||
|
||||
- Note-Typ: `project`
|
||||
- `edge_defaults: ["references","depends_on"]`
|
||||
- Explizit: `references` von `Edge Index` → `Embeddings 101`
|
||||
- Abgeleitete Kanten:
|
||||
- `kind="references"`, `rule_id="edge_defaults:project:references"`, `confidence=0.7`
|
||||
- `kind="depends_on"`, `rule_id="edge_defaults:project:depends_on"`, `confidence=0.7`
|
||||
|
||||
- Symmetrische Kanten:
|
||||
- Für einige Relations-Typen (z. B. `related_to`) wird zusätzlich eine **spiegelbildliche** Kante Ziel → Quelle erzeugt.
|
||||
- Dies erklärt in Tests z. B. Paare wie:
|
||||
- `related_to (Source=Ollama LLM, Target=Qdrant Vektordatenbank)`
|
||||
- `related_to (Source=Qdrant Vektordatenbank, Target=Ollama LLM)`
|
||||
|
||||
- Deduplication:
|
||||
- Es wird verhindert, dass mehrfach identische Kanten geschrieben werden.
|
||||
- Schlüssel: `(kind, scope, source_id, target_id, rule_id)`.
|
||||
|
||||
---
|
||||
|
||||
### 5.5 `rule_id` & `confidence` – Bedeutung
|
||||
|
||||
**`rule_id`** kodiert **Herkunft** und **Regel**, z. B.:
|
||||
|
||||
- `structure:belongs_to`, `structure:next`, `structure:prev`
|
||||
- `explicit:wikilink`
|
||||
- `inline:rel`
|
||||
- `edge_defaults:<type>:<relation>`
|
||||
|
||||
**`confidence`** dient als späterer Gewichtungsfaktor im Retrieval / Ranking:
|
||||
|
||||
- `1.0` → harte Struktur- oder explizite Kante (vom Nutzer direkt gesetzt)
|
||||
- `0.95` → Inline-Relation (stark, aber nicht so „hart“ wie die reine Referenz)
|
||||
- `0.7` → Typ-Default-Edges (abgeleitete Hypothesen aus Typwissen)
|
||||
|
||||
Ein künftiger LLM-Agent kann daraus z. B.:
|
||||
|
||||
- Kanten mit hoher `confidence` priorisieren,
|
||||
- Default-Edges als schwächere Hinweise betrachten,
|
||||
- Inline-Relationen gezielt als semantische Aussagen über die Beziehung interpretieren.
|
||||
|
||||
---
|
||||
|
||||
## 6. Qdrant-Schema & Indizes
|
||||
|
||||
### 6.1 Erstellung & Indizes
|
||||
|
||||
Die Erstellung erfolgt über:
|
||||
|
||||
```bash
|
||||
python3 -m scripts.reset_qdrant --mode wipe --prefix "$COLLECTION_PREFIX"
|
||||
```
|
||||
|
||||
Intern:
|
||||
|
||||
- `qdrant.py` / `setup_mindnet_collections.py`:
|
||||
- Collections anlegen, ggf. mit Vektor-Konfiguration (Single-Vector; Named-Vectors sind vorbereitet aber optional).
|
||||
- `ensure_payload_indexes()`:
|
||||
- Legt für jede Collection die relevanten Payload-Indizes an.
|
||||
|
||||
Verifizierbar durch:
|
||||
|
||||
```bash
|
||||
python3 -m tests.ensure_indexes_and_show
|
||||
```
|
||||
|
||||
Beispielauszug (bereits real so gesehen):
|
||||
|
||||
- **mindnet_notes**
|
||||
|
||||
```json
|
||||
{
|
||||
"tags": { "data_type": "keyword", "points": 8 },
|
||||
"note_id":{ "data_type": "keyword", "points": 8 },
|
||||
"title": { "data_type": "text", "points": 8 },
|
||||
"updated":{ "data_type": "integer", "points": 0 },
|
||||
"type": { "data_type": "keyword", "points": 8 }
|
||||
}
|
||||
```
|
||||
|
||||
- **mindnet_chunks**
|
||||
|
||||
```json
|
||||
{
|
||||
"note_id": { "data_type": "keyword", "points": 44 },
|
||||
"chunk_id":{ "data_type": "keyword", "points": 44 },
|
||||
"index": { "data_type": "integer", "points": 44 },
|
||||
"type": { "data_type": "keyword", "points": 44 },
|
||||
"tags": { "data_type": "keyword", "points": 44 }
|
||||
}
|
||||
```
|
||||
|
||||
- **mindnet_edges**
|
||||
|
||||
```json
|
||||
{
|
||||
"scope": { "data_type": "keyword", "points": 135 },
|
||||
"kind": { "data_type": "keyword", "points": 135 },
|
||||
"chunk_id":{ "data_type": "keyword", "points": 135 },
|
||||
"target_id":{ "data_type": "keyword", "points": 135 },
|
||||
"note_id": { "data_type": "keyword", "points": 135 },
|
||||
"source_id":{ "data_type": "keyword", "points": 135 }
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Schema-Validierung
|
||||
|
||||
`tests/assert_payload_schema.py` prüft:
|
||||
|
||||
- ob die Minimalfelder (`note_id`, `type`, `title`, `updated`, `tags` etc.) vorhanden sind,
|
||||
- berichtet fehlende Felder.
|
||||
|
||||
Nach Korrektur der Indexanlage sollte:
|
||||
|
||||
- `ok = true` sein, bzw. zumindest alle Kernfelder unter `present` geführt werden.
|
||||
(Im aktuellen Stand werden sie als Indizes geführt; Qdrant speichert zusätzliche Felder ohne explizite Schema-Zeile, was technisch unproblematisch ist.)
|
||||
|
||||
---
|
||||
|
||||
## 7. Nutzung im Retrieval (Ausblick)
|
||||
|
||||
### 7.1 Typbewusstes Chunk-Retrieval
|
||||
|
||||
Ein typischer Query-Pfad eines LLM-Agents:
|
||||
|
||||
1. Query → Embedding
|
||||
2. Suche in `mindnet_chunks` mit Filter, z. B.:
|
||||
|
||||
```python
|
||||
f = rest.Filter(
|
||||
must=[
|
||||
rest.FieldCondition(key="type", match=rest.MatchValue(value="concept")),
|
||||
rest.FieldCondition(key="tags", match=rest.MatchAny(any=["ki","ollama"]))
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
3. Treffer werden nach:
|
||||
- Vektor-Score,
|
||||
- `retriever_weight`,
|
||||
- ggf. Edges (z. B. Projekte mit `depends_on` → Konzepte) gewichtet.
|
||||
|
||||
### 7.2 Graph-orientierte Navigation
|
||||
|
||||
Basierend auf `mindnet_edges` kann der Agent:
|
||||
|
||||
- ausgehend von einem relevanten Chunk:
|
||||
- Struktur-Kanten (`next`, `prev`) zur Kontextvergrößerung nutzen,
|
||||
- `references` / `depends_on` / `related_to` interpretieren,
|
||||
- `edge_defaults` als semantische Ausdehnung (z. B. von `project` zu seinen `concept`-Bausteinen) nutzen.
|
||||
|
||||
Die Bedeutung der Kanten ist dabei durch `rule_id` und `confidence` klar nachvollziehbar.
|
||||
|
||||
---
|
||||
|
||||
## 8. Offene Punkte / Known Limitations
|
||||
|
||||
1. **Multi-Target Inline-Relationen**
|
||||
Syntax wie:
|
||||
|
||||
```md
|
||||
[[rel:similar_to Vector DB Basics]] [[rel:similar_to Embeddings 101]]
|
||||
```
|
||||
funktioniert (zwei separate Inline-Links).
|
||||
**Nicht** unterstützt (Stand jetzt):
|
||||
|
||||
```md
|
||||
[[rel:similar_to Vector DB Basics]] [[Embeddings 101]]
|
||||
rel: similar_to [[Vector DB Basics]] [[Embeddings 101]]
|
||||
```
|
||||
|
||||
→ Offener Task: Parser erweitern, um mehrere Ziele in einer Inline-Relation zu unterstützen.
|
||||
|
||||
2. **Callout-Edges (`[!edge]`)**
|
||||
- Konzeptionell vorgesehen, z. B.:
|
||||
|
||||
```md
|
||||
> [!edge] related_to: [[Vector DB Basics]]
|
||||
```
|
||||
|
||||
- Im aktuellen Implementierungsstand werden Callouts entweder:
|
||||
- gar nicht, oder
|
||||
- nur teilweise verarbeitet (in den aktuellen Test-Runs war `callout_total = 0`).
|
||||
→ Offener Task: Callout-Parser robust implementieren und mit Tests absichern.
|
||||
|
||||
3. **Frontmatter-Overrides für `retriever_weight` / `chunk_profile`**
|
||||
- Aktuell **nicht** wirksam.
|
||||
- Typdefaults aus `types.yaml` sind der **Single Source of Truth**.
|
||||
- Später können kontrollierte Overrides (z. B. für Sonderfälle) eingeführt werden.
|
||||
|
||||
4. **Vektor-Konfiguration für Edges**
|
||||
- `mindnet_edges` wird derzeit ohne Vektoren betrieben (`vectors = null`).
|
||||
- Später könnte z. B. eine semantische Repräsentation von Kanten eingeführt werden (z. B. Embedding von `(source_chunk_text + relation + target_title)`).
|
||||
|
||||
---
|
||||
|
||||
## 9. Zusammenfassung
|
||||
|
||||
- Die Mindnet-V2-Architektur implementiert einen **dreistufigen Speicher**:
|
||||
- Notes (`mindnet_notes`),
|
||||
- Chunks (`mindnet_chunks`),
|
||||
- Edges (`mindnet_edges`).
|
||||
|
||||
- **`types.yaml`** steuert:
|
||||
- `retriever_weight` und `chunk_profile` je Typ (inkl. Default),
|
||||
- `edge_defaults` je Typ (automatische Relations-Ableitung).
|
||||
|
||||
- Das **Chunking** ist profilbasiert (short/medium/long/by_heading) und deterministisch.
|
||||
|
||||
- Die **Kantenableitung** in `derive_edges.py` erzeugt:
|
||||
- strukturelle Kanten (belongs_to / next / prev),
|
||||
- explizite Kanten aus Wiki-Links (`[[Target]]`),
|
||||
- semantische Inline-Relationen (`[[rel:depends_on Target]]`),
|
||||
- typbasierte Default-Kanten (z. B. `edge_defaults:project:depends_on`).
|
||||
|
||||
- `rule_id` und `confidence` machen die Herkunft jeder Kante transparent und ermöglichen später eine **gewichtete Auswertung** durch LLM-basierte Agenten.
|
||||
|
||||
- Qdrant-Collections und Payload-Indizes sind konsistent eingerichtet und durch Tests verifiziert.
|
||||
|
||||
Dieses Dokument bildet damit das aktuelle technische Gesamtbild von Mindnet V2 ab, inkl. aller relevanten Mechanismen rund um Typen, Chunking, Edges und Qdrant-Schema.
|
||||
|
||||
## 7. Offene Punkte / Known Limitations
|
||||
|
||||
1. **Multi-Target Inline-Relations:**
|
||||
* `rel: similar_to [[A]] [[B]]` wird aktuell parser-seitig nicht unterstützt.
|
||||
* Workaround: Zwei separate Inline-Links `[[rel:similar_to A]] [[rel:similar_to B]]`.
|
||||
2. **Unresolved Targets:**
|
||||
* Kanten zu Notizen, die noch nicht existieren, werden mit `target_id="Titel"` angelegt.
|
||||
* Heilung durch `scripts/resolve_unresolved_references.py` möglich.
|
||||
3. **Vektor-Konfiguration für Edges:**
|
||||
* `mindnet_edges` hat aktuell keine Vektoren (`vectors = null`). Eine semantische Suche *auf Kanten* ist noch nicht möglich.
|
||||
Loading…
Reference in New Issue
Block a user