Files
search/search/README.md
T
2026-10-17 18:32:59 +02:00

9.4 KiB
Raw Blame History

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

  1. Retrieval: Die Anfrage wird per websearch_to_tsquery geparst und per @@ gegen pages.search_vector über den GIN-Index ausgewertet. Nutzersyntax: wort1 wort2 (UND), "exakte phrase", OR, -ausschluss. Da die Suchvektoren mit der Textsuch-Konfiguration simple gebaut werden (DB-Trigger), matched die Suche ohne Stemming – exakte Lexeme, Groß-/Kleinschreibung egal.

  2. 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_WEIGHTS einstellbar (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 aus pages.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_MS nicht fertig (z. B. Einzelwort mit zigtausend Treffern), wird automatisch ein zweiter Versuch über die ersten JONASSEARCH_MAX_RANK_CANDIDATES (Default 5000) Kandidaten gefahren – die Antwort kommt dann garantiert, die Antwortmarke ranking_capped: true zeigt die Einschränkung an.

  3. Filter (kombinierbar): site= (Domain inkl. Subdomains), tld=, after=/before= (ISO-Datum oder 24h/7d/30d/365d), nur http_status = 200 und nur HTML-Inhalt (jeweils abschaltbar).

  4. Snippets: ts_headline wird 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/stats nutzt nur Schätzwerte aus pg_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.