Architektur + Code-Audit a-icking

1. Inventur

Repo ist ~12k LOC Python, nicht 2.8k — der Pipeline-Kern ist klein (~1.5k), aber Fine-Tuning, Evaluation und Daten-Tools sind dick.

  • search_pipeline/ (~1500 LOC, der produktive Kern): cli.py (691, SearchPipeline-Klasse mit run, run_batch, async-Rerank), pg_client.py (488, postgres_search + postgres_hybrid_search), lambda_handler.py (255, SQS-Entry, ThreadPool), lambda_embedding.py/lambda_rerank.py (~430 zusammen, boto3-Wrapper auf Container-Lambdas), pg_jobs.py (140, sauber), audit_logger.py (161, DSGVO-Log), query_preprocessor.py (260, Maß-Extraktion DN/mm/m²).
  • lambdas/ (~320 LOC, drei dünne Handler): api_handler (SQS-Enqueue), auth_handler (HMAC-API-Key gegen Secrets Manager), status_handler (DB-Read, eigene psycopg2-Connection, eigener Health-Check).
  • models/embedding/ + models/reranking/ (~150 LOC + Dockerfiles, Container-Lambdas mit BGE-M3 / BGE-Reranker — Standard SentenceTransformer-Pattern, in Ordnung).
  • fine-tuning/ (~2700 LOC): SageMaker-Launcher, lokale Training-Skripte, gaeb_extractor.py (607), labeling_cli.py (512). Eigener Mini-Stack neben der Pipeline.
  • Eval-Scripts im Root (~2500 LOC, evaluate_baseline.py, evaluate_pipeline_local.py, generate_training_data.py, benchmark_rerankers.py, reembed_catalog.py) — Datascience-Scratchpad, kein App-Code.
  • terraform/ mit dev1/prod/shared/global + ~11 Modulen — Struktur ist OK, nicht inspiziert.
  • Tot/quasi-tot: run_batch und der ganze Hybrid-Pfad (postgres_hybrid_search, SearchMode.HYBRID, query_preprocessor.build_keyword_boost_query) werden vom Production-Handler nicht aufgerufen — nur von Benchmarks und labeling_cli. Das sind ~350 LOC Code, die niemand schaltet.

2. Architektur — was rauswürfe

  • SQS für synchronen Use-Case. Client postet, bekommt job_id, pollt /status/{id} (README empfiehlt 2 s Interval, Job-Lebensdauer 5–25 s). Das ist ein Sync-Request mit drei zusätzlichen Hops, drei Audit-Log-Stellen und einer Jobs-Tabelle, die niemand braucht. Direkt-Invoke der Pipeline-Lambda mit synchronem Response (oder Function URL) spart ~5–6 s Polling-Overhead pro Call.
  • Drei Lambdas wo eine reicht. Embedding-Lambda und Rerank-Lambda sind beide Container, die warm gehalten werden müssen. Embedding + Rerank gemeinsam in einer Container-Lambda (oder direkt im Search-Pipeline-Container) eliminiert zwei boto3-invoke-Hops und macht Trace+Debug trivial.
  • Pipeline-Lambda nutzt run statt run_batch. Ironie: cli.py hat eine fertige optimierte Batch-Methode mit get_embedding_batch (1 Lambda-Call statt N) und asyncio.gather-Rerank. Der Production-Handler ignoriert sie und feuert für jeden Query separat SearchPipeline().run() im ThreadPool, der intern wieder einzelne sync-boto3.invoke-Calls macht. Toter Optimierungs-Pfad neben der echten Code-Bahn.
  • asyncio + aioboto3 in cli.run_batch parallel zu ThreadPool in lambda_handler. Zwei Concurrency-Modelle, die einander widersprechen. Einer fliegt raus.

3. Code-Qualität (Findings)

  • pg_client.py:124,138,143,158,312,349,364,372,407 — SQL via f-string. Vektor, top_k, score_threshold, Schema-/Tabellenname, sogar query_text (mit halbgarem replace("'", "''")) werden in den Query-String formatiert. Blocker. Über die API kommt zwar nur ein Vektor (numerisch validiert) und kein roher Text in postgres_search — aber postgres_hybrid_search und labeling_cli nehmen User-Text. Sobald jemand Hybrid scharfschaltet: Injection-Vektor. pg_jobs.py macht es nebenan korrekt mit psycopg.sql — das Muster existiert im Repo, wurde nur nicht angewendet.
  • lambda_handler.py:171 — ThreadPool um synchrones boto3. ThreadPoolExecutor um SearchPipeline().run(), das intern blockierende boto3.invoke macht. Funktioniert, ist aber teurer als ein einziger Batch-Embedding-Call + async Rerank, der schon im Code liegt (run_batch). Major.
  • cli.py:11–20 — try/except-Import. Zwei Import-Pfade weil der Lambda-Zip die Files flach legt, das Package aber als search_pipeline.cli läuft. Funktioniert, riecht. Minor.
  • DB-Connection-Management dreigleisig. pg_client.py psycopg3 + ConnectionPool (autocommit) + lru_cache-Config; pg_jobs.py greift auf denselben Pool zu, ruft aber conn.commit() trotz autocommit (No-Op, harmlos); lambdas/status_handler/handler.py benutzt psycopg2 (nicht 3), globale Single-Connection mit _conn.closed == 0-Check, kein Pool, kein SSL-Erzwingen. Major — inkonsistent, jeder Lambda kennt nur einen Teil des Wahrheitsbilds.
  • status_handler/handler.py:30 kein sslmode=require, der direkte Fallback in pg_jobs._get_db_conn:24 setzt es — Inkonsistenz. Major im Kontext einer DSGVO-Pipeline mit Audit-Logging.
  • pg_client.py:46 Pool max_size=10 global auf Modul-Ebene, aber lambda_handler.py:171 startet bis zu 5 Threads, jeder ruft init_job/update_partial_result/complete_job plus einen Search — bei concurrent Lambdas summiert sich das schnell auf die RDS-Verbindungsgrenze. Major.
  • Hardcoded/streng env-getrieben: MAX_TOP_K, MAX_QUERY_LENGTH, MAX_TEXT_LENGTH, MAX_PARALLEL_WORKERS, MAX_CONCURRENT_RERANKS, SEARCH_TOP_K, SEARCH_LIMIT, AWS_REGION-Default eu-central-1 an drei Stellen kopiert. Jede neue Env-Var ist eine neue Stelle die schiefgehen kann. Minor.
  • audit_logger.py schreibt strukturierte Logs, aber api_key_hash=None wird vom Handler immer übergeben — die ganze Pseudonymisierungs-Logik ist ungenutzt. Minor.
  • Test-Coverage realistisch: 5 Unit-Files, ~1100 LOC Tests, davon mocken nur test_cli.py + test_hybrid_search.py + test_query_preprocessor.py echt. test_api.py und test_workflow.py sind Integration gegen die echte API mit time.sleep(5) zwischen jedem Test. Heisst: Tests laufen nur wenn die Stage läuft. Major — du kannst nichts lokal regressionssicher refactoren.

4. Was fehlt fürs “5-Minuten-Debug-Versprechen”

  • Kein .github/-Verzeichnis, also keine CI, kein automatisches Test-Run, kein Lint, kein Deploy-Trigger. Terraform muss manuell laufen.
  • Keine Tracing-Spans (X-Ray würde mit 3 Zeilen aws-xray-sdk greifen, gerade weil drei Lambdas hintereinander hängen — genau dafür gebaut). Logs sind Plain-Text, kein structured-logging, keine Correlation-IDs (job_id ist vorhanden, wird aber nicht konsistent durch alle Lambdas geloggt).
  • Health-Endpoint ist halbgar: status_handler antwortet auf /health, prüft DB. Prüft aber nicht Embedding-/Rerank-Lambda und nicht SQS. Wenn die Pipeline-Lambda hängt sagt der Health-Check trotzdem healthy.
  • Keine Cloudwatch-Alarms im Repo definiert.
  • Schema-Migrations laufen manuell über apply_migrations.py — nicht im Deploy-Pipeline.
  • Keine ONBOARDING.md, kein ARCHITECTURE.md.

5. Kill-or-Keep

Keep (in einen Neubau mitnehmen):

  • Terraform-Module-Struktur (envs/modules/stacks), das ist solide aufgesetzt.
  • models/embedding/app.py + models/reranking/app.py + die zwei Dockerfiles.
  • db/migrations/00*.sql — pgvector + HNSW + jobs-Tabelle.
  • query_preprocessor.py Maß-Extraktion (DN/mm/m²) — Fachlogik die Wert hat.
  • auth_handler/handler.py — 79 LOC, HMAC-Vergleich, sauber.
  • Eval-Scripts als Referenz für Quality-Gates (nicht als Production-Code).
  • Den gesamten Fine-Tuning-Tract als separates Repo/Sub-Projekt — der hat mit der Online-Pipeline nichts zu tun.

Kill (im Neubau nicht reproduzieren):

  • SQS + Jobs-Tabelle + Polling-Pattern.
  • Getrennte Embedding-Lambda und Rerank-Lambda. Eine Container-Lambda mit beiden Modellen.
  • pg_client.py als f-string-Builder — neu mit psycopg.sql + Parameter-Binding.
  • cli.py-Doppel-Code-Bahn (run und run_batch und Hybrid).
  • Drei DB-Connect-Patterns.
  • audit_logger Pseudonymisierungs-Logik die nie scharfgeschaltet wird.

Fazit

Die Behauptung “Qualität passt nicht” stimmt — aber der technische Schaden ist überschaubar. Der produktive Kern ist ~1500 LOC, davon 300–400 tot, 200 SQL-Injection-Reparatur, der Rest funktioniert.

Was wirklich nicht stimmt sind die Architektur-Entscheidungen (SQS, getrennte Lambdas, Polling-Pattern, kein CI) — und die kosten Latenz, Lambda-Minuten und Debug-Zeit. Empfehlung: Rewrite wäre machbar in 2-3 Tagen, aber Refactor genauso schnell und mit weniger Risiko. Schema + Container-Modelle + Terraform-Module + auth_handler + query_preprocessor übernehmen, Rest neu strukturieren.