# mdgrep – Bedienungsanleitung **mdgrep** ist eine Volltextsuche für Markdown-Dateien und beliebige andere Texte. Sie versteht Wortbestandteile, Phrasen, reguläre Ausdrücke und logische Verknüpfungen mit Klammern – und lässt sich auf vier Arten nutzen: | Baustein | Wofür | |---|---| | **Kommandozeile** `mdgrep` | schnell im Terminal suchen, wie `grep` – aber blockweise und mit UND/ODER/NICHT | | **Python-Bibliothek** | Suche in eigene Programme einbauen, auch für Nicht-Markdown-Daten | | **Web-API** (Flask) | JSON-Suchschnittstelle für Webseiten und andere Dienste | | **Such-Widget** (JavaScript) | fertige Suchmaske mit Live-Ergebnissen für jede Webseite | Version: 0.1.1 · Lizenz: MIT · Repository: https://kitux.de/forgejo/dschlueter/mdgrep --- ## Inhaltsverzeichnis 1. [Installation](#1-installation) 2. [Suchsyntax](#2-suchsyntax) 3. [Kommandozeile](#3-kommandozeile) 4. [Python-Bibliothek](#4-python-bibliothek) 5. [Web-API](#5-web-api) 6. [Such-Widget](#6-such-widget) 7. [Betrieb auf einem Server](#7-betrieb-auf-einem-server) 8. [Voraussetzungen an die Dokumente](#8-voraussetzungen-an-die-dokumente) 9. [Rezepte](#9-rezepte) 10. [Fehlermeldungen](#10-fehlermeldungen) 11. [Fehlersuche](#11-fehlersuche) 12. [Entwicklung & neue Versionen](#12-entwicklung--neue-versionen) --- ## 1. Installation Voraussetzung: Python 3.10 oder neuer, `git` (für die Installation aus dem Repository). ```bash # nur Bibliothek + Kommandozeile pip install "mdgrep @ git+https://kitux.de/forgejo/dschlueter/mdgrep.git@v0.1.1" # zusätzlich Web-API (installiert Flask) pip install "mdgrep[web] @ git+https://kitux.de/forgejo/dschlueter/mdgrep.git@v0.1.1" ``` In einer `requirements.txt`: ``` mdgrep[web] @ git+https://kitux.de/forgejo/dschlueter/mdgrep.git@v0.1.1 ``` Prüfen: ```bash mdgrep --version # mdgrep 0.1.1 python -c "import mdgrep; print(mdgrep.__version__)" ``` > **Hinweis:** mdgrep ist nicht auf PyPI veröffentlicht. Immer mit Git-URL und Versions-Tag > installieren – ein bloßes `pip install mdgrep` findet das Paket nicht (oder, falls jemand den > Namen dort später belegt, ein fremdes). --- ## 2. Suchsyntax Groß- und Kleinschreibung spielen **nie** eine Rolle (auch nicht bei Umlauten). | Eingabe | Bedeutung | Beispiel findet … | |---|---|---| | `rust` | Wortbestandteil | „Rust", „Rustacean", „trusted" | | `"open source"` | genaue Phrase; beliebiger Leerraum (auch Zeilenumbruch) zwischen den Wörtern | „Open Source", „open
source" | | `/llms?\b/` | regulärer Ausdruck | „LLM", „LLMs" | | `a b` · `a AND b` · `a UND b` · `a & b` · `a && b` | beide Begriffe | | | `a OR b` · `a ODER b` · `a \| b` · `a \|\| b` | mindestens einer | | | `NOT a` · `NICHT a` · `-a` · `!a` | Begriff darf nicht vorkommen | | | `( … )` | Klammern | | **Vorrang:** NICHT bindet stärker als UND, UND stärker als ODER. `a OR b AND c` bedeutet also `a OR (b AND c)`. Im Zweifel Klammern setzen. **Hinweise:** - Operatoren werden nur in **Großbuchstaben** erkannt: `and` ist ein normales Suchwort, `AND` der Operator. - Wortbestandteile treffen auch mitten im Wort: `go` findet „**Go**ddard". Für **ganze Wörter** einen regulären Ausdruck mit Wortgrenzen verwenden: `/\bgo\b/`. - Im regulären Ausdruck einen Schrägstrich als `\/` schreiben: `/tcp\/ip/`. - In Phrasen ein Anführungszeichen als `\"` schreiben. - Wörter mit Sonderzeichen funktionieren direkt: `AT&T`, `TCP/IP`, `e-mail` (ein `-` oder `&` *innerhalb* eines Wortes ist kein Operator). **Beispiele:** ```text (rust ODER go) UND NICHT crypto "machine learning" -/pytorch|tensorflow/ llm (datenschutz OR privacy) /\bgpt-\d+\b/ NICHT "open source" ``` **Grenzen (Standard):** höchstens 500 Zeichen und 50 Suchbegriffe pro Anfrage (die Web-API begrenzt zusätzlich auf 300 Zeichen). --- ## 3. Kommandozeile ```bash mdgrep SUCHANFRAGE [PFAD ...] [Optionen] ``` Ohne Pfad wird das aktuelle Verzeichnis durchsucht. Verzeichnisse werden rekursiv nach `*.md` durchsucht. Die Suchanfrage immer in **einfache Anführungszeichen** setzen, damit die Shell Klammern, `!`, `|` und `&` nicht selbst auswertet. ### 3.1 Beispiele ```bash mdgrep '(rust OR go) AND NOT crypto' notizen/ # alle *.md unter notizen/ mdgrep '/\bllms?\b/' docs/*.md --split list # ein Treffer pro Listenpunkt mdgrep 'todo' . -l # nur Dateinamen mdgrep 'release' . -c # nur Anzahl der Treffer mdgrep '"open source"' . --json > treffer.json # JSON für andere Programme mdgrep 'fehler' logs/ -g '*.txt' # andere Dateien als Markdown mdgrep 'rust' . -m 5 # nach 5 Treffern aufhören ``` Ausgabe (Fundstellen farbig, wenn die Ausgabe ein Terminal ist): ```text notizen/sprachen.md:5 Rust guarantees memory safety without a garbage collector. Its borrow checker rejects data races at compile time. ``` ### 3.2 Optionen | Option | Bedeutung | Standard | |---|---|---| | `-s`, `--split MODUS` | Einheit, die durchsucht und angezeigt wird (siehe 3.3) | `paragraph` | | `-g`, `--glob MUSTER` | Dateimuster in Verzeichnissen | `*.md` | | `-c`, `--count` | nur Anzahl der Treffer-Blöcke ausgeben | | | `-l`, `--files-with-matches` | nur Dateinamen ausgeben | | | `-j`, `--json` | JSON-Ausgabe (gleiches Format wie die Web-API) | | | `-C`, `--context N` | Zeichen Kontext um Treffer bei langen Blöcken | `80` | | `--max-chars N` | Blöcke über N Zeichen kürzen (`0` = nie) | `400` | | `-m`, `--max-count N` | nach N Treffern aufhören (`0` = alle) | `0` | | `--timeout SEK` | Zeitlimit für die ganze Suche | `30` | | `--color auto\|always\|never` | Farbe (`NO_COLOR` wird beachtet) | `auto` | | `-V`, `--version` | Version anzeigen | | ### 3.3 Split-Modi – was ist ein Treffer? | Modus | Ein Block ist … | Geeignet für | |---|---|---| | `paragraph` | ein durch Leerzeilen getrennter Absatz | Fließtext, Notizen | | `list` | ein Listenpunkt der obersten Ebene (mit eingerückten Unterpunkten); Text außerhalb von Listen wird zu Absätzen | Aufzählungen, Berichte, Changelogs | | `heading` | ein Abschnitt von Überschrift zu Überschrift | Dokumentation, Wikis | | `file` | eine ganze Datei | „In welchen Dateien kommt X und Y vor?" | Die logische Verknüpfung gilt **pro Block**: `rust AND go` findet nur Blöcke, in denen beide Wörter vorkommen. Mit `--split file` gilt sie für die ganze Datei. ### 3.4 Rückgabewerte (für Skripte) | Code | Bedeutung | |---|---| | `0` | Treffer gefunden | | `1` | kein Treffer | | `2` | Fehler: Syntaxfehler (mit Markierung der Position), Datei nicht lesbar oder Zeitlimit erreicht | ```bash if mdgrep 'TODO OR FIXME' src/ -c > /dev/null; then echo "Es gibt noch offene Punkte"; fi ``` --- ## 4. Python-Bibliothek ### 4.1 Nur die Suchsprache ```python from mdgrep import compile_query, QuerySyntaxError q = compile_query('(rust OR go) AND NOT /crypto(currency)?/') q.matches("Rust 2.0 is out") # True q.spans("Rust and Go") # [(0, 4), (9, 11)] → Positionen zum Hervorheben try: compile_query("(rust OR go") except QuerySyntaxError as e: print(e.code, e.position, e.message) # unclosed_paren 0 Missing closing parenthesis ``` - `compile_query(text, max_length=500, max_terms=50)` → `Query` - `Query.matches(text, timeout=None)` → `bool` - `Query.spans(text, timeout=None)` → Liste von `(start, ende)`, sortiert und zusammengefasst; Begriffe unter NICHT werden nicht markiert - Bei Überschreitung von `timeout` (Sekunden) wird `QueryTimeout` ausgelöst ### 4.2 Markdown-Dateien durchsuchen ```python from mdgrep import load_markdown, SearchEngine blocks = load_markdown("docs/", split="list", relative_to="docs/") engine = SearchEngine(blocks) result = engine.search("rust -crypto", limit=20) print(result.total_blocks, "Treffer") for group in result.groups: b = group.block print(f"{b.source}:{b.line}", "".join(s["t"] for s in group.segments)) ``` `load_markdown(pfade, ...)`: | Parameter | Bedeutung | Standard | |---|---|---| | `paths` | Datei, Verzeichnis oder Liste davon | | | `split` | `file` · `heading` · `list` · `paragraph` | `paragraph` | | `meta` | Funktion `pfad → dict`; wird in die Metadaten jedes Blocks der Datei übernommen | | | `relative_to` | Verzeichnis, relativ zu dem `source`/`id` angegeben werden (wichtig für Web: keine Serverpfade preisgeben) | absolute/angegebene Pfade | | `pattern` | Dateimuster in Verzeichnissen | `*.md` | | `dialect` | `commonmark` oder `python-markdown` (siehe 4.6) | `commonmark` | | `encoding` | Zeichenkodierung | `utf-8` | Jeder Block bekommt in `meta` automatisch: `section` (nächste Überschrift darüber), `kind` (`file`/`section`/`item`/`paragraph`) und `markdown` (Original-Quelltext). Durchsucht wird der Text **ohne** Markdown-Zeichen (Links, Fett, Code …). ### 4.3 Eigene Daten durchsuchen mdgrep ist nicht auf Markdown beschränkt. Jede Datenquelle lässt sich als Liste von `Block`-Objekten übergeben: ```python from mdgrep import Block, SearchEngine tickets = [ Block(id="42", title="Ticket 42", text="Login schlägt mit SSO fehl", meta={"status": "offen"}), Block(id="43", title="Ticket 43", text="Dunkelmodus-Schalter defekt", meta={"status": "erledigt"}), ] engine = SearchEngine(tickets) engine.search("login ODER sso", filters={"status": "offen"}) ``` | Feld | Bedeutung | |---|---| | `id` | eindeutige Kennung | | `title` | wird durchsucht und als Überschrift angezeigt (nicht im `text` wiederholen) | | `text` | wird durchsucht und als Ausschnitt angezeigt | | `anchor` | optionale Sprungmarke (`#anchor`), Linkziel im Widget, wenn kein `href` gesetzt ist | | `source`, `line` | Herkunft (Datei, Zeile ab 1) | | `meta` | beliebiges Wörterbuch; wird unverändert durchgereicht, Filter arbeiten darauf | ### 4.4 Suchoptionen ```python result = engine.search( "rust", filters={"lang": ["de", "en"]}, # alle Schlüssel müssen passen; leere Werte = egal predicate=lambda b: b.line > 1, # zusätzlicher eigener Filter group_by=lambda b: b.meta.get("url"), # gleiche Schlüssel → ein Ergebnis, Rest in group.also sort_key=lambda b: b.meta["date"], reverse=True, # neueste zuerst offset=0, limit=30, # Seiten (zählt Gruppen) time_budget=1.5, # Sekunden; danach Teilergebnis mit truncated=True snippet_chars=700, context=120, # lange Texte auf Fenster um die Treffer kürzen ) ``` Das Ergebnis (`SearchResult`): | Attribut | Bedeutung | |---|---| | `groups` | Treffer der aktuellen Seite; je Gruppe `block`, `segments`, `title_segments`, `hits`, `also` | | `total_groups`, `total_blocks` | Anzahl Ergebnisse bzw. Fundstellen insgesamt | | `next_offset` | `offset` der nächsten Seite oder `None` | | `truncated` | `True`, wenn das Zeitbudget aufgebraucht wurde | | `elapsed_ms` | Suchdauer | | `to_dict()` | JSON-fähiges Wörterbuch (Format siehe 5.3) | `segments` ist eine Liste `[{"t": "Text", "hit": False}, {"t": "Treffer", "hit": True}, …]` – fertig zum Anzeigen mit Hervorhebung. ### 4.5 Automatisch neu laden Ändern sich die Dateien, lädt die Suchmaschine sie selbst neu – ohne Neustart: ```python from pathlib import Path def stand(verzeichnis=Path("docs")): dateien = [p.stat() for p in verzeichnis.rglob("*.md")] return len(dateien), max((s.st_mtime for s in dateien), default=0) engine = SearchEngine(lambda: load_markdown("docs", relative_to="docs"), reload_check=stand, reload_interval=60) ``` `reload_check` liefert einen beliebigen vergleichbaren Wert; ändert er sich, wird neu geladen. Geprüft wird höchstens alle `reload_interval` Sekunden. `engine.reload()` lädt sofort neu. ### 4.6 Markdown-Dialekt – wenn Treffer auf HTML-Elemente verlinken Wer Treffer mit Sprungmarken in gerenderten HTML-Seiten verknüpft, braucht dieselbe Aufteilung wie der Markdown-Renderer. Die Renderer unterscheiden sich bei Listen: | | `commonmark` (Standard) | `python-markdown` | |---|---|---| | Liste direkt unter einer Absatzzeile (ohne Leerzeile) | beginnt eine Liste | gehört zum Absatz | | Unterpunkt mit 1–3 Leerzeichen Einrückung | verschachtelt | eigener Punkt auf oberster Ebene | ```python split_markdown(text, split="list", dialect="python-markdown") ``` ### 4.7 Weitere Funktionen | Funktion | Zweck | |---|---| | `split_markdown(text, source=…, split=…, meta=…, dialect=…)` | einen Markdown-Text (statt Dateien) zerlegen | | `markdown_to_text(md)` | Markdown-Zeichen entfernen, Zeilen erhalten | | `segments(text, spans, max_chars=…, context=…)` | Text + Fundstellen → Anzeige-Segmente | | `export_static(zielverzeichnis)` | Widget-Dateien (JS + CSS) kopieren, siehe 6.4 | --- ## 5. Web-API Benötigt `mdgrep[web]`. ### 5.1 Eigenständige App ```python # app.py from mdgrep import SearchEngine, load_markdown from mdgrep.web import create_app engine = SearchEngine(lambda: load_markdown("docs", split="list", relative_to="docs")) app = create_app(engine, filters=["kind"]) # GET /search?q=…&kind=… ``` ### 5.2 In eine bestehende Flask-App einhängen ```python from mdgrep.web import create_blueprint app.register_blueprint(create_blueprint(engine, filters=["lang"]), url_prefix="/api") # → GET /api/search?q=… und Widget-Dateien unter /api/mdgrep-static/ ``` Optionen von `create_blueprint()` (zusätzlich zu `group_by`, `sort_key`, `reverse`, `time_budget`, `snippet_chars`, `context` aus 4.4): | Option | Bedeutung | Standard | |---|---|---| | `filters` | Metadaten-Schlüssel, die als URL-Parameter filterbar sind | keine | | `default_limit` / `max_limit` | Ergebnisse pro Seite / Obergrenze | `30` / `100` | | `max_query_length` | maximale Länge der Suchanfrage | `300` | | `hide_meta` | Metadaten-Schlüssel, die nicht ausgeliefert werden | `("markdown",)` | | `name` | Blueprint-Name (bei mehreren Suchen in einer App) | `mdgrep` | `create_app(engine, url_prefix="", static_dir=None, **optionen)` – mit `static_dir` liefert die App zusätzlich die Dateien dieses Verzeichnisses unter `/` aus (praktisch zum Testen einer statischen Seite). ### 5.3 Anfragen und Antworten ```text GET /search?q=rust%20-crypto&lang=de&offset=0&limit=30 ``` - `q` – Suchanfrage (leer → leeres Ergebnis) - je Name in `filters` ein Parameter: leer oder `all` = keine Einschränkung, `de,en` = mehrere Werte - `offset`, `limit` – Seiten Antwort (HTTP 200): ```json { "query": "rust -crypto", "total_groups": 12, "total_blocks": 15, "offset": 0, "limit": 30, "next_offset": null, "truncated": false, "elapsed_ms": 3.2, "results": [{ "block": { "id": "a.md:4", "title": "…", "anchor": "", "source": "a.md", "line": 4, "meta": {}, "title_segments": [{"t": "…", "hit": false}], "segments": [{"t": "The ", "hit": false}, {"t": "Rust", "hit": true}, {"t": " team …", "hit": false}], "hits": 1 }, "also": [{"id": "b.md:9", "title": "…", "anchor": "", "source": "b.md", "line": 9, "meta": {}}] }] } ``` Fehler (HTTP 400): ```json {"error": {"code": "unclosed_paren", "message": "Missing closing parenthesis", "position": 0}} ``` ### 5.4 Entwicklungsserver ```bash python -m mdgrep.web --dir docs --split list --static site --port 8000 # → http://127.0.0.1:8000/ (Dateien aus site/) # → http://127.0.0.1:8000/api/search (API) ``` | Option | Bedeutung | Standard | |---|---|---| | `--dir` | Verzeichnis mit Markdown-Dateien (Pflicht) | | | `--split` | Split-Modus | `paragraph` | | `--static` | dieses Verzeichnis unter `/` ausliefern; API dann unter `--api-prefix` | | | `--api-prefix` | Pfad der API, wenn `--static` gesetzt ist | `/api` | | `--filters` | kommagetrennte Metadaten-Schlüssel als Filter, z. B. `kind,section` | | | `--host`, `--port` | Adresse | `127.0.0.1`, `8000` | Neue oder geänderte Dateien werden im Entwicklungsserver nach spätestens 5 Sekunden übernommen. Ein vollständiges Beispiel liegt in `examples/`: ```bash python -m mdgrep.web --dir examples/docs --split list --static examples/site ``` --- ## 6. Such-Widget Eine fertige Suchmaske: Ergebnisse erscheinen beim Tippen, veraltete Anfragen werden abgebrochen, die Suche steht in der URL (teilbar, Zurück-Taste funktioniert), Filter, „Mehr laden", aufklappbare Syntaxhilfe, Fehlermeldungen mit Position, Beschriftung Deutsch oder Englisch. Alle Texte werden sicher als Text eingefügt (kein HTML vom Server → kein Cross-Site-Scripting). Keine Abhängigkeiten, kein Framework. ### 6.1 Einbinden ```html
``` Ganz ohne eigenes JavaScript: ```html
``` ### 6.2 Optionen | Option | Bedeutung | Standard | |---|---|---| | `endpoint` | URL der Such-API (relativ oder absolut) | `search` | | `labels` | `'de'`, `'en'` oder Objekt mit einzelnen Texten (siehe 6.5) | `'en'` | | `filters` | Auswahlfelder `[{name, label, options:[{value,label}], value}]`; `name` = URL-Parameter der API | `[]` | | `live` | beim Tippen suchen | `true` | | `debounce` | Wartezeit nach dem letzten Tastendruck (ms) | `300` | | `minChars` | Mindestlänge der Eingabe | `2` | | `limit` | Ergebnisse pro Seite | `30` | | `syncUrl` | Suche und Filter in der Seiten-URL halten | `true` | | `autofocus` | Eingabefeld beim Laden fokussieren | `true` | | `help` | Syntaxhilfe anzeigen | `true` | | `hrefField` | Metadaten-Feld mit dem Linkziel | `href` | | `href(block)` | eigene Funktion für das Linkziel (hat Vorrang) | | | `dateField` | Metadaten-Feld, das als Datum angezeigt wird | | | `badgeFields` | Metadaten-Felder, die als Plaketten angezeigt werden | `[]` | | `formatBadge(feld, wert, block)` | Text einer Plakette anpassen | | | `alsoText(block)` | Linktext in der Liste „auch in …" | Datum bzw. Titel | Ohne `href` und ohne `hrefField`-Wert verlinkt ein Treffer auf `#anchor` des Blocks. ### 6.3 Steuern per JavaScript ```javascript var suche = MdGrep.mount(element, optionen); suche.setQuery('rust -crypto'); // Suchbegriff setzen und suchen suche.search(); // erneut suchen suche.input; // das Eingabefeld element.addEventListener('mdgrep:results', function (e) { console.log(e.detail.total_groups); // vollständige API-Antwort }); ``` Ist die Seite mit `?q=…` aufgerufen worden, sucht das Widget sofort. So kann ein einfaches Formular auf einer anderen Seite zur Suchseite weiterleiten: ```html
``` ### 6.4 Statische Webseiten Liegt die Seite nicht in Flask, die Widget-Dateien einmalig (oder bei jedem Build) kopieren: ```python import mdgrep mdgrep.export_static("site/static") # schreibt mdgrep-widget.js und mdgrep-widget.css ``` Die Dateien werden nur neu geschrieben, wenn sich ihr Inhalt geändert hat. ### 6.5 Aussehen anpassen Farben und Rundungen über CSS-Variablen (an `:root` oder einem umgebenden Element): ```css :root { --mdgrep-text: #1a1d2e; /* Text */ --mdgrep-muted: #6b7394; /* Nebentexte */ --mdgrep-surface: #ffffff; /* Kästchen, Eingabefeld */ --mdgrep-border: #d5d8e3; /* Rahmen */ --mdgrep-accent: #4a7cff; /* Fokus, Hover */ --mdgrep-mark-bg: #ffe58a; /* Hervorhebung */ --mdgrep-mark-text: inherit; --mdgrep-error: #c0392b; /* Fehlermeldungen */ --mdgrep-radius: 0.5rem; } ``` Für einen Dunkelmodus die Variablen auf die eigenen Farbvariablen abbilden, z. B. `--mdgrep-surface: var(--meine-flaeche);` – dann folgt das Widget dem Seitenthema automatisch. Einzelne Texte überschreiben: ```javascript labels: { lang: 'de', none: 'Leider nichts gefunden.', more: 'Weitere Treffer' } ``` --- ## 7. Betrieb auf einem Server Der Flask-Entwicklungsserver ist nur zum Testen gedacht. Für den Dauerbetrieb: ### 7.1 Mit gunicorn ```bash pip install gunicorn gunicorn --workers 2 --bind 127.0.0.1:8790 app:app ``` Jeder Worker hält alle Blöcke im Speicher (Richtwert: ca. 90 MB für 15 MB Markdown / 26 000 Blöcke). ### 7.2 Als systemd-Dienst ```ini # /etc/systemd/system/meine-suche.service [Unit] Description=Volltextsuche (mdgrep) After=network.target [Service] User=meinuser WorkingDirectory=/home/meinuser/projekt ExecStart=/home/meinuser/projekt/venv/bin/gunicorn --workers 2 --bind 127.0.0.1:8790 app:app Restart=on-failure NoNewPrivileges=true PrivateTmp=true [Install] WantedBy=multi-user.target ``` ```bash sudo systemctl daemon-reload && sudo systemctl enable --now meine-suche systemctl status meine-suche journalctl -u meine-suche -n 50 ``` ### 7.3 Hinter nginx mit Ratenbegrenzung ```nginx # http-Kontext, z. B. /etc/nginx/conf.d/suche.conf limit_req_zone $binary_remote_addr zone=suche:10m rate=5r/s; # im server-Block location = /api/search { limit_req zone=suche burst=20 nodelay; limit_req_status 429; proxy_pass http://127.0.0.1:8790/search$is_args$args; proxy_read_timeout 10s; } ``` Das Widget zeigt bei HTTP 429 „Zu viele Anfragen – bitte einen Moment warten." ### 7.4 Sicherheit - **Bösartige reguläre Ausdrücke** (z. B. `/(a|aa)+$/`) können exponentiell lange rechnen. mdgrep begrenzt jede Suche auf `time_budget` (Web-API: 1,5 s) und liefert danach ein Teilergebnis mit `truncated: true`. - **Ratenbegrenzung** im Reverse-Proxy (7.3) verhindert, dass viele Anfragen den Server auslasten. - **Keine Serverpfade** preisgeben: `load_markdown(..., relative_to=…)` verwenden. - **Interne Metadaten** mit `hide_meta` aus Antworten entfernen. - Das Widget fügt Ergebnisse nur als Text ein – auch Markdown mit eingebettetem HTML ist sicher. ### 7.5 Leistung Ohne Index werden alle Blöcke bei jeder Suche durchlaufen. Richtwerte für 26 000 Blöcke (15 MB): einfache Begriffe und Phrasen 30–70 ms, reguläre Ausdrücke und Verknüpfungen 100–150 ms. Für deutlich größere Datenmengen (Hunderte MB) ist ein Index-basiertes Werkzeug besser geeignet. --- ## 8. Voraussetzungen an die Dokumente Welche Bedingungen eine Sammlung (z. B. Markdown-Dateien) erfüllen sollte – getrennt nach „sinnvoll durchsuchen" und „Treffer springen direkt an die Fundstelle". ### 8.1 Für die Suche 1. **Textdateien mit bekannter Kodierung** – Standard UTF-8; sonst `encoding=`. Andere Endungen als `.md` über `-g '*.txt'` bzw. `pattern=`. 2. **Größe passend zum Ansatz** – mdgrep arbeitet ohne Index: alle Blöcke liegen im Speicher und werden bei jeder Suche durchlaufen. Bis etwa 50 MB Text unproblematisch (Richtwert: 15 MB → 30–150 ms pro Suche); bei Hunderten MB ist ein Index-basiertes Werkzeug besser. 3. **Gliederung, die zu einem Split-Modus passt** – die UND/ODER/NICHT-Logik gilt **pro Block**, die Blockgröße bestimmt also die Trefferqualität: | Aufbau der Dokumente | Split-Modus | |---|---| | Absätze, durch Leerzeilen getrennt | `paragraph` | | Einträge als Listenpunkte (Berichte, Changelogs, Protokolle) | `list` | | Abschnitte mit Überschriften (Dokumentation, Wiki) | `heading` | | kurze Einzeldokumente | `file` | Zu große Blöcke machen `a AND b` beliebig (in einer langen Datei steht fast alles), zu kleine verfehlen Begriffe, die in benachbarten Sätzen stehen. 4. **Ableitbare Metadaten** – für Filter (Sprache, Kategorie), Sortierung (Datum) und Gruppierung (gleicher Eintrag in mehreren Dateien) braucht jeder Block Werte in `meta`: - am einfachsten über ein **einheitliches Datei- oder Pfad-Schema** (z. B. `2026_10_12_bericht_de.md`) und `meta=lambda pfad: {…}`; - YAML-Frontmatter wird **nicht** ausgewertet, sondern als Text durchsucht – dafür einen kleinen Adapter schreiben, der Frontmatter liest und `Block`-Objekte erzeugt (siehe 4.3); - zum Zusammenfassen von Duplikaten (`group_by`) einen **stabilen Schlüssel** pro Eintrag (URL, ID). 5. **Grenzen einplanen** - **Keine Wortstammsuche:** „Haus" findet nicht „Häuser" → `/h(au|äu)s/` oder `haus ODER häuser`. - **Keine Relevanz-Sortierung:** die Reihenfolge bestimmt `sort_key` (z. B. Datum). - Markdown-Zeichen (Links, Fett, Code) werden vor der Suche entfernt; **direkt eingebettetes HTML** (`
`) und Tabellen-Striche bleiben als Text stehen. ### 8.2 Für Sprungmarken Damit ein Treffer direkt an die Fundstelle einer gerenderten Seite springt, müssen **zwei Seiten exakt zusammenpassen**: die Blöcke von mdgrep und die IDs im HTML. 1. **Gerenderte Seiten mit vorhersagbarer URL** – aus jeder Quelldatei muss sich ihr HTML-Pfad berechnen lassen (`docs/a/b.md` → `site/a/b.html`). Diesen Pfad mit Sprungmarke in `meta["href"]` schreiben (das Widget verlinkt darauf, siehe 6.2). 2. **Jeder Block hat eine ID, die im HTML existiert** – drei Wege: | Weg | Voraussetzung | Bewertung | |---|---|---| | **Überschriften** (`split="heading"`) | Der Renderer erzeugt Überschriften-IDs mit **demselben** Verfahren wie mdgrep (`slugify`, GitHub-Stil) | wenig Aufwand, aber prüfen: Python-Markdown (`toc`), MkDocs, Hugo und GitHub behandeln Umlaute, Sonderzeichen und doppelte Überschriften unterschiedlich | | **Listenpunkte/Absätze** (`split="list"` / `paragraph`) | Eigenes Nachbearbeitungs-Skript nummeriert die HTML-Elemente nach **derselben Reihenfolge-Regel** wie die Blöcke und setzt `block.anchor` entsprechend | mittlerer Aufwand, robust | | **Text Fragments** (`seite.html#:~:text=…`) | Aktueller Browser; keine Änderung am HTML. Im Widget über die Option `href(block)` bauen | kein Aufwand, aber unzuverlässig bei mehrdeutigem oder umformatiertem Text | > **Bekannte Einschränkung:** `slugify` hängt bei **doppelten** Überschriften kein `-1`, `-2` an > (anders als GitHub). Kommen Überschriften mehrfach vor, springen Treffer zur ersten. 3. **Gleicher Markdown-Dialekt wie der Renderer** – Renderer zerlegen Listen unterschiedlich (siehe 4.6). Passt `dialect` nicht, verschiebt sich die Nummerierung und Links zeigen auf falsche Absätze. 4. **Quelle und HTML auf demselben Stand** – nach jeder Änderung einer Quelldatei deren Seite neu erzeugen; ändert sich die Zählregel, **alle** Seiten. 5. **Einmal vollständig prüfen** – für jeden Block muss die Sprungmarke im HTML existieren **und** auf ein Element zeigen, das seinen Text enthält. Skizze: ```python import re from bs4 import BeautifulSoup def pruefe(blocks, html_von): # html_von(block) → gerendertes HTML der Seite fehler = [] for b in blocks: el = BeautifulSoup(html_von(b), "html.parser").find(id=b.anchor) probe = re.sub(r"\s+", " ", (b.title or b.text))[:40].lower() if el is None or probe not in re.sub(r"\s+", " ", el.get_text()).lower(): fehler.append((b.source, b.anchor)) return fehler ``` Das deckt Formatvarianten älterer Dokumente auf, die man sonst übersieht. ### 8.3 Checkliste | | Bedingung | |---|---| | ☐ | Textdateien, bekannte Kodierung, insgesamt höchstens im zweistelligen MB-Bereich | | ☐ | Gliederung passt zu einem Split-Modus; Blöcke sind sinnvolle Einheiten | | ☐ | Metadaten über einheitliches Datei-Namensschema (oder Adapter) | | ☐ | stabiler Schlüssel pro Eintrag, falls Duplikate zusammengefasst werden | | ☐ | *Sprungmarken:* HTML-Pfad aus Quellpfad berechenbar | | ☐ | *Sprungmarken:* IDs im HTML nach derselben Regel wie die Blöcke, gleicher Dialekt | | ☐ | *Sprungmarken:* HTML nach jeder Änderung neu erzeugt; Konsistenz einmal geprüft | --- ## 9. Rezepte **Notizen nach zwei Begriffen im selben Absatz durchsuchen** ```bash mdgrep 'kubernetes "persistent volume"' ~/notizen ``` **Dateien finden, die A enthalten, aber nicht B** ```bash mdgrep 'docker -podman' ~/wiki --split file -l ``` **Changelog: alle Einträge zu Sicherheitslücken** ```bash mdgrep '/\bCVE-\d{4}-\d+\b/ OR security' CHANGELOG.md --split list ``` **Treffer in einem Skript weiterverarbeiten** ```bash mdgrep 'rust' docs --json | python -c "import json,sys; print([r['block']['source'] for r in json.load(sys.stdin)['results']])" ``` **Suche mit Sprachfilter aus dem Dateinamen** (`bericht_de.md`, `bericht_en.md`) ```python engine = SearchEngine(load_markdown("berichte", split="list", relative_to="berichte", meta=lambda p: {"lang": p.stem.rsplit("_", 1)[-1]})) engine.search("datenschutz", filters={"lang": "de"}) ``` **Gleiche Einträge aus mehreren Dateien zusammenfassen, neueste zuerst** ```python engine.search("rust", group_by=lambda b: b.meta.get("url"), sort_key=lambda b: b.meta["date"], reverse=True) ``` **Praxisbeispiel:** Die Hacker-News-Berichte auf https://jamulix.de/HackerNews/search.html nutzen mdgrep mit einem kleinen Adapter (`hnews_search.py` im Projekt *hackernews*): Stories und Insights als Blöcke, Sprache/Datum/Linkziel als Metadaten, Gruppierung gleicher Stories, Sprungmarken in den HTML-Seiten (`dialect="python-markdown"`). --- ## 10. Fehlermeldungen Ungültige Anfragen liefern einen festen Code (für eigene Übersetzungen), eine Meldung und die Position (0-basiert; das Widget zeigt sie 1-basiert an). | Code | Bedeutung | Beispiel | |---|---|---| | `empty_query` | leere Anfrage | ` ` | | `empty_phrase` | leere Phrase | `""` | | `unterminated_phrase` | schließendes Anführungszeichen fehlt | `"open source` | | `unterminated_regex` | schließender Schrägstrich fehlt | `/abc` | | `invalid_regex` | regulärer Ausdruck ungültig | `/a(b/` | | `missing_operand` | Suchbegriff erwartet | `rust OR` · `AND go` | | `unexpected_paren` | schließende Klammer ohne öffnende | `rust)` | | `unexpected_token` | Operator an unerwarteter Stelle | | | `unclosed_paren` | schließende Klammer fehlt | `(rust OR go` | | `empty_group` | leere Klammern | `()` | | `too_long` | Anfrage zu lang | | | `too_many_terms` | zu viele Suchbegriffe | | Auf der Kommandozeile wird die Stelle markiert: ```text mdgrep: Missing closing parenthesis (rust OR go ^ ``` --- ## 11. Fehlersuche | Problem | Ursache / Lösung | |---|---| | `bash: !…: event not found` | Suchanfrage in **einfache** Anführungszeichen setzen: `mdgrep 'rust !crypto'` | | `go` findet zu viel | Wortbestandteile treffen auch in „Goddard" – ganze Wörter mit `/\bgo\b/` | | `and`/`or` wirken nicht als Operator | Operatoren nur in Großbuchstaben: `AND`, `OR`, `NOT` (oder `UND`, `ODER`, `NICHT`) | | Keine Treffer in `.txt`-Dateien | Standardmuster ist `*.md` → `-g '*.txt'` bzw. `pattern="*.txt"` | | `ModuleNotFoundError: flask` | Web-Teil braucht `pip install "mdgrep[web] @ git+…"` | | Widget zeigt „Die Suche ist fehlgeschlagen" | API nicht erreichbar: `endpoint` prüfen, Browser-Konsole/Netzwerk ansehen | | Widget zeigt „Zu viele Anfragen" | Ratenbegrenzung des Proxys greift (HTTP 429) – kurz warten oder `burst` erhöhen | | „Zeitlimit erreicht" | sehr aufwendiger regulärer Ausdruck; vereinfachen oder `time_budget` erhöhen | | Neue Dateien werden nicht gefunden | `reload_check` gesetzt? Geprüft wird nur alle `reload_interval` Sekunden | | Sprungmarken zeigen auf falsche Stellen | gleichen Markdown-`dialect` wie der HTML-Renderer verwenden (4.6) | | Widget-Darstellung „verbogen" | Seiten-CSS überschreibt Elemente (z. B. `details`, `ol`, `button`); gezielt unter `.mdgrep …` zurücksetzen | --- ## 12. Entwicklung & neue Versionen ```bash git clone https://kitux.de/forgejo/dschlueter/mdgrep.git && cd mdgrep python -m venv .venv && .venv/bin/pip install -e ".[dev]" .venv/bin/pytest # alle Tests ``` Aufbau: ``` src/mdgrep/ ├── query.py # Suchsprache: Tokenizer, Parser, Auswertung, Fundstellen ├── engine.py # Block, SearchEngine, SearchResult ├── snippets.py # Anzeige-Segmente, Kürzen um Treffer ├── markdown.py # Markdown → Blöcke, Markdown → Text ├── cli.py # Kommandozeile ├── web.py # Flask-Blueprint/-App, Entwicklungsserver └── static/ # mdgrep-widget.js, mdgrep-widget.css ``` Neue Version veröffentlichen: 1. Version in `pyproject.toml` **und** `src/mdgrep/__init__.py` erhöhen (bei Widget-Änderungen auch im Kopf von `mdgrep-widget.js`) 2. Tests laufen lassen: `.venv/bin/pytest` 3. Commit, Tag und Push: `git tag -a v0.1.2 -m "mdgrep 0.1.2" && git push origin main --tags` 4. In den nutzenden Projekten den Tag in `requirements.txt` anpassen und `pip install -r requirements.txt` ausführen; laufende Dienste neu starten. Versionsregel: Änderungen an öffentlichen Funktionen, am JSON-Format oder an Widget-Optionen, die bestehende Nutzer brechen, nur mit erhöhter Nebenversion (0.**2**.0) und Hinweis im Commit.