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.
|
# 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
@@ -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
@@ -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
@@ -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,27 +755,15 @@ 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"],
|
||||||
|
", degradiert" if ranking_degraded else "",
|
||||||
)
|
)
|
||||||
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)."
|
|
||||||
)
|
|
||||||
|
|
||||||
if with_snippets and rows:
|
if with_snippets and rows:
|
||||||
ids = [r["id"] for r in rows[:per_page]]
|
ids = [r["id"] for r in rows[:per_page]]
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user