docs: add German user manual (BEDIENUNGSANLEITUNG.md)

Installation, query syntax, CLI, Python API, Web API, widget, server
operation (gunicorn, systemd, nginx), security, recipes, error codes,
troubleshooting and release workflow. Linked from README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
dschlueter 2026-10-12 19:55:30 +02:00
commit ec4bfc5939
2 changed files with 748 additions and 0 deletions

746
BEDIENUNGSANLEITUNG.md Normal file
View file

@ -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<br>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
<link rel="stylesheet" href="/api/mdgrep-static/mdgrep-widget.css">
<div id="suche"></div>
<script src="/api/mdgrep-static/mdgrep-widget.js"></script>
<script>
MdGrep.mount(document.getElementById('suche'), {
endpoint: '/api/search',
labels: 'de',
filters: [
{ name: 'lang', label: 'Sprache', options: [
{ value: '', label: 'Alle' }, { value: 'de', label: 'Deutsch' }, { value: 'en', label: 'Englisch' } ] },
],
badgeFields: ['lang'],
dateField: 'date',
});
</script>
```
Ganz ohne eigenes JavaScript:
```html
<div data-mdgrep data-endpoint="/api/search" data-labels="de"
data-filters='[{"name":"kind","label":"Art","options":[{"value":"","label":"Alle"},{"value":"item","label":"Listenpunkte"}]}]'
data-badge-fields="kind"></div>
```
### 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
<form action="suche.html"><input type="search" name="q"><button>Suchen</button></form>
```
### 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.

View file

@ -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