Performance-Audit a-icking Search-Pipeline
TL;DR
Der BENCHMARK.md-Breakdown ist falsch geframed. Die 12s sind nicht “5–6s Polling-Overhead”, sondern eine Mischung aus Retry-After: 5 (zwingt Client zu 5s Wartezeit), Pipeline-Lambda-Cold-Start (512 MB, NICHT provisioned), Polling-Round-Trips und SQS-Hop. Bei voll warmem System schafft die Pipeline 1.5–2.5s. Architektur ist für ~1s-Latenz übertrieben und sabotiert sich selbst.
1. Latenz-Budget bei “kalt” (12s realistisch)
| Phase | Geschätzt | Wo |
|---|---|---|
| API-Gateway → API-Handler-Lambda (128 MB, kalt) | 200–400 ms | lambdas/api_handler/handler.py:1 |
| SQS-Hop (send + EventSourceMapping-Poll) | 300–800 ms | searchpipeline/main.tf:72 batch_size=1, kein Long-Poll-Tuning |
| Search-Pipeline-Lambda Cold-Start (Container 512 MB) | 2.5–4.0 s | searchpipeline/main.tf:35 — 512 MB deutlich zu wenig für Container-Image |
_get_lambda_client() lazy init + erster boto3-Call SigV4 | 200–400 ms | lambda_embedding.py:36-47 |
| Embedding-Lambda warm (Provisioned, ~2 GB BGE-M3 schon geladen) | 80–150 ms | models/embedding/app.py:15-28 |
| pgvector HNSW Query (RDS, Pool kalt beim ersten Hit) | 200–800 ms | pg_client.py:117-168 |
| Rerank-Lambda warm (Provisioned, Reranker schon geladen) | 400–900 ms | models/reranking/app.py:30-35 (BGE-reranker-base CPU, 20 Kandidaten) |
Retry-After: 5 Client wartet 5s vor erstem Poll | 5.000 ms | api_handler/handler.py:79 |
| Polling-Overhead (alle 2 s) | 0–2 s | Client-seitig |
Summe Cold-Path: ~10–18 s — passt zu den 12 s. Warm-Path ohne Polling: ~1.5–2.5 s.
Erste Quick-Wins schon vor Refactor:
Retry-After: 1statt 5 → spart 4s sofort- Pipeline-Lambda 2048 MB statt 512 → spart 1-2s Cold-Start + 200-400ms Steady-State
- Models eager im Init laden statt lazy → spart 2-4s bei erstem echten Request (gratis, Init-Phase wird nicht gebillt)
2. Top-5 Hotspots
-
searchpipeline/main.tf:35— Search-Pipeline-Lambda hat 512 MB. Container-Image-Lambda mit boto3 + psycopg + asyncio braucht 1024–2048 MB damit Init <1 s ist. AWS-Lambda-CPU skaliert linear mit Memory bis ~1.8 GB → mit 512 MB hat die Function ~0.3 vCPU. Gewinn: 1–2 s Cold-Start, 200–400 ms Steady-State. -
models/embedding/app.py:15-28undmodels/reranking/app.py:30-35— Lazy Model Load. Das BGE-M3-Model wird erst beim ersten Request geladen, nicht in der Init-Phase. Bei Provisioned Concurrency egal (warm), aber wenn man PC abschaltet relevant: Cold-Start zählt damit voll im User-Path. Fix:_get_model()in Modul-Top-Level aufrufen (Lambda gibt 10 s Init-Phase außerhalb der Billing). Gewinn: 2–4 s beim ersten echten Request nach PC-Aus. -
api_handler/handler.py:75-83— SQS+Polling für 10s-Workload. Komplette SQS+Jobs-Tabelle+Polling-Maschinerie für eine Operation die in 30s-API-Gateway-Timeout passt. Jeder Hop addiert 200–800 ms. Gewinn: 1–2 s wenn synchron, plus Wegfall desRetry-After: 5-Lags clientseitig. -
cli.py:413—asyncio.run()pro Request. Im Lambda-Handler wird inlambda_handler.py:171einThreadPoolExecutormitmax_workers=5aufgemacht und jeder Worker macht incli.py:413ein eigenesasyncio.run()mit eigener aioboto3-Session. Bei Single-Query wird Batch-Code gar nicht genutzt —pipeline.run()(Zeile 155) geht den synchronen Pfad und ruftrerank_documentssync auf. Async-Overhead ohne Async-Nutzen. Jeder Thread öffnet eine eigene aioboto3-Session → 50–150 ms Setup pro Query. Gewinn: 100–500 ms pro Query. -
pg_client.py:124,138,161— SQL-Injection durch f-String + Vector im Klartext. Der Vector mit 1024 Floats wird als String in den Query injected (vector_str = f"[{','.join(...)}]"→ ~10–20 KB Query-Text). Damit wird Postgres’ Prepared-Statement-Cache useless, jede Query ist ein neuer Parse+Plan. Fix:cursor.execute(sql, (vector_param,))mit psycopg-Adapter. Außerdem:SET search_pathin eigener Cursor-Roundtrip (pg_client.py:122) ist ein extra Round-Trip pro Request. Gewinn: 30–100 ms pro Query plus saubere Security.
3. Quality-Probleme
- Embedding-Spalte uneinheitlich. Index existiert auf
kurztext_embedding,langtext_embedding,suchwort_embedding. Suche nutzt nurlangtext_embedding. Konsistent mit Reranking-Doc — abersuchwort_embeddingundkurztext_embeddingsind tote Indizes die nur Storage+Insert-Last erzeugen. Entweder Hybrid-Multi-Index oder weg. - Hybrid-Search-Implementierung in
pg_client.py:339-410solide aufgebaut (Semantic-Candidates → Phrase + OR-Match, Score-Normalisierung über Window-Function). Aber:candidate_pool = top_k * 3zieht nur 60 Kandidaten — bei m=32 ist das eng. Wenn das exakte Keyword-Match nicht in den Top-60-Semantic-Hits ist, findet’s hybrid nie. Für “DN 100 vs DN 150”-Problematik unzureichend. - HNSW-
ef_searchwird nirgends gesetzt.006_tune_hnsw_indexes.sqltuntm/ef_construction, aberef_search(Query-Time-Recall) bleibt auf Default 40. Für Recall sinnvoll:SET hnsw.ef_search = 100per Session. - Fine-Tuned-Model: commit
1669bb5deployt fine-tuned model, abermodels/embedding/app.py:11zeigt hardcodedMODEL_DIR=/opt/models/bge-m3undMODEL_ID=BAAI/bge-m3. Wenn das Fine-Tuned-Model wirklich live ist, ist es nur über CodeBuild-Bake-Step ins Image gekommen — die env-Variable lügt. Prüfen: ist das deployte Image-Tag wirklich der fine-tuned? Embedding-Dimensionvector(1024)passt zu BGE-M3 — aber Katalog-Embeddings müssen mit demselben Model gemacht worden sein wie Query-Embeddings, sonst silent Recall-Verlust.
4. Architektur-Kritik
SQS+Jobs+Polling rechtfertigt sich nur wenn (a) Operation >30s dauert, (b) Burst-Smoothing nötig ist, oder (c) Retry-Semantik garantiert sein muss. Hier ist nichts davon der Fall. Eine 10s-Pipeline (warm: 2s) passt in API-Gateway-29s-Timeout. Polling verschlechtert Wahrnehmungs-Latenz um 2–7 s und addiert vier Komponenten (SQS, Jobs-Tabelle, Status-Lambda, Polling-Client-Logik) die alle Fehlerquellen sind.
Die zwei separaten Modell-Lambdas (Embedding + Rerank) sind eine Anti-Optimierung: Beide laden PyTorch+SentenceTransformers (~500 MB Boot-Footprint), beide haben getrennte Cold-Starts. Wenn Embedding und Rerank in einem Container liefen, wäre der Cold-Start nur einmal zu zahlen (~3-4s statt ~6-7s) und der Lambda-Invoke-Hop entfällt — 100–300 ms pro Call. Memory: 6 GB → ~8 GB. Bei warmem Path null Nachteil.
5. Targets
| Variante | Steady-State p50 | Cold-Start p99 | Aufwand |
|---|---|---|---|
(a) Quick-Tuning (Pipeline-Mem auf 2048, Models eager-load, ef_search=100, parametrisierte SQL, Retry-After: 1 statt 5, optional Provisioned-Concurrency-Tuning) | 1.2–2.0 s | ~6 s | 1–2 Tage |
(b) Moderater Refactor (sync API-Path, SQS nur für Batch >5 Queries, kombinierte Embed+Rerank-Lambda, aioboto3-Session pro Container, HNSW ef_search-Tuning, Multi-Field-Embedding falls Recall-Test es bringt) | 500–900 ms | ~3 s | 1 Woche |
| (c) Neubau (Fargate-Service mit beiden Modellen in-process, Postgres-Connection-Pool warm, gRPC oder direkt-HTTP, kein API-Gateway-Overhead, Embedding+Search+Rerank in einem Function-Call) | 200–400 ms | nie kalt | 2–3 Wochen |
Empfehlung: Variante (a) bringt schon Faktor 5–10. Vor Refactor erst messen ob (a) reicht.
Relevante Files
~/source/a-icking/BENCHMARK.md~/source/a-icking/search_pipeline/cli.py~/source/a-icking/search_pipeline/lambda_handler.py~/source/a-icking/search_pipeline/lambda_embedding.py~/source/a-icking/search_pipeline/pg_client.py~/source/a-icking/models/embedding/app.py~/source/a-icking/models/reranking/app.py~/source/a-icking/lambdas/api_handler/handler.py~/source/a-icking/terraform/modules/searchpipeline/main.tf~/source/a-icking/terraform/stacks/app/variables.tf~/source/a-icking/terraform/modules/lambda-warmer/main.tf~/source/a-icking/db/migrations/006_tune_hnsw_indexes.sql