diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md new file mode 100644 index 0000000..04809c5 --- /dev/null +++ b/BEDIENUNGSANLEITUNG.md @@ -0,0 +1,746 @@ +# 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. [Rezepte](#8-rezepte) +9. [Fehlermeldungen](#9-fehlermeldungen) +10. [Fehlersuche](#10-fehlersuche) +11. [Entwicklung & neue Versionen](#11-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. 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"`). + +--- + +## 9. 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 + ^ +``` + +--- + +## 10. 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 | + +--- + +## 11. 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. diff --git a/README.md b/README.md index cdfacc5..377767f 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ Boolean full-text search over Markdown (and any other text) – as a **Python li - protection against catastrophic regular expressions (time limit per search) - no search index: blocks live in memory, a few MB of text are searched in milliseconds +Ausführliche deutsche Anleitung: [BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md) + ## Installation ```bash