fix: Bank-Suche verlor die besten Treffer beim Kuerzen

Der Fehler, den der Nutzer gefunden hat: Die Suche nach "n" lieferte
"genau 200" Treffer und darunter nie N26. Ursache war die Reihenfolge –
alphabetisch sortiert und ERST DANN bei 200 gekuerzt. "N26" liegt hinter
"Aachener Bank", fiel also hinter zweihundert Banken heraus, die irgendwo
ein n im Namen tragen. Die Bank war die ganze Zeit da.

Jetzt wird zuerst bewertet (genauer Name > Namensanfang > Wortanfang >
Teiltreffer > nur BIC/Gruppe) und erst danach gekuerzt. Ausserdem wird die
ECHTE Trefferzahl gemeldet statt der gekuerzten – "genau 200" sah aus wie ein
Suchergebnis, war aber die Obergrenze.

Ein Regressionstest stellt den Fall nach: 300 Banken mit "n" im Namen,
alphabetisch vor N26. Ohne den Fix faellt er um.

Nebenbei geprueft und ausgeschlossen: /aspsps blaettert nicht, die Liste ist
also vollstaendig; Sandbox und alphabetische Kuerzung waren es auch nicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-26 22:10:41 +02:00
co-authored by Claude Opus 5
parent f53efc4767
commit beb1bb3419
5 changed files with 98 additions and 25 deletions
+41 -22
View File
@@ -65,6 +65,26 @@ def _client(db: Session, user: User) -> BankClient:
MAX_TREFFER = 200
def _rang(name: str, begriff: str, in_name: bool) -> int:
"""Wie gut passt ein Name zum Suchbegriff? Kleiner ist besser.
Ohne Rangfolge entscheidet das Alphabet, welche Treffer die Obergrenze
überleben – und dann fällt „N26" bei der Suche nach „n" hinter 200 Banken
heraus, deren Name irgendwo ein n enthält. Genau dieser Fehler hat hier
einen Abend gekostet.
"""
klein = name.lower()
if klein == begriff:
return 0
if klein.startswith(begriff):
return 1
if any(wort.startswith(begriff) for wort in klein.replace("-", " ").split()):
return 2
if begriff in klein:
return 3
return 4 if not in_name else 3
def banken(db: Session, user: User, land: str | None = None,
suche: str | None = None) -> dict:
"""Banken suchen – in einem Land oder über alle hinweg.
@@ -75,43 +95,42 @@ def banken(db: Session, user: User, land: str | None = None,
"""
roh = _client(db, user).banken(land)
begriff = (suche or "").strip().lower()
treffer = []
passend = []
for b in roh:
name = b.get("name")
if not name:
continue
# Auch im BIC und im Gruppennamen suchen: Banken heissen in der Liste
name = str(name)
# Auch BIC und Gruppenname durchsuchen: Banken heissen in der Liste
# nicht immer so wie im Alltag.
heu = " ".join(str(x) for x in (name, b.get("bic"), b.get("group")) if x).lower()
if begriff and begriff not in heu:
neben = " ".join(str(x) for x in (b.get("bic"), b.get("group")) if x).lower()
if begriff and begriff not in name.lower() and begriff not in neben:
continue
treffer.append({"name": name, "country": b.get("country"),
"logo": b.get("logo"), "psu_types": b.get("psu_types", []),
"beta": bool(b.get("beta"))})
treffer.sort(key=lambda b: (b["name"].lower(), b.get("country") or ""))
# „gesamt" und eine Kostprobe mitliefern: Sonst sieht „keine Treffer"
# gleich aus, egal ob die Liste leer kam, der Suchbegriff nicht passte –
# oder die Anwendung als Sandbox angelegt wurde und lauter Test-Institute
# liefert. An der Kostprobe sieht man Letzteres sofort.
# Beispiele ueber den ganzen Bereich streuen, nicht nur den Anfang zeigen.
# Endet die Liste alphabetisch bei „B", ist sie abgeschnitten – und man
# sucht sonst ewig nach einer Bank, die schlicht nicht mitgeliefert wurde.
passend.append({
"name": name, "country": b.get("country"), "logo": b.get("logo"),
"psu_types": b.get("psu_types", []), "beta": bool(b.get("beta")),
"_rang": _rang(name, begriff, True) if begriff else 3,
})
# Erst nach Güte, dann alphabetisch – und ERST DANN kürzen.
passend.sort(key=lambda b: (b["_rang"], b["name"].lower(), b.get("country") or ""))
gezeigt = [{k: v for k, v in b.items() if k != "_rang"} for b in passend[:MAX_TREFFER]]
namen = sorted({str(b.get("name")) for b in roh if b.get("name")})
beispiele = (namen[:3] + namen[len(namen) // 2: len(namen) // 2 + 2] + namen[-3:]
if len(namen) > 8 else namen)
return {
"gesamt": len(roh),
"treffer": treffer[:MAX_TREFFER],
"gekuerzt": len(treffer) > MAX_TREFFER,
# Die ECHTE Trefferzahl, nicht die gekürzte. Sonst steht dort immer
# „200" und man haelt die Obergrenze für ein Suchergebnis.
"treffer_gesamt": len(passend),
"treffer": gezeigt,
"gekuerzt": len(passend) > MAX_TREFFER,
"beispiele": beispiele,
"von": namen[0] if namen else None,
"bis": namen[-1] if namen else None,
# Fängt überhaupt eine Bank mit diesen Buchstaben an? Wenn nicht, fehlt
# der Teil des Alphabets, und der Suchbegriff ist unschuldig.
"anfangsbuchstaben": sorted({n[:1].upper() for n in namen}),
# Alles mit demselben Anfangsbuchstaben wie der Suchbegriff. Damit
# sieht man, unter welchem Namen die gesuchte Bank dort wirklich steht
# – Raten über Namensvarianten hat lange genug gedauert.
"gleicher_buchstabe": (
[n for n in namen if begriff and n[:1].lower() == begriff[:1]][:40]
if begriff else []