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.
# 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(
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";
const capHint = data.ranking_capped ?
" · Ranking auf Top-Kandidaten eingegrenzt" : "";
const degradedHint = data.ranking_degraded ?
" · Trefferreihenfolge vereinfacht (Suchbegriffe sehr häufig)" : "";
$("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) {
$("results").innerHTML =
+2 -2
View File
@@ -244,8 +244,8 @@ def main(argv=None):
print(f"Fehlerhafte Anfrage: {exc}", file=sys.stderr)
return 2
except engine.SearchTimeout:
print("Zeitlimit überschritten – bitte eingrenzen (site=, nach=, "
"weitere Begriffe).", file=sys.stderr)
print("Die Suche dauert ungewöhnlich lange. Neuer Versuch oder ein "
"Filter (site=, nach=, weitere Begriffe) hilft.", file=sys.stderr)
return 4
except engine.SearchEngineError as exc:
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).
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,
# Überschriften = B, Inhalt = C, (D = ungewichteter Rest).
# 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):
return f"""
SELECT count(*) AS n
@@ -600,6 +637,50 @@ def _round(value, digits=6):
# Ö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,
after=None, before=None, sort="relevance",
include_errors=False, include_non_html=False,
@@ -665,6 +746,7 @@ def search(query, *, page=1, per_page=None, sites=None, tlds=None,
snippets = {}
total = None
ranking_capped = False
ranking_degraded = False
if not empty_tsq:
_set_statement_timeout(cur, STATEMENT_TIMEOUT_MS)
@@ -673,26 +755,14 @@ def search(query, *, page=1, per_page=None, sites=None, tlds=None,
params)
rows = cur.fetchall()
except psycopg2.errors.QueryCanceled:
# Sehr häufiger Begriff: volles Ranking über alle
# Kandidaten ist zu langsam. Zweiter Versuch über
# ein begrenztes Kandidatenfenster (bounded latency).
ranking_capped, ranking_degraded, rows = _fallback_search(
cur, vector_col, sort, filter_sql, params,
)
log.info(
"Volles Ranking hat das Zeitlimit überschritten; "
"Fallback auf %d Kandidaten.", MAX_RANK_CANDIDATES
)
ranking_capped = True
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)."
"Volles Ranking übersprungen (Timeout); "
"Fallback: %d Kandidaten%s.",
params["rank_cap"],
", degradiert" if ranking_degraded else "",
)
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,
"sort": sort,
"ranking_capped": ranking_capped,
"ranking_degraded": ranking_degraded,
"total": total,
"has_more": has_more,
"results": results,