10 KiB
search
das komplette serverjonas search
crawler.py– PostgreSQL-Crawler, baut den Suchindex auflauncher.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.mdweb/– Statistik-Webseite (Port 8081,jonassearch-stats.service). Seiten werden alle 30 Min. gezählt; Bilder alle 2 h exakt, dazwischen nur geschätzt. Die Image-Diagramme zeigen standardmäßig nur exakte Messpunkte; ein umschaltbarer, im Browser gespeicherter Schalter kann Schätzwerte in allen Image-Diagrammen, Records und der Heatmap einblenden. Mit jeder Messung wird zusätzlich die Anzahl derpending-Einträge incrawl_queueerfasst (exakterCOUNT, keine Schätzung) und mit dem kompletten Diagrammsatz (Verlauf, Change/5 min, kumulativ, pro Stunde, Tagesstunden, Records, Heatmap) dargestellt. Die Hauptseite hat drei Tabs mit Symbolen (Pages, Images, Queue): Jeder Tab zeigt nur die Kennzahlen und Diagramme seines Datentyps, die Auswahl wird im Browser gespeichert. CSV/JSON-Export folgt dem aktiven Tab. Über das Feld „Reach target“ lässt sich eine beliebige Zielzahl eingeben; die Seite schätzt aus dem Trend der letzten zwei Stunden (des gewählten Zeitraums), wann dieser Stand erreicht ist – inklusive Datum und Uhrzeit. Das Ziel wird pro Tab im Browser gespeichert.web/tui.py– Terminal-Dashboard für dieselben Statistiken (siehe unten)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-Triggerpages_search_vector_update()into_tsvector()einfließt (Postgres hartes Limit: 1.048.575 Bytes protsvector)add_pages_pictures.sql– einmalig laufen lassen: legtpages.pictures(text[]) für die Bild-URLs der gecrawlten Seiten an (idempotent)
Terminal-Dashboard (web/tui.py)
Zeigt Seiten- und Bildanzahl, Wachstum, einen Sparkline-Verlauf und die
letzten Messwerte direkt im Terminal. Liest dieselbe
web/page_counts.json wie der Statserver – kein Webserver, keine
zusätzlichen Pakete (nur curses aus der Standardlib).
python3 web/tui.py # interaktiv, Aktualisierung alle 2 s
python3 web/tui.py --window 7d # 24h (Default) | 7d | 30d | all
python3 web/tui.py --once # Momentaufnahme, z. B. für Logs/Skripte
python3 web/tui.py --once --width 100 --window all
Tasten im interaktiven Modus: q beendet, r lädt die Datei sofort neu,
t wechselt das Zeitfenster. --ascii ersetzt die Unicode-Blöcke der
Sparkline. Läuft die Ausgabe nicht in einem Terminal (Pipe, Cron), wird
automatisch die --once-Ausgabe verwendet. Fehlt die Datei, zeigt das
Dashboard „No samples yet“; ist sie defekt, erscheint die Fehlermeldung
im Dashboard (bei --once auf stderr, Exit-Code 1).
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:
- DB-Trigger (empfohlen, einmalig einspielen):
fix_pages_search_vector_trigger.sqlersetzt die Trigger-Funktion so, dass nur die ersten 200.000 Zeichen into_tsvector()einfließen. Der volle Text bleibt incontentgespeichert, 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'; - Im Crawler:
save_page()kappelt den Inhalt vor dem Insert zusätzlich aufCONTENT_INDEX_MAX_CHARS(200.000 Zeichen) – wirkt sofort, auch ohne Trigger-Fix. Überlange Seiten werden dann normal gespeichert. - Fehlerbehandlung: Sollte der Insert trotzdem an einem Datenbank-Limit
scheitern, fängt der Worker
ProgramLimitExceededab, loggt die URL als Warnung, rollt die Transaktion zurück, markiert den Queue-Eintrag alsfailed(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:
-
Claim mit Host-Check: Beim Holen des nächsten Queue-Eintrags prüft der Worker, 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 beiJONASSEARCH_MAX_IDLE_SLEEP, dann wird neu gescannt).Wichtig bei großen Queues: Die bekannten blockierten Hosts werden direkt in der Claim-Query ausgeschlossen (nicht nur innerhalb des kleinen Kandidatenfensters von
JONASSEARCH_CLAIM_SCAN_LIMIT). Blockierte Zeilen bleiben sonst alspendingoben in der Prioritätsreihenfolge stehen: Ein Host mit Tausenden wartenden URLs würde dann bei jedem Rescan dasselbe Fenster füllen und alle anderen Hosts der Queue unsichtbar machen. HöchstensJONASSEARCH_CLAIM_BUSY_HOST_LIMITHosts (die mit der längsten Restwartezeit) gehen in die Query; weitere werden weiterhin pro Kandidat übersprungen. -
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. -
Retry statt Verlust: Transiente Fehler (429/5xx/Netzwerk) werden wieder in die Queue gestellt (
pending) statt endgültigfailed– begrenzt durchJONASSEARCH_MAX_ATTEMPTS(Standard 5), damit dauerhaft kaputte Hosts nicht ewig rotieren. Wartezeiten größer alsJONASSEARCH_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.
Bildersuche (pages.pictures)
Der Crawler speichert zu jeder Seite die gefundenen Bild-URLs in
pages.pictures: absolute http(s)-URLs, pro Seite dedupliziert, ohne
URL-Fragmente. Erfasst werden <img src>, gängige Lazy-Loading-Attribute
(data-src, data-lazy-src, data-original), srcset (auch in
<picture><source>), og:image/twitter:image sowie
<link rel="image_src">. Relative Pfade werden gegen die Seiten-URL
aufgelöst; data:, javascript:, ftp: und nicht-öffentliche Hosts
werden verworfen. Die Bilder selbst werden nicht heruntergeladen oder
lokal gespeichert.
Einmalige Migration der Datenbank:
psql -h 127.0.0.1 -p 25432 -U jonassearch -d jonassearch \
-f add_pages_pictures.sql
Die Migration ist idempotent (ADD COLUMN IF NOT EXISTS) und lässt
bestehende Zeilen unverändert. Bereits gecrawlte Seiten erhalten ihre
Bilder beim nächsten Crawl; bis dahin behandeln die Abfragen NULL und
'{}' identisch als „keine Bilder“. Die Websuche ist davon unberührt.
Die Suchmaschine bietet dafür den Tab Bilder (GET /api/images).
Pro Quellseite werden höchstens JONASSEARCH_IMAGES_PER_SOURCE_PAGE
Bilder angezeigt; die Kacheln laden die Bilder direkt von der
Original-URL (Lazy Loading, defekte Bilder werden ausgeblendet). Details
stehen in search/README.md.
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:
- Einmalig initialisieren:
python3 crawler.py --init-deployment - Launcher starten:
python3 launcher.py(startet + überwacht den Crawler) - Neue Version verteilen:
- Neue
crawler.pymit erhöhterJONASSEARCH_CRAWLER_VERSIONbereitstellen (Update-Quelle im.envsetzen oder Datei nachreleases/<VERSION>/crawler.pylegen) 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)
- Neue
- Alle laufenden alten Crawler merken das beim nächsten sicheren Punkt
(vor jedem relevanten Commit bzw. spätestens nach
JONASSEARCH_UPDATE_CHECK_SECONDSim Hintergrund-Check), rollen ihre offene Transaktion sauber zurück und beenden sich mit Exit-Code 42 – nie mitten in einer Transaktion, kein hartes Killen. - 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