mdgrep/BEDIENUNGSANLEITUNG.md
dschlueter ec4bfc5939 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>
2026-10-12 19:55:30 +02:00

27 KiB
Raw Blame History

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
  2. Suchsyntax
  3. Kommandozeile
  4. Python-Bibliothek
  5. Web-API
  6. Such-Widget
  7. Betrieb auf einem Server
  8. Rezepte
  9. Fehlermeldungen
  10. Fehlersuche
  11. Entwicklung & neue Versionen

1. Installation

Voraussetzung: Python 3.10 oder neuer, git (für die Installation aus dem Repository).

# 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:

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 „Goddard". 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:

(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

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

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):

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

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

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:

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

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:

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

# 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

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

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):

{
  "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):

{"error": {"code": "unclosed_paren", "message": "Missing closing parenthesis", "position": 0}}

5.4 Entwicklungsserver

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/:

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

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

<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

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:

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

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):

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

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

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

# /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
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

# 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

mdgrep 'kubernetes "persistent volume"' ~/notizen

Dateien finden, die A enthalten, aber nicht B

mdgrep 'docker -podman' ~/wiki --split file -l

Changelog: alle Einträge zu Sicherheitslücken

mdgrep '/\bCVE-\d{4}-\d+\b/ OR security' CHANGELOG.md --split list

Treffer in einem Skript weiterverarbeiten

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)

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

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:

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

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.