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:
parent
d9f81ac918
commit
ec4bfc5939
1 changed files with 748 additions and 0 deletions
746
BEDIENUNGSANLEITUNG.md
Normal file
746
BEDIENUNGSANLEITUNG.md
Normal 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.
|
||||
Loading…
Reference in a new issue