2026-10-09 16:42:15 +02:00
2026-10-09 16:42:15 +02:00
2026-10-09 16:42:15 +02:00
2026-10-04 01:13:13 +02:00
2026-10-09 16:42:15 +02:00
2026-09-13 01:57:46 +02:00
2026-10-09 16:42:15 +02:00
2026-10-09 16:42:15 +02:00
2026-09-13 18:01:58 +02:00
2026-09-13 01:57:46 +02:00
2026-09-02 16:14:24 +00:00
2026-10-09 16:42:15 +02:00
2026-10-09 16:42:15 +02:00

search

das komplette serverjonas search

  • crawler.py – PostgreSQL-Crawler, baut den Suchindex auf
  • launcher.py – startet/überwacht den Crawler und rollt Updates aus (siehe unten)
  • search/ – Suchmaschine darüber (CLI + HTTP-API + Weboberfläche, Port 8083), siehe search/README.md
  • web/ – Statistik-Webseite (Port 8081, jonassearch-stats.service)
  • priorityweb/ – Vorschlagsverfeinerer (Port 8082, jonassearch-priority.service)
  • search/jonassearch-engine.service – systemd-Unit für die Suchmaschine (Port 8083, jonassearch-engine.service)
  • fix_pages_search_vector_trigger.sql – einmalig laufen lassen: kappelt den Text, der im DB-Trigger pages_search_vector_update() in to_tsvector() einfließt (Postgres hartes Limit: 1.048.575 Bytes pro tsvector)

Sehr große Seiten / tsvector-Limit

PostgreSQL begrenzt einen einzelnen tsvector-Wert hart auf 1.048.575 Bytes (nicht konfigurierbar). Der Trigger pages_search_vector_update() baut diesen aus der content-Spalte – Seiten mit sehr viel Text (beobachtet: ~2 MB, z. B. pkg.go.dev/...?tab=importedby) haben den Insert dadurch mit ProgramLimitExceeded abgebrochen und alle Worker crashen lassen.

Dagegen wirken drei Maßnahmen zusammen:

  1. DB-Trigger (empfohlen, einmalig einspielen): fix_pages_search_vector_trigger.sql ersetzt die Trigger-Funktion so, dass nur die ersten 200.000 Zeichen in to_tsvector() einfließen. Der volle Text bleibt in content gespeichert, nur der Suchindex ist gekürzt. Vor dem Einspielen die eigene Funktions-Version prüfen: SELECT prosrc FROM pg_proc WHERE proname = 'pages_search_vector_update';
  2. Im Crawler: save_page() kappelt den Inhalt vor dem Insert zusätzlich auf CONTENT_INDEX_MAX_CHARS (200.000 Zeichen) – wirkt sofort, auch ohne Trigger-Fix. Überlange Seiten werden dann normal gespeichert.
  3. Fehlerbehandlung: Sollte der Insert trotzdem an einem Datenbank-Limit scheitern, fängt der Worker ProgramLimitExceeded ab, loggt die URL als Warnung, rollt die Transaktion zurück, markiert den Queue-Eintrag als failed (kein endloses Wiederholen) und läuft weiter, statt zu sterben.

Zusätzlich werden bekannt wertlose URLs (pkg.go.dev/...?tab=importedby – Megabyte-große reine Rückverweis-Listen) über SKIP_URL_PATTERNS schon vor dem Fetch übersprungen und als done markiert.

Domain-Switching / Block-Handling

Der Crawler wartet nie auf einer langsamen oder blockierten Domain:

  1. Claim mit Host-Check: Beim Holen des nächsten Queue-Eintrags prüft der Worker pro Kandidat (Prioritätsreihenfolge, bis zu JONASSEARCH_CLAIM_SCAN_LIMIT Kandidaten), ob der Host gerade crawlbar ist (Per-Host-Delay aus robots.txt bzw. JONASSEARCH_DELAY, aktive Backoff-Sperre). Nicht crawlbare Hosts werden übersprungen – der Worker arbeitet automatisch auf einer anderen Domain weiter und kehrt zurück, sobald der Host wieder darf. Ist gerade gar kein Host crawlbar, schläft der Worker bis zum frühesten freigegebenen Host (gekappet bei JONASSEARCH_MAX_IDLE_SLEEP, dann wird neu gescannt).
  2. Backoff bei Blocks: HTTP 429/5xx/403/401 und Netzwerkfehler setzen den Host auf exponentielles Backoff (z. B. 429: 60 s, sich verdoppelnd bis 30 min; 403/401: 10 min bis 2 h). Ein Retry-After-Header wird zusätzlich beachtet. Bei einem erfolgreichen Fetch zählt der Zähler wieder von vorn.
  3. Retry statt Verlust: Transiente Fehler (429/5xx/Netzwerk) werden wieder in die Queue gestellt (pending) statt endgültig failed – begrenzt durch JONASSEARCH_MAX_ATTEMPTS (Standard 5), damit dauerhaft kaputte Hosts nicht ewig rotieren. Wartezeiten größer als JONASSEARCH_MAX_INLINE_WAIT (z. B. großer robots-crawl-delay) werden nicht inline geschlafen: das Item wird ohne Versuchs-Zählung zurückgestellt und solange eine andere Domain gecrawlt.

Nicht crawlbare Ziele (localhost, IPs, nicht-echte Domains)

Der Crawler folgt nur echten, öffentlich auflösbaren Domains. Bereits in normalize_url() werden abgelehnt und nie in die Queue gestellt:

  • localhost, *.localhost, localhost.localdomain, *.local, *.internal
  • reine IP-Adressen als Host (IPv4 und IPv6, z. B. 127.0.0.1, http://[::1]/)
  • Hostnamen ohne Punkt (Einzel-Labels wie http://myserver/ – LAN-Namen)

Alte Queue-Einträge mit solchen Hosts werden beim Zugriff als done markiert und nicht mehr geholt. Zeigt der Start-URL auf so einen Host, beendet sich der Crawler mit einem klaren Fehler (Exit 1) statt endlos zu rotieren.

Rolling Updates (Generation-Mechanismus)

Zentrale Tabelle (eine Zeile, id = 1):

CREATE TABLE IF NOT EXISTS deployment_state (
    id INTEGER PRIMARY KEY,
    generation BIGINT NOT NULL,
    required_version INTEGER NOT NULL
);

Ablauf:

  1. Einmalig initialisieren: python3 crawler.py --init-deployment
  2. Launcher starten: python3 launcher.py (startet + überwacht den Crawler)
  3. Neue Version verteilen:
    • Neue crawler.py mit erhöhter JONASSEARCH_CRAWLER_VERSION bereitstellen (Update-Quelle im .env setzen oder Datei nach releases/<VERSION>/crawler.py legen)
    • python3 launcher.py --check-update → neue Version wird gestaget → geprüft (--self-update) → aktiviert → erst dann wird die Generation in PostgreSQL erhöht (--update-deployment)
  4. Alle laufenden alten Crawler merken das beim nächsten sicheren Punkt (vor jedem relevanten Commit bzw. spätestens nach JONASSEARCH_UPDATE_CHECK_SECONDS im Hintergrund-Check), rollen ihre offene Transaktion sauber zurück und beenden sich mit Exit-Code 42 – nie mitten in einer Transaktion, kein hartes Killen.
  5. Der jeweilige Launcher erkennt 42 und startet die bereits bereitgestellte neue Version, die die neue Generation beim Start liest.

Exit-Codes des Crawlers: 0 = normal beendet, 1 = Fehler/Crash, 42 = ausschließlich „Update erforderlich".

Nützliche Befehle:

python3 crawler.py --version              # installierte Version
python3 launcher.py --status              # Generation + Versionen anzeigen
python3 crawler.py --self-update 2        # gestagte Release prüfen (ohne zu aktivieren)
python3 crawler.py --update-deployment 2  # Generation+1 und required_version=2 setzen
S
Description
das komplette serverjonas search
Readme MIT
538 KiB
Languages
Python 98.1%
Shell 1.3%
PLpgSQL 0.6%