Vissza a tudásbázisba
WhitepaperSzemantikus keresésEmbeddingpgvectorRAGKnowledge GraphGraphRAGCosine SimilarityHybrid SearchAI architektúraMulti-tenant

Szemantikus Keresés és Embedding Stratégiák — Whitepaper CTO-knak és IT Döntéshozóknak

ÁZ&A
Ádám Zsolt & AIMY
||36 perc

Vezetői összefoglaló

A szemantikus keresés az AI-alapú üzleti alkalmazások kritikus infrastrukturális rétege. Míg a hagyományos kulcsszó-keresés pontos egyezéseket keres, a szemantikus keresés a jelentést érti meg — lehetővé téve, hogy egy AI asszisztens valóban releváns kontextussal válaszoljon az üzleti kérdésekre.

Ez a whitepaper egy valós, produkciós multi-tenant SaaS rendszer architektúráját mutatja be, amely:

  • 9 különböző adattípust kezel (email, naptár, CRM, számlázás)
  • pgvector-t használ dedikált vektor-adatbázis helyett
  • Knowledge graph-fal gazdagítja a vektorkeresést
  • Greedy token budget rendszerrel optimalizálja az LLM kontextust
  • BullMQ aszinkron pipeline-nal kezeli a nagy volumenű embedding-generálást

A whitepaper 15 fejezeten keresztül végigvezeti az olvasót az embedding-modellek kiválasztásától a produkciós monitoringig.


1. A probléma — amikor a keresés nem érti, amit keresünk

A szemantikus szakadék

Képzeld el: egy szépségszalon AI asszisztensét megkérdezik: „Mikor volt utoljára Kiss Anna?"

Kulcsszó-keresés eredménye: Semmi. A szó, hogy „utoljára" nem szerepel egyetlen naptárbejegyzésben sem.

Szemantikus keresés eredménye: Megtalálja Kiss Anna utolsó naptárbejegyzését (2026-03-15, hajvágás + festés), mert megérti, hogy az „utoljára" = legutóbbi időpont.

De ez még nem elég. Mi van, ha a kérdés: „Mikor volt utoljára Kiss Anna, és mit csináltunk?"

Ehhez kell a tudásgráf is: a naptáresemény mellé betölti a kliens-profilt (VIP, allergiás bizonyos festékekre) és az előző alkalommal írt jegyzetet.

Keresési megközelítés         Mit talál?                A kérdés megválaszolható?
─────────────────────────────────────────────────────────────────────────────────
Kulcsszó (LIKE, tsvector)     Pontos szó-egyezés        ❌ Nem
Vektoros (embedding)          Szemantikailag hasonló    ⚠️ Részben
Vektor + gráf                 Hasonló + kapcsolódó      ✅ Igen, kontextussal

Ez a whitepaper azt mutatja be, hogyan jutottunk el a kulcsszó-keresésből a vektor + knowledge graph architektúráig — és milyen döntéseket kellett meghoznunk az úton.


2. Embedding modellek — a szemantikus keresés motorja

2.1 Mi az embedding?

Az embedding egy szöveg (jelen esetben üzleti adat: email, naptáresemény, ügyfélprofil) átalakítása egy numerikus vektorrá — jellemzően 256-3072 dimenziós térben. Az ilyen vektorok között cosine similarity-vel mérhetjük a szemantikai hasonlóságot.

2.2 A fő választási szempontok

Modell Dimenziók Ár (1M token) MTEB Átlag Magyar nyelv
OpenAI text-embedding-3-small 1536 $0.02 62.3 Jó (multilingvális)
OpenAI text-embedding-3-large 3072 $0.13 64.6
Cohere embed-v4 1024 $0.10 66.1 Közepes
Google text-embedding-005 768 $0.025 63.8 Jó (multilingvális)
BAAI/bge-m3 (lokális) 1024 $0 (GPU) 62.0
E5-mistral-7b-instruct (lokális) 4096 $0 (GPU) 66.6 Közepes

2.3 A mi választásunk: OpenAI text-embedding-3-small (1536d)

Miért?

  1. Multilingvális: A magyar üzleti szövegeket (email, CRM jegyzet) jól kezeli, anélkül, hogy külön magyar modellt kellene üzemeltetni
  2. API egyszerűség: Már használjuk az OpenAI API-t az LLM-hez — egyetlen vendor, egyetlen API kulcs
  3. Költséghatékony: $0.02/1M token — egy 10.000 node-os knowledge graph teljes embedding-je ~$0.50
  4. Dimenzió: Az 1536 jó egyensúly a pontosság és a tárolási/keresési költség között

2.4 Magyar nyelvi sajátosságok

A magyar agglutináló nyelv — a „futottam", „futottál", „futottunk" mind más token, de szemantikailag közel vannak. Az OpenAI modellek BPE tokenizáció-ja ezt részben kezeli, de a nagyon ritka összetett szavak (pl. „munkavállalói-érdekképviseleti-tanácsadó") fragmentálódhatnak.

Gyakorlati tapasztalat: A text-embedding-3-small a magyar üzleti szövegekre (email, CRM, naptár) meglepően jól működik — a cosine similarity értékek konzisztensek a szemantikai hasonlósággal. A ritka szakkifejezéseknél (pl. specifikus kozmetikai eljárások) a contextualized embedding (ld. 14.2 fejezet) segíthet.

2.5 Döntési fa a modellválasztáshoz

Kell-e az adat az EU-n belül maradjon?
├── Igen → Lokális modell (bge-m3, E5-mistral) vagy EU-régió API
│          ├── Van GPU infrastruktúra? → Lokális
│          └── Nincs → EU-régió OpenAI / Cohere
└── Nem → API-alapú
           ├── Költségérzékeny? → OpenAI text-embedding-3-small ($0.02)
           ├── Maximális pontosság? → Cohere embed-v4 vagy E5-mistral
           └── Már van OpenAI integráció? → text-embedding-3-small (egyszerűség)

3. Vektortárolás — pgvector vs. dedikált vektor-adatbázisok

3.1 A döntés: PostgreSQL + pgvector

Szempont pgvector Pinecone Qdrant Weaviate
Üzemeltetés Meglévő PostgreSQL Managed SaaS Self-hosted / Cloud Self-hosted / Cloud
Költség $0 (extension) $70+/hó $0-65/hó $25+/hó
Skálázhatóság ~5M vektorig kiváló Korlátlan 100M+ 100M+
SQL integráció Natív (JOIN, WHERE) Nincs Nincs GraphQL
Multi-tenant WHERE provider_id= Namespace Collection / payload Tenant
ACID tranzakciók Igen Nem Nem Nem
Knowledge graph Ugyanabban a DB-ben Külön rendszer kell Külön rendszer kell Beépített (részleges)

A döntő érv: A knowledge graph node-ok és élek ugyanabban az adatbázisban vannak, mint a vektorok. Egyetlen SQL query-vel tudunk vektoros keresést + gráf-bejárást + tenant-szűrést csinálni — nincs hálózati latency két rendszer között.

3.2 A knowledge_nodes tábla

CREATE TABLE knowledge_nodes (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    provider_id UUID NOT NULL REFERENCES providers(id),
    type VARCHAR(50) NOT NULL,              -- 'email', 'calendar_event', 'client', stb.
    source VARCHAR(50),                      -- 'gmail', 'google_calendar', 'crm', stb.
    external_id VARCHAR(255),                -- eredeti rendszerbeli ID
    label VARCHAR(500),                      -- emberi olvasható cím
    content TEXT,                             -- a teljes szöveges tartalom
    properties JSONB DEFAULT '{}',           -- típus-specifikus metaadatok
    embedding vector(1536),                  -- OpenAI text-embedding-3-small
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),

    CONSTRAINT unique_external UNIQUE (provider_id, source, external_id)
);

-- Vektoros keresési index
CREATE INDEX idx_knowledge_nodes_embedding
    ON knowledge_nodes USING ivfflat (embedding vector_cosine_ops)
    WITH (lists = 100);

-- Tenant-szűrés index
CREATE INDEX idx_knowledge_nodes_provider
    ON knowledge_nodes (provider_id);

3.3 Indexelés: IVFFlat vs. HNSW

Szempont IVFFlat HNSW Megjegyzés
Építési idő Gyors Lassú (10-100x) HNSW nagy adatnál órákig tarthat
Keresési sebesség Jó (~5ms) Kiváló (~1ms) HNSW gyorsabb, de IVFFlat is elég
Recall@10 95-98% 99%+ IVFFlat a `probes` paraméterrel hangolható
Memória Alacsony Magas (2-3x) HNSW gráf-struktúrát tart memóriában
Inkrementális index Nem (rebuild kell) Igen HNSW-nél új vektorok azonnal indexelődnek

A mi választásunk: IVFFlat (lists=100). Egy multi-tenant SaaS-ben a tenant-ek jellemzően 100-10.000 node-ot tartanak — ezen a méretkategórián az IVFFlat bőven elegendő, és az index rebuild a BullMQ pipeline-ba integrálható.


4. Aszinkron embedding pipeline — BullMQ architektúra

4.1 Miért aszinkron?

Az embedding-generálás nem szinkron: egy email-szinkronizáció 500 emailt hoz, mindegyikhez API hívás kell. Ha szinkron lenne, egy Gmail sync 500 × 200ms = 100 másodpercig tartana — elfogadhatatlan.

4.2 A pipeline architektúra

Gmail/Calendar Connector
        │
        ▼
   EventWorker (concurrency: 5)
        │  Node létrehozás/frissítés
        │  Él-kezelés (BELONGS_TO, EMAILED, stb.)
        ▼
   EmbeddingQueue.add({ nodeId, content })
        │
        ▼
   EmbeddingWorker (concurrency: 3, rate: 50/min)
        │  OpenAI API hívás
        │  Vektor mentés → knowledge_nodes.embedding
        ▼
   Keresésre kész ✓

4.3 A szöveg-előkészítés

Az embedding minősége a bemeneti szöveg minőségétől függ. A pipeline:

  1. Tartalom összeállítás típus szerint:

    • Email: Tárgy: ${subject}\nFeladó: ${from}\n${body}
    • Naptár: ${summary} - ${start} - ${location}\n${description}
    • Kliens: ${name} - ${email} - ${notes}
  2. Tisztítás: HTML tag-ek eltávolítása, whitespace normalizálás

  3. Truncate: Max 8000 karakter (a text-embedding-3-small 8191 token limites, de a karakter/token arány ~4:1 magyar szövegnél)

function prepareTextForEmbedding(node) {
    let text = '';
    switch (node.type) {
        case 'email':
            text = `Email tárgy: ${node.label}\n${node.content}`;
            break;
        case 'calendar_event':
            text = `Esemény: ${node.label}\n${node.properties?.location || ''}\n${node.content}`;
            break;
        case 'client':
            text = `Ügyfél: ${node.label}\n${node.content}`;
            break;
        default:
            text = `${node.label}\n${node.content}`;
    }
    return text.replace(/\s+/g, ' ').trim().substring(0, 8000);
}

4.4 Rate limiting és hibakezelés

Az OpenAI embedding API rate limit-je (Tier 3): ~5000 RPM. De a biztonságos operálás érdekében 50 job/percre korlátozzuk:

const embeddingWorker = new Worker('embedding-queue', processEmbedding, {
    connection: redis,
    concurrency: 3,
    limiter: {
        max: 50,
        duration: 60000  // 50 job/perc
    }
});

429 (rate limit) kezelés: Ha az OpenAI 429-et ad, a worker 1 órás szünetet tart (az OpenAI rate limit reset window-ja alapján), majd folytatja. Ez agresszívabb, mint egy exponential backoff, de megbízhatóbb — a 429 után az exponential backoff gyakran „oszcillál".

Hibaizoláció: Egy sikertelen embedding nem blokkolja a többi node feldolgozását. A BullMQ attempts: 3 + backoff: exponential konfiguráció automatikusan újrapróbálja, és ha 3 próba után sem sikerül, a node embedding = NULL marad, ami azt jelenti, hogy a keresés nem találja — de a rendszer működik.


5. Chunking stratégiák — vagy inkább: miért NEM chunkolunk?

5.1 A chunking mítosza

A legtöbb RAG tutorial a „chunking"-ot tanítja elsőként: vágd fel a dokumentumokat 500-1000 tokenes darabokra, mindegyiket embedding-eld külön. Ez dokumentum-alapú rendszerekre kiváló (pl. egy 200 oldalas kézikönyv feldolgozása).

De az üzleti adatoknál más a helyzet:

  • Egy email átlagosan 200-500 token — természetes egység, nem kell vágni
  • Egy naptáresemény 50-150 token
  • Egy ügyfélprofil 100-300 token
  • Egy CRM jegyzet 50-200 token

Ezek az adatok „természetes chunk-ok" — ha felvágjuk őket, elveszítjük a kontextust.

5.2 Az entitás-alapú megközelítés

A mi rendszerünk entitás-alapú, nem dokumentum-alapú:

Dokumentum-alapú:                    Entitás-alapú:
─────────────────                    ─────────────────
  PDF → 50 chunk → 50 vektor          Email → 1 node + élek
  Nincs kapcsolat a chunk-ok között   Naptár → 1 node + élek
  Sok redundancia                      Kliens → 1 node + élek
                                       Természetes gráf-struktúra

Minden üzleti entitás (email, esemény, ügyfél, számla) egy node a knowledge graph-ban, egy embedding-gel. A kontextust nem a chunk-ok átfedése adja, hanem a gráf élek — egy email node össze van kötve a küldővel (client), a szálával (thread), és a benne említett eseményekkel.

5.3 Kivétel: hosszú szövegek

Ha mégis van hosszú szöveg (pl. egy 10.000 szavas termékkatalógus), három strategy közül választhatunk:

  1. Fixed-size chunking: 500 tokenes darabok, 50 token átfedéssel. Egyszerű, de kontextus-veszteség.
  2. Recursive character text splitting: Bekezdésenként, majd mondatonként vág. Jobb kontextus-megőrzés.
  3. Semantic chunking: Embedding-alapú hasonlósággal dönt, hol vágjon. Legjobb minőség, de drágább.

A mi rendszerünkben a hosszú szövegek ritkák (az üzleti adatok 95%-a < 1000 token), ezért az egyszerű truncate (8000 karakter) elegendő.


6. Keresési finomhangolás — cosine similarity, threshold, top-K

6.1 A cosine similarity röviden

Két vektor (A, B) cosine hasonlósága: cos(θ) = (A · B) / (||A|| × ||B||). Értéke -1 és 1 között lehet; 1 = tökéletesen hasonló, 0 = ortogonális (nincs kapcsolat).

A gyakorlatban az embedding modellek 0.3-0.9 tartományban adnak értékeket — a 0.60 feletti érték jellemzően „releváns"-t jelent.

6.2 A threshold paradoxon

Kézenfekvőnek tűnik: állítsuk magasra a threshold-ot (pl. 0.80), és csak a nagyon releváns találatokat adjuk vissza. De:

Threshold Precision Recall Tapasztalat
0.80 Kiváló Nagyon alacsony Alig talál valamit — „nincs adat" élmény
0.70 Közepes Néha kimaradnak releváns dolgok
0.60 Optimális egyensúly az OpenAI modellel
0.50 Alacsony Magas Sok irreleváns találat — „zajos" kontextus

A 0.60 az optimális threshold a text-embedding-3-small modellel, üzleti adatokon tesztelve. Ez modell-specifikus — más modellel más érték lehet optimális.

6.3 Top-K: miért 8?

A top-K (hány találatot kérünk) a token budget-tel összefügg:

  • Token budget: 3000 token (~12.000 karakter)
  • Átlagos node tartalom: 300-400 karakter (75-100 token)
  • Formázási overhead: ~20 token/node (Markdown fejlécek, szeparátorok)
  • Gráf-szomszédok: A top-3 találat szomszédai is bekerülnek

→ 8 direkt találat + ~5 gráf-szomszéd = ~13 node × ~100 token = ~1300 token, ami jól belefér a 3000-es budget-be, marad hely a formázásnak és a „biztonsági tartaléknak".

6.4 Multi-tenant keresési izoláció

A keresés mindig tenant-szűrt:

SELECT id, label, content, type, source, properties,
       1 - (embedding <=> $1::vector) AS similarity
FROM knowledge_nodes
WHERE provider_id = $2
  AND embedding IS NOT NULL
ORDER BY embedding <=> $1::vector
LIMIT $3;

A provider_id = $2 feltétel biztosítja, hogy egy tenant soha nem lát másik tenant adatait — még véletlenül sem. Ez nem csak GDPR compliance, hanem üzleti kritikus: egy szépségszalon ne lássa egy másik szalon ügyfél-adatait.


7. Kontextus-gazdagítás — a gráf ereje

7.1 A probléma: izolált vektortalálatok

A vektoros keresés izolált node-okat ad vissza. De az üzleti kérdések kontextust igényelnek:

  • „Mikor jön legközelebb Kiss Anna?" → Kell a naptáresemény + az ügyfélprofil (pl. allergia-információk)
  • „Mi volt a szombati email lényege?" → Kell az email + a teljes szál (thread) + az érintett ügyfél
  • „Mennyi bevétel volt márciusban?" → Kell a számlák + az érintett ügyfélrek

7.2 Az 1-hop szomszédok betöltése

A megoldás: a top vektortalálatok gráf-szomszédjait is betöltjük. Az 1-hop = a direkt szomszédok (1 él távolságra lévő node-ok).

Vektor-találat: event_calendar_77 (sim: 0.82)
                    │
                    ├── BOOKED ──▶ client_15 „Kiss Anna" (VIP, allergia: X festék)
                    ├── BELONGS_TO ──▶ calendar_google_main
                    └── MENTIONS ──▶ note_23 „Múltkor szólni kell a balayage-ról"

A client_15 és note_23 nem szerepeltek a vektoros keresés top-K találatai között (más az embedding-jük), de kritikus kontextust adnak a válaszhoz.

7.3 A relevancia-öröklés (decay factor)

A szomszédok relevancia-pontszámát a szülő similarity-jéből öröklik, egy decay faktorral csökkentve:

neighbor_score = parent_similarity × decay_factor

A rendszerünkben a decay_factor = 0.8:

  • Ha a szülő similarity = 0.82 → a szomszéd score = 0.82 × 0.8 = 0.656
  • Ez még a threshold (0.60) felett van → bekerül az eredménybe
  • Egy 2-hop szomszéd: 0.82 × 0.8 × 0.8 = 0.525 → threshold alatt → nem kerül be

A decay factor automatikusan szabályozza a gráf-bejárás mélységét: minél távolabbi egy szomszéd, annál alacsonyabb a score-ja, és természetes módon kiesik.

7.4 Miért működik ez a megközelítés?

A relevancia-öröklés három fontos tulajdonsággal bír:

  1. A direkt találatok mindig előrébb vannak a rangsorban
  2. A szomszédok nem „szorítják ki" a direkt találatokat a token budget-ből
  3. Többszörösen hivatkozott entitások (pl. Kiss Anna client node két email-ből is elérhető) magasabb relevancia-pontszámot kapnak

7.5 A rekurzív CTE gráf-bejárás

A szomszédok lekérdezése hatékony SQL-lel történik, rekurzív CTE-vel:

WITH RECURSIVE related AS (
    -- Kiindulás: a start node
    SELECT kn.id, kn.type, kn.label, kn.content, kn.properties,
           0 AS depth, ARRAY[kn.id] AS path
    FROM knowledge_nodes kn
    WHERE kn.id = $1

    UNION ALL

    -- Rekurzió: mindkét irányú élek mentén
    SELECT kn2.id, kn2.type, kn2.label, kn2.content, kn2.properties,
           r.depth + 1, r.path || kn2.id
    FROM related r
    JOIN knowledge_edges ke ON ke.from_node_id = r.id OR ke.to_node_id = r.id
    JOIN knowledge_nodes kn2 ON kn2.id = CASE
        WHEN ke.from_node_id = r.id THEN ke.to_node_id
        ELSE ke.from_node_id
    END
    WHERE r.depth < $2           -- max mélység: 1 (vagy 2 speciális esetben)
      AND NOT kn2.id = ANY(r.path)  -- ciklus-megelőzés
)
SELECT DISTINCT ON (id) * FROM related WHERE depth > 0;

Ciklus-megelőzés: A path tömb tartalmazza az eddig érintett node-okat. Ha egy node már szerepel a path-ban, a rekurzió nem folytatódik azon az ágon. Ez megakadályozza a végtelen ciklusokat kölcsönös hivatkozásoknál (pl. email → thread → email).

7.6 Konfigurálható paraméterek

Paraméter Érték Hatás
graphMaxDepth 1 Hány hop-ra megyünk a gráfban (1 = közvetlen szomszédok)
graphMaxNeighbors 5 Maximum szomszédok száma per kiindulási node
graphMaxEnriched 3 A top-K vektortalálatok közül hányat gazdagítunk gráf-szomszédokkal
decayFactor 0.8 Szomszédok relevancia-öröklési tényezője

Miért 1 hop és nem 2? A 2-hop szomszédok exponenciálisan növelik az eredményhalmazt (5 szomszéd × 5 = 25 second-hop node), és a relevancia a decay² = 0.64-re csökken — ami már a threshold (0.60) közelében van. Ezért az 1-hop az optimális egyensúly a kontextus-gazdagság és a költséghatékonyság között.


8. A teljes RAG pipeline — 5 lépésben

8.1 Architekturális áttekintés

Felhasználói kérdés
      │
      ▼
  ┌─────────────────────────────────────────────────────────┐
  │  retrieveRAGContext(providerId, message)                  │
  │                                                          │
  │  1. Guard: message.length >= 3?                          │
  │     └─ Nem → return emptyResult()                        │
  │                                                          │
  │  2. Vector Search                                        │
  │     generateEmbedding(message)                           │
  │     → searchByEmbedding(embedding, providerId, topK=8)   │
  │     → filter: similarity > 0.60                          │
  │                                                          │
  │  3. Graph Enrichment                                     │
  │     Top-3 talált → getNeighbors() per node               │
  │     → szomszédok decay = 0.8                             │
  │     → max 5 szomszéd per node                            │
  │                                                          │
  │  4. Dedup + Rank + Token Budget                          │
  │     → deduplikálás ID alapján (magasabb score marad)      │
  │     → rendezés similarity desc                           │
  │     → greedy packing: 3000 token budget                  │
  │                                                          │
  │  5. Format + Inject                                      │
  │     → Markdown kontextus típus szerinti csoportosítással  │
  │     → Source objektumok a frontend-nek                    │
  │     → System message-ként az LLM-nek                     │
  └─────────────────────────────────────────────────────────┘
      │
      ▼
  LLM válaszgenerálás a kontextus alapján

8.2 Lépés 1: Guard — bemeneti validáció

if (!message || message.trim().length < RAG_CONFIG.minQueryLength) {
    return emptyResult();
}

Miért kell? Egy 1-2 karakteres üzenet (pl. „hi", „ok") nem hordoz szemantikus tartalmat — az embedding-je „átlagos", ami irreleváns találatokat adna. A 3 karakteres minimum kiszűri ezeket.

8.3 Lépés 2: Vektoros keresés

A felhasználói üzenetet ugyanazzal a modellel embedding-eljük, mint az adatokat (text-embedding-3-small). Ez kritikus: ha a query embedding és a tárolt embedding más modellből jön, a cosine similarity értelmetlen.

A keresés pgvector cosine distance operátorral történik, provider_id tenant-szűréssel.

8.4 Lépés 3: Gráfgazdagítás

A top-3 vektortalálathoz (nem mind a 8-hoz — teljesítmény-tudatos) betöltjük az 1-hop szomszédokat. A szomszédok a szülő similarity-jét × 0.8 decay-jel kapják.

8.5 Lépés 4: Deduplikáció, rangsorolás, token budget

Az összes node (vektor + gráf) egyetlen listába kerül:

  1. Deduplikáció: Ha egy node többször jelenik meg (pl. egy ügyfél két email szomszédja is), a magasabb score marad
  2. Rangsorolás: Csökkenő similarity szerint
  3. Token budget packing: Greedy algoritmus — a rangsort követve addig adagoljuk a node-okat, amíg a 3000 tokenes budget (karakter/4 heurisztikával becsülve) elfogy. Minden node max 400 karakter tartalmat kap.
let usedTokens = 0;
const selected = [];

for (const node of rankedNodes) {
    const content = (node.content || '').substring(0, RAG_CONFIG.maxContentLength);
    const estimatedTokens = Math.ceil(content.length / 4) + 20; // +20 a formázásnak
    if (usedTokens + estimatedTokens > RAG_CONFIG.maxContextTokens) break;
    usedTokens += estimatedTokens;
    selected.push({ ...node, content });
}

8.6 Lépés 5: Markdown formázás és injektálás

A kiválasztott node-ok típus szerint csoportosított Markdown-ná alakulnak:

📧 **Emailek:**
- **Időpont változtatás** (2026-03-10)
  Kedves Salon! Szeretném módosítani a pénteki időpontomat...
  _Forrás: Gmail_

📅 **Naptár események:**
- **Hajvágás + festés** (2026-03-15 14:00)
  Kiss Anna - 90 perc
  _Forrás: Google Calendar_

👤 **Ügyfelek:**
- **Kiss Anna**
  Tel: +36-30-123-4567, VIP ügyfél, allergia: bizonyos festékek
  _Forrás: CRM_

Ez a Markdown system message-ként kerül az LLM kontextusába — elkülönítve a fő system prompt-tól:

LLM üzenet-tömb:
  [0] system  → Fő system prompt (személyiség, szabályok, elérhető tool-ok)
  [1] system  → RAG kontextus (a fenti Markdown)
  [2-N] user/assistant → Korábbi beszélgetés (max 50 üzenet)
  [N+1] user  → Aktuális kérdés

8.7 Forrás-attribúció a frontend-nek

A RAG pipeline nem csak az LLM-nek ad kontextust, hanem a frontend-nek is visszaküldi a forrás-referenciákat:

const sources = selectedNodes.map(node => ({
    type: node.type,
    label: node.label,
    snippet: node.content?.substring(0, 150),
    source: SOURCE_LABELS[node.source],
    icon: SOURCE_ICONS[node.source],
    similarity: node.similarity,
    nodeId: node.id
}));

Ez lehetővé teszi, hogy a frontend „Források" szekciót jelenítsen meg a válasz alatt — az LLM válasza mellett látszik, milyen adatokra alapozta a választ.


9. Hybrid keresés — a következő lépés

9.1 A szemantikus keresés korlátjai

A tisztán vektoros keresés nem tökéletes:

Helyzet Probléma
Pontos név keresése: „Kiss Anna" A vektor a „jelentést" keresi — „Kiss Anna" szemantikailag hasonló „Nagy Éva"-hoz (mindkettő személynév)
Számazonosítók: „#INV-2024-0042" A vektornak nincs fogalma a pontos számmintáról
Ritka szakkifejezés: „balayage" Ha a modell nem ismeri, a vektor nem reprezentálja jól
Rövid, pontos kérdés: „email cím" Túl generikus, sok hamis pozitív

9.2 Hybrid: vektor + BM25

A megoldás: kombináljuk a szemantikus keresést kulcsszó-alapú kereséssel (BM25 vagy PostgreSQL full-text search):

Felhasználói kérdés
      │
      ├─── Semantic Search (pgvector cosine) → top-K lista + score
      │
      └─── Keyword Search (tsvector/BM25) → top-K lista + score
                │
                ▼
        Reciprocal Rank Fusion (RRF)
                │
                ▼
        Összesített, rangsorolt eredmény

Reciprocal Rank Fusion (RRF) képlet:

RRF(d) = Σ 1 / (k + rank_r(d))   ahol k tipikusan 60

Az RRF előnye, hogy nincs szükség a score-ok normalizálására — a rangsorolás pozíciója számít, nem az abszolút érték.

9.3 PostgreSQL-natív implementáció

A pgvector + pg_trgm + tsvector kombináció lehetővé teszi a hybrid keresést egyetlen adatbázisban:

-- Full-text search index (ha még nincs)
CREATE INDEX idx_knowledge_nodes_fts
    ON knowledge_nodes USING gin (to_tsvector('hungarian', label || ' ' || content));

-- Hybrid query: vektor + full-text, RRF
WITH vector_results AS (
    SELECT id, label, content,
           ROW_NUMBER() OVER (ORDER BY embedding <=> $1::vector) AS v_rank
    FROM knowledge_nodes
    WHERE provider_id = $2 AND embedding IS NOT NULL
    LIMIT 20
),
text_results AS (
    SELECT id, label, content,
           ROW_NUMBER() OVER (ORDER BY ts_rank_cd(
               to_tsvector('hungarian', label || ' ' || content),
               plainto_tsquery('hungarian', $3)
           ) DESC) AS t_rank
    FROM knowledge_nodes
    WHERE provider_id = $2
      AND to_tsvector('hungarian', label || ' ' || content)
          @@ plainto_tsquery('hungarian', $3)
    LIMIT 20
)
SELECT COALESCE(v.id, t.id) AS id,
       COALESCE(v.label, t.label) AS label,
       1.0 / (60 + COALESCE(v.v_rank, 1000))
         + 1.0 / (60 + COALESCE(t.t_rank, 1000)) AS rrf_score
FROM vector_results v
FULL OUTER JOIN text_results t ON v.id = t.id
ORDER BY rrf_score DESC
LIMIT 8;

9.4 Mikor érdemes hybrid-re váltani?

Jel Indikáció
A felhasználók gyakran keresnek névre, azonosítóra Erős indikáció
A precision alacsony (sok irreleváns találat) Közepes indikáció
A recall alacsony (releváns adatok kimaradnak) Közepes indikáció
A rendszer jól működik vektoros kereséssel Nincs indikáció — ne optimalizálj ok nélkül

10. Re-ranking — a minőség utolsó mérföldje

10.1 A probléma

A bi-encoder (embedding modell) gyors és hatékony, de a hasonlóságot külön-külön számolt vektorok összehasonlításával méri. Nem „olvassa" egyszerre a query-t és a dokumentumot.

A cross-encoder együtt olvassa a query-t és a dokumentumot — ezért pontosabb, de lassabb:

Bi-encoder (embedding):
  Query → [vektor_q]    Document → [vektor_d]    cosine(q, d) → score
  Sebesség: ~1000 doc/sec    Pontosság: ★★★☆☆

Cross-encoder (re-ranker):
  [Query + Document] → score
  Sebesség: ~50 doc/sec     Pontosság: ★★★★★

10.2 A kétfázisú keresés

A megoldás: a bi-encoder (pgvector) szűr (top-K), a cross-encoder rangsorol:

Kérdés → pgvector cosine (1ms, 50K dokumentumból top-20)
           │
           ▼
         Cross-encoder re-rank (200ms, 20 dokumentumon)
           │
           ▼
         Top-8 valóban releváns találat

10.3 Elérhető re-ranker megoldások

Megoldás Típus Latency Pontosság Ár
Cohere Rerank v3 API ~200ms/20 doc Kiváló $2/1000 search
Jina Reranker v2 API/lokális ~150ms/20 doc $1/1000 search
cross-encoder/ms-marco-MiniLM-L-12-v2 Lokális ~300ms/20 doc $0 (GPU)
Voyage Reranker API ~180ms/20 doc $0.05/1M token
OpenAI Nincs natív re-ranker

10.4 Mikor érdemes re-ranker-t használni?

Helyzet Ajánlás
< 5K node a tenant-ben Nem szükséges — a top-8 cosine elég jó
5K – 50K node Mérlegelendő — hybrid + re-rank javíthat
50K+ node Erősen ajánlott — a precision számottevően javul
A threshold-finomhangolás nem segít Re-rank megoldhatja

11. Evaluation framework — honnan tudod, hogy jól működik?

11.1 A RAG kiértékelés problémája

A szemantikus keresés és a RAG pipeline kiértékelése nehezebb, mint egy hagyományos keresőé, mert:

  1. Nincs egyértelmű „helyes válasz" — a relevancia szubjektív
  2. A válasz minősége a keresés + az LLM együttes teljesítménye
  3. A kiértékelés drága (emberi annotálás vagy LLM-alapú scoring)

11.2 RAGAS metrikák

A RAGAS framework az iparági standard a RAG kiértékelésre:

Metrika Mit mér? Hogyan?
Context Precision A kontextus releváns a kérdéshez? A top-K találatok közül hány releváns (pl. top-1 a legfontosabb)
Context Recall A válaszhoz szükséges összes információ benne van a kontextusban? LLM összehasonlítja a ground truth-ot a kontextussal
Faithfulness A válasz hű a kontextushoz? Nincs hallucináció? LLM ellenőrzi, hogy a válasz állításai a kontextusból származnak-e
Answer Relevancy A válasz tényleg a kérdésre válaszol? Az LLM generál kérdéseket a válaszból, és összehasonlítja az eredetivel

11.3 Gyakorlati kiértékelési módszer

50-100 valós kérdéssel készíts golden dataset-et:

{
    "question": "Mikor volt utoljára Kiss Anna?",
    "expected_context": ["event_77", "client_15"],
    "expected_answer_contains": ["2026-03-15", "hajvágás"],
    "category": "appointment_lookup"
}

Automatizált kiértékelési ciklus:

  1. Futtasd a 50 kérdést a RAG pipeline-on
  2. Mérd: Context Precision, Context Recall, Faithfulness
  3. Variáld a paramétereket (threshold: 0.55/0.60/0.65, topK: 5/8/12)
  4. Vizualizáld: precision-recall görbék a különböző konfigurációkra
  5. Válaszd a legjobb egyensúlyt

11.4 A/B tesztelés produkciókban

Ha van elegendő forgalom, az A/B teszt az igazság:

Csoport Konfiguráció Mért KPI
A (kontroll) threshold=0.60, topK=8, no re-rank Felhasználói elégedettség, back-question arány
B (teszt) threshold=0.55, topK=12, Cohere re-rank Felhasználói elégedettség, back-question arány

A „back-question arány" (hányszor kérdez vissza a felhasználó, mert nem kapott jó választ) az egyik legjobb proxy-metrika a RAG minőségére.


12. Knowledge Graph + RAG: a GraphRAG megközelítés

12.1 Mit ad hozzá a gráf a RAG-hoz?

A hagyományos RAG „lapos" — dokumentumokat keres. A GraphRAG struktúrát ad:

Szempont Hagyományos RAG GraphRAG
Keresés Vektor hasonlóság Vektor + gráf-bejárás
Kontextus Izolált dokumentumok Összefüggő entitás-hálózat
„Ki csinálta?" típusú kérdés Gyenge (a név csak szöveg) Erős (van client entity + élek)
Multi-hop kérdés Nem tudja 2-hop: ügyfél → assigned → naptár
Deduplikáció Nincs (dupla chunk-ok) Természetes (egy entitás = egy node)

12.2 A mi GraphRAG implementációnk

A rendszerünk 9 node-típust és 8 éltípust kezel:

Node típusok:

Típus Forrás Jellemző tartalom
email Gmail connector Emailtörzs, tárgy, dátum
email_thread Gmail connector Teljes szálak összefoglalása
calendar_event Google Calendar Időpont, résztvevők, helyszín
client CRM modul Név, elérhetőség, preferenciák
deal CRM modul Értékesítési lehetőség, összeg, státusz
task CRM modul Feladat leírás, határidő, felelős
appointment Foglalási rendszer Időpont, szolgáltatás, kliens
note CRM modul Szabad szöveges jegyzet
invoice Számlázz.hu / Billingo Tétel, összeg, dátum, státusz

Éltípusok (kétirányú bejárással):

EMAILED     — email küldés/fogadás kapcsolat
BOOKED      — foglalás kapcsolat (client → appointment)
PAID        — fizetés kapcsolat (client → invoice)
MENTIONS    — hivatkozás (bármely node → bármely node)
TAGGED      — címkézés
ASSIGNED    — hozzárendelés (task → user)
BELONGS_TO  — csoportosítás (email → thread, deal → client)
SENT_TO     — célzott küldés

12.3 A gráf előnyei valós kérdéseknél

Kérdés: „Mennyit költött Kiss Anna az elmúlt 3 hónapban?"

Hagyományos RAG eredménye:
  → Talál egy emailt, amiben szó van fizetésről
  → LLM becslést ad

GraphRAG eredmény:
  1. Vektor keresés → deal_15 „Kiss Anna csomag" (sim: 0.75)
  2. Gráf enrichment:
     deal_15 ──BELONGS_TO──▶ client_15 „Kiss Anna"
     client_15 ──PAID──▶ invoice_23 „45.000 Ft 2026-02"
     client_15 ──PAID──▶ invoice_31 „38.000 Ft 2026-01"
     client_15 ──BOOKED──▶ appointment_44 „2026-03-15"
  3. LLM pontos választ ad: „Kiss Anna az elmúlt 3 hónapban
     83.000 Ft-ot költött"

13. Produkciós üzemeltetés — monitoring, drift, re-indexelés

13.1 Mit kell monitorozni?

Metrika Hogyan? Riasztás ha...
Embedding queue depth BullMQ getJobCounts() Waiting > 1000 (lemaradás)
Embedding queue error rate Failed jobs / total > 5% (API probléma)
Average search latency pgvector query time > 500ms (index probléma)
Average similarity score RAG pipeline log Átlag < 0.55 (drift vagy rossz adat)
Empty RAG result rate RAG emptyResult() arány > 40% (threshold túl magas, vagy kevés adat)
429 rate limit events Embedding worker log > 3/nap (rate limit túl agresszív)
Node count per tenant COUNT(*) group by provider_id < 50 (tenant nem használja)

13.2 Embedding drift

A modellek változnak. Ha az OpenAI frissíti a text-embedding-3-small-t (eddig nem tették, de a korábbi text-embedding-ada-002-t deprecated-nek jelölték), a régi és új vektorok inkompatibilisek. Ez a „drift" — a keresési minőség fokozatosan romlik.

Megelőzés:

  1. Modellverzió logolás: Tárold, melyik modellverzióval generáltad az embeddinget
  2. Teljes re-embedding képesség: Legyen script, ami az összes node-ot újra embedding-eli
  3. Canary tesztek: A golden dataset-et rendszeresen futtasd — ha a precision csökken, gyanakodj drift-re

13.3 Re-indexelés stratégia

Mikor kell re-indexelni?

Ok Gyakoriság
pgvector index-típus váltás (IVFFlat → HNSW) Egyszeri
IVFFlat lists paraméter növelés (adatmennyiség-növekedés) Negyedévente ellenőrizni
Embedding modell váltás Egyszeri, teljes
Jelentős adatmennyiség-változás (+100%) A lists paramétert újrakalkulálni

Zero-downtime re-indexelés:

-- 1. Új indexet építünk CONCURRENTLY (nem zárolja a táblát)
CREATE INDEX CONCURRENTLY idx_knowledge_nodes_embedding_new
    ON knowledge_nodes USING hnsw (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 200);

-- 2. Régi index törlése
DROP INDEX idx_knowledge_nodes_embedding;

-- 3. Átnevezés
ALTER INDEX idx_knowledge_nodes_embedding_new
    RENAME TO idx_knowledge_nodes_embedding;

13.4 A resilience mátrix

A teljes pipeline-ban minden komponens fail-safe:

Komponens Hiba esetén Hatás a felhasználóra
RAG pipeline (rag.js) emptyResult() visszatérés LLM válaszol, de kontextus nélkül
Gráf enrichment Per-node try/catch, skip Kevesebb kontextus, de működik
Embedding worker Queue szünet + retry Új adatok átmenetileg nem kereshetőek
Event worker Per-entity try/catch Részleges feldolgozás, a többi megy
Connector sync SyncLog-ba írja: PARTIAL/FAILED Admin dashboard-on látható
Kontextus-betöltés try/catch → null System prompt knowledge context nélkül működik
Tool-végrehajtás Per-tool try/catch, error → LLM Az LLM új stratégiát próbál
Token budget Hard cap 3000 token Sosem overflow-ol

Az elv: Az AI asszisztens mindig válaszol. Ha nincs kontextus, tudásgráf nélkül válaszol. Ha nincs tool, nélkülük. A degradáció fokozatos, sosem totális.


14. Fine-tuning embeddings — mikor érdemes?

14.1 A fine-tuning ígérete és valósága

Az embedding modell fine-tuning-ja a domain-specifikus szókincsre hangolhatja a modellt. De:

Szempont Előny Hátrány
Domain szókincs Jobb similarity a szakterületi szavakra Adatgyűjtés szükséges (1000+ páros)
Nyelvi sajátosságok Jobb magyar ragozás-kezelés Drága (API: ~$50-200/training)
Pontosság +3-8% MTEB javulás domain-on belül Általános tudás romolhat
Karbantartás Minden modellváltáskor újra kell csinálni

14.2 Alternatíva: prompt-szintű embedding javítás

Mielőtt fine-tune-olnál, próbáld a bemeneti szöveg javítását:

// Ahelyett, hogy:
generateEmbedding(email.body)

// Kontextualizáld:
generateEmbedding(`Email tárgy: ${email.subject}\nFeladó: ${email.from}\n${email.body}`)

Ez a „contextualized embedding" meglepően sokat javíthat — a modell többet tud a szöveg kontextusáról, anélkül hogy fine-tune-olnánk.

14.3 Mikor érdemes fine-tune-olni?

Feltétel Igen Nem
Van 1000+ releváns (query, document, label) hármas?
A jelenlegi precision < 70% a golden dataset-en?
A prompt-szintű javítás nem segített?
Az összes fenti igaz? → Fine-tune → Ne fine-tune-olj

15. Összefoglalás és döntési mátrix

15.1 Az architektúra rétegei

┌──────────────────────────────────────────────────────────────┐
│                     Felhasználói kérdés                       │
├──────────────────────────────────────────────────────────────┤
│  RAG Pipeline                                                 │
│  ┌─────────────┐  ┌──────────────┐  ┌─────────────────────┐ │
│  │ Vector Search│→ │ Graph Enrich │→ │ Dedup + Token Pack  │ │
│  │ (pgvector)   │  │ (1-hop CTE)  │  │ (3000 token budget) │ │
│  └─────────────┘  └──────────────┘  └─────────────────────┘ │
├──────────────────────────────────────────────────────────────┤
│  Knowledge Graph (PostgreSQL)                                 │
│  ┌──────────────────┐  ┌──────────────────┐                  │
│  │  KnowledgeNode    │──│  KnowledgeEdge   │                  │
│  │  (9 típus, 1536d) │  │  (8 éltípus)     │                  │
│  └──────────────────┘  └──────────────────┘                  │
├──────────────────────────────────────────────────────────────┤
│  Embedding Pipeline (BullMQ)                                  │
│  ┌─────────────┐  ┌──────────────┐  ┌─────────────────────┐ │
│  │ Event Worker │→ │ Embedding Q  │→ │ OpenAI API          │ │
│  │ (conc: 5)   │  │ (50/min)     │  │ (text-emb-3-small)  │ │
│  └─────────────┘  └──────────────┘  └─────────────────────┘ │
├──────────────────────────────────────────────────────────────┤
│  Connectors                                                   │
│  Gmail │ Google Calendar │ CRM │ Számlázz.hu │ Billingo      │
└──────────────────────────────────────────────────────────────┘

15.2 CTO döntési checklist

Kérdés Ha igen... Ha nem...
Van PostgreSQL? → pgvector (ingyenes, egyszerű) → Managed vektor DB (Pinecone)
< 5M vektor? → pgvector bőven elég → Qdrant/Milvus mérlegelés
Multi-tenant? → Provider-szintű szűrés kötelező! → Egyszerűbb architektúra
Strukturált üzleti adat? → Knowledge graph + entitás-alapú → Chunking + dokumentum-alapú
Van graph-like kapcsolat az adatokban? → GraphRAG enrichment → Tisztán vektoros RAG
A precision kritikus? → Hybrid search + re-ranker → Tisztán vektoros keresés
Magyar nyelv az elsődleges? → OpenAI text-embedding-3-small → Modell benchmark a saját szövegeden

15.3 A legfontosabb tanulságok

  1. Kezdj egyszerűen: pgvector + text-embedding-3-small + cosine search. Ez 30 perc alatt működik, és a legtöbb KKV use case-re elegendő.

  2. Ne chunkold, ami természetes egység: Emailek, események, ügyféladatok egyben jobbak. Az entitás-alapú knowledge graph megoldja a granularitás kérdését.

  3. A gráf-gazdagítás az igazi differenciátor: A „Ki?" „Mikor?" „Mennyit?" típusú kérdésekre a vektoros keresés önmagában gyenge — a szomszédok betöltése drámai minőségjavulást hoz.

  4. A resilience nem opcionális: Produkciós rendszerben a RAG pipeline nem blokkolhatja az AI választ. Ha bármi elromlik, graceful degradation: kevesebb kontextussal válaszolunk, de válaszolunk.

  5. Mérj 50 kérdéssel, mielőtt bármit változtatsz: A threshold, top-K, token budget mind finomhangolható — de adatvezérelt döntéssel, nem megérzéssel.


Szeretné implementálni a szemantikus keresést a saját rendszerében? Az Atlosz Interactive csapata produkciós tapasztalattal rendelkezik pgvector, knowledge graph és RAG pipeline architektúrában. Vegye fel velünk a kapcsolatot egy ingyenes technikai konzultációért.