commit local changes
This commit is contained in:
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user