9.4 KiB
JonasSearch Suchmaschine (search/)
Die eigentliche Websuchmaschine über der PostgreSQL-Datenbank jonassearch:
Volltextsuche auf dem vorhandenen tsvector-Index (pages.search_vector,
GIN-Index idx_pages_search) mit mehrstufigem Ranking, Filtern, Snippets,
Bildersuche (Tab „Bilder“), Weboberfläche, HTTP-API und Kommandozeile.
| Baustein | Zweck | Port |
|---|---|---|
search/api.py |
HTTP-API + Weboberfläche | 8083 |
search/cli.py |
Kommandozeile (ohne Webserver) | – |
search/engine.py |
Suchlogik (von api/cli genutzt) | – |
search/jonassearch-engine.service |
systemd-Dienst | 8083 |
Bestehende Dienste: Statserver 8081 (web/app.py), Vorschlagsverfeinerer
8082 (priorityweb/app.py), Suchmaschine 8083.
Wie gesucht wird
-
Retrieval: Die Anfrage wird per
websearch_to_tsquerygeparst und per@@gegenpages.search_vectorüber den GIN-Index ausgewertet. Nutzersyntax:wort1 wort2(UND),"exakte phrase",OR,-ausschluss. Da die Suchvektoren mit der Textsuch-Konfigurationsimplegebaut werden (DB-Trigger), matched die Suche ohne Stemming – exakte Lexeme, Groß-/Kleinschreibung egal. -
Ranking (in SQL):
score = ts_rank_cd(Gewichte, search_vector, tsquery, Norm=8) – Titel=A, Überschriften=B, Inhalt=C * (1 + 0.25 · exp(−Alter_in_Tagen / 180)) – Aktualität (crawled_at/discovered_at)Die Gewichte sind über
JONASSEARCH_RANK_WEIGHTSeinstellbar (A=1.0,B=0.6,C=0.3,D=0.1). Die ts_rank_cd-Normalisierung (Bitmaske 8 = Rang / (1 + log Dokumentlänge),JONASSEARCH_RANK_NORMALIZATION) verhindert, dass riesige Seiten kurze, präzise Treffer dominieren. Nicht in die Formel eingeflossen (noch): Link-Autorität auspages.links/source_url– dafür liegt mit dem gespeicherten Webgraph die Grundlage bereit.Fallback bei sehr häufigen Begriffen: Ist das volle Ranking über alle Kandidaten nach
JONASSEARCH_STATEMENT_TIMEOUT_MSnicht fertig (z. B. Einzelwort mit zigtausend Treffern), wird automatisch ein zweiter Versuch über die erstenJONASSEARCH_MAX_RANK_CANDIDATES(Default 5000) Kandidaten gefahren – die Antwort kommt dann garantiert, die Antwortmarkeranking_capped: truezeigt die Einschränkung an. -
Filter (kombinierbar):
site=(Domain inkl. Subdomains),tld=,after=/before=(ISO-Datum oder24h/7d/30d/365d), nurhttp_status = 200und nur HTML-Inhalt (jeweils abschaltbar). -
Snippets:
ts_headlinewird nur für die Treffer der angeforderten Seite berechnet –content(TOAST-lastig) wird nie für die ganze Kandidatenmenge gelesen. Markierungen im Snippet:⟦…⟧.
Bildersuche
Der Tab Bilder (GET /api/images) nutzt denselben Suchvektor,
dieselben Filter und dieselbe Relevanz-/Frischeformel wie die Websuche.
Die Bild-URLs stammen aus pages.pictures, das der Crawler füllt
(absolute http(s)-URLs aus <img>, srcset, <picture>, og:image,
twitter:image, link rel="image_src"; Migration:
add_pages_pictures.sql im Projektstamm). Kandidatenseiten werden also
anhand von Titel/Überschriften/Inhalt gefunden, nicht anhand des
Bildinhalts.
Pro Quellseite werden höchstens JONASSEARCH_IMAGES_PER_SOURCE_PAGE
Bilder geliefert (Default 4), damit Seiten mit vielen Bildern die
Ergebnisse nicht dominieren. Die Gesamtzahl ist eine Schätzung
(SUMME(LEAST(cardinality(pictures), Limit)); total: null bei
Timeout). Bei einem Ranking-Timeout greift derselbe begrenzte
Kandidaten-Fallback wie bei der Websuche. Die Bilder werden direkt von
der Original-URL geladen – kein Proxy, kein Download, keine lokale
Bildspeicherung.
Schutz der Produktivdatenbank (~10 GB)
- Jede Operation nutzt eine eigene Verbindung mit
statement_timeout(Default 10 s); Zeitüberschreitung → HTTP 504 bzw. CLI-Hinweis zum Eingrenzen, niemals eine blockierte Datenbank. - Die Gesamt-Trefferzahl läuft mit eigenem, kürzerem Timeout (4 s) und
ist best effort: bei Überschreitung liefert die API
total: null(„viele Treffer“), die Ergebnisse bleiben unbeeinflusst. /api/statsnutzt nur Schätzwerte auspg_class/pg_stats, keine Full-Scans.
HTTP-API (Port 8083)
| Endpunkt | Beschreibung |
|---|---|
GET / |
Weboberfläche (Suchseite, Deutsch, Dark/Light) |
GET /api/search |
Suche – Parameter siehe unten |
GET /api/images |
Bildersuche – gleiche Filter wie /api/search, liefert image_url, page_url, title, domain |
GET /api/suggest?q=begr |
Autocomplete (Präfix auf letztem Wort, to_tsquery 'begr:*') |
GET /api/page?id=42 oder ?url=… |
gespeicherte Seite (Inhalt gekürzt) |
GET /api/stats |
Index-Statistik (Seiten, Domains, Queue, DB-Größe) |
GET /health |
Healthcheck inkl. DB-Status |
Parameter von /api/search:
| Parameter | Bedeutung | Default |
|---|---|---|
q |
Suchbegriffe (Pflicht) | – |
page, per_page |
Paginierung (max 50/Seite) | 1, 10 |
site |
Domain-Filter, mehrfach möglich (site=example.com) |
– |
tld |
TLD-Filter, mehrfach möglich (tld=de) |
– |
after / before |
Zeitfilter: ISO oder 24h/7d/2w |
– |
sort |
relevance (Default) oder freshness |
relevance |
include_errors=1 |
auch Seiten ohne HTTP 200 | aus |
include_non_html=1 |
auch Nicht-HTML-Dokumente | aus |
no_snippets=1 |
Snippets weglassen (schneller) | aus |
Beispiel:
curl "http://localhost:8083/api/search?q=volltextsuche&site=postgresql.org&after=30d"
Antwort (Auszug):
{
"query": "volltextsuche",
"parsed_query": "'volltextsuche'",
"page": 1, "per_page": 10,
"total": 1234,
"has_more": true,
"results": [
{
"id": 42, "url": "https://…", "title": "…",
"domain": "…", "tld": ".de", "http_status": 200,
"crawled_at": "2026-09-30T18:12:03+00:00",
"score": 0.081203, "text_rank": 0.078341, "freshness": 1.074,
"snippet": "… PostgreSQL ⟦Volltextsuche⟧ mit tsvector …"
}
],
"took_ms": 31.4
}
Parameter von /api/images sind q (Pflicht), page, per_page
(Default 40, max 100), site, tld, after/before und sort.
Antwort von /api/images (Auszug):
{
"query": "katze", "page": 1, "per_page": 40,
"total": 128, "has_more": true,
"results": [
{
"image_url": "https://…/katze.jpg",
"page_url": "https://…/katzen",
"title": "…", "domain": "…", "tld": ".de",
"crawled_at": "2026-09-30T18:12:03+00:00",
"score": 0.42, "text_rank": 0.40, "freshness": 1.05
}
]
}
Fehler: 400 ungültige Parameter · 404 Seite unbekannt ·
503 Datenbank nicht erreichbar · 504 Zeitlimit (bitte eingrenzen).
Kommandozeile
python3 search/cli.py "postgresql volltextsuche"
python3 search/cli.py "crawler" --site example.com --sort freshness --limit 20
python3 search/cli.py "python" --after 7d
python3 search/cli.py --suggest "suchmasch"
python3 search/cli.py --stats
python3 search/cli.py --page-id 42
python3 search/cli.py "test" --json
Installation als Dienst
sudo cp /srv/serverjonas/search/search/jonassearch-engine.service \
/etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now jonassearch-engine.service
journalctl -u jonassearch-engine -f
Port-Auflösung: --port (manueller Start) > JONASSEARCH_API_PORT
(.env) > 8083. Der systemd-Dienst pinnt JONASSEARCH_API_PORT=8083
nach dem EnvironmentFile und überschreibt damit einen eventuell
abweichenden Wert aus .env – der Dienst läuft garantiert auf 8083.
Konfiguration (.env)
| Variable | Default | Bedeutung |
|---|---|---|
JONASSEARCH_DB_HOST/PORT/NAME/USER/PASSWORD |
wie crawler.py | Datenbankzugriff |
JONASSEARCH_API_HOST |
0.0.0.0 |
Bind-Adresse |
JONASSEARCH_API_PORT |
8083 |
Port (Dienst pinnt 8083) |
JONASSEARCH_TS_CONFIG |
simple |
Muss zur Trigger-Konfiguration des Suchvektors passen! |
JONASSEARCH_VECTOR_COLUMN |
auto | tsvector-Spalte erzwingen (sonst automatische Erkennung) |
JONASSEARCH_RANK_WEIGHTS |
A=1.0,B=0.6,C=0.3,D=0.1 |
ts_rank_cd-Gewichte |
JONASSEARCH_FRESHNESS_WEIGHT |
0.25 |
Stärke des Aktualitätsbonus |
JONASSEARCH_FRESHNESS_HALF_LIFE_DAYS |
180 |
Halbwertszeit der Aktualität |
JONASSEARCH_RANK_NORMALIZATION |
8 |
ts_rank_cd-Normalisierung (8 = 1+log Länge, 0 = aus) |
JONASSEARCH_MAX_RANK_CANDIDATES |
5000 |
Kandidaten-Fenster des Ranking-Fallbacks |
JONASSEARCH_STATEMENT_TIMEOUT_MS |
10000 |
Timeout pro Suchstatement |
JONASSEARCH_COUNT_TIMEOUT_MS |
4000 |
Timeout der Trefferzählung |
JONASSEARCH_RESULTS_PER_PAGE |
10 |
Treffer pro Seite |
JONASSEARCH_MAX_RESULTS_PER_PAGE |
50 |
Maximum pro Seite |
JONASSEARCH_IMAGE_RESULTS_PER_PAGE |
40 |
Bildkacheln pro Ergebnisseite |
JONASSEARCH_MAX_IMAGE_RESULTS_PER_PAGE |
100 |
Maximum Bildkacheln pro Seite |
JONASSEARCH_IMAGES_PER_SOURCE_PAGE |
4 |
Höchstens so viele Bilder pro Quellseite |
JONASSEARCH_HEADLINE_SOURCE_CHARS |
100000 |
content-Anteil für ts_headline |
Tests
python3 search/test_engine.py
python3 search/test_api.py
Getestet werden die DB-freien Teile: Parameter-Normalisierung, Gewichte,
Zeitfilter, SQL-Zusammensetzung (Web- und Bildersuche), Query-Parsing
sowie Routing/Fehler-Mapping des /api/images-Endpunkts und die
Einbindung des Bilder-Tabs. Alles ohne Datenbankzugriff.