commit local changes

This commit is contained in:
2026-10-11 19:43:25 +02:00
parent 19730e1711
commit c5f963f08f
4 changed files with 97 additions and 23 deletions
+1 -1
View File
@@ -125,7 +125,7 @@ WORKERS = int(os.getenv("JONASSEARCH_WORKERS", "3"))
# Minimum delay between requests to the same host, per worker. # Minimum delay between requests to the same host, per worker.
# robots.txt can specify a larger crawl delay. # robots.txt can specify a larger crawl delay.
DEFAULT_DELAY = float(os.getenv("JONASSEARCH_DELAY", "3.0")) DEFAULT_DELAY = float(os.getenv("JONASSEARCH_DELAY", "0.0"))
ROBOTS_CACHE_SECONDS = int( ROBOTS_CACHE_SECONDS = int(
os.getenv("JONASSEARCH_ROBOTS_CACHE_SECONDS", str(24 * 60 * 60)) os.getenv("JONASSEARCH_ROBOTS_CACHE_SECONDS", str(24 * 60 * 60))
+4 -1
View File
@@ -443,8 +443,11 @@ async function run() {
" · keine durchsuchbaren Begriffe gefunden"; " · keine durchsuchbaren Begriffe gefunden";
const capHint = data.ranking_capped ? const capHint = data.ranking_capped ?
" · Ranking auf Top-Kandidaten eingegrenzt" : ""; " · Ranking auf Top-Kandidaten eingegrenzt" : "";
const degradedHint = data.ranking_degraded ?
" · Trefferreihenfolge vereinfacht (Suchbegriffe sehr häufig)" : "";
$("info").textContent = totalTxt + " · Seite " + data.page + $("info").textContent = totalTxt + " · Seite " + data.page +
" · " + (data.took_ms / 1000).toFixed(2) + " s" + emptyHint + capHint; " · " + (data.took_ms / 1000).toFixed(2) + " s" + emptyHint +
capHint + degradedHint;
if (!data.results.length) { if (!data.results.length) {
$("results").innerHTML = $("results").innerHTML =
+2 -2
View File
@@ -244,8 +244,8 @@ def main(argv=None):
print(f"Fehlerhafte Anfrage: {exc}", file=sys.stderr) print(f"Fehlerhafte Anfrage: {exc}", file=sys.stderr)
return 2 return 2
except engine.SearchTimeout: except engine.SearchTimeout:
print("Zeitlimit überschritten – bitte eingrenzen (site=, nach=, " print("Die Suche dauert ungewöhnlich lange. Neuer Versuch oder ein "
"weitere Begriffe).", file=sys.stderr) "Filter (site=, nach=, weitere Begriffe) hilft.", file=sys.stderr)
return 4 return 4
except engine.SearchEngineError as exc: except engine.SearchEngineError as exc:
print(f"Suchfehler: {exc}", file=sys.stderr) print(f"Suchfehler: {exc}", file=sys.stderr)
+90 -19
View File
@@ -112,6 +112,19 @@ RANK_NORMALIZATION = int(os.getenv("JONASSEARCH_RANK_NORMALIZATION", "8"))
# Ranking dann innerhalb dieses Fensters (Antwortflag ranking_capped). # Ranking dann innerhalb dieses Fensters (Antwortflag ranking_capped).
MAX_RANK_CANDIDATES = int(os.getenv("JONASSEARCH_MAX_RANK_CANDIDATES", "5000")) MAX_RANK_CANDIDATES = int(os.getenv("JONASSEARCH_MAX_RANK_CANDIDATES", "5000"))
# Notfall-Stufe: selbst das Kandidatenfenster ist zu groß (extrem häufige
# Begriffe). Dann wird in Etappen immer weiter reduziert, bis die Antwort
# innerhalb des Zeitlimits kommt. Der Nutzer bekommt trotzdem Ergebnis-
# seiten statt einer Fehlermeldung – bei reduzierter Trefferqualität
# (Antwortflag ranking_degraded).
RANK_CAP_RETRIES = int(os.getenv("JONASSEARCH_RANK_CAP_RETRIES", "3"))
# Sortierung "relevance" fällt bei Überschreitung aller Ranking-Timeouts
# auf diese Basisreihenfolge zurück (kein ts_rank_cd nötig).
FALLBACK_SORT_SQL = (
"COALESCE(p.crawled_at, p.discovered_at) DESC NULLS LAST, p.id ASC"
)
# ts_rank_cd-Gewichte. Reihenfolge im DB-Trigger: Titel = A, # ts_rank_cd-Gewichte. Reihenfolge im DB-Trigger: Titel = A,
# Überschriften = B, Inhalt = C, (D = ungewichteter Rest). # Überschriften = B, Inhalt = C, (D = ungewichteter Rest).
# Format: "A=1.0,B=0.6,C=0.3,D=0.1" # Format: "A=1.0,B=0.6,C=0.3,D=0.1"
@@ -572,6 +585,30 @@ def _headline_sql():
""" """
def _emergency_search_sql(vector_col, filter_sql):
"""
Notfall-Query ganz ohne ts_rank_cd: Kandidaten werden in fester
Basisreihenfolge (neueste zuerst) geliefert. Auch bei extrem häufigen
Begriffen garantiert schnell, weil kein Ranking über die Treffermenge
nötig ist.
"""
return f"""
SELECT p.id, p.url, p.title, p.domain, p.tld,
p.http_status, p.content_type,
COALESCE(p.crawled_at, p.discovered_at) AS crawled_at,
0.0 AS text_rank,
1.0 AS freshness,
0.0 AS score
FROM pages p
CROSS JOIN LATERAL (
SELECT websearch_to_tsquery(%(ts_config)s, %(query)s) AS tsq
) q
WHERE p.{vector_col} @@ q.tsq{filter_sql}
ORDER BY {FALLBACK_SORT_SQL}
LIMIT %(limit)s OFFSET %(offset)s
"""
def _count_sql(vector_col, filter_sql): def _count_sql(vector_col, filter_sql):
return f""" return f"""
SELECT count(*) AS n SELECT count(*) AS n
@@ -600,6 +637,50 @@ def _round(value, digits=6):
# ÖFFENTLICHE API # ÖFFENTLICHE API
# ============================================================ # ============================================================
def _fallback_search(cur, vector_col, sort, filter_sql, params):
"""
Mehrstufiger Fallback, wenn das volle Ranking das Zeitlimit
überschritten hat. Liefert so gut wie immer Ergebnis-Seiten statt
eines harten Timeout-Fehlers:
1. Kandidatenfenster MAX_RANK_CANDIDATES (gs. 5000):
normales, aber begrenztes Ranking.
2. Danach in Etappen verkleinern (z. B. 2000 → 800 → 320 …), bis
die Antwort innerhalb des Zeitlimits kommt.
3. Letzte Stufe: ganz ohne ts_rank_cd-Ranking – nur Index-Kandidaten
in definierter Reihenfolge (z. B. neueste zuerst).
Rückgabe: (ranking_capped, ranking_degraded, rows).
"""
cap = int(params.get("rank_cap") or MAX_RANK_CANDIDATES)
capped = True
degraded = False
# Stufe 2: Fenster in Etappen verkleinern.
for _ in range(max(0, RANK_CAP_RETRIES)):
try:
cur.execute(_search_sql(vector_col, sort, filter_sql, capped=True),
params)
return capped, degraded, cur.fetchall()
except psycopg2.errors.QueryCanceled:
cap = max(50, int(cap * 0.4))
params["rank_cap"] = cap
degraded = True
# Stufe 3: Notfall ohne Ranking – Kandidaten in Basisreihenfolge.
try:
cur.execute(
_emergency_search_sql(vector_col, filter_sql), params,
)
return capped, True, cur.fetchall()
except psycopg2.errors.QueryCanceled:
raise SearchTimeout(
"Die Suche dauert ungewöhnlich lange – probiere es nochmal "
"oder nutze einen Filter wie site=<domain>, um schneller zu "
"Ergebnissen zu kommen."
)
def search(query, *, page=1, per_page=None, sites=None, tlds=None, def search(query, *, page=1, per_page=None, sites=None, tlds=None,
after=None, before=None, sort="relevance", after=None, before=None, sort="relevance",
include_errors=False, include_non_html=False, include_errors=False, include_non_html=False,
@@ -665,6 +746,7 @@ def search(query, *, page=1, per_page=None, sites=None, tlds=None,
snippets = {} snippets = {}
total = None total = None
ranking_capped = False ranking_capped = False
ranking_degraded = False
if not empty_tsq: if not empty_tsq:
_set_statement_timeout(cur, STATEMENT_TIMEOUT_MS) _set_statement_timeout(cur, STATEMENT_TIMEOUT_MS)
@@ -673,26 +755,14 @@ def search(query, *, page=1, per_page=None, sites=None, tlds=None,
params) params)
rows = cur.fetchall() rows = cur.fetchall()
except psycopg2.errors.QueryCanceled: except psycopg2.errors.QueryCanceled:
# Sehr häufiger Begriff: volles Ranking über alle ranking_capped, ranking_degraded, rows = _fallback_search(
# Kandidaten ist zu langsam. Zweiter Versuch über cur, vector_col, sort, filter_sql, params,
# ein begrenztes Kandidatenfenster (bounded latency). )
log.info( log.info(
"Volles Ranking hat das Zeitlimit überschritten; " "Volles Ranking übersprungen (Timeout); "
"Fallback auf %d Kandidaten.", MAX_RANK_CANDIDATES "Fallback: %d Kandidaten%s.",
) params["rank_cap"],
ranking_capped = True ", degradiert" if ranking_degraded else "",
try:
cur.execute(
_search_sql(vector_col, sort, filter_sql,
capped=True),
params,
)
rows = cur.fetchall()
except psycopg2.errors.QueryCanceled:
raise SearchTimeout(
"Suchanfrage hat das Zeitlimit überschritten – "
"bitte weiter eingrenzen (z. B. site=, after= "
"oder zusätzliche Begriffe)."
) )
if with_snippets and rows: if with_snippets and rows:
@@ -744,6 +814,7 @@ def search(query, *, page=1, per_page=None, sites=None, tlds=None,
"per_page": per_page, "per_page": per_page,
"sort": sort, "sort": sort,
"ranking_capped": ranking_capped, "ranking_capped": ranking_capped,
"ranking_degraded": ranking_degraded,
"total": total, "total": total,
"has_more": has_more, "has_more": has_more,
"results": results, "results": results,