188 lines
9.4 KiB
Markdown
188 lines
9.4 KiB
Markdown
# 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](search/README.md)
|
||
- `web/` – Statistik-Webseite (Port 8081, `jonassearch-stats.service`).
|
||
Seiten werden alle 30 Min. gezählt; Bilder alle 2 h exakt, dazwischen nur
|
||
geschätzt. Alle Diagramme zeigen ausschließlich die exakten Messpunkte.
|
||
- `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-Trigger `pages_search_vector_update()` in `to_tsvector()` einfließt
|
||
(Postgres hartes Limit: 1.048.575 Bytes pro `tsvector`)
|
||
- `add_pages_pictures.sql` – einmalig laufen lassen: legt `pages.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).
|
||
|
||
```bash
|
||
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:
|
||
|
||
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, 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).
|
||
|
||
**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 als `pending` oben 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öchstens `JONASSEARCH_CLAIM_BUSY_HOST_LIMIT` Hosts (die mit der längsten
|
||
Restwartezeit) gehen in die Query; weitere werden weiterhin pro Kandidat
|
||
übersprungen.
|
||
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.
|
||
|
||
## 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:
|
||
|
||
```bash
|
||
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](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`):
|
||
|
||
```sql
|
||
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:
|
||
|
||
```bash
|
||
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
|
||
``` |